[SmartFox] Board game (part 2)
[ August 10, 2004 ] by Marco Lapi, a.k.a Lapo
Article 10: a step by step guide on how to build a turn-based multiplayer game (part 2)


[ READY TO PLAY? ]
Now we can analyze the code in the "game" label. Please open the actionscript panel (F9).
The first lines of code are mostly initialization stuff:

inGame = true
moveCount = 0
_global.gameStarted = false
_global.whoseTurn = 1
 

The inGame flag is finally set, then the moveCount counter is initialized together with two more _global vars:

_global.gameStarted: a flag that tells us if the game is started
_global.whoseTurn: a number telling whose player turn is


The code below shows the player name on screen and the set the opponentID variable:

_root["player" + _global.myID].name.text = _global.myName
opponentID = (_global.myID == 1) ? 2 : 1

As you may have noticed the application can dynamically behave like if it was player one or player two.
This is achieved using the _global.myID variable which in turn is a copy of the smartfox.playerId variable: by knowing which player is currently playing we can easily play both sides.

The line where we set the opponentID may look a little tricky if you're not used to this type of syntax, however the same line
could have been written like this:

if (_global.myID == 1)
opponentID = 2
else
opponentID = 1
 

It's a 4 line block of code compressed into a single line!

We've already commented the following lines: here the room variable for the player is created.

vObj = new Array()
vObj.push({name:"player" + _global.myID, val:_global.myName})
 
smartfox.setRoomVariables(vObj)

This is where we notify our arrival to the room and this is also the mechanism that will enable us to control the
game flow.

To see why, please check the onRoomVariablesUpdate handler:

smartfox.onRoomVariablesUpdate = function(roomObj)
{
if (inGame)
{
var rVars = roomObj.variables
if (rVars["player1"].length > 0 && rVars["player2"].length > 0)
{
if (!_global.gameStarted)
{
_global.gameStarted = true
hideWindow("gameMessage")
_root["player" + opponentID].name.text = rVars["player" + opponentID]
_global.whoseTurn = 1
waitMove()
}
}
else
{
// Reset game status
_global.gameStarted = false
resetGameBoard()
moveCount = 0
var win = showWindow("gameMessage")
win.message_txt.text = "Waiting for player " + ((_global.myID == 1) ? "2" : "1") + newline + newline + "press [cancel] to leave the game"
}
}
}


The code above handles three possible cases:

1) We are the first user to enter the room, so we need to wait for our opponent
2) We are playing the game but our opponent leaves the room or gets disconnected
3) The second player has entered and we can start the game.

If only one player variable exists in the room we will show a small dialog box that will tell the user to wait his/her opponent.
This will also hanlde the case in which one of the two players exits from the room during the game. Also the game board is cleared and the moveCount is set back to zero.

If both player variables exist in the room we can set the _global.gameStarted to true, remove the dialog box and initialize the game. Player one will always move first in the first game, then the first move will be done by the client who won the previous game.

The control is then passed to the waitMove() method:

function waitMove()
{
var msg = (_global.whoseTurn == _global.myID) ? "It's your turn" : "It's your opponent turn"
_root.turn.text = msg
}

The simple code above shows the turn message based on the player Id and then the application will wait for user interaction.

When a user clicks on one of the squares in the game board this code is executed:

on (release)
{
if (gameStarted)
{
if (_global.myID == _global.whoseTurn)
{
if (this.status != "R" && this.status != "G")
{
var stat = (_global.myColor == "red") ? "R" : "G"
this.status = stat
this.ball.gotoAndStop(_global.myColor)
_root.moveDone(this)
}
}
}
}
 

We have three nested if(s) to see if the game is started, if it's the user's turn and finally if the clicked item was not yet taken.

Each square has a "status" property that can have 3 different values:

undefined : if never clicked before
"R" : if it contains a red ball
"G" : if it contains a green ball

If the board cell is free we set it to the user color and invoke the moveDone() method in the main timeline passing a reference
to the square movieclip.

The moveDone function uses the sendObject API method to send the move data to our opponent:

var x = tile._name.substr(3,1)
var y = tile._name.substr(5,1)
 
smartfox.sendObject({type:"move", x:x, y:y})
 
moveCount++
 
checkBoard()
nextTurn()
 

The x and y vars are extracted from the _name String property: as you may remember each square has an instance name like "sq_x_y" By respectively taking the 3rd and 5th char from the instance name we obtain the x and y values.

These values are then passed to the sendObject function together with a property called "type" that can have two values:

"move", when we're sending a game board move
"restart" when we're restarting the game at the end of it.

After each move is peformed the checkBoard() function will loop through the board to see if there's a winner and in such case it will
stop the game showing the win/lose message. We'll take a closer look to it later.

If no winner is found the nextTurn() function is called:

function nextTurn()
{
_global.whoseTurn = (_global.whoseTurn == _global.myID) ? ((_global.myID == 1) ? 2:1) : _global.myID
waitMove()
}
 


What this does is pretty strightforward: the whoseTurn variable is inverted and then we go back to the waitMove()

