App Data Game Store Our game store in app data will consist of some JSON objects in an array.We’ll build onsome of the code shown previously in the book for using the app data store.This
Trang 14 Immediately after the opponentPickedfunction, add the following
loadFriendPickerfunction This function makes use of the widgetBootstrapper object to initialize the FriendPicker widget The Bootstrapper allows
us to safely create a widget from a dynamically included script library.The call to
createWidgettakes three parameters: the object name of the widget being created, an initializer function to fire when the widget has been created, and anobject of parameters to pass into the widget at initialization In this case, we’repassing in the ID of our target div element, a callback function to executewhenever a friend is picked, and a flag to build the “Selected User” user interface element
function loadFriendPicker(){
MyOpenSpace.Widgets.Bootstrapper.createWidget(
"MyOpenSpace.Widgets.FriendPicker", function(p){
window.friendPicker = p;
}, { element: "opponentPicker", buildSelectedUI: true, friendClickAction: opponentPicked });
try{
initializeGame();
} catch(ex){}
loadFriendPicker();
}
Trang 2Warning
The call to initialize a FriendPicker must be made from a gadgets.util.
registerOnLoadHandler-triggered function If we try to initialize from an inline call, the initializer will fail in some browsers Not all browsers handle dynamic script loading in the same manner, so we must use the least common denominator behavior.
If we save and view our app, we’ll see the FriendPicker initialized below the currentviewer UI Clicking on the [Click for Recipient] item shown in Figure 7.4 brings upthe FriendPicker UI and allows the user to pick a friend
App Data Game Store
Our game store in app data will consist of some JSON objects in an array.We’ll build onsome of the code shown previously in the book for using the app data store.This timewe’ll clean things up a bit by placing all our code under the TTTnamespace, instead ofleaving a bunch of dangling functions hanging off the global window object
Storage JSON ObjectsThe first step is to create the GameInfostorage object In your script code, add the following code:
Figure 7.4 FriendPicker below player info.
Trang 3In earlier chapters we’ve shown some techniques by making use of globalfunctions While convenient for small projects, it is good practice to namespace yourscript code.We’re going to encapsulate all of this in a static object namespace of
TTT.AppDataPlay
Logging to Debug Apps
The gadgets spec provides for a logging mechanism similar to the one popularized by log4j under the gadgets.log call Logging is an incredibly useful way to debug an app.
It’s not always convenient to set breakpoints and single steps A log lets you dissect the execution sequence after the fact.
At the time of this writing, gadgets.log isn’t implemented in the MySpace container.
It’s simple to add, though In our case, we just want to write out to a scrolling div After development we can hide the div and leave the log calls in place Here’s how:
Trang 41 Create the basic object skeleton and add it into your script source.The code isshown here:
TTT.AppDataPlay = { }
2 Add a Keysobject to this namespace that will encapsulate all the app data keysused for game play:
}
}
3 Now we will add some empty object key definitions to hold our data Eventhough JavaScript allows for dynamic creation of variables, it is good practice toexplicitly declare and comment any variables that are used in multiple places Addthis code inside our TTT.AppDataPlay object:
Trang 5placehold-TTT.AppDataPlay = {
.
/**
* Retrieves the gameInfo object for the specified opponent.
* If one doesn't exist, a new gameInfo object is created.
*/
getCurrentGameObject: function(opponentId){},
/**
* Convenience method to save the data currently in
* this.myGameAppData to the backing app data store.
*/
updateStoredGameObject: function(){},
/**
* Loads the specified gameInfo object into
* window.currentGame and updates the board
Trang 6getGameMovesString: function(game){},
}
5 Add the methods for retrieval and saving of the Viewer’s app data.These methodsare adaptations of what we wrote in Chapter 4, Persisting Information.The onlyreal difference is that we include them in our TTT.AppDataPlaynamespace
TTT.AppDataPlay = {
/**
* Retrieve the current Viewer's app data.
* This is an upgrade of the method introduced in
* Chapter 4, Persisting Information.
*/
getMyAppData: function (){
var idparams = {};
idparams[opensocial.IdSpec.Field.USER_ID] = opensocial.IdSpec.PersonId.VIEWER;
idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 0;
idparams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.SELF;
var id = opensocial.newIdSpec(idparams);
var req = opensocial.newDataRequest();
req.add(
req.newFetchPersonAppDataRequest(id, "*"), TTT.AppDataPlay.Keys.AppDataGetKey);
req.send(function(data){
TTT.AppDataPlay.loadAppDataCallback(data, TTT.AppDataPlay.myGameAppData);
});
},
/**
* Sets an AppData key for the current Viewer
* @param {String} key
* @param {JSON object} value
*/
setAppDataKey: function (key, value) {
var req = opensocial.newDataRequest();
Trang 76 Referenced in the methods just shown is a general-purpose app data callbackhandler adapted from Chapter 4, Persisting Information Add the function
loadAppDataCallback in as well.This function places all the AppDatakeys andvalues in the response into the specified object hash In that way we make onlyone call for all the data
TTT.AppDataPlay = {
/**
* Callback wrapper method for loadAppData.
* This loads all the data into myLocalAppData
* and triggers the loadedCallback function, if specified
* @param {Object} data
* @param {Function} loadedCallback
var mydata = data.get(TTT.AppDataPlay.Keys.AppDataGetKey).getData();
//App data has an additional layer of indirection.
//We reassign mydata object to the data map object //Circumvent Viewer lookup by getting key
if(targetCollection == null){
gadgets.log("Bad collection in load callback");
Trang 8targetCollection = {};
} var key;
for(key in mydata){
targetCollection[key] = mydata[key];
} } if(loadedCallback && typeof(loadedCallback) == "function"){
loadedCallback(targetCollection);
} },
}
7 Finally, add in the function that retrieves a friend’s app data We touched on thisbriefly in Chapter 4, Persisting Information, but did not write the code forretrieving other people’s app data.You can read, but cannot write, your friend’sdata, provided the friend has the app installed as well The main differencesbetween this and Viewer are the idSpecused and the callback function
idparams[opensocial.IdSpec.Field.NETWORK_DISTANCE] = 1;
if(!friendId){
friendId = window.friendPicker.selectedFriend.getId();
} idparams[opensocial.IdSpec.Field.GROUP_ID] = friendId;
//If MySpace fixes the idSpec bug, use this /*
if(!friendId){
friendId = window.friendPicker.selectedFriend.getId();
} idparams[opensocial.IdSpec.Field.USER_ID] = friendId;
idparams[opensocial.IdSpec.Field.GROUP_ID] = opensocial.IdSpec.GroupId.SELF;
*/
Trang 9TTT.AppDataPlay.loadAppDataCallback(data, TTT.AppDataPlay.myOpponentAppData, TTT.AppDataPlay.selectedOpponentDataCallback);
});
},
}
Warning
You may notice a commented-out block of code within this function At the time of this writing, MySpace had an inconsistency in its interpretation of the idSpec object from how other OpenSocial implementations interpret the same object MySpace requires the user ID
to always be VIEWER and the target friend ID to be placed in the GROUP_ID field Other implementations put the target friend ID in the user ID field and use the reserved enum value opensocial.IdSpec.GroupId.SELF for the GROUP_ID value.
8 Stub out the callback function TTT.AppDataPlay.
selectedOpponentDataCallback.This function holds much of the logic forgame management For now, we’re just going to stub it out and add a log state-ment so our code will run
}
Trang 10At this point, we have the skeleton of the AppDataPlayobject built and the utilitymethods required for communicating with the app data store of both players
Supporting Person-to-Person Game Play
Thus far, our actual game engine (TTT.Game) has supported only human-versus-computergame play.We need to make a few tweaks to allow for dual-mode play (person-to-person,
or P2P) In our game engine, we’ll always consider our opponent to be “Computer,”
even when the opponent is another person.This saves us from a broader rewrite of the display logic and game engine A simple flag value will give us enough information todifferentiate computer from human opponents
Adding P2P Game Play Support in the Game Engine
The first step is to make a few minor modifications to the original Tic-Tac-Toegame engine in order to support a human opponent in a turn-based system Wemust change the “made a move” trigger to store data and wait instead of triggering
a computer move We also now need to handle cases when the game ends in adraw
1 Add a property in the TTT.Gameobject to flag the game as being against a humanopponent.This will be used in the game logic to trigger a computer move ortrigger an app data store update
var TTT = { Game: function(){
2 Add a new method to the TTT.Gameprototype object to test for a draw Inhuman-versus-computer play we didn’t need to test for a draw When no squareswere available, there were no moves to make.With a human opponent we need to
be able to identify the end of a game when there is no winner Search for the
lookForWinfunction and add the isADrawfunction immediately below
Trang 11var openCell = false;
for(var i=0; i < 9; i++){
if(!this.moves[i]){
openCell = true;
break;
} } return (!openCell);
},
3 Edit the game play event handler makePlayerMoveto support P2P game play
Previously, this function would trigger a computer move if a winner was not discovered.We need to update this ifstatement with an elseand include a checkforisHumanOpponent
function makePlayerMove(e){
if(!currentGame.hasWinner && !currentGame.isHumanOpponent){
window.setTimeout(function(){
currentGame.makeComputerMove();
}, 500);
} else{
} }
4 Add the game logic for taking a turn within the elsestatement in the
makePlayerMovefunction from the preceding step.You will notice some newmethods on the TTT.AppDataPlayobject.We’ll fill in those methods in the nextsection
else{
// USER FEEDBACK MESSAGES HERE
var adGame = TTT.AppDataPlay.getCurrentGameObject(
TTT.AppDataPlay.currentOpponentId);
adGame.moveNumber = adGame.moveNumber+1;
if(currentGame.hasWinner){
msg = "You Win!";
Trang 12adGame.winner = window.viewer.getId();
} else{
msg += "<br />Waiting for your opponent";
}
adGame.boardLayout = TTT.AppDataPlay.getGameMovesString(currentGame);
//Wait for next move }
TTT.AppDataPlay.updateGameStatusMessage(msg);
}
Adding User Feedback
Feedback to the user is an important part of the game play In turn-based game play,sometimes it’s simply just not the Viewer’s turn.Without feedback the Viewer has theimpression that the game is broken
When our Tic-Tac-Toe game is in P2P game play mode instead of P2C to-computer), the computer takes on the role of commentator/narrator instead ofopponent Since this is a game, we like to add some sass to the commentary Feel free tocustomize the messages to your liking
(person-1 Above the table with an ID of gameboard, add a div element with an ID of
gameStatus.This is where the feedback messages are displayed
<div id="gameStatus"></div>
2 In the makePlayerMovefunction, add a check and associated message to short-circuit processing if it is not the Viewer’s turn to make a move Place this code immediately after the line where the var cell = statement
Trang 133 In the makePlayerMovefunction, add a random message generator within the
elseblock that is executed during P2P game play In our case we’ll create threemessages and use a modulo operation coupled with a timestamp to pick a message
in a quasi-random fashion.To the end user this will appear as a running tary by the computer
commen-var quotes = ["Nice Move!", " interesting strategy",
"What were you thinking?"];
var d = new Date();
var ndx = d.getMilliseconds() % 3;
var msg = quotes[ndx]; //Random quote
Modulo Magic
Modulo operations can be one of the most useful bits of programming pixie dust to put
in your bag of tricks A modulo operation calculates the remainder after a division tion between two numbers To illustrate, if we divide the number 5 by 2, it divides evenly into two parts with 1 left over The modulo (or “mod”) operation returns the 1.
opera-5/2 = 2 with remainder of 1,
or 5 mod 2 = 1
In JavaScript the statement would be written as 5 % 2.
By far the most common usage is for displaying rows with alternating colors This is accomplished with the following if statement:
for(var i=0; i<rows.length;i++){
There are many more uses for modulo operations Modulo is great any time you want
a quasi-random distribution of data All it takes is an incrementing counter (like a stamp) and the number of buckets to test for True random numbers are much more cumbersome and computationally expensive than a modulo operation.
time-The following code is the final code listing for makePlayerMove:
function makePlayerMove(e){
if(!e) var e = window.event;
var cell = (e.target) ?
Trang 14e.target.getAttribute("gamecell") : e.srcElement.getAttribute("gamecell");
var quotes = [
"Nice Move!",
" interesting strategy",
"What were you thinking?"];
var d = new Date();
msg += "<br />Waiting for your opponent";
}
adGame.boardLayout = TTT.AppDataPlay.getGameMovesString(currentGame);
Trang 15TTT.AppDataPlay.startGameplayPolling();
} TTT.AppDataPlay.updateStoredGameObject();
TTT.AppDataPlay.updateGameStatusMessage(msg);
}
}
Fleshing Out P2P Game Logic
We have now created all the underpinnings of the game play Now it’s time to glue it alltogether and implement our logic flows from the beginning of this chapter
Logic for Selecting an Opponent and Loading the GameSelecting an opponent and activating a game constitute the bulk of the workflowoutlined at the beginning of the chapter.The app has to get both the Viewer’s and theopponent’s game data, reconcile which version is the latest, and determine whose move
it is If there is no active game, a new one is created
Find the function selectedOpponentDataCallbackin the TTT.AppDataPlay
object namespace that we stubbed out earlier.This is a big function, so we will discusseach section of it in turn
The first section of this function retrieves the GameInfoobject for the two associatedplayers by examining all the games stored in the selected opponent’s app data store until
it finds one where the current Viewer’s ID matches the opponentIdvalue in the
GameInfoobject.This object is then assigned to the variable TTT.AppDataPlay.
myOpponentCurrentGame At the very end of this section the code block similarlyretrieves the GameInfoobject associated with this opponent from the Viewer’s localcopy of the app data store by calling getCurrentGameObject
if(!data){
return;
} //Since we're in a callback, "this" has lost scope var thisObj = TTT.AppDataPlay;
var myId = window.viewer.getId();
thisObj.myOpponentCurrentGame = null;
var opponentStoredGames = data[thisObj.Keys.activeGames];
if(opponentStoredGames != undefined && opponentStoredGames != null){
Trang 16var myGameInfo = thisObj.getCurrentGameObject(thisObj.currentOpponentId);
The second section of the selectedOpponentDataCallbackfunction reconcilesthe two GameInfoobjects It does this by identifying which object has the latest move
TheboardLayoutis then copied into the Viewer’s GameInfoobject to ensure that theViewer’s copy is the latest copy
latestGameInfo = myGameInfo;
}
Once the latestGameInfohas been determined, the code tests to see if a winner hasbeen declared.When a winner has been declared, the ID of the winning player is placed inthis field If there is no winner, this value is 0 (equivalent to false) If there is a winner,the board is loaded, a message is displayed to state who won, and the game is marked asfinished
If there isn’t a winner, the game is loaded into view via a call to the loadGamefunction(described later in this section) User feedback is generated to indicate whose turn it is
if(latestGameInfo.winner != 0){
if(latestGameInfo.winner == myId){
thisObj.updateGameStatusMessage("YOU WON!!!");
}
Trang 17else if(latestGameInfo.winner == -1){
thisObj.updateGameStatusMessage("ITS A DRAW!!!");
} else{
thisObj.updateGameStatusMessage("YOUR OPPONENT WON!!!");
} //debugger;
msg += "<br />It is your turn";
} else{
msg += "<br />Still waiting on opponent";
thisObj.startGameplayPolling();
}
The preceding code describes what occurs when a game is discovered to be inprogress.The second branch of this is what occurs when this is a new game or theopponent has not yet accepted the game In that instance the Viewer’s game is loadedand appropriate user feedback is displayed
} else{
latestGameInfo = myGameInfo thisObj.loadGame(latestGameInfo);
//Save the new game back if(latestGameInfo.moveNumber==0){
msg += "<br />New game started - make your move";
} else{
msg += "<br />Waiting for your opponent to move";
} } thisObj.updateGameStatusMessage(msg);
That finishes the selectedOpponentDataCallbackfunction.There were two otherfunctions touched on in the preceding description:getCurrentGameObjectand
loadGame Both methods are fairly simple
Trang 18The function getCurrentGameObjectretrieves the GameInfoobject from theViewer’s app data store for the associated opponent If there is no game in progress, anew GameInfoobject is initialized for this opponent and returned
/**
* Retrieves the gameInfo object for the specified opponent.
* If one doesn't exist, a new gameInfo object is created.
*
*/
getCurrentGameObject: function(opponentId){
var result = null;
if(this.myGameAppData && this.myGameAppData.activeGames != null){
var items = this.myGameAppData.activeGames;
//Is object var x;
result = new TTT.GameInfo(opponentId);
if(!this.myGameAppData.activeGames){
this.myGameAppData.activeGames = [];
} this.myGameAppData.activeGames.push(result);
}
return result;
}
The function loadGameloads the GameInfointo the game board and initializes the
window.currentGameobject.The bulk of this method is display logic In our
Trang 19implementation, the Viewer’s moves are always represented by an X (TTT.Players.
Human) and the opponent’s moves are always represented by an O (TTT.Players.
Computer)
The board layout is represented by a pipe-delimited list of moves.This represents the nine
squares of the Tic-Tac-Toe board Each move is either an X, an O, or nothing Depending on
the originating source of the GameInfoobject (Viewer’s object or opponent’s object), asdetermined by matching the Viewer’s ID against gameInfo.dataSourceId, these moves aretranscribed to be appropriate for the Viewer For example, if the latest GameInfoobject
originated from the Viewer’s app data store, the X’s and O’s remain the same If the latest
GameInfoobject was from the opponent’s app data store, the X’s and O’s have to be flipped.
/**
* Loads the specified gameInfo object into
* window.currentGame and updates the board
currentGame = new TTT.Game();
} currentGame.isHumanOpponent = true; //Flag to not make computer moves var myId = window.viewer.getId();
var fromMe = (gameInfo.dataSourceId == myId);
if(gameInfo.boardLayout){
var moves = gameInfo.boardLayout.split("|");
var nOneMark, nZeroMark;
if(fromMe){
nOneMark = TTT.Players.Human;
nZeroMark = TTT.Players.Computer;
} else{
if(moves[i]=="1"){
Trang 20currentGame.currentPlayer = nOneMark;
currentGame.makeMove(nOneMark, i);
} else{
currentGame.currentPlayer = nZeroMark;
currentGame.makeMove(nZeroMark, i);
} } }
//Now init to current player //debugger;
if(gameInfo.currentPlayer == myId){
currentGame.currentPlayer = TTT.Players.Human;
} else{
if(game.moves[i] == TTT.Players.Computer){
result += "0|";
} else{
result += "1|";
} }
Trang 21} return result;
}
Finishing and Clearing a Game
Finishing a game ends up being a complex problem If you remember the earlier sion, we had two options to handle finishing and clearing a game.The first was through
discus-a complex three-wdiscus-ay hdiscus-andshdiscus-ake.The second wdiscus-as to discus-allow for mdiscus-anudiscus-al clediscus-aring We’regoing to implement the latter
1 The first step is to add in some methods for removal of a game.This deletes thecurrent client snapshot of the app data store games, saves an updated app data storekey back to the server, and clears currentGame Add the three methods
removeGame, clearGame, and clearAllGamesto the TTT.AppDataPlay
object:
removeGame: function(opponentId){
if(this.myGameAppData && this.myGameAppData.activeGames != null){
var items = this.myGameAppData.activeGames;
//Is object var x;
for(x in items){
if(items[x].opponentId != opponentId){
newItems.push(items[x]);
} } } } this.myGameAppData.activeGames = newItems;
this.updateStoredGameObject();
}, clearGame: function(opponentId){
//debugger;
this.cancelPolling();
Trang 22var gameInfo = this.getCurrentGameObject(this.currentOpponentId);
var myId = window.viewer.getId();
Clear All games</button>
3 Now we have to modify the selectedOpponentDataCallbackfunction torecognize when one person has started a new game.The following code listingdemonstrates identifying a winner It is the first thing checked after identifyingthat the opponent’s current game is not null with the call if(thisObj.
myOpponentCurrentGame != null) Download the full app code for Chapter 7from our Google Code listing (http://code.google.com/p/opensocialtictactoe/
source/browse/#svn/trunk/chapter7) if you wish to see it in place
var isNewGame = false;
if(myGameInfo.winner != 0 && thisObj.myOpponentCurrentGame.moves < 3){
isNewGame = true;
latestGameInfo = myGameInfo;
}
Trang 23“Real-Time” Play
We use quotes to signify the air quotes we’d use if this conversation was happening face
to face It’s not actually real-time play.That would require an event-routing system, which
we don’t have at our disposal in this design Instead we’ll set up a polling system so thatthe game can periodically update without reloading the page
A simple polling design sets a time-out after the player makes a move.This time-outcontinues to retrigger at intervals until the code detects that the other player has made amove and control has returned to the Viewer In practice, however, a polling designrequires numerous safety checks and throttling mechanisms
Throughout this section we’ll talk about each part of the code in turn.You may alsohave noticed calls to the polling triggers in the code we’ve introduced already.We won’t
be reviewing those calls in detail but will leave it to the reader to explore how they allinteract in depth
Before we start, there are two constraints to put in place.The first is the polling val.The second is the maximum number of times to retrigger the poll test If a userwalks away from the machine for half a day, we don’t want the game to continue pollingevery ten seconds, so we set a maximum count it can trigger before getting a reply
inter-/**
* Time (in seconds) that the system will wait
* between polling to see
* if the opponent has made a move
*/
OPPONENT_POLLING_TIME: 10,
/**
* Maximum number of times the poll will fire
* without a response before
Trang 24}, (TTT.AppDataPlay.OPPONENT_POLLING_TIME * 1000));
}
This code sets the initial time-out trigger, which in turn calls the function
pollForOpponentUpdatedMove.The pollForOpponentUpdatedMovefunction makes
a call to re-retrieve the opponent’s app data and reinitialize the game
}
Within the code path of getFriendGameDatais a trigger to cancel the polling.Thisinvolves clearing the time-out handle we saved in startGameplayPollingandresetting all the flags and counters
if(this.gamePollTimer && this.gamePollTimer > 0){
return;
}
Unfortunately, this was too heavy-handed, as the timers would simply die out in somesituations.To combat this, we added an additional counter to allow the trigger to sleep afew times before triggering the die code path:
if(this.gamePollTimer && this.gamePollTimer > 0){
if(TTT.AppDataPlay.pollSleepCount++ > 5){
Trang 25TTT.AppDataPlay.pollSleepCount = 0;
return;
} }
That seemed to work.We may have been able to get there more cleanly by using
window.setIntervalinstead of a retriggering time-out.We’ll leave it to the reader toexplore different permutations of this design Asynchronous processing, as many of youmay know, can be a complicated problem
Advantages and Disadvantages of App Data P2P Play
There are a few advantages and a few disadvantages to this kind of architecture for P2Pgame play First, let’s go with the advantages:
n There’s no infrastructure required for your game to operate
n There are no explicit scaling considerations—the problem of scaling the game ispushed onto MySpace
Now let’s consider the downsides of this technique:
n Logic can be much more complex than in a shared read/write database
n The app data store can be read and written to by anyone with a little knowledge
of Firebug, essentially allowing a malicious user to cheat
n If your app takes off, it could flatten the MySpace app data infrastructure
Oh, we didn’t mention that before? App data in its current state is not explicitlydesigned for high-volume read/write operations Its initial conception was more forstorage of settings, such as background colors and default fonts.There is much discussionwithin the spec group (those involved with the development of the OpenSocial spec)about the shortcomings of the app data store Currently the main discussions centeraround both a private (not friend-readable) store and a real-time messaging system
As always, you can download the source code for this chapter from the Google Codesite, http://code.google.com/p/opensocialtictactoe, or directly from the source repository