Programming in Java: Socket Gaming

Lets take a look at an example of using Java for communication across a network. The program being demonstrated uses a server and a client program to network a game of “Wheel of Fortune” between 3 players.

Each “player” is represented by a class Player and is a thread on the server. This class also does the bulk of the logic. The reason for designing it this way is that we can easily scale the server to any number of players and also reuse several pieces of our code.

Let’s take a look at our Server.main():


        public static void main(String[] args)
	{

		  ////////////////////////////////
		 //    ARGUMENT ACCEPTANCE     //
		////////////////////////////////

		// Argument Protection: avoid any non-whole number strings.
		try
		{
			//- Avoid blank arguments.
			if(args[0].equals("")){
				System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
				System.exit(0);
			}
			//- Avoid negative and zero arguments.
			if(Integer.parseInt(args[0]) < 0){
				System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
				System.exit(0);
			}
		}
		catch(NumberFormatException e)
		{
			//- Avoid arguments that are not numeric.
			System.out.println("Usage Error: invalid argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
			System.exit(0);
		}
		catch(ArrayIndexOutOfBoundsException e)
		{
			//- Avoid arguments that are not existent.
			System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
			System.exit(0);
		}

		// Save the passed port number
		int port = Integer.parseInt(args[0]);

		// Create our Server object
		Server server = new Server(port);

		// Initiate game
		server.listenSocket();

		// Close game and server
		System.exit(0);
	}

Outside of the normal argument validation the main only initiates one line of code to run the game: server.listenSocket();. This method performs all of the game creation code such as player joining etc. Lets look at Server.listenSocket() now:


	public void listenSocket()
	{

		  ///////////////////////////////
		 // CREATE SERVER
		///////////////////////////////
		try
		{
			server = new ServerSocket(this.port);
		}
		catch (IOException e) {
			System.out.println("Could not listen on port " + this.port);
			System.exit(-1);
		}

		  ///////////////////////////////
		 // CREATE PHRASE
		///////////////////////////////

		// Read the phrases file into an array
		this.phrases = readFile("phrases.txt");

		// Pick a random phrase line from the file
		Server.phrase = this.phrases.get(generator.nextInt(this.phrases.size()));

		// Split the phrase line by tab
		this.phrase_split = Server.phrase.split("\t");

		// Save our phrase
		Server.phrase = this.phrase_split[0];

		// Save the phrase's category
		Server.category = this.phrase_split[1];

		// Create our hidden phrase to display made of hyphens
		Server.displayPhrase = "";
		for(int i=0; i < Server.phrase.length(); i++)
		{
			// IF there is a space, make it shown
			if(Server.phrase.charAt(i) == ' '){
				Server.displayPhrase = Server.displayPhrase.concat(" ");
			}
			// For each letter represent it with a -
			else{
				Server.displayPhrase = Server.displayPhrase.concat("-");
			}
		}

		  ///////////////////////////////
		 // ACCEPT PLAYERS
		///////////////////////////////

		System.out.println("Server initiated and game is set. Awaiting players...");

		// Keep waiting until all players have joined
		while(true)
		{
			// Temporary player object
			Player p;
			try
			{
				// Create our player object for the client joining
				p = new Player(server.accept(), playerCount, openPlay);

				// Turn that player object into a thread
				Thread t = new Thread(p);

				// Add the player to the player list
				players_o.add(playerCount, p);

				// Add the thread to the player thread list
				players.add(playerCount, t);

				// Notify server of new player
				System.out.println("Player " + playerCount + " Accepted!");
				playerCount++;

			}
			catch (IOException e){
				System.out.println("Accept failed: " + this.port);
				System.exit(-1);
			}

			// IF we have all three players needed, stop accepting more players
			if(players.size() >= 3){
				System.out.println("Maximum amount of players have joined.");
				break;
			}
		}

		// Start the threads running, therefore starting the game
		System.out.println("Game has begun.");
		for(int j=0; j < playerCount; j++)
		{
			players.get(j).start();
		}

		// Once the threads are done the game is done
		// The server simply needs to wait until the player threads have completed their game
		for(int j=0; j < playerCount; j++)
		{
			// Close down the thread when it is complete
			try{
				players.get(j).join();
			} catch (InterruptedException e){}
		}

		// Notify the server that the game is over
		System.out.println("Game is over.");
	}

Server.listenSocket() operates in the following steps:

  1. First it opens the port set in the argument for listening.
  2. The server then uses other logic to parse a random phrase for the game from a file
  3. Once that is complete, the server awaits the players to connect.
  4. Each time a player connects, it creates an instance of the Player class but does not begin their thread immediately.
  5. After all players have joined, the threads for each player are started in the order by which they have joined
  6. At that point the logic of the Server class is done, and it simply idles until each player’s thread has closed.

So the server is quite simple in that is sets up the game and dictates when it should end and begin. Like I said previously, the real logic of the game lies within the Player class threads. Lets take a look at some pieces of that code:


///////////////////////////////
// ESTABLISH CONNECTION TO CLIENT
///////////////////////////////
try
{
	// Create our Communication Tools
	in = new BufferedReader(new InputStreamReader(player.getInputStream()));
	out = new PrintWriter(player.getOutputStream(), true);
}
catch (IOException e) {
	System.out.println("in or out failed");
	System.exit(-1);
}

The Player.run() method for the thread starts with establishing a connection to the client over the network. The reference to “player” is actually an instance of the Java class Socket that was passed into the constructor of the player instance by the server. This code merely demonstrates that each player instance on the server is a one-to-one relationship with a client across a network.


// Continue running the game until the game has been ended
while(Server.gameOn){

	// THE FOLLOWING IS A CRITICAL SECTION AND SHOULD ONLY BE RUN IF IT IS THE PLAYERS TURN
	// IF it is the player's turn to play
	if(Server.playerTurn == playerId){

		// Utilize the Semaphore to ensure no one else can play
		// WAIT (openPlay);
		try{
			openPlay.acquire();
		}catch (InterruptedException e){ System.out.println("wait(openPlay) failure"); }

		// Announce to the player that it is their turn
		Server.announce("Player " + this.playerId + "'s turn...", this.playerId);

		// Loop until turn has ended
		while(true){

The next section of code is more or less the game loop. Each player thread will loop continually while the game is on, but will not continue logically until until it is their turn as designated by the serve. Once it becomes the player’s turn an internal game play loop begins. Here is an overview of the internal player loop:


			// IF the player has not made a spin
			if(this.spin == 0)
			{
				// Ask the player to spin the wheel
				out.println("respond@It is your turn. Type \"spin\" to spin the wheel.");
				try {
					line = in.readLine().trim();
				}
				catch (IOException e) {
					System.out.println("Read failed");
					System.exit(-1);
				}

				// IF the player has spun the wheel
				if(line.equals("spin")){

					// Choose a random value for the spin wheel
					this.spin_val = generator.nextInt(24);

					// CODE removed for tutorial but it contains logic for three possible spins:

					// Bankrupt and Lose Turn
					if(this.spin_val == 22){
						...
					}
					// Else If Lose Turn
					else if(this.spin_val == 23){
						...
					}
					// Else Save the dollar amount spun
					else{
						...
					}
				}
				else{
					this.spin = 0;
				}
			}
			// The player has already spun the wheel and is ready to play
			else{
				// CODE removed for tutorial but it contains logic for three possible spins:

				// IF the player has not made a choice for their turn
				if(this.decision == 0 || this.decision > 3){

					// Provide the player with the menu to choose from
					// 1. Guess the Phrase
					// 2. Buy a vowel
					// 3. Guess a consonant
					...

				}	

				// IF the player has decided to "Guess phrase"
				else if(this.decision == 1){

					// Ask the player for their guess at the phrase
					...

					// Check to see if the player's guess was right

					// IF the player is correct
					if(line.toLowerCase().equals(Server.phrase.toLowerCase())){

						// Award the player their money and announce their victory to waiting players
						out.println("null@Phrase \"" + line + "\" is correct! You win $" + this.budget + "!");
						Server.announce("Player " + this.playerId + " has won the game and $" + this.budget + " by guessing \"" + line + "\".", this.playerId);

						// Turn off the game
						Server.gameOn = false;
						break;
					}
					// IF the player was wrong with their guess
					else{

						// Notify them and the other players they were wrong
						out.println("null@Phrase \"" + line + "\" is incorrect. Your turn is over.");
						Server.announce("Player " + this.playerId + " guessed \"" + line + "\", but was wrong.", this.playerId);

						// Cause the player's turn to end
						// Unset their spin and decision
						// Move to the next player
						// Release the critcal section
						this.spin = 0;
						this.decision = 0;
						this.nextPlayerTurn();
						openPlay.release();
						break;

					}
				}	

				// IF the player decided to "Buy a vowel"
				else if(this.decision == 2){

					// Ensure the player has enough money to buy a vowel
					if(this.budget >= 250){

						// Ask the player which vowel they want
						...

						// IF there is a single letter that is a vowel
						if(line.length() == 1){

							// While checking if the letter has been guessed already, add it to the guessed list
							if(!this.addCheckLetter(line)){

								// Lookup the number of instances the letter appears in the phrase
								letterLocations = findLetterOccurances(line, Server.phrase);

								// IF there are letter occurances
								// The player is CORRECT
								if(letterLocations.size() > 0){

									// Remove the cost of the vowel from their budget
									...

									// Notify the player and others of their correct guess
									...

									// Update the hidden phrase, showing the vowel
									// Notify everyone of the new, partially exposed, phrase
									...

									// The player retains their turn
									// Reset their decision and spin and keep control of critical section
									...

								}
								// IF there are no letter instances
								// The player is INCORRECT
								else{

									// Notify the player and others of the mistake
									...

									// Cause the player to lose their turn
									// unset their spin and decision
									// move to the next player
									// and release the critical section
									...

								}
							}
							// IF the vowel has already been guessed before
							else{
								// ask again
								...
							}
						}
						// IF the player did not respond with a single letter vowel
						else{
							...
						}

					}
					// IF the player did not have enough money to purchase a vowel
					else{
						..
					}
				}

				// IF the player chose to "Guess a consonant"
				else if(this.decision == 3){

					// Ask the player for the consonant guess
					...

					// IF the consonant response is valid
					if(line.length() == 1){

						// While checking if the letter has been guessed already, add it to the guessed list
						if(!this.addCheckLetter(line)){

							// Lookup the number of instances the letter appears in the phrase
							letterLocations = findLetterOccurances(line, Server.phrase);

							// IF there are letter occurances
							// The player is CORRECT
							if(letterLocations.size() > 0){

								// Reward the player with their spin money times the number of letter occurances
								...

								// Notify the player and others of the correct guess
								.

								// Update the hidden phrase, showing the vowel
								// Notify everyone of the new, partially exposed, phrase
								...

								// The player retains their turn
								// Reset their decision and spin and keep control of critical section
								...

							}
							// IF the player's guess was wrong
							else{	

								// Notify the player and others of the mistake
								...

								// Cause the player to lose their turn
								// unset their spin and decision
								// move to the next player
								// and release the critical section
								...
							}

						}
						// IF the consonant has already been guessed before
						else{
							...
						}

					}
					// IF the player did not respond with a single letter consonant
					else{
						....
					}

				} // END DECISION IFS

			} // END SPIN IF 

		} // END WHILE TRUE
	} // END IF TURN

} // END WHILE gameOn

I removed sections of code related to the presentation of game state to the players as well as general logic for gameplay like if the player had guessed correctly or incorrectly. The focus here is more to present you with the notion that the internal player loop comprises most of the game logic. The reason for this design was so we could have any number of players, and using a semaphores and threads they could interact without having to organize their interactions.

The last piece of important code is our client program. Lets take a look at its main() first:


public static void main(String[] args)
{

	  ////////////////////////////////
	 //    ARGUMENT ACCEPTANCE     //
	////////////////////////////////

	// Argument Protection: avoid any non-whole number strings.
	try
	{
		//- Avoid blank arguments.
		if(args[0].equals("") && args[1].equals("")){
			System.out.println("Usage Error: missing arguments\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
			System.exit(0);
		}
		//- Avoid negative and zero arguments.
		if(Integer.parseInt(args[1]) < 0){
			System.out.println("Usage Error: invalid arguments\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
			System.exit(0);
		}
	}
	catch(NumberFormatException e)
	{
		//- Avoid arguments that are not numeric.
		System.out.println("Usage Error: invalid argument\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
		System.exit(0);
	}
	catch(ArrayIndexOutOfBoundsException e)
	{
		//- Avoid arguments that are not existent.
		System.out.println("Usage Error: missing argument\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
		System.exit(0);
	}

	// Save our host and port arguments
	int port = Integer.parseInt(args[1]);
	String host = args[0];

	// Create our client object
	Client client = new Client(host, port);

	// Game on
	client.listenSocket();
	client.communicate();
}

Very similarly structured to the server program the only real portions of importance are the lines “client.listenSocket();” and “client.communicate();”. Here are the two methods highlighted:


  ///////////////////////////////
 // OPEN SOCKET CONNECTION
///////////////////////////////

public void listenSocket(){
	try{
		// Create our socket
		socket = new Socket(this.host, this.port);

		// Create our communication tools
		out = new PrintWriter(socket.getOutputStream(), true);
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
	}
	catch (UnknownHostException e) {
		System.out.println("Unknown host: " + this.host);
		System.exit(1);
	}
	catch (IOException e) {
		System.out.println("No I/O");
		System.exit(1);
	}

	System.out.println("Welcome, please wait to begin.");
}

  ///////////////////////////////
 // READ or WRITE to SOCKET
///////////////////////////////

public void communicate()
{
	// Create our communication buffers and tools
	Scanner sc = new Scanner(System.in);
	String[] command = new String[2];
	String line = "";
	char inline[] = new char[1000];

	// Keep looking for messages from the server
	while(true)
	{
		// Reset the command recieved from the server to make sure the next one is clear
		command = new String[2];
		command[0] = "";
		command[1] = "";

		// READ FROM THE SERVER
		try{

			// Take a line from the server
			line = in.readLine();

			// Split the line so we know the reponse indetifier and the message
			command = line.split("@");

			// Special terminate command to end our client object
			if(command[0].equals("terminate"))
				System.exit(0);

			// If the command identifier and the message are existant
			if(!command[0].equals("") && !command[1].equals("")){

				// Print the message
				System.out.println(command[1]);
			}
		}
		catch (IOException e){
			System.out.println("Read failed");
			System.exit(1);
		}
		catch (ArrayIndexOutOfBoundsException e){}
		catch (NullPointerException e){}

		// IF response identifier given, ask for a respons to send to the server
		if(command[0].equals("respond")){

			// Ask the user to respond
			String name = sc.nextLine();

			// Send the server the response
			out.println(name);
		}
	}
}

The method listenSocket() only exists to initialize our input and output streams for the network communication. Simple stuff really thanks to Java.

The method communicate() on the other hand contains all the important logic for the client. As you can see the method itself is extremely generic and has no concept of game rules or game messages. In fact its so generic that this method could be used in conjunction with any game server.

That means that our client is coded in a fashion that makes it highly reusable which is a very vital aspect to good software design. In fact the server program itself is very generic and reusable since the entire game logic is within the Player class’ run() method.

This is both very important in that we simply have to swap out Player classes to have entirely different games, making the communication and thread logic reusable and therefore more valuable.

Feel free to download the source and run it for yourself.

Download: SocketWheelOfFortune.zip

To run:

  1. Download source zip.
  2. Decompress and compile Server.Java and Client.java
  3. Run Server with arguments [port], like “java Server 4444″
  4. Run 3 instances of Client with [host] [port] like “java Client localhost 4444″
  5. The server will wait until all three players have joined.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>