At this point the game flow should be clear:

1) Wait for player move
2) Check if there's a winner
3) If no winner is found switch the active player and go back to 1 else the game is over

Now that we've seen how the player move is sent to the opponent is time to check the code that handles the reception
of a move from the other player(s):

smartfox.onObjectReceived = function(o)
{
if (inGame)
{
// we received a move from the opponent
if (o.type == "move")
{
// Visualize opponent move
var tile = "sq_" + o.x + "_" + o.y
var ballColor = (_global.myID == 1) ? "red" : "green"
 
board[tile].ball.gotoAndStop(ballColor)
board[tile].status = ballColor.substr(0,1).toUpperCase()
 
moveCount++
 
checkBoard()
nextTurn()
}
else if (o.type == "restart")
{
restartGame()
}
}
}


If the "type" property is set to "move" we have to display the opponent move in our board to keep all client's boards in sync.

The "tile" variable represents the movieclip name of the cell in the board where the client clicked and the "ballColor" is found
by assigning the opposite color to the one we're using.

The status property is set by taking the first uppercase char in the ballColor variable ("R" or "G") then the moveCount is incremented
and the checkBoard() and nextTurn() methods are called, just like we did previously.

You will be able to send a "restart" command when the game finishes and you will be presented a dialog box were you can return
in the main chat area or continue playing.

The restartGame() method will clear all current game values and start a new game:

function restartGame()
{
hideWindow("gameEnd")
 
resetGameBoard()
moveCount = 0
 
_global.gameStarted = true
 
nextTurn()
}


Now that we've described the flow of the application, we can take a closer look to the checkBoard() function:
function checkBoard()
{
 
var solution = []
// All Rows
for (var i = 1; i < 4; i++)
{
solution.push(board["sq_1_" + i].status + board["sq_2_" + i].status + board["sq_3_" + i].status)
}
// All Columns
for (var i = 1; i < 4; i++)
{
solution.push(board["sq_" + i + "_1"].status + board["sq_" + i + "_2"].status + board["sq_" + i + "_3"].status)
}
// Diagonals
solution.push(board["sq_1_1"].status + board["sq_2_2"].status + board["sq_3_3"].status)
solution.push(board["sq_1_3"].status + board["sq_2_2"].status + board["sq_3_1"].status)
 
var winner = null
 
for (var i in solution)
{
var st = solution.pop()
if (st == "RRR")
{
winner = "red"
break
}
else if (st == "GGG")
{
winner = "green"
break
}
}
// TIE !!!
if (winner == null && moveCount == 9)
{
var win = showWindow("gameEnd")
opaqueCover._visible = true
win.message_txt.text = "Tie !"
}
else if (winner != null)
{
// There is a winner !
_global.gameStarted = false
var win = showWindow("gameEnd")
opaqueCover._visible = true
if (_global.myColor == winner)
{
// I WON! In the next match, it will be my turn first
var message = "You WIN !"
_global.whoseTurn = _global.myID
}
else
{
// I LOST! Next match i will not move first
var message = "You LOOSE !"
_global.whoseTurn = (_global.myID == 1) ? 2 : 1
}
win.message_txt.text = message
}
 
}

Even if there is a lot of code the function works in a very simple way: it creates an empty array called "solutions" and fills it with
all possible rows and columns where you can put three items in a row.

The available solutions are 8 in total: 3 columns + 3 rows + 2 diagonals.

When the array is populated we loop through it and if one combinantion of three is found then we have a winner! Also we check if
there's no more moves available. In that case we'll have a tie. When the game ends the "gameEnd" window is shown and you will be able to start a new game or just leave the room.

Leaving the room is done by calling the quitGame() method which in turn will call the "leaveGameRoom" function
function leaveGameRoom()
{
inGame = false
gotoAndStop("chat")
smartfox.getRoomList()
}

The inGame flag is put back to "false" and we're sent back to the chat label. The last line of code will refresh the roomList in the current zone and automatically join us in the main chat room called "The Entrance"


[ CONCLUSIONS ]
We have analyzed some of the techniques for building a simple multiplayer turn-based game and what you have learned so far can be applied to many different types of games, not just board ones.
Also the limit of two users in very game room can be expanded for games with 4, 6, 8 or more players.

As usual we'd like to suggest you to analyze the code samples and experiment your own variations, and then in a few time you'll be able to create your own multiplayer games from scratch!

Have fun! :-)

Lapo


    
 
 
Name: Marco Lapi, a.k.a Lapo
Location: Fossano, Italy
Age: 34
Flash experience: started out with Flash 4 back in 1999
Job: web designer/developer
Website: http://www.gotoandplay.it/
 
 
| Homepage | News | Games | Articles | Multiplayer Central | Reviews | Spotlight | Forums | Info | Links | Contact us | Advertise | Credits |

| www.smartfoxserver.com | www.gotoandplay.biz | www.openspace-engine.com |

gotoAndPlay() v 3.0.0 -- (c)2003-2008 gotoAndPlay() Team -- P.IVA 03121770048