5.9 Tutorials: Board game (part 2)
The source FLA of this example is available for both Flash MX and MX2004 under the Examples/(Actionscript version)/smartFoxTris folder. |
» Introduction
SmartFoxTris is a complete mutiplayer version of the famous "Tic-Tac-Toe" game. The related source file for this article is found in the "Examples/smartFoxTris/" folder under the name of "smartFoxTris.fla". In the first part of this tutorial we have introduced a number of features, among which the "Room variables".
In this second part we'll analyze the gameplay and finish this first "turn-based" game.
» Ready to play
Open the actionscript panel (F9) to analyze the code in the "game" label. The first lines of code are mostly initialization stuff:
inGame = true moveCount = 0 _global.gameStarted = false _global.whoseTurn = 1
» _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 set the opponentID variable:
_root["player" + _global.myID].name.text = _global.myName opponentID = (_global.myID == 1) ? 2 : 1
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
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)
Here is where we notify our arrival into the room, and also where we can control the game flow.
To understand why, you have to 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 check: 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.
doc index |