A Sample Game: RPS (Rock, Paper, Scissors)

With all the framework code in place, it is actually very simple to implement a game. You only need to create server- and client-side logic and associated data classes. This sample game is a networked rock, paper, scissors (RPS) game. In this game, any number of players can connect to the server and challenge each other to a game. After they are joined in a game, the two clients can play as many rounds as they desire; when they are finished, the server returns the tally of how many games have been won, lost, or tied. Clients also can chat with one another before and during games. Let's take a look at the classes needed to implement RPS.

Classes

We'll be using the PlayerDefault and GameEventDefault classes here, so we don't need to build custom Player or GameEvent implementations. For the server side, we need a GameController, RPSController. Additionally, we'll add a class to hold game data, RPSGame. For the client side, subclass GameClient with the RPSClient class and also add RPSConsoleEventReader for handling user input. shows the same class diagram from , with the addition of the RPS classes.

Game Server Framework plus RPS classes.
Game Server Framework plus RPS classes.

RPSController

The game logic will be very simple, yet still require a number of event types and add a good amount of logic code. RPSController keeps track of the RPS players and their games. We don't go into all the details of the logic here; we discuss just how the RPSController interacts with the framework. Incoming client events are delivered to the controller's EventQueue and then are picked up by the run() method in the GameController base class and passed to the RPSController's processEvent() method. To send events to clients, RPSController uses the sendEvent() or sendBroadcastEvent() methods of the base class. These methods call GameServer.write(), which drops the events into the EventQueue of the EventWriter class. Note that the RPSController class is part of the com.hypefiend.javagamebook. server.controller package, whereas the other classes are in com.hypefiend. javagamebook.games.rps. This is a consequence of the method that the GameServer uses to dynamically load the GameControllers.

RPSGame

RPSGame represents a set of rock, paper, scissors games between a pair of players. It keeps track of the player's moves during each round and tallies wins, losses, and ties for each player.

RPSClient

For the client, we need to program only a bit of rudimentary logic to handle reading input from the console and to handle incoming events from the server, as well as do a bit of housekeeping to keep track of the player and game/challenge state. shows the entire RPSClient code. As you can see, there isn't much to it. For most events, we simply write the contents of GameEvent.getMessage() to STDOUT. Everything else is handled by the GameClient base class.

Listing 6.12 RPSClient.java

public class RPSClient extends GameClient {
 protected static Logger log = Logger.getLogger("RPSClient");
 protected RPSConsoleEventReader consoleReader;
 public static void main(String args[] ) {
 BasicConfigurator.configure();
 RPSClient gc = new RPSClient();
 gc.init(args);
 gc.start();
 }
 public void init(String args[]) {
 super.init(args);
 consoleReader = new RPSConsoleEventReader(this, inQueue, outQueue);
 consoleReader.start();
 }
 protected void shutdown() {
 consoleReader.shutdown();
 super.shutdown();
 }
 protected void processIncomingEvents() {
 GameEvent inEvent;
 while (inQueue.size() > 0) {
 try {
 inEvent = inQueue.deQueue();
 switch (inEvent.getType()) {
 case GameEventDefault.S_LOGIN_ACK_OK:
 break;
 case GameEventDefault.SB_LOGIN:
 stdOut( "login: " + inEvent.getMessage());
 break;
 case GameEventDefault.SB_LOGOUT:
 stdOut( "logout: " + inEvent.getMessage());
 break;
 case GameEventDefault.SB_CHAT_MSG:
 stdOut( inEvent.getPlayerId() + ": " +
 inEvent.getMessage());
 break;
 case GameEventDefault.S_DISCONNECT:
 stdErr( "disconnected from server: " +
 inEvent.getMessage());
 shutdown();
 break;
 case GameEventDefault.S_JOIN_GAME_ACK_OK:
 stdOut( inEvent.getMessage());
 inGame = true;
 break;
 case GameEventDefault.S_JOIN_GAME_ACK_FAIL:
 stdOut( inEvent.getMessage());
 inGame = false;
 break;
 case GameEventDefault.SB_PLAYER_QUIT:
 stdOut( inEvent.getMessage());
 inGame = false;
 break;
 default:
 stdOut( inEvent.getMessage());
 break;
 }
 }
 catch (InterruptedException ie) {}
 }
 }
 public String getGameName() {
 return "RPS";
 }
 public GameEvent createGameEvent() {
 return new GameEventDefault();
 }
 public GameEvent createLoginEvent() {
 return new GameEventDefault(GameEventDefault.C_LOGIN);
 }
 public GameEvent createDisconnectEvent(String reason) {
 return new GameEventDefault(GameEventDefault.S_DISCONNECT, reason);
 }
}

RPSConsoleEventReader

ConsoleEventReader is a client class that is used to read input from the user via STDIN. We use a BufferedReader to read full lines of text, and use a StringTokenizer to break out commands from the String. Based on the tokens, a GameEvent is generated for the appropriate action and fed to the client's outgoing EventQueue.

Running the RPS Game

First, grab a copy of the code and then use Ant to build the game server and client:

$ cd chap06
$ ant

Just as you did with the ChatterBox example, you need three separate terminal windows open. In the first terminal, run the server with the provided shell script:

$ ./bin/server.sh

Then start the first client. The script takes two parameters: the IP address of the server and a player name:

$ ./bin/client.sh 10.0.0.1 bret

and the other client:

$ ./bin/client.sh 10.0.0.1 brackeen

Each client is shown the game welcome text, the help text, and the list of online players:

Welcome to the RPS (Rock, Paper, Scissors) Multi-player Game commands:
'/quit' quit the app
'/help' show this help
'/players' list players online
'/newgame <opponent name>' start a new game against opponent
'/move <(r)ock|(p)aper|(s)cissors>' enter your move
'/endgame' end the game all other input is treated as a chat message players online:
brackeen bret brackeen >

Start a game by having the first player enter the newgame command. Note that all commands are preceded with a forward slash (like IRC commands), and all other input is treated as chat messages:

bret> /newgame brackeen

Both players are notified that a game has started. Now enter the moves

brackeen > /move rock

and

bret > /move paper

The results of the game are displayed to each player, like this:

Opponent chooses rock You Win bret vs. brackeen >

To play more games against the same player, simply keep entering moves. When you want to stop, you can enter the endgame command to see a tally of your wins, losses, and ties for this session:

bret vs. brackeen > /endgame GameOver, player bret has quit.
Final tallies bret wins: 1
brackeen wins: 3
ties: 1

To exit the client, enter the quit command. That was fun, right? We've successfully created a multi-player game, based on an extensible Game Server Framework. You are encouraged to rummage through the rest of the code to get better acquainted with all the details.