#summary Documentation for the Woshambo gadget, a demonstration of OpenSocial. = Introduction = The Woshambo gadget is a sample interactive game gadget that demonstrates how to use the OpenSocial API for game development. This page documents the source code (and is current as of version 1.1). = Game Description = The concept of the game is simple, and based on the simple game of [http://en.wikipedia.org/wiki/Rock,_Paper,_Scissors Roshambo] (also known as Rock, Paper, Scissors). You can challenge your friends to multiple games, and even queue up moves for future attacks. In the future, features such as scoring and weapon upgrades will be added. == Screenshot == http://opensocial-resources.googlecode.com/files/woshambo-screenshot.png == Playing the Game == The game shows several control panels, each with a light-blue frame and a title. Clicking on the small triangle next to the title shrinks/expands the panel. The top panel is labeled "Opponent Selection". It contains a drop-down box that lists all your friends, as determined by the site you are on. There are two check boxes that allow you to filter the list by friends who are playing WoShamBo, or by friends who are "pending" (waiting for you to make a move). There is a Refresh button to refresh the friends list. When you have a selected a friend, your "Match" against that friend is then displayed in the "Match Status" and "Match History" panels. A Match is composed of several Combats between the two players involved in the Match. A Combat is composed of two Attacks, one from each player. Each Attack uses a Weapon (usually Rock, Paper, or Scissors). The two Attacks in a Combat determine which player has won the Combat (if any), and the aggregate results of the Combat are displayed in the Match panels. = Internals = For Version 1.1, the code lives in two files: * [http://opensocial-resources.googlecode.com/svn/samples/woshambo/woshambo.xml woshambo.xml], which is the core code for the gadget; and * [http://opensocial-resources.googlecode.com/svn/samples/woshambo/woshambo-data.js woshambo-data.js], several external constants, functions, and libraries for supporting the woshambo gadget. == WoShamBo-specific Data == The only place to store custom data under OpenSocial is to put it into what is called *app data*. Think of app data as a giant database, where you use many layers of indices to store and retrieve the data you want. To access a chunk of data (which is usually some sort of string), you need three indices: * An appID (what gadget is getting this data?) * A userID (what user's information are we looking at?) * A key (any string that we want to use as an index) There are specific permission issues that are based on the first two fields: * A gadget cannot view nor change any app data that is under an appID other than its own; * The person viewing the page cannot change any app data that is under a userID other than their own; * The person viewing the page cannot view any app data that is under a userID other than their own or their friends. Within a gadget, we never need to specify the app ID; that's automatically handled for us. The WoShamBo gadget makes liberal use of all the other features, though. Specifically: Each user has a set of statistics and data called a CharSheet (short for "character sheet"). We store this under the key "WoshamboCharsheet". To represent a Match between two users, we store a History for each user under a key which is the other user's ID. So, for example, if Alice's ID is 289, and Bob's ID is 777, then all of Alice's Attacks on Bob would be in a History object stored with userID = 289 and key = 777, while all of Bob's Attacks on Alice would be in a History object stored with userID = 777 and key = 289. History and CharSheet objects are javascript objects, but before storage they are converted into a string using JSON (JavaScript Object Notation), which is handled by a separate library. == Block Diagram == http://opensocial-resources.googlecode.com/files/woshambo-v5.png == Overview == The user interacts with the gadget in four ways: * starting the gadget * choosing an opponent (only if the user is in "self view" -- looking at the gadget on their own page); * attacking the opponent; * purchasing "items" in the "shop". The application-specific data on the server is stored in three types of objects: * openSocial data (who is friends with whom); * charsheet (character sheet); * history (what moves the player has made); The API means that reading and writing this data all has to be done through asynchronous callbacks. Therefore, the structure of the code often deals with pairs of functions, an "invoking function" calling the API, and a "callback function" that will be called when the API returns. These are represented in the block diagram as a two-layer rectangle, but keep in mind there is a server communication in between. (Not strictly true, as the layer between {{{woshambo.F.makeMove}}} and {{{handleAttackResult}}} actually uses a "Woshambo API" and stays purely on the client, but it uses a similar callback structure.) The general flow goes top-to-bottom, where user actions tend to enable user actions eventually. For example, during startup, if the user is in "self view" mode, the initial routine {{{handleOwnerViewerData}}} will realize this and shunt off to {{{loadSelfView}}}, and eventually the selection menu will be displayed, enabling the user to choose an opponent. Storing new data onto the server is in the {{{saveHistory}}} and {{{saveCharsheet}}} functions -- note that when playing against a robot, no history is stored. (The robot is the equivalent of weak NPC monsters that give very little experience.) = Code Documentation = This section gives an overview of the gadget, with excerpts from the code. There are sections of the code that are omitted from this documentation, mostly status messages and error handling and the like. The focus of this documentation is on how the gadget interacts with the OpenSocial API. Many functions of the gadget are in regards to things like UI and Javascript DOM and stuff; this should be reasonably documented and understandable from the source code. == Preamble == {{{ }}} As per the [http://code.google.com/apis/gadgets/docs/dev_guide.html Google Gadgets API], the XML file starts with a ModulePrefs tag with header information about the gadget. Note that it loads the OpenSocialAPI via the Require tag: {{{}}}. (We'll upgrade to version 0.6 eventually, we hope.) The actual html code (as will be seen by the client) is in the Content tag. One of the first things we do there is to load {{{woshambo-data.js}}}, so that we'll have that code available to us. (Read the file itself for documentation on what it provides.) We also load a small [http://www.json.org/ JSON] library, as we will be using that to store information that passes between users, and {{{general-lib.js}}}, which contains some general-purpose functions for DOM manipulation. == loadOwnerViewerData == We start by requesting data from the server. {{{ _IG_RegisterOnloadHandler(getOwnerViewerData); function loadOwnerViewerData() { // sends request to look at owner and viewer. var req = opensocial.newDataRequest(); req.add(req.newFetchPersonRequest('VIEWER'), 'viewer'); req.add(req.newFetchPersonRequest('OWNER'), 'owner'); var opt_params = { }; opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 500; req.add( req.newFetchPeopleRequest('OWNER_FRIENDS', opt_params), 'ownerFriends' ); req.send(handleOwnerViewerData); } }}} We use the special function {{{ _IG_RegisterOnloadHandler }}}, which calls its parameter as a callback when the gadget is loaded. So the first thing that happens is {{{loadOwnerViewerData()}}}, which attempts to load information from the server using an OpenSocial {{{DataRequest}}}. We create the {{{DataRequest}}} with three {{{FetchPersonRequest}}}, specifically asking for information about the viewer (the user that is viewing the gadget), the owner (the user that owns the gadget), and the friends of the owners. We then use the {{{send}}} function to send off the request, expecting a callback on {{{handleOwnerViewerData}}} when the request has been fulfilled. Note that we use an optional parameter to the friendlist request, as the default maximum number of friends returned is 20, which we override to 500. == handleOwnerViewerData == {{{ function handleOwnerViewerData(dataResponse) { viewer = dataResponse.get("viewer").getData(); owner = dataResponse.get("owner").getData();; ownerFriends = dataResponse.get("ownerFriends").getData().asArray() || []; for (var i=0; i"); debugLog("to " + current_opponent.getId() + "
"); req.add(req.newUpdatePersonAppDataRequest('VIEWER', current_opponent.getId(), JSON.stringify(my_history))); req.send(saveHistoryCallback); } function saveHistoryCallback(dataResponse) { refreshDisplays(); } function refreshDisplays() { reloadOpponentHistory(); _IG_AdjustIFrameHeight(); } }}} The {{{makeMove}}} function simply calculates the result of this most recent action and calls the callback function we pass to it, which happens to be {{{handleViewerAttackResult}}}. This function, when stripped of debugging output, does just one thing, call {{{saveHistory}}}. That function, in turn, uses JSON to turn the viewer's history object into a string, and then creates an OpenSocial {{{UpdatePersonAppDataRequest}}} to update the value. A callback from that is then used to refresh the Display (and write more debug messages), which reloads the opponent's history, in case they made any more moves in the meantime. = How Woshambo Points Work = The Woshambo point scoring system is simple in principle, but getting the code to work is a complicated mess due to read/write permissions, as well as asynchronous activity among players. Here's an overview. == External behavior == When the player plays a human, they get 105 points for a win, 60 points for a tie, and 15 points for a loss. When the player plays a robot, they get 4 points for a win, 3 points for a tie, and 2 points for a loss. A player can purchase items at the Shop. Notably, weapons eventually run out (each player starts with 100 of each), and must be purchased. Purchasing items costs points. == Implementation == A player's charsheet contains three values: * {{{spent}}}: number of points spent by the player. * {{{score}}}: number of points the player has left. * {{{earned}}}: number of points the player has earned over a lifetime. * {{{robot_points}}}: number of points player has earned versus robots. The {{{spent}}} field is held steady, and increases as players buy items at the shop. So far, so good. The other two fields, however, often need to be recalculated: * When the player loads the gadget under self-view, {{{score}}} and {{{earned}}} are reset, and regenerated as we iterate through all the viewer's friends. After the recalculation, the charsheet is then saved to the app datastore -- this is done by a routine called {{{recalculateScore}}}. This also happens when the player chooses a human opponent. * When the player selects a human under self-view, a local variable called {{{earned_sans_current_opponent}}} is created, which is the number of points earned, but not counting those against the current human player. Each time the Match Status is redisplayed, the {{{score}}} and {{{earned}}} values in the charsheet are recalculated based on this local variable and the current history, and the charsheet is saved to the app datastore -- this is done by a routine called {{{incorporateNewMatchStatusIntoCharsheet}}}. * When the player is playing against a robot, the behavior is similar, except it's the {{{robot_points}}} that are excluded. * When the player is playing under friendView, we don't have access to the viewer's other battles (well, technically we could probably get it, but it would be rather cost-prohibitive to do so). Therefore, the best we can do is assume that the {{{earned}}} value in the stored charsheet is correct, and we use that to calculate {{{earned_sans_current_opponent}}}, using the same {{{incorporateNewMatchStatusIntoCharsheet}}} above. The effect is that occasionally a player's score will have unexpected changes. This can happen when two humans are playing each other at around the same time, or when, say, human X is responding to human Y's moves at the same time as human Y is choosing a new opponent (even if that new opponent is not human X). = Known Bugs and Future Features = * There is a cap of the number of friends we can load at one time; currently it is set at 500. Weird behavior, especially with score, can result if someone is playing against more than 500 friends, or if whatever backend store is implementing OpenSocial refuses to give more than 500 friends at one time (they can set any arbitrary max value that is >= 20). This can be fixed, using a form of 20-at-a-time pagination through friends, but is a bit complex to implement at this point. * Compressed History isn't implemented at the moment (it's a bit hard to figure out how to make sure both players in a Match do compression at the same time). However, once that _is_ figured out, the code should be able to support compressed history. * Right now the History length is hard-coded at 2. It would be nice if this is something that can be purchased at the stop, and maybe in some way such that it expires and the player needs to buy more. * Smarter bots would be nice. (Talk to Dan Egnor about Iocaine Powder?) * Would be good to sort friends by their score.