Game Tutorial: Guess the Number

From EsWiki

Jump to: navigation, search

This tutorial will walk you through the process of creating a game plug-in.

Contents

What You'll Learn

  • How to create a Plugin
  • How to read the gameDetails EsObject
  • How to listen for and process client requests
  • How to send messages to the client

Prerequisites


Download

If you wish to simply download the entire set of Guess The Number files (both server and client, source code and compiled), it is found in GuessTheNumberTutorial.zip.

Let's Get Started!

We will make a simple multi-player number guessing game. The server will choose a random secret integer in a given range, then each player will submit a guess. When the time is up, the player(s) whose guess comes closest wins. This game will be available as an example with ElectroServer 4.0.5.

There are lots of ways to improve this game. We don't want to make our first attempt at a game too complicated.

Plugin

Create a new Java class that extends BasePlugin. This will handle the server side of the game, so let's call it GuessTheNumber. Since we will eventually have many games, we will need a separate GMSInitializer class as well, but we will start with GuessTheNumber.

gameDetails parameters

The gameDetails EsObject is available from the api, and will include parameters that the player creating the game can set. For this game, the following parameters make sense:

  • minimum possible secret number
  • maximum possible secret number
  • number of seconds to wait after the second person joins, before starting the game (so others may join)
  • number of seconds in a round
  • number of seconds after the round ends, before the next round starts automatically

We choose strings for the name of each of these parameters, when they are saved in an EsObject, so that we can unpack them. This leads us to a set of global constants and variables for GuessTheNumber.

Class variables and constants

Let's add the following lines to GuessTheNumber:

   private int minimumSecretNumber         = 1;
   private int maximumSecretNumber         = 100;
   private int countdownDurationInSeconds  = 30;
   private int roundDurationInSeconds      = 60;
   private int pauseAfterDurationInSeconds = 30;
       
   public static final String TAG_MINIMUM_SECRET_NUMBER = "min";
   public static final String TAG_MAXIMUM_SECRET_NUMBER = "max";
   public static final String TAG_COUNTDOWN_DURATION    = "countdown";
   public static final String TAG_ROUND_DURATION        = "round";
   public static final String TAG_PAUSE_DURATION        = "pause";

This will take care of the gameDetails. We will also need global variables to store the secret number itself, and names of the players and their guesses. A ConcurrentHashMap is a good choice for a multiplayer game that does not take turns. For convenience, let's also add variables for keeping track of the number of players and the game state.

   private int secretNumber;
   private ConcurrentHashMap<String, Integer> playerGuesses;
   private int numberOfPlayers = 0;
   private int gameState = 0;

Planning

This is a good point to plan what we will be doing. What are the various tasks?

  • Notice when each player enters; if there are at least 2 players, start the countdown.
  • Possibly send a message during the countdown, every 5 seconds, giving the number of seconds left.
  • When the countdown finishes, lock the game, then generate the secret number and tell the players that the game has started. Start the timer for the round.
  • Possibly send a message during the game, every 5 seconds, giving number of seconds left.
  • Record guesses sent by the players.
  • When the round finishes, determine winners and announce everybody's guesses. Start a timer for the next round, and unlock the game (so more players can join).

Client to Server messages

What messages will the client need to send the server? For this game, just the guess. We will need a tag for guesses: TAG_GUESS = "guess". Each guess will arrive in the EsObject requestParameters from a client's plugin request.

Server to Client messages

What messages will the server need to send to the client? Each of these messages will be inside an EsObject in a plugin message to the player. Since there will be different types of messages, let's have one tag called TAG_ACTION = "action" that tells the client the type of message it is.

  • Countdown, with time remaining. The value of TAG_ACTION will be "countdown". We will also add an integer variable named TAG_TIME_LEFT = "time", with value of the number of seconds left in the countdown.
  • Time left message (optional). The value of TAG_ACTION can be "time", and we can use the same integer variable TAG_TIME_LEFT.
  • Start game, with time remaining. The value of TAG_ACTION will be "start", and we can use the same TAG_TIME_LEFT.
  • Stop game, announce winners and time until next round. The value of TAG_ACTION will be "stop", and we can use the same TAG_TIME_LEFT for the time. For the winners, let's add an EsObject array variable and call it TAG_PLAYERS = "players". Each element in the array will be an EsObject that holds the player's name (String), guess (integer), and whether they won (boolean). We will use TAG_NAME = "name", the same TAG_GUESS, and TAG_WINNER = "won". Let's also announce the secret number, using TAG_SECRET = "secret".

More Class constants

Gather all the tags that we plan to use, and add them to the set of global constants:

   public static final String TAG_GUESS             = "guess";
   public static final String TAG_ACTION            = "action";
   public static final String TAG_TIME_LEFT         = "time";
   public static final String TAG_PLAYERS           = "players";
   public static final String TAG_NAME              = "name";
   public static final String TAG_WINNER            = "won";
   public static final String TAG_SECRET            = "secret";
   public static final String TAG_ACTION_COUNTDOWN  = "countdown";
   public static final String TAG_ACTION_START      = "start";
   public static final String TAG_ACTION_STOP       = "stop";

Game state constants

What are the different states for the game? Let's make an integer constant for each of them. An enumeration is an even better choice, but let's keep this tutorial simple.

   public static final int STATE_WAITING            = 0;
   public static final int STATE_COUNTING_DOWN      = 1;
   public static final int STATE_IN_PLAY            = 2;
   public static final int STATE_GAME_OVER          = 3;
   

Override methods

We will need to override the following methods of BasePlugin:

  • userEnter
  • userExit
  • request

The init method will be handled by a separate GMSInitializer class, although it could be done here if we wanted to.

userEnter

Each time a user enters the game, this method will be triggered. What do we want to do when a user enters? For this game, let's not keep track of names except for players who submit guesses. We don't really need to have an exact count of players in the room either, except that we don't want to start a game with only a single player. So when a user enters, we need to:

  • increment numberOfPlayers
  • if the count down hasn't started yet, and numberOfPlayers >= 2, start the count down

This becomes:

   @Override
   public ChainAction userEnter(UserEnterContext context) {
       numberOfPlayers++;
       if (gameState == STATE_WAITING && numberOfPlayers >= 2)
           startCountdown();
       return ChainAction.OkAndContinue;
   }

We will have to write a startCountdown method later.

userExit

For this game, we won't worry about players leaving after the countdown starts so that only one player remains. All the userExit method needs is to update the number of players.

   @Override
   public void userExit(String userName) {
       numberOfPlayers--;
   }

request

The request method handles all the messages from the client. In this case, there's just one possible message, the guess, so it's fairly short:

   @Override
   public final void request(String playerName,
           EsObjectRO requestParameters) {
       if (requestParameters.variableExists(TAG_GUESS)) {
           if (gameState != STATE_IN_PLAY)
               return;     // ignore guesses that are too early, too late
           int guess = requestParameters.getInteger(TAG_GUESS);
           playerGuesses.put(playerName, guess);
       }
   }

Since we are using a ConcurrentHashMap to store the names and guesses, we don't need to mark anything synchronized.

Timers

Earlier we added 4 constants for the game state. When the game is first created, it will be in gameState = STATE_WAITING. When we have at least 2 players, we want it to be in STATE_COUNTING_DOWN for a given amount of time, then move to STATE_IN_PLAY for a certain amount of time, then state STATE_GAME_OVER for a certain amount of time. In addition, we want to send messages every 5 seconds announcing the time left. We could do this with a lot of separate scheduled callbacks, but it will be easier to just have a single infinitely repeating scheduled callback, with a duration of 5 seconds. We will need three more global variables:

   private int callBackId = 0;
   private int nextEvent = -1;
   private int tickerCount = 0;

Let's also cancel this scheduled callback if the last player leaves the game, so userExit becomes:

   @Override
   public void userExit(String userName) {
       numberOfPlayers--;
       if (numberOfPlayers < 1) {
           getApi().cancelScheduledExecution(callBackId);            
       }
   }

We add a method that will be invoked every 5 seconds:

   // will run every 5 seconds, after we have at least 2 players
   private void tick() {
       tickerCount += 5;
       if (nextEvent >= 0 && tickerCount >= nextEvent) {
           switch (gameState) {
               case STATE_COUNTING_DOWN:
                   startGame();
                   break;
               case STATE_IN_PLAY:
                   endGame();
                   break;
               case STATE_GAME_OVER:
                   startCountdown();
                   break;
           }
       } else {
           broadcastTimeLeft();
       }
   }

This of course will need several other methods written:

  • startCountdown
  • broadcastTimeLeft
  • startGame
  • endGame

Game state methods

broadcastTimeLeft

Looking at our notes above, the time left message sent by the server will consist of two variables:

  • TAG_ACTION variable with String value of TAG_TIME_LEFT
  • TAG_TIME_LEFT variable with integer value of number of seconds left
   private void broadcastTimeLeft() {
       EsObject message = new EsObject();
       message.setString(TAG_ACTION, TAG_TIME_LEFT);
       message.setInteger(TAG_TIME_LEFT, nextEvent - tickerCount);
       getApi().sendPluginMessageToRoom(getApi().getZoneId(), 
               getApi().getRoomId(), message);
   }

startCountdown

To make this cleaner, let's have the countdown state cancel any ticker and start a new one. We use 5000 for the duration on the callback, so that it runs every 5 seconds, and -1 for the number of times to run it, so that it runs until we cancel it. Our notes above outlined the type of message we want to broadcast.

   private void startCountdown() {
       getApi().cancelScheduledExecution(callBackId);            
       gameState = STATE_COUNTING_DOWN;
       tickerCount = 0;
       nextEvent = countdownDurationInSeconds;
   
       // tell players to start the countdown
       EsObject message = new EsObject();
       message.setString(TAG_ACTION, TAG_ACTION_COUNTDOWN);
       message.setInteger(TAG_TIME_LEFT, nextEvent - tickerCount);
       getApi().sendPluginMessageToRoom(getApi().getZoneId(), 
               getApi().getRoomId(), message);
   
       // start the ticker
       callBackId = getApi().scheduleExecution(5000,
               -1,
               new ScheduledCallback() {
                   public void scheduledCallback() {
                       tick();
                   }
                   });
   }

startGame

There are several things we need to do when a round starts:

  • Lock the game so no other players can join until this round ends.
  • Wipe the playerGuesses map.
  • Generate the secret number.
  • Broadcast a message so players know the game has started.
   private void startGame() {
       // lock the game
       getApi().setGameLockState(true);
          
       //  Wipe the playerGuesses map
       playerGuesses = new ConcurrentHashMap<String, Integer>();
          
       //  Generate the secret number
       Random rnd = new Random();
       int range = maximumSecretNumber - minimumSecretNumber + 1;
       secretNumber = minimumSecretNumber + rnd.nextInt(range);
   
       //  Broadcast a message so players know the game has started
       gameState = STATE_IN_PLAY;
       nextEvent = roundDurationInSeconds + tickerCount;
       EsObject message = new EsObject();
       message.setString(TAG_ACTION, TAG_ACTION_START);
       message.setInteger(TAG_TIME_LEFT, roundDurationInSeconds);
       getApi().sendPluginMessageToRoom(getApi().getZoneId(), 
               getApi().getRoomId(), message);
   }

endGame

What does the endGame method need to do?

  • Update the game state, so no more guesses are accepted.
  • If only one player, new state should be WAITING, if at least 2, GAME_OVER. If at least 2 players, set the nextEvent, otherwise cancel the callback.
  • Unlock the game, so more players can join.
  • Determine winner(s).
  • Broadcast the STOP message, with the guesses, as noted above.
   private void endGame() {
       gameState = STATE_GAME_OVER;
       if (numberOfPlayers < 2) {
           gameState = STATE_WAITING;
           getApi().cancelScheduledExecution(callBackId);
       } else {
           nextEvent = pauseAfterDurationInSeconds + tickerCount;
       }
           
       // unlock the game
       getApi().setGameLockState(false);
           
       determineWinners();
   }
   private void determineWinners() {
       // determine the closest error
       int closestError = Integer.MAX_VALUE;
       for (Enumeration<Integer> e = playerGuesses.elements();
               e.hasMoreElements();) {
           int thisGuess = e.nextElement();
           int thisError = Math.abs(thisGuess - secretNumber);
           if (thisError < closestError) {
               closestError = thisError;
           }
       }
          
       // build the winners array
       Vector<EsObject> playerInfoList = new Vector<EsObject>();
       for (Enumeration<String> e = playerGuesses.keys();
               e.hasMoreElements();) {
           String thisPlayer = e.nextElement();
           int thisGuess = playerGuesses.get(thisPlayer);
           int thisError = Math.abs(thisGuess - secretNumber);
           EsObject playerInfo = new EsObject();
           playerInfo.setString(TAG_NAME, thisPlayer);
           playerInfo.setInteger(TAG_GUESS, thisGuess);
           playerInfo.setBoolean(TAG_WINNER, thisError <= closestError);
           playerInfoList.addElement(playerInfo);
       }
       EsObject[] playerInfoListArray = new EsObject[playerInfoList.size()];
       playerInfoListArray = playerInfoList.toArray(playerInfoListArray);
          
       // build the message and broadcast it
       EsObject message = new EsObject();
       message.setString(TAG_ACTION, TAG_ACTION_STOP);
       message.setInteger(TAG_TIME_LEFT, nextEvent - tickerCount);
       message.setEsObjectArray(TAG_PLAYERS, playerInfoListArray);
       message.setInteger(TAG_SECRET, secretNumber);
       getApi().sendPluginMessageToRoom(getApi().getZoneId(), 
               getApi().getRoomId(), message);
   }

Full game plugin source

You can download the full source of the game plugin.

GMSInitializer

The next step is to initialize the game. We can either override BasePlugin's init method, or we can create a separate GMSInitializer class. Since we probably want to eventually have several games, the separate GMSInitializer class makes more sense. Follow the instructions in GMSInitializer and Deploy the Extension.

The full source of GMSInitializer and Extension.xml is also available.

Finished!

Congratulations! You have finished making a game plugin!

A game's not a game unless you can play it, and just because it compiles doesn't mean there aren't any bugs. For complicated games, it is best to build the plugin and client in parallel, and install the extension early so you can test frequently.

Building the game client is a separate article: Game Tutorial: Guess the Number Client

Personal tools
download