/**
* External Libraries used for the WoShamBo Gadget
* Author: Wei-Hwa Huang
*
* This file contains multiple utilities used for calculating
* Woshambo competition information; it can be considered the
* "Woshambo API".
*
* This file contains code that is very WoShamBo-specific;
* code that is used for general Javascript manipulation, such
* as DOM and UI stuff, is in general-lib.js.
*/
// The top-level object, all of our other functions and constants
// are fields of this object.
woshambo = {};
///////////////////////////////////////////////
// Constants
woshambo.C = {};
// Version number. Bigger numbers are later versions; ideally,
// when we encounter data in the datastore that is of an earlier
// version, we should be able to handle it gracefully.
woshambo.C.VERSION = 5;
// Values that indicate outcomes of a specific combat.
woshambo.C.WIN = 0;
woshambo.C.TIE = 1;
woshambo.C.LOSS = 2;
// Storing an unlimited number of combats puts too much stress
// on server storage; hence we will occasionally compress
// previous results until something called a "past". A past
// consists of just raw data recording the number of wins,
// ties, and losses.
woshambo.C.EMPTYPAST = [0,0,0]; // wins, ties, losses
// The possible weapons a player can use in combat.
woshambo.C.ROCK = 0;
woshambo.C.PAPER = 1;
woshambo.C.SCISSORS = 2;
woshambo.C.INVALID = 99;
// Source directory for the combat images.
woshambo.C.IMAGES = "http://opensocial-resources.googlecode.com/"
+ "svn/samples/woshambo/images/";
// Status of opponents.
woshambo.C.WAITING = 99; // opponent has not made a move yet.
woshambo.C.DEAD = 100; // opponent hasn't played WoShamBo
woshambo.C.SLOW = 101; // opponent has played but hasn't responded yet
// Robot IDs.
woshambo.C.ROCKMAN = 0; // "Rockman" robot.
woshambo.C.FLIPPER = 1; // "Flipper" robot.
// Scores for getting results
woshambo.C.POINTS_HUMAN_WIN = 105;
woshambo.C.POINTS_HUMAN_TIE = 60;
woshambo.C.POINTS_HUMAN_LOSS = 15;
woshambo.C.POINTS_HUMAN_AVERAGE = 60; // (105 + 15 + 60) / 3
woshambo.C.POINTS_ROBOT_WIN = 4;
woshambo.C.POINTS_ROBOT_TIE = 3;
woshambo.C.POINTS_ROBOT_LOSS = 2;
/////////////////////////////////////////
// Charsheet
//
// A Charsheet, short for "character sheet", is a list
// of stats and information about the player. If the player
// levels up or has any personal information not related
// to specific combat, it goes here.
//
// Fields in the charsheet:
// version
// spent: How many points the player has spent.
// score: How many points the player has.
// earned: How many points the player has earned over a lifetime.
// Note that score and earned are "estimates" only, and may not be accurate
// due to other combats.
// storage: Weapons storage. A player only has a finite
// number of weapons and needs to buy more with
// their score.
// robot_points: How many points the player has won against robots.
// log: Text string of logs.
// news: Alerts and information to the player.
// Constructor.
woshambo.Charsheet = function() {
this.version = woshambo.C.VERSION;
this.spent = 0;
this.score = 0;
this.earned = 0;
this.robot_points = 0;
this.storage = [20,20,20]; // 20 rocks, 20 paper, 20 scissors
this.log = "";
this.news = "";
};
/////////////////////////////////////////
// History
//
// A History is a list of all the Moves a player
// has used in a Match (a Match being a series of Combats
// against a specific player).
//
// Fields:
// version
// weapons: an array of previous Moves (usually Weapons).
// past: When the list of weapons gets too big, it gets
// compressed into this "past", which is an aggregate
// list of previous weapons. (Nothing does the compression
// yet, though.)
// past_points: The number of points earned by the "past".
// Constructor.
woshambo.History = function() {
this.version = woshambo.C.VERSION;
this.past = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
this.weapons = []; // list of Moves
this.past_points = 0; // points_earned in the past
};
///////////////////////////////////////////////
///////////////////////////////////////////////
// Functions for manipulating woshambo objects
//
// All these utility functions are stored under "F".
woshambo.F = {};
/******************************************
This is the old spec for History and Charsheet, mostly used in
upgrading and downgrading.
VERSIONS 1-2:
woshambo.Charsheet = function() {
this.version = woshambo.C.VERSION;
this.score = 0;
this.earned = 0;
this.storage = [100,100,100]; // 100 rocks, 100 paper, 100 scissors
this.log = "";
this.news = "";
};
woshambo.History = function() {
this.version = woshambo.C.VERSION;
this.past = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
this.weapons = []; // list of Moves
};
VERSION 3:
Added "past_points" to History.
VERSION 4:
Added "spent" to Charsheet.
VERSION 5:
Added "robotpoints" to Charsheet.
******************************************/
///////////////////////////////////////////////////////
// Functions for encoding and decoding history objects.
///////////////////////////////////////////////////////
// Given an object that is suspected to be a history object,
// attempt to turn it into a workable history object of the
// current version and return it.
woshambo.F.canonHistory = function(history) {
if (history == undefined || history.version == undefined) {
debugLog("Invalid history! Making new one...
");
history = new woshambo.History();
} else if (history.version < woshambo.C.VERSION) {
debugLog("History is an old version! Upgrading...
");
history = woshambo.F.upgradeHistory(history);
} else if (history.version > woshambo.C.VERSION) {
debugLog("History is an newer version! Downgrading...
");
history = woshambo.F.downgradeHistory(history);
}
return history;
}
// Given a history from an older version, return a upgraded version of it.
woshambo.F.upgradeHistory = function(original) {
// check to make sure we are supposed to be called.
if (original.version == woshambo.C.VERSION)
return original;
if (original.version > woshambo.C.VERSION)
return woshambo.F.downgradeHistory(original);
var result = new woshambo.History();
if (original.version <= 2) {
result.past = woshambo.F.copyPast(original.past);
result.weapons = original.weapons.slice();
} else if (original.version <= 4) {
// no change needed
result.past = woshambo.F.copyPast(original.past);
result.weapons = original.weapons.slice();
result.past_points = original.past_points;
} else {
// we shouldn't be here, but let's just make guesses.
if (original.past != undefined)
result.past = woshambo.F.copyPast(original.past);
if (original.weapons != undefined)
result.weapons = original.weapons.slice();
if (original.past_points != undefined)
result.past_points = original.past_points;
}
return result;
}
// Given a history from an newer version, attempt to
// return a current version of it.
woshambo.F.downgradeHistory = function(original) {
// check to make sure we are supposed to be called.
if (original.version == woshambo.C.VERSION)
return original;
if (original.version < woshambo.C.VERSION)
return woshambo.F.upgradeHistory(original);
// Rewrite it to be the current version
original.version = woshambo.C.VERSION;
// all we can do is make guesses and hope that the fields are still there.
// if they aren't, just initialize them.
if (original.past != undefined)
original.past = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
if (original.weapons != undefined) original.weapons = [];
if (original.past_points != undefined) original.past_points = 0;
return original;
}
///////////////////////////////////////////////////////
// Functions for encoding and decoding charsheet objects.
///////////////////////////////////////////////////////
// Given an object that is suspected to be a charsheet object,
// attempt to turn it into a workable charsheet object of the
// current version and return it.
woshambo.F.canonCharsheet = function(charsheet) {
if (charsheet == undefined || charsheet.version == undefined) {
debugLog("Invalid charsheet! Making new one...
");
charsheet = new woshambo.Charsheet();
} else if (charsheet.version < woshambo.C.VERSION) {
debugLog("Charsheet is an old version! Upgrading...
");
charsheet = woshambo.F.upgradeCharsheet(charsheet);
} else if (charsheet.version > woshambo.C.VERSION) {
debugLog("Charsheet is an newer version! Downgrading...
");
charsheet = woshambo.F.downgradeCharsheet(charsheet);
}
return charsheet;
}
// Given a charsheet from an older version, return a upgraded version of it.
woshambo.F.upgradeCharsheet = function(original) {
// check to make sure we are supposed to be called.
if (original.version == woshambo.C.VERSION) return original;
if (original.version > woshambo.C.VERSION)
return woshambo.F.downgradeCharsheet(original);
var result = new woshambo.Charsheet();
if (original.version <= 4) {
result.score = original.score;
result.earned = original.earned;
result.spent = original.earned - original.score;
result.storage = original.storage.slice();
result.log = original.log;
result.news = original.news;
} else {
// we shouldn't be here, but let's just make guesses.
if (original.score != undefined) result.score = original.score;
if (original.earned != undefined) result.earned = original.earned;
if (original.spent != undefined) result.spent = 0;
if (original.storage != undefined) result.storage = original.storage.slice();
if (original.log != undefined) result.log = original.log;
if (original.news != undefined) result.news = original.news;
if (original.robot_points != undefined)
result.robot_points = original.robot_points;
}
return result;
}
// Given a charsheet from an newer version, attempt to
// return a current version of it.
woshambo.F.downgradeCharsheet = function(original) {
// check to make sure we are supposed to be called.
if (original.version == woshambo.C.VERSION) return original;
if (original.version < woshambo.C.VERSION)
return woshambo.F.upgradeCharsheet(original);
original.version = woshambo.C.VERSION;
// all we can do is make guesses and hope that the fields are still there.
// if they aren't, just initialize them.
if (original.score != undefined) original.score = 0;
if (original.earned != undefined) original.earned = 0;
if (original.spent != undefined) original.spent = 0;
if (original.storage != undefined) original.storage = [20, 20, 20];
if (original.log != undefined) original.log = "";
if (original.news != undefined) original.news = "";
if (original.robot_points != undefined) original.robot_points = 0;
return original;
}
////////////////////////////////////////////////
// functions that deal with combat and weapons
////////////////////////////////////////////////
// Given an old "past" object, outputs a new copy of it.
woshambo.F.copyPast = function(old) {
var newPast = [];
newPast[woshambo.C.WIN] = old[woshambo.C.WIN];
newPast[woshambo.C.TIE] = old[woshambo.C.TIE];
newPast[woshambo.C.LOSS] = old[woshambo.C.LOSS];
return newPast;
}
// Converts a combat result to a human-readable string.
woshambo.F.resultToString = function(result) {
if (result == woshambo.C.WIN) return "Win";
if (result == woshambo.C.LOSS) return "Loss";
if (result == woshambo.C.TIE) return "Tie";
return "?";
}
// Converts a weapon to a single letter.
woshambo.F.weaponToLetter = function(weapon) {
if (weapon == woshambo.C.ROCK) return "R";
if (weapon == woshambo.C.PAPER) return "P";
if (weapon == woshambo.C.SCISSORS) return "S";
return "?";
}
// Converts a weapon to a DOM object of the image.
// explode should be a boolean value specifying whether
// the weapon should have an "explosion" graphic or not.
woshambo.F.weaponToImage = function(weapon, explode) {
var result = document.createElement("img");
if (weapon == woshambo.C.ROCK && explode) {
result.src = woshambo.C.IMAGES + "weapon-rock-inverse-small.png";
return result;
}
if (weapon == woshambo.C.ROCK && !explode) {
result.src = woshambo.C.IMAGES + "weapon-rock-normal-small.png";
return result;
}
if (weapon == woshambo.C.PAPER && explode) {
result.src = woshambo.C.IMAGES + "weapon-paper-inverse-small.png";
return result;
}
if (weapon == woshambo.C.PAPER && !explode) {
result.src = woshambo.C.IMAGES + "weapon-paper-normal-small.png";
return result;
}
if (weapon == woshambo.C.SCISSORS && explode) {
result.src = woshambo.C.IMAGES + "weapon-scissors-inverse-small.png";
return result;
}
if (weapon == woshambo.C.SCISSORS && !explode) {
result.src = woshambo.C.IMAGES + "weapon-scissors-normal-small.png";
return result;
}
}
// Calculates the result of a combat (relative to the host)
// based on the weapons used.
woshambo.F.combatResult = function(hostWeapon, guestWeapon) {
if (hostWeapon == woshambo.C.ROCK) { // rock
if (guestWeapon == woshambo.C.ROCK) return woshambo.C.TIE;
if (guestWeapon == woshambo.C.PAPER) return woshambo.C.LOSS;
if (guestWeapon == woshambo.C.SCISSORS) return woshambo.C.WIN;
} else if (hostWeapon == woshambo.C.PAPER) { // paper
if (guestWeapon == woshambo.C.ROCK) return woshambo.C.WIN;
if (guestWeapon == woshambo.C.PAPER) return woshambo.C.TIE;
if (guestWeapon == woshambo.C.SCISSORS) return woshambo.C.LOSS;
} else if (hostWeapon == woshambo.C.SCISSORS) { // scissors
if (guestWeapon == woshambo.C.ROCK) return woshambo.C.LOSS;
if (guestWeapon == woshambo.C.PAPER) return woshambo.C.WIN;
if (guestWeapon == woshambo.C.SCISSORS) return woshambo.C.TIE;
}
// if all else fails, tie.
return woshambo.C.TIE;
}
// Given a history object, output how long the past in this history is.
woshambo.F.pastLength = function(history) {
var wins = history.past[woshambo.C.WIN];
var ties = history.past[woshambo.C.TIE];
var losses = history.past[woshambo.C.LOSS];
return (wins + ties + losses);
}
// Given a history object, output how many total weapons are in the history.
woshambo.F.weaponCount = function(history) {
return (woshambo.F.pastLength(history) + history.weapons.length);
}
// Given a history object, return a past that is the inverse of
// what the past in the object is.
woshambo.F.invertPast = function(history) {
var result = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
result[woshambo.C.WIN] = history.past[woshambo.C.LOSS];
result[woshambo.C.TIE] = history.past[woshambo.C.TIE];
result[woshambo.C.LOSS] = history.past[woshambo.C.WIN];
return result;
}
// Given a combat result, calculate the number of points won.
woshambo.F.calcPoints = function (is_human, result) {
if (is_human) {
if (result == woshambo.C.WIN) return woshambo.C.POINTS_HUMAN_WIN;
if (result == woshambo.C.TIE) return woshambo.C.POINTS_HUMAN_TIE;
if (result == woshambo.C.LOSS) return woshambo.C.POINTS_HUMAN_LOSS;
} else {
if (result == woshambo.C.WIN) return woshambo.C.POINTS_ROBOT_WIN;
if (result == woshambo.C.TIE) return woshambo.C.POINTS_ROBOT_TIE;
if (result == woshambo.C.LOSS) return woshambo.C.POINTS_ROBOT_LOSS;
}
}
//////////////////////////////////////////////////
// functions that calculate values from charsheets and histories
//////////////////////////////////////////////////
// Remove the points earned and points holding in a charsheet.
woshambo.F.resetCharsheetPoints = function(charsheet) {
charsheet.score = charsheet.robot_points - charsheet.spent;
charsheet.earned = charsheet.robot_points;
}
// Given a host charsheet, increment by the specified number of points.
woshambo.F.addCharsheetPoints = function(charsheet, pts) {
charsheet.score += pts;
charsheet.earned += pts;
}
// Given a host charsheet and two histories (that are involved in a war),
// increment the charsheet to reflect the points earned in the two histories.
woshambo.F.addCharsheetHistoryPoints
= function(charsheet, hostHistory, guestHistory) {
if (hostHistory == undefined) return;
if (guestHistory == undefined) return;
var pts = woshambo.F.totalPointsEarned(hostHistory, guestHistory, true);
charsheet.score += pts;
charsheet.earned += pts;
}
// Given two histories (that are involved in a war), output
// the total number of points earned by the host.
//
// If the host's past is smaller than the guest's past, we have a
// problem -- we don't know what moves the guest did in our first moves.
// For now, let's assume that the host got an "average" score; in a
// future version we'll attempt to do this the "correct" way,
// maybe by recording the guest's moves as a backup.
//
woshambo.F.totalPointsEarned = function(hostHistory, guestHistory, is_human) {
var answer = hostHistory.past_points;
var hostPast = woshambo.F.pastLength(hostHistory);
var guestPast = woshambo.F.pastLength(guestHistory);
var result = null;
if (hostPast < guestPast) {
var hostPos = guestPast - hostPast;
var guestPos = 0;
while (hostPos < hostHistory.weapons.length
&& guestPos < guestHistory.weapons.length) {
answer += woshambo.F.calcPoints(
is_human,
woshambo.F.combatResult(hostHistory.weapons[hostPos],
guestHistory.weapons[guestPos])
);
hostPos++;
guestPos++;
}
} else {
result = woshambo.F.copyPast(hostHistory.past);
var guestPos = hostPast - guestPast;
answer += guestPos * woshambo.C.POINTS_HUMAN_AVERAGE;
// don't count the stuff that is in the host's past
var hostPos = 0;
while (hostPos < hostHistory.weapons.length
&& guestPos < guestHistory.weapons.length) {
answer += woshambo.F.calcPoints(
is_human,
woshambo.F.combatResult(hostHistory.weapons[hostPos],
guestHistory.weapons[guestPos])
);
hostPos++;
guestPos++;
}
}
return answer;
}
// Given two histories (that are involved in a war), output
// a past object that lists the the aggregate result of the two
// histories facing off against each other.
woshambo.F.netResults = function(hostHistory, guestHistory) {
var hostPast = woshambo.F.pastLength(hostHistory);
var guestPast = woshambo.F.pastLength(guestHistory);
var result = null;
if (hostPast < guestPast) {
result = woshambo.F.copyPast(woshambo.F.invertPast(guestHistory));
var hostPos = guestPast - hostPast;
var guestPos = 0;
while (hostPos < hostHistory.weapons.length
&& guestPos < guestHistory.weapons.length) {
result[woshambo.F.combatResult(hostHistory.weapons[hostPos],
guestHistory.weapons[guestPos])]++;
hostPos++;
guestPos++;
}
} else {
result = woshambo.F.copyPast(hostHistory.past);
var guestPos = hostPast - guestPast;
var hostPos = 0;
while (hostPos < hostHistory.weapons.length
&& guestPos < guestHistory.weapons.length) {
result[woshambo.F.combatResult(hostHistory.weapons[hostPos],
guestHistory.weapons[guestPos])]++;
hostPos++;
guestPos++;
}
}
return result;
}
// Add a move by the "guest", of the specified weapon.
// Call the callback function with the combat result of this weapon.
woshambo.F.makeMove = function (hostHistory, guestHistory, weapon, callback) {
var turn = guestHistory.weapons.length + woshambo.F.pastLength(guestHistory);
var myPosition = guestHistory.weapons.length;
var hostPosition = turn - woshambo.F.pastLength(hostHistory);
guestHistory.weapons.push(weapon);
if (hostPosition < 0) {
// host's move has been lost to history -- oops!
callback(woshambo.C.INVALID);
} else if (hostPosition >= hostHistory.weapons.length) {
// host hasn't made a move yet
callback(woshambo.C.WAITING);
} else {
callback(woshambo.F.combatResult(guestHistory.weapons[myPosition],
hostHistory.weapons[hostPosition]));
}
}
///////////////////////////////////////////////
// functions for woshambo robots
///////////////////////////////////////////////
// Returns a random weapon.
woshambo.F.randomWeapon = function () {
var temp = Math.random() * 3;
if (temp < 1) return woshambo.C.ROCK;
if (temp < 2) return woshambo.C.PAPER;
return woshambo.C.SCISSORS;
}
// Adds a move from the robot to robot_history.
woshambo.F.robotMove = function (robotID, robot_history, opponent_history) {
if (robotID == woshambo.C.ROCKMAN) {
woshambo.F.robotRockmanMove(robot_history, opponent_history);
} else if (robotID == woshambo.C.FLIPPER) {
woshambo.F.robotFlipperMove(robot_history, opponent_history);
} else {
// don't recognize the robot; default to rockman.
woshambo.F.robotRockmanMove(robot_history, opponent_history);
}
}
woshambo.F.robotRockmanMove = function (robot_history, opponent_history) {
robot_history.weapons.push(woshambo.C.ROCK);
}
woshambo.F.robotFlipperMove = function (robot_history, opponent_history) {
if (robot_history.weapons.length == 0) {
robot_history.weapons.push(woshambo.F.randomWeapon());
} else {
var last = robot_history.weapons[robot_history.weapons.length-1];
var next = woshambo.F.randomWeapon();
while (next == last) {
next = woshambo.F.randomWeapon();
}
robot_history.weapons.push(next);
}
}
// Returns a printable display name for that robot.
woshambo.F.robotName = function (robotID) {
if (robotID == woshambo.C.ROCKMAN) {
return "Rockman the Robot";
} else if (robotID == woshambo.C.FLIPPER) {
return "Flipper the Robot";
} else {
// don't recognize the robot; default to rockman.
return "Rockman the Robot";
}
}