TicTacToe Tutorial: Server
From EsWiki
This tutorial covers the creation of the game Tic-Tac-Toe.
What You'll Learn
- How to put together all you have learned, to make a game of Tic-Tac-Toe
Prerequisites
- You must have ElectroServer 4 installed and running.
- You need the client-side ElectroServer 4 API.
- This tutorial comes after the Guess The Number game tutorial, so it is recommended you complete that first.
Download
If you wish to simply download the entire set of TicTacToe files (both server and client, source code and compiled), it is found in TicTacToeTutorial.zip.
Let's Get Started
The following steps will show you how to make a game of tic-tac-toe.
The Plan
The first and most important step in making any multiplayer game (or likely any game), is to plan out the game. In tic-tac-toe, the main pieces are:
- Game is intialized (Two players in room) and a team is assigned to each player (in this case X or O)
- One player takes a turn, while the other waits for that move. Once he has moved, the other player moves
- If any player gets three in a row on the board, they are the winner
- If no one has three in a row, and all the positions are full, the game is a tie
So this is an outline of what will happen in our game. Our next step is decide what is handled by the server and what by the client and when data needs to be sent back and forth.
Deciding Where to put the Logic
We can now use our list from before to easily see what data needs to be transferred and accounted for. When we look at the list, we see that the only data that needs to be sent to the server would be when a player joins (so it knows the game can begin), and what moves the player makes. But since the plugin we will make gets the room enter and exit messages without the client sending it explicitly, we don't have to worry about it on the client side.
Note: In this example, nearly all the logic of the game is on the server, but this may not always be the case.
So the messages being sent by the client to the server will be:
- The move the player decides to make in tic-tac-toe
In this case, the client will only be sending one type of message to the server, but in other games it is likely they will send a few different message types (eg the client could send where his mouse is also).
The way we are setting up the game is such that the server does all the thinking and sends the results to clients. Thus, we are going to need quite a few different messages that will be sent from the server to the clients.
The messages being sent by the server to the client will be:
- The game is ready to begin, set up the board
- It is now the given players turn (depending on who just took a turn)
- The board has been changed, a marker has been placed
- The game is over (including whether it was a win, tie or a loss)
As you can see, the server has many more messages it will send to the players because it is controlling the logic of the game. Now we are ready to begin coding the plugin we will use.
Setting up the Main Class
We are going to be coding the server plugin in Java.
Open up the project you have been using (for GuessTheNumber etc..) and a new package called com.electrotank.electroserver4.examples.tictactoe. Add a new class to it called Main.
If you are using NetBeans, open the project tree on the top left of the screen. Right-click on Source Packages and select New-> Java Package. Name it com.electrotank.electroserver4.examples.tictactoe . Then right click on that package in the tree and select New -> Java Class. Name it Main.
It is a good time now to add all the imports we will need when making the plugin. Copy and paste this after the line package com.electrotank.electroserver4.examples.tictactoe.
import com.electrotank.electroserver4.extensions.ChainAction; import com.electrotank.electroserver4.extensions.api.ScheduledCallback; import com.electrotank.electroserver4.extensions.api.PluginApi; import com.electrotank.electroserver4.extensions.api.value.EsObject; import com.electrotank.electroserver4.extensions.api.value.EsObjectRO; import com.electrotank.electroserver4.extensions.api.value.UserEnterContext; import com.electrotank.electroserver4.extensions.api.value.UserPublicMessageContext; import com.electrotank.electroserver4.extensions.BasePlugin;
Now we are ready to setup the outline of the class.
Defining the Variables
Since this class will be used as a plugin, it should extend BasePlugin, so add extends BasePlugin after public class Main. Note: If the class was auto-generated for you in NetBeans, you can remove the function called "main", as we won't be using it.
The server is going to have to keep track of nearly all of the data in the game, including how many players are in the game, the current game state, the names of the players, which player is which team and the current board layout.
Copy and paste this within the class definition.
private int m_numPlayers; private int m_gameState; private String [] m_playerNames; private int [] m_playerTeamID; private int [][] m_board; private int m_turnCounter;
The server also keeps track of how many turns have passed so it can know when the game should end and whose turn it is.
We will also need the following variables to simplify the communication between the server and the client, and for readability.
public static final String TAG_TILEX = "tilex"; public static final String TAG_TILEY = "tiley"; public static final String TAG_TEAMID = "teamID"; public static final String TAG_PLAYERNAME = "playername"; public static final String TAG_MESSAGETYPE = "messagetype"; public static final String TAG_ISTIE = "istie"; public static final int ACTION_TAKETURN = 0; public static final int ACTION_GAME_OVER = 1; public static final int ACTION_UPDATEBOARD = 2; public static final int ACTION_SETUPGAME = 3; public static final int STATE_WAITING = 0; public static final int STATE_IN_PLAY = 1; public static final int STATE_GAME_OVER = 2; public static final int TEAM_X = 1; public static final int TEAM_O = 2;
The different TAG variables are the different properties we will use on the EsObjects the messages will contain. The ACTION variables
are the different types of messages that will be sent. The STATE variables store the possible game states and the TEAM variables store the different teams.
Initializing the Plugin
We will now go linearly through the server code, so we can easily figure out what needs to be done when.
The first thing that occurs is that the plugin is initialized, where we will need to setup default values and initialize arrays.
@Override public void init( EsObjectRO info ) { m_numPlayers = 0; m_gameState = STATE_WAITING; m_playerNames = new String [2]; m_playerTeamID = new int [2]; m_board = new int [3][3]; }
In this function, the default values are set for most variables and the arrays are initialized. This function is called automatically each time the plugin is created ( ie each time a new game room is made ). It should be noted that the EsObjectRO that you see as a parameter can be used to setup default or initial values for a game, but that is not covered is this tutorial.
We also need to add the following:
@Override
public void setApi(PluginApi api) {
this.api = api;
}
@Override
public PluginApi getApi() {
return api;
}
These functions are used later to be able to send message from the plugin.
Users Entering the Game
The next thing we need to handle is when players enter the room so we know when the game should start. In this example, once there are two people in the room, the game begins. The BasePlugin class, which our class extends, has a userEnter function which we will override in order to keep track of the players.
@Override
public ChainAction userEnter(UserEnterContext context)
{
if( m_numPlayers < 2)
{
m_playerNames[ m_numPlayers ] = context.getUserName();
m_numPlayers ++;
//If there are less than two players let the player in
AttemptToStartGame();
return ChainAction.OkAndContinue;
}
}
The above code makes sure there are less than two players in the game, and if so, adds the new player to the game.
The name is then stored in the appropriate spot in the array. Also a function is called to try to start the
game, which we will look at now.
Note: The code does not currently prohibit more than two players, but they are not involved in the game.
public void AttemptToStartGame()
{
if(m_numPlayers == 2 && m_gameState == STATE_WAITING)
{
StartGame();
}
}
The AtemptToStartGame function makes sure there are two players in the game and that the game in a state of waiting. If so, the game begins.
Starting the Game
When we start the game, we will need to reset (or intially set up, if this is the first game ) the board for both the server and the clients. The server can directly reset its own board, while a message must be sent to the clients telling them to set up the game. Another thing that is done here is tell each player which team they are on. A similar message is sent to each player, except that the team value is changed.
private void StartGame()
{
m_gameState = STATE_IN_PLAY;
ResetBoard();
//An EsObject is created, then the property TAG_TEAMID
//is set to TEAM_X
//Also, the property TAG_MESSAGETYPE is set to ACTION_SETUPGAME
//The TAG_MESSAGETYPE allows the client to know what type of message it is
//And it uses the TAG_TEAMID to store which team it is
EsObject message = new EsObject();
message.setInteger( TAG_TEAMID, TEAM_X );
message.setInteger( TAG_MESSAGETYPE, ACTION_SETUPGAME);
//Send a message to the players
m_playerTeamID[0] = TEAM_X;
getApi().sendPluginMessageToUser( m_playerNames[0], message);
m_playerTeamID[1] = TEAM_O;
//Changes the values to the other team, and sends it to the other player
message.setInteger( TAG_TEAMID, TEAM_O );
getApi().sendPluginMessageToUser( m_playerNames[1], message);
ChangeTurns();
}
private void ResetBoard()
{
m_turnCounter = 0;
int i = -1;
int j = -1;
while( ++i< 3)
{
j = -1;
while( ++j < 3)
{
m_board[i][j] = 0;
}
}
}
The game state is also set to play and the teamID's of the players is stored on the server. The ResetBoard
function sets all of the board values to 0, a blank board.
The ChangeTurns function is the same that is called when a turn is done, so this sends a message to the players as to whose turn it is.
private void ChangeTurns()
{
EsObject message = new EsObject();
message.setString( TAG_PLAYERNAME, m_playerNames[m_turnCounter%2]);
message.setInteger( TAG_MESSAGETYPE, ACTION_TAKETURN);
//Send a message to the players
m_turnCounter++;
getApi().sendPluginMessageToRoom(getApi().getZoneId(), getApi().getRoomId(), message);
}
A message is sent out with the action as take turn and the parameter as the player's name whose turn it is.
The client will see if it is their name. If it is not, it is not their turn.
The above code is now enough on the server side of things to handle the creation of the game. Congratulations, you finished a major segment.
Now on to handling when the player sends a message to the server.
Handling Client to Server Messages
We have already figured out what the client will be sending to the server, just the moves taken. Whenever a client sends a message to a plugin, the request function is called on that plugin. The request function is given the players name, and the message as an EsObjectRO (read-only EsObject).
In this case, since there is only one message sent to the server directly from the client, we do not have to worry about what kind of message type it is.
@Override
public final void request( String playerName, EsObjectRO requestParameters)
{
//Validates what we want is likely in the object
if( requestParameters.variableExists(TAG_TILEX) )
{
//Get the name of whose turn it is
String currentPlayerName = m_playerNames[m_turnCounter%2];
//Make sure the game is playing and that the correct player is sending the information
if(m_gameState == STATE_IN_PLAY && playerName.equals( currentPlayerName ))
{
int tileX = requestParameters.getInteger(TAG_TILEX);
int tileY = requestParameters.getInteger(TAG_TILEY);
int teamID = requestParameters.getInteger(TAG_TEAMID);
TurnTaken( tileX, tileY, teamID );
}
}
}
We first make sure this message is what we expect, and then make sure the game is playing and the current player whose turn it is sent the message. Finally, the TurnTaken function is called to update the board for the server and clients.
Taking a Turn
The TurnTaken function will update both the board on the server, and send a message to players to also update their own local boards. Note: In this game, the player will send a message to the server that they have done a move, but they will not update their board until the server tells them to
public void TurnTaken( int tileX, int tileY, int teamID )
{
//Update the board on the server
//But make sure the tile isn't already chosen by a player
if(m_board[tileX][tileY] == 0)
{
m_board[tileX][tileY] = teamID;
//Send this update to all players
EsObject message = new EsObject();
message.setInteger( TAG_MESSAGETYPE, ACTION_UPDATEBOARD );
message.setInteger( TAG_TILEX, tileX);
message.setInteger( TAG_TILEY, tileY);
message.setInteger( TAG_TEAMID, teamID);
getApi().sendPluginMessageToRoom(getApi().getZoneId(), getApi().getRoomId(), message);
if(!CheckForCompletion())
{
ChangeTurns();
}
}
}
A message is sent to both players (all the players in the room) containing a message to update the board.
The game then checks to see if it is finished, but if not, just change the turns again.
We will now briefly look at the CheckForCompletion function.
Checking for Victory
The CheckForCompletion function will check to see if any team has won. If a team has won, it finds out the name of the player that is on that team, and then calls the PlayerWon function with their name. If no one has won, and 9 moves have been made, the game is a tie. Otherwise, the game continues.
We won't go into much detail about the HasTeamWon and IsLineComplete functions, but basically the IsLineComplete function checks to see if any given line on the board is complete, and the HasTeamWon function checks all 8 possible lines.
public boolean CheckForCompletion()
{
//Check to see if anyone has three in a row
//If so, send a message to the room saying which player won
if( HasTeamWon( TEAM_X ))
{
//Get the name of the player
String playerName;
if( TEAM_X == m_playerTeamID[0])
{
playerName = m_playerNames[0];
}
else
{
playerName = m_playerNames[1];
}
PlayerWon( playerName );
return true;
}
else if( HasTeamWon( TEAM_O ))
{
//Get the name of the player
String playerName;
if( TEAM_O == m_playerTeamID[0])
{
playerName = m_playerNames[0];
}
else
{
playerName = m_playerNames[1];
}
PlayerWon( playerName );
return true;
}
else if( m_turnCounter >= 9 )
{
//One 9 turns have passed, game is tied, since we know neither player has won if it gets here
Tie();
return true;
}
return false;
}
public boolean IsLineComplete( int x, int y, int xChange, int yChange, int teamID )
{
//Returns true if a whole line in tic-tac-toe is controlled by the one team
if( (m_board[x][y] == teamID) && (m_board[x+xChange][y+yChange] == teamID) && (m_board[x+2*xChange][y+2*yChange] == teamID) )
{
return true;
}
else
{
return false;
}
}
public boolean HasTeamWon( int teamID )
{
if( IsLineComplete(0,0,1,0, teamID ) || IsLineComplete(0,1,1,0, teamID ) || IsLineComplete(0,2,1,0, teamID ) )
{
//Check horizontal rows
return true;
}
else if( IsLineComplete(0,0,0,1, teamID ) || IsLineComplete(1,0,0,1, teamID ) || IsLineComplete(2,0,0,1, teamID ) )
{
//Check vertical rows
return true;
}
else if( IsLineComplete(0,0,1,1, teamID ) || IsLineComplete(0,2,1,-1, teamID ) )
{
//Check diagonals
return true;
}
else
{
return false;
}
}
So if the game is over, either the PlayerWon or Tie function will have been called, which we will look at next.
Sending the Results
In the PlayerWon function a message is sent out to both players containing the name of the player who won. The action type is game over which is shared with the Tie function, which is why the parameter TAG_ISTIE must be set to false. The WaitThenReset function will cause the game to wait for 5 seconds, and then attempt to start another game (and will unless someone leaves). The game state on the server is also set to game over.
public void PlayerWon( String playerName )
{
m_gameState = STATE_GAME_OVER;
EsObject message = new EsObject();
message.setString( TAG_PLAYERNAME, playerName );
message.setBoolean( TAG_ISTIE, false );
message.setInteger( TAG_MESSAGETYPE, ACTION_GAME_OVER);
//Send a message to the players
getApi().sendPluginMessageToRoom(getApi().getZoneId(), getApi().getRoomId(), message);
//Start a timer to reset the game in 5 seconds, and will only run once
WaitThenReset();
}
In the Tie function, the game state is also set to game over and this time the TAG_ISTIE parameter is set to true.
Note that the TAG_PLAYERNAME parameter is not needed, since the client won't attempt to access it unless the game is not a tie.
As in the PlayerWon function, the message is sent to both players, and the game will attempt to restart in the WaitThenReset function.
public void Tie()
{
m_gameState = STATE_GAME_OVER;
//Send to the players that the game is a tie
EsObject message = new EsObject();
message.setInteger( TAG_MESSAGETYPE, ACTION_GAME_OVER);
message.setBoolean( TAG_ISTIE, true );
//Send a message to the players
getApi().sendPluginMessageToRoom(getApi().getZoneId(), getApi().getRoomId(), message);
//Start a timer to reset the game in 5 seconds, and will only run once
WaitThenReset();
}
The WaitThenReset function uses a server side command to have a function called in 5000 milliseconds (or 5 seconds).
The second parameter means it will only be called once, and the third is ScheduledCallback object, which is created
to just call the SetGameToWaiting function.
The SetGameToWaiting function sets the game state to waiting (which is necessary to start a game), and attempts to start a new game again.
private void WaitThenReset()
{
getApi().scheduleExecution
( 5000, 1,
new ScheduledCallback()
{
public void scheduledCallback()
{
SetGameToWaiting();
}
}
);
}
private void SetGameToWaiting()
{
m_gameState = STATE_WAITING;
AttemptToStartGame();
}
Congratulations, you have finished the main part of the game code on the server side!
Users exiting
The last thing we will want to handle is if a player leaves while the game is running, or even when the game is waiting to restart.
Like the userEnter function, the userExit function is called when a user exits the game. The name of the user is passed into the function.
We first decrease the number of players, and then if the player was the first player in the array, we shift the second player into his position so a new player can fill the second position in the userEnter function.
Then, if the game is currently in play, we say whoever did not exit is the winner.
@Override
public void userExit(String userName)
{
m_numPlayers--;
if( userName.equals( m_playerNames[0]))
{
//Fix for how the players are sorted on reentry
//Allows a new game
//Since the new player that join will become m_playerNames[1], save the player who is still in the game
m_playerNames[0] = m_playerNames[1];
}
if( m_gameState == STATE_IN_PLAY )
{
//If a player exits while playing, the other play wins
String winnersName;
if( userName.equals( m_playerNames[0] ) )
{
winnersName = m_playerNames[1];
}
else
{
winnersName = m_playerNames[0];
}
PlayerWon( winnersName );
}
}
You will need to edit the GMSInitializer class to add the game type TicTacToe, as you needed to do for Guess The Number. Set the capacity for the game room to 2.
Finished
Well done! You are now complete the Tic-Tac-Toe server side tutorial. Continue on to the TicTacToe Tutorial: Client to complete the game.
