A simple 'multi-player game using Protobuf as protocol.
We want to implement and design a simple game, where users can play a "version" of Battleship together. The server is the one that sets the ships and knows the board, the clients are the one trying to find all the ships together.
The server also keeps track of a leaderboard and a log file.
The server and clients communicate through a given Protobuf protocol. See protobuf files.
YOU HAVE TO implement the given protocol as described in the README with the given proto file (DO NOT CHANGE THE PROTO FILE!!
Your code needs to work with our protocol exactly as defined in the .proto file, if you change it then our client would not work with our server. Theoretically, everyone's client should work with everyones server if you implement the given protocol.
Server and client should also not crash, it should be well implemented, readable, use good coding practices. If you do not do the above you might loose points even if the functionality is fulfilled.
Hint: place your project so that there are no whitespaces in the path. The way files are read right now will not work with whitespaces in the path.
1. The project needs to run through Gradle.
2. You need to implement the given protocol (Protobuf) see the Protobuf files, the README and the Protobuf example in the examples repo (this is NOT an optional requirement).
3. The main menu gives the user 3 options: 1: leaderboard, 2 play game, 3 quit. After a game is done the menu should pop up again. Implement the menu on the Client side (not optional). So the menu is not sent from the server to the client, but the client will "know" the menu and display it. The server will then only get the choice that the user made.
4. When the user chooses the option 1, a leader board will be shown (does not have to be sorted) or especially pretty.
5. The leaderboard is the same for all clients, take care of multiple clients not overwriting it and the leader board persists even if the server crashes. The leaderboard keeps track of how many points/wins each gamer has. We assume same name equals same gamer.
6. Client chooses option 2 (the game), then the client will either start a new game (if none are started by someone else yet) or will join a running game from another player (if someone else already started a game).
7. Multiple clients can enter the SAME game and will thus find the ships faster.
8. Clients win when finding all ships (always 12 'x no matter how big the board is) and get back to main menu, multiple clients can win together. They will all get 1 point when they are in the game together and find all the ships.
9. Be fancy and give not just one point for winning but make the points dependent on board size and how fast the "team" finds all the ships.
10. After the game is started the server waits for the client to enter a row and column. Please do it in the same way I did in the video. The row/column info will be sent to the server in one request. The server will then check if the values are on the board (so inside the board dimension) and if it was a hit or miss or already opened. The new board and the hit/miss info will be sent to the client.
11. Client should be easy to use and the user should be guided well through the process of playing the game.
12. Game quits gracefully when option 3 is chosen in main menu.
13. In my version the board on the client is only updated after they made a "guess", change the implementation so that as soon as the server updates the board the client will get this information and will update the board and inform the user, e.g. client B makes a correct guess, so the board updates and all other clients will get the information about the new board right away.
14. If user types in "exit" while in the game (instead of row or column), the client will exit gracefully.
15. Server does not crash when the client just disconnect (without choosing option 3)
16. You need to run and keep your server running on your AWS instance (or somewhere others can reach it and test it) - if you used the protocol correctly everyone should be able to connect with their client. Keep a log of who logs onto your server (this is already included).
17. You test at least 3 other servers.
Player.java
package client;
import java.util.*;
import java.util.stream.Collectors;
/**
* Class: Player
* Description: Class that represents a Player, I only used it in my Client
* to sort the LeaderBoard list
* You can change this class, decide to use it or not to use it, up to you.
*/
public class Player implements Comparable< Player > {
private int wins;
private String name;
// constructor, getters, setters
public Player(String name, int wins){
this.wins = wins;
this.name = name;
}
public int getWins(){
return wins;
}
// override equals and hashCode
@Override
public int compareTo(Player player) {
return (int)(player.getWins() - this.wins);
}
@Override
public String toString() {
return ("\n" +this.wins + ": " + this.name);
}
}
SockBaseClient.java
package client;
import java.net.*;
import java.io.*;
import org.json.*;
import buffers.RequestProtos.Request;
import buffers.ResponseProtos.Response;
import buffers.ResponseProtos.Entry;
import java.util.*;
import java.util.stream.Collectors;
class SockBaseClient {
public static void main (String args[]) throws Exception {
Socket serverSock = null;
OutputStream out = null;
InputStream in = null;
int i1=0, i2=0;
int port = 9099; // default port
// Make sure two arguments are given
if (args.length != 2) {
System.out.println("Expected arguments: < host(String) > < port(int) >");
System.exit(1);
}
String host = args[0];
try {
port = Integer.parseInt(args[1]);
} catch (NumberFormatException nfe) {
System.out.println("[Port] must be integer");
System.exit(2);
}
// Ask user for username
System.out.println("Please provide your name for the server.");
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
String strToSend = stdin.readLine();
// Build the first request object just including the name
Request op = Request.newBuilder()
.setOperationType(Request.OperationType.NAME)
.setName(strToSend).build();
Response response;
try {
// connect to the server
serverSock = new Socket(host, port);
// write to the server
out = serverSock.getOutputStream();
in = serverSock.getInputStream();
op.writeDelimitedTo(out);
// read from the server
response = Response.parseDelimitedFrom(in);
// print the server response.
System.out.println(response.getMessage());
System.out.println("* \nWhat would you like to do? \n 1 - to see the leader board \n 2 - to enter a game \n 3 - quit the game");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) in.close();
if (out != null) out.close();
if (serverSock != null) serverSock.close();
}
}
}
Game.java
package server;
import java.util.Scanner;
import java.util.*;
import java.io.*;
/**
* Class: Game
* Description: Game class that can load an ascii image
* Class can be used to hold the persistent state for a game for different threads
* synchronization is not taken care of .
* You can change this Class in any way you like or decide to not use it at all
* I used this class in my SockBaseServer to create a new game and keep track of the current image evenon differnt threads.
* My threads each get a reference to this Game
*/
public class Game {
private int idx = 0; // current index where x could be replaced with original
private int idxMax; // max index of image
private char[][] original; // the original image
private char[][] hidden; // the hidden image
private int col; // columns in original, approx
private int row; // rows in original and hidden
private boolean won; // if the game is won or not
private List< String > files = new ArrayList< String >(); // list of files, each file has one image
public Game(){
// you can of course add more or change this setup completely. You are totally free to also use just Strings in your Server class instead of this class
won = true; // setting it to true, since then in newGame() a new image will be created
files.add("battle1.txt");
files.add("battle2.txt");
files.add("battle3.txt");
}
/**
* Sets the won flag to true
* @param args Unused.
* @return Nothing.
*/
public void setWon(){
won = true;
}
/**
* Method loads in a new image from the specified files and creates the hidden image for it.
* @return Nothing.
*/
public void newGame(){
if (won) {
idx = 0;
won = false;
List< String > rows = new ArrayList< String >();
try{
// loads one random image from list
Random rand = new Random();
col = 0;
int randInt = rand.nextInt(files.size());
System.out.println("File " + files.get(randInt));
File file = new File(
Game.class.getResource("/"+files.get(randInt)).getFile()
);
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
while ((line = br.readLine()) != null) {
if (col < line.length()) {
col = line.length();
}
rows.add(line);
}
}
catch (Exception e){
System.out.println("File load error: " + e); // extremely simple error handling, you can do better if you like.
}
// this handles creating the orinal array and the hidden array in the correct size
String[] rowsASCII = rows.toArray(new String[0]);
row = rowsASCII.length;
// Generate original array by splitting each row in the original array.
original = new char[row][col];
for(int i = 0; i < row; i++) {
char[] splitRow = rowsASCII[i].toCharArray();
for (int j = 0; j < splitRow.length; j++) {
original[i][j] = splitRow[j];
}
}
// Generate Hidden array with X's (this is the minimal size for columns)
hidden = new char[row][col];
for(int i = 0; i < row; i++){
for(int j = 0; j < col; j++){
hidden[i][j] = 'X';
}
}
setIdxMax(col * row);
}
else {
}
}
/**
* Method returns the String of the current hidden image
* @return String of the current hidden image
*/
public String getImage(){
StringBuilder sb = new StringBuilder();
for (char[] subArray : hidden) {
sb.append(subArray);
sb.append("\n");
}
return sb.toString();
}
/**
* Method replaces the row and column value given with te value of the
* original. If it was a part of the ship the idx will be incremented, so it can keep
* track of how many ship parts were already found
*/
public String replaceOneCharacter(int row, int column) {
hidden[row][column] = original[row][column];
if (hidden[row][column] == '.'){
hidden[row][column] = ' ';
}
if (original[row][column] == 'x') {
idx++;
}
return(getImage());
}
public int getIdxMax() {
return idxMax;
}
public void setIdxMax(int idxMax) {
this.idxMax = idxMax;
}
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
}
SockBaseServer.java
package server;
import java.net.*;
import java.io.*;
import java.util.*;
import org.json.*;
import java.lang.*;
import buffers.RequestProtos.Request;
import buffers.RequestProtos.Logs;
import buffers.RequestProtos.Message;
import buffers.ResponseProtos.Response;
import buffers.ResponseProtos.Entry;
class SockBaseServer {
static String logFilename = "logs.txt";
ServerSocket serv = null;
InputStream in = null;
OutputStream out = null;
Socket clientSocket = null;
int port = 9099; // default port
Game game;
public SockBaseServer(Socket sock, Game game){
this.clientSocket = sock;
this.game = game;
try {
in = clientSocket.getInputStream();
out = clientSocket.getOutputStream();
} catch (Exception e){
System.out.println("Error in constructor: " + e);
}
}
// Handles the communication right now it just accepts one input and then is done you should make sure the server stays open
// can handle multiple requests and does not crash when the server crashes
// you can use this server as based or start a new one if you prefer.
public void start() throws IOException {
String name = "";
System.out.println("Ready...");
try {
// read the proto object and put into new objct
Request op = Request.parseDelimitedFrom(in);
String result = null;
// if the operation is NAME (so the beginning then say there is a commention and greet the client)
if (op.getOperationType() == Request.OperationType.NAME) {
// get name from proto object
name = op.getName();
// writing a connect message to the log with name and CONNENCT
writeToLog(name, Message.CONNECT);
System.out.println("Got a connection and a name: " + name);
Response response = Response.newBuilder()
.setResponseType(Response.ResponseType.GREETING)
.setMessage("Hello " + name + " and welcome. Welcome to a simple game of battleship. ")
.build();
response.writeDelimitedTo(out);
}
// Example how to start a new game and how to build a response with the image which you could then send to the server
// LINE 67-108 are just an example for Protobuf and how to work with the differnt types. They DO NOT
// belong into this code.
game.newGame(); // starting a new game
// adding the String of the game to
Response response2 = Response.newBuilder()
.setResponseType(Response.ResponseType.TASK)
.setImage(game.getImage())
.setTask("Select a row and column.")
.build();
// On the client side you would receive a Response object which is the same as the one in line 70, so now you could read the fields
System.out.println("Task: " + response2.getResponseType());
System.out.println("Image: \n" + response2.getImage());
System.out.println("Task: \n" + response2.getTask());
// Creating Entry and Leader response
Response.Builder res = Response.newBuilder()
.setResponseType(Response.ResponseType.LEADER);
// building an Entry for the leaderboard
Entry leader = Entry.newBuilder()
.setName("name")
.setWins(0)
.setLogins(0)
.build();
// building another Entry for the leaderboard
Entry leader2 = Entry.newBuilder()
.setName("name2")
.setWins(1)
.setLogins(1)
.build();
// adding entries to the leaderboard
res.addLeader(leader);
res.addLeader(leader2);
// building the response
Response response3 = res.build();
// iterating through the current leaderboard and showing the entries
for (Entry lead: response3.getLeaderList()){
System.out.println(lead.getName() + ": " + lead.getWins());
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (out != null) out.close();
if (in != null) in.close();
if (clientSocket != null) clientSocket.close();
}
}
/**
* Writing a new entry to our log
* @param name - Name of the person logging in
* @param message - type Message from Protobuf which is the message to be written in the log (e.g. Connect)
* @return String of the new hidden image
*/
public static void writeToLog(String name, Message message){
try {
// read old log file
Logs.Builder logs = readLogFile();
// get current time and data
Date date = java.util.Calendar.getInstance().getTime();
System.out.println(date);
// we are writing a new log entry to our log
// add a new log entry to the log list of the Protobuf object
logs.addLog(date.toString() + ": " + name + " - " + message);
// open log file
FileOutputStream output = new FileOutputStream(logFilename);
Logs logsObj = logs.build();
// This is only to show how you can iterate through a Logs object which is a protobuf object
// which has a repeated field "log"
for (String log: logsObj.getLogList()){
System.out.println(log);
}
// write to log file
logsObj.writeTo(output);
}catch(Exception e){
System.out.println("Issue while trying to save");
}
}
/**
* Reading the current log file
* @return Logs.Builder a builder of a logs entry from protobuf
*/
public static Logs.Builder readLogFile() throws Exception{
Logs.Builder logs = Logs.newBuilder();
try {
// just read the file and put what is in it into the logs object
return logs.mergeFrom(new FileInputStream(logFilename));
} catch (FileNotFoundException e) {
System.out.println(logFilename + ": File not found. Creating a new file.");
return logs;
}
}
public static void main (String args[]) throws Exception {
Game game = new Game();
if (args.length != 2) {
System.out.println("Expected arguments: < port(int) > < delay(int) >");
System.exit(1);
}
int port = 9099; // default port
int sleepDelay = 10000; // default delay
Socket clientSocket = null;
ServerSocket serv = null;
try {
port = Integer.parseInt(args[0]);
sleepDelay = Integer.parseInt(args[1]);
} catch (NumberFormatException nfe) {
System.out.println("[Port|sleepDelay] must be an integer");
System.exit(2);
}
try {
serv = new ServerSocket(port);
} catch(Exception e) {
e.printStackTrace();
System.exit(2);
}
clientSocket = serv.accept();
SockBaseServer server = new SockBaseServer(clientSocket, game);
server.start();
}
}
request.proto
syntax = "proto2";
package operation;
option java_package = "buffers";
option java_outer_classname = "RequestProtos";
// every request has one of these types
message Request {
enum OperationType {
NAME = 0; // when the user sends over their name -- has the name field as data
LEADER = 1; // when the user wants to see the leader board - no further data
NEW = 2; // when the user wants to enter a game -- no further data
ROWCOL = 3; // when the user sends a row and column to the server -- has the row and column as data
QUIT = 4; // when the user wants to quit the game -- has no further data
}
optional OperationType operationType = 1 [default = NAME]; // has the operation type
optional string name = 2; // the name field used for NAME request
optional int32 row = 3; // row field for the ROWCOL request
optional int32 column = 4; // column field for the ROWCOL request
}
// see the starter code on how to use this, e.g. writeToLog("Mehlhase", Message.CONNECT) would write a log for connecting to your server for me into your log file
enum Message {
CONNECT = 0; // when a client connects to your server
START = 1; // when a client starts a gam e
WIN = 2; // when a client wins
}
message Logs {
repeated string log = 1; // basically a list of log messages
}
response.proto
syntax = "proto2";
package operation;
option java_package = "buffers";
option java_outer_classname = "ResponseProtos";
// A response from the server can be any of these types
message Response {
enum ResponseType {
GREETING = 0; // after the client sends the name they get greeted -- message field
LEADER = 1; // sends the leader board -- field leader
TASK = 2; // sends the "task" which includes what the user needs (basically asking them to give row and column) to do and the image and a hit - fields task, image, hit (optional)
WON = 3; // if all ships have been found -- image field
ERROR = 4; // if something went wrong, e.g. out of bounds row/col or wrong request -- message field informing the user what went wrong in detail
BYE = 5; // client wants to quit - message field with bye message
}
optional ResponseType responseType = 1 [default = GREETING];
// Possible fields, see above for when to use which field
repeated Entry leader = 3; // leader board as repeated list -- LEADER
optional string task = 4; // the task for the client -- TASK
optional string image = 5; // the current image -- TASK, WON
optional bool hit = 6; // true if it is a hit, false if it is a miss or was already revealed -- TASK
optional string message = 7; // message for bye, greeting and error -- GREETING, ERROR, BYE
}
// entry for the leader board
message Entry {
optional string name = 1; // name of user
optional int32 wins = 2; // how many wins
optional int32 logins = 3; // how many logins
}
battle1.txt
...xxxx
x......
x......
x....xx
.x.....
.x.....
.x.....
battle2.txt
xxxx...
.......
x......
x....xx
x......
.......
.xxx...
battle3.txt
.......
xxxx...
.....xx
xx.....
.xxxx..
.......
.......