|
WoShamBoDocumentation
Documentation for the Woshambo gadget, a demonstration of OpenSocial.
IntroductionThe 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 DescriptionThe concept of the game is simple, and based on the simple game of 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
Playing the GameThe 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. InternalsFor Version 1.1, the code lives in two files:
WoShamBo-specific DataThe 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:
There are specific permission issues that are based on the first two fields:
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
OverviewThe user interacts with the gadget in four ways:
The application-specific data on the server is stored in three types of objects:
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 DocumentationThis 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<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs
title="WoShamBo"
description="An MMORPS (Massively Multiplayer Online Rock Paper Scissors)"
author="Wei-Hwa Huang"
author_email="weihwa.feedback+woshambo@gmail.com"
author_link="http://weihwa.feedback.googlepages.com/home"
title_url="http://weihwa.feedback.googlepages.com/home"
author_location="Mountain View, CA, USA"
author_affiliation="Google, Inc."
screenshot="http://opensocial-resources.googlecode.com/svn/samples/woshambo/images/screenshot.png"
thumbnail="http://opensocial-resources.googlecode.com/svn/samples/woshambo/images/thumbnail.png"
>
<Require feature="opensocial-0.5"/>
<Require feature="dynamic-height"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<script src="http://opensocial-resources.googlecode.com/svn/samples/woshambo/woshambo-data.js"></script>
<script src="http://opensocial-resources.googlecode.com/svn/samples/woshambo/general-lib.js"></script>
<script src="http://opensocial-resources.googlecode.com/svn/samples/woshambo/json.js"></script>
As per the 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: <Require feature="opensocial-0.5"/>. (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 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. loadOwnerViewerDataWe 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<ownerFriends.length; ++i) {
ownerFriendsById[ownerFriends[i].getId()] = ownerFriends[i];
}
viewerIsOwner = (owner.getId() == viewer.getId());
setViewerIsOwnerFriend();
prepareUI();
hideMatchBoxes();
_gel("status").style.display = "none";
if (viewerIsOwner) {
setText(_gel("welcome"), "Welcome back, " + owner.getDisplayName() + "!");
loadSelfViewData();
} else if (viewerIsOwnerFriend) {
setText(_gel("welcome"), "Welcome to " + owner.getDisplayName() + "'s arena, " + viewer.getDisplayName() + "! Here you can challenge "
+ owner.getDisplayName() + " to a match of WoShamBo.");
current_opponent = owner;
loadFriendViewData();
} else {
setText(_gel("welcome"), "Welcome to " + owner.getDisplayName() + "'s arena, " + viewer.getDisplayName() + "!");
handleNonFriendView();
}
}The response to our DataRequest is passed to us in the form opensocial.dataResponse. To extract the data from it, we need to use the get() function on that object. Since our request was for information about the viewer, owner, and ownerFriends, those are what is in there and we store it in some local variables. We don't get the friends in a particular order, though, so we also create an array called ownerFriendsById so that we can look up the friends by their unique Id. Now, there are three Owner/Viewer relationships:
These three cases are handled by loadSelfViewData(), loadFriendViewData(), and handleNonFriendView(), respectively. We won't bother with annotating the last one because it's pretty simple; just print a "sorry" message to the user. (Ideally it would also provide a link where you can ask the owner to be your friend, but OpenSocial doesn't support that... yet.) We'll start with describing loadFriendViewData(), because with one opponent, it's simpler. loadFriendViewData function loadFriendViewData() {
var req = opensocial.newDataRequest();
req.add(req.newFetchPersonAppDataRequest('VIEWER',['WoshamboCharsheet']), 'viewercharsheet');
req.add(req.newFetchPersonAppDataRequest('OWNER',['WoshamboCharsheet']), 'ownercharsheet');
req.add(req.newFetchPersonAppDataRequest('VIEWER',[owner.getId()]), 'viewerhistory');
req.add(req.newFetchPersonAppDataRequest('OWNER',[viewer.getId()]), 'opponenthistory');
req.send(handleFriendViewData);
}
function handleFriendViewData(dataResponse) {
my_charsheet = getViewerCharsheet(dataResponse.get('viewercharsheet').getData());
their_charsheet = getOwnerCharsheet(dataResponse.get('ownercharsheet').getData());
handleViewerHistoryData(dataResponse);
handleOpponentHistoryData(dataResponse);
initTimer();
displayShop();
showMatchBoxes();
}As with most parts of working with the OpenSocial API, this is a two-step process -- building and sending the request, then having a callback function that handles it. In this case, we need to get this information:
Note that in our data requests to get app data, we have to specify the userID (using the special tag 'VIEWER' or 'OWNER'), as well as a key (which is either 'WoshamboCharsheet' for the character sheet, or the userID of the opponent). We also provide a string (e.g., 'viewercharsheet') to tell OpenSocial what special key to store it under in our datarequest object. This special key isn't particularly needed for us, but it's in the API because keys in the appdata won't always be unique. The callback function then extracts this data from the dataResponse, and then displays the boxes that allow the match to happen. loadSelfViewData function loadSelfViewData() {
var req = opensocial.newDataRequest();
prepareFriendSelect();
req.add(req.newFetchPersonAppDataRequest('OWNER',['WoshamboCharsheet']), 'ownercharsheet');
req.add(req.newFetchPersonAppDataRequest('OWNER_FRIENDS',['WoshamboCharsheet']), 'friendcharsheets');
req.add(req.newFetchPersonAppDataRequest('OWNER_FRIENDS',[owner.getId()]), 'friendhistories');
for (var i=0; i < ownerFriends.length; i++) {
req.add(req.newFetchPersonAppDataRequest('OWNER',[ownerFriends[i].getId()]), ('ownerhistory'+ownerFriends[i].getId()));
}
req.send(handleSelfViewData);
}
function handleSelfViewData(dataResponse) {
var f_id;
my_charsheet = getOwnerCharsheet(dataResponse.get('ownercharsheet').getData());
their_charsheets = dataResponse.get('friendcharsheets').getData() || {};
their_histories = dataResponse.get('friendhistories').getData() || {};
for (var i=0; i < ownerFriends.length; i++) {
f_id = ownerFriends[i].getId();
var data = dataResponse.get('ownerhistory'+f_id).getData();
if (data != undefined) {
my_histories[f_id] = data[owner.getId()][f_id];
}
decodeMyHistory(ownerFriends[i]);
decodeTheirHistory(ownerFriends[i]);
updateFriendSelectMenuOption(ownerFriends[i]);
}
}As with most parts of working with the OpenSocial API, this is a two-step process -- building and sending the request, then having a callback function that handles it. In this case, we need to get this information:
This is more complicated than a FriendView, because when we use the special userID OWNER_FRIENDS, we don't get back a single value; instead, we get an array that is indexed by each userID of each of the owner's friends. Ideally, we want the list of the owner's moves to be in the same structure, so some juggling is necessary to "invert the keys" of the 'ownerhistory' requests. Finally, for each friend, we call updateFriendSelectMenuOption. This updates the description of the friend in the drop-down menu so that the owner can have an idea of what their match progress on the friend is. Writing Data to the DatastoreThe process of writing data starts when a user clicks on an Attack Button in the Attack Buttons panel. The function buttonAttack is called, setting off a series of callback functions: function buttonAttack(weapon) {
woshambo.F.makeMove(their_history, my_history, weapon, handleViewerAttackResult);
}
function handleViewerAttackResult(result) {
saveHistory();
}
function saveHistory() {
var write_request;
var req = opensocial.newDataRequest();
debugLog("Writing " + JSON.stringify(my_history) + "<br>");
debugLog("to " + current_opponent.getId() + "<br>");
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 WorkThe 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 behaviorWhen 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. ImplementationA player's charsheet contains three values:
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:
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
|
Sign in to add a comment

Is there a version for the 0.7 containers?
How is the block diagram generated? Is there any tool which can be used to do this?
Thanks, Maku ApnaBill.com