package util.match;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import external.JSON.JSONArray;
import external.JSON.JSONException;
import external.JSON.JSONObject;
import util.game.Game;
import util.game.RemoteGameRepository;
import util.gdl.factory.GdlFactory;
import util.gdl.factory.exceptions.GdlFormatException;
import util.gdl.grammar.GdlSentence;
import util.statemachine.Move;
import util.statemachine.Role;
import util.symbol.factory.SymbolFactory;
import util.symbol.factory.exceptions.SymbolFormatException;
import util.symbol.grammar.SymbolList;
/**
* Match encapsulates all of the information relating to a single match.
* A match is a single play through a game, with a complete history that
* lists what move each player made at each step through the match. This
* also includes other relevant metadata about the match, including some
* unique identifiers, configuration information, and so on.
*
* NOTE: Match objects created by a player, representing state read from
* a server, are not completely filled out. For example, they only get an
* ephemeral Game object, which has a rulesheet but no key or metadata.
* Gamers which do not derive from StateMachineGamer also do not keep any
* information on what states have been observed, because (somehow) they
* are representing games without using state machines. In general, these
* player-created Match objects shouldn't be sent out into the ecosystem.
*
* @author Sam
*/
public final class Match
{
private final String matchId;
private final String randomToken;
private final String spectatorAuthToken;
private final int playClock;
private final int startClock;
private final Date startTime;
private final Game theGame;
private final List<String> theRoleNames;
private final List<List<GdlSentence>> moveHistory;
private final List<Set<GdlSentence>> stateHistory;
private final List<Date> stateTimeHistory;
private boolean isCompleted;
private final List<Integer> goalValues;
public Match(String matchId, int startClock, int playClock, Game theGame)
{
this.matchId = matchId;
this.startClock = startClock;
this.playClock = playClock;
this.theGame = theGame;
this.startTime = new Date();
this.randomToken = getRandomString(32);
this.spectatorAuthToken = getRandomString(12);
this.isCompleted = false;
this.theRoleNames = new ArrayList<String>();
for(Role r : Role.computeRoles(theGame.getRules())) {
this.theRoleNames.add(r.getName().getName().toString());
}
this.moveHistory = new ArrayList<List<GdlSentence>>();
this.stateHistory = new ArrayList<Set<GdlSentence>>();
this.stateTimeHistory = new ArrayList<Date>();
this.goalValues = new ArrayList<Integer>();
}
public Match(String theJSON, Game theGame) throws JSONException, SymbolFormatException, GdlFormatException {
JSONObject theMatchObject = new JSONObject(theJSON);
this.matchId = theMatchObject.getString("matchId");
this.startClock = theMatchObject.getInt("startClock");
this.playClock = theMatchObject.getInt("playClock");
if (theGame == null) {
this.theGame = RemoteGameRepository.loadSingleGame(theMatchObject.getString("gameMetaURL"));
if (this.theGame == null) {
throw new RuntimeException("Could not find metadata for game referenced in Match object: " + theMatchObject.getString("gameMetaURL"));
}
} else {
this.theGame = theGame;
}
this.startTime = new Date(theMatchObject.getLong("startTime"));
this.randomToken = theMatchObject.getString("randomToken");
this.spectatorAuthToken = null;
this.isCompleted = theMatchObject.getBoolean("isCompleted");
this.theRoleNames = new ArrayList<String>();
for(Role r : Role.computeRoles(theGame.getRules())) {
this.theRoleNames.add(r.getName().getName().toString());
}
this.moveHistory = new ArrayList<List<GdlSentence>>();
this.stateHistory = new ArrayList<Set<GdlSentence>>();
this.stateTimeHistory = new ArrayList<Date>();
JSONArray theMoves = theMatchObject.getJSONArray("moves");
for (int i = 0; i < theMoves.length(); i++) {
List<GdlSentence> theMove = new ArrayList<GdlSentence>();
JSONArray moveElements = theMoves.getJSONArray(i);
for (int j = 0; j < moveElements.length(); j++) {
theMove.add((GdlSentence)GdlFactory.create(moveElements.getString(j)));
}
moveHistory.add(theMove);
}
JSONArray theStates = theMatchObject.getJSONArray("states");
for (int i = 0; i < theStates.length(); i++) {
Set<GdlSentence> theState = new HashSet<GdlSentence>();
SymbolList stateElements = (SymbolList) SymbolFactory.create(theStates.getString(i));
for (int j = 0; j < stateElements.size(); j++)
{
theState.add((GdlSentence)GdlFactory.create("( true " + stateElements.get(j).toString() + " )"));
}
stateHistory.add(theState);
}
JSONArray theStateTimes = theMatchObject.getJSONArray("stateTimes");
for (int i = 0; i < theStateTimes.length(); i++) {
this.stateTimeHistory.add(new Date(theStateTimes.getLong(i)));
}
this.goalValues = new ArrayList<Integer>();
try {
JSONArray theGoalValues = theMatchObject.getJSONArray("goalValues");
for (int i = 0; i < theGoalValues.length(); i++) {
this.goalValues.add(theGoalValues.getInt(i));
}
} catch(JSONException je) {}
}
/* Mutators */
public void appendMoves(List<GdlSentence> moves) {
moveHistory.add(moves);
}
public void appendMoves2(List<Move> moves) {
// NOTE: This is appendMoves2 because it Java can't handle two
// appendMove methods that both take List objects with different
// templatized parameters.
if (moves.get(0) instanceof Move) {
List<GdlSentence> theMoves = new ArrayList<GdlSentence>();
for(Move m : moves) {
theMoves.add(m.getContents());
}
appendMoves(theMoves);
}
}
public void appendState(Set<GdlSentence> state) {
stateHistory.add(state);
stateTimeHistory.add(new Date());
}
public void markCompleted(List<Integer> theGoalValues) {
this.isCompleted = true;
if (theGoalValues != null) {
this.goalValues.addAll(theGoalValues);
}
}
/* Complex accessors */
public String toJSON() {
StringBuilder theJSON = new StringBuilder();
theJSON.append("{\n");
// Uniquification variables
theJSON.append(" \"matchId\": \"" + matchId + "\",\n");
theJSON.append(" \"randomToken\": \"" + randomToken + "\",\n");
theJSON.append(" \"startTime\": " + startTime.getTime() + ",\n");
// Game information
if (getGameName() != null) {
theJSON.append(" \"gameName\": \"" + getGameName() + "\",\n");
} else {
theJSON.append(" \"gameName\": null,\n");
}
if (getGameRepositoryURL() != null) {
theJSON.append(" \"gameMetaURL\": \"" + getGameRepositoryURL() + "\",\n");
} else {
theJSON.append(" \"gameMetaURL\": null,\n");
}
theJSON.append(" \"gameRoleNames\": " + renderArrayAsJSON(theRoleNames, true) + ",\n");
// States/moves
theJSON.append(" \"isCompleted\": " + isCompleted + ",\n");
theJSON.append(" \"states\": " + renderArrayAsJSON(renderStateHistory(stateHistory), true) + ",\n");
theJSON.append(" \"moves\": " + renderArrayAsJSON(renderMoveHistory(moveHistory), false) + ",\n");
theJSON.append(" \"stateTimes\": " + renderArrayAsJSON(stateTimeHistory, false) + ",\n");
if (goalValues.size() > 0) {
theJSON.append(" \"goalValues\": " + renderArrayAsJSON(goalValues, false) + ",\n");
}
// Protocol information
theJSON.append(" \"startClock\": " + startClock + ",\n");
theJSON.append(" \"playClock\": " + playClock + "\n");
theJSON.append("}");
return theJSON.toString();
}
public List<GdlSentence> getMostRecentMoves() {
if (moveHistory.size() == 0)
return null;
return moveHistory.get(moveHistory.size()-1);
}
public Set<GdlSentence> getMostRecentState() {
if (stateHistory.size() == 0)
return null;
return stateHistory.get(stateHistory.size()-1);
}
public String getGameName() {
return getGame().getName();
}
public String getGameRepositoryURL() {
return getGame().getRepositoryURL();
}
public String toString() {
return toJSON();
}
/* Simple accessors */
public String getMatchId() {
return matchId;
}
public String getRandomToken() {
return randomToken;
}
public String getSpectatorAuthToken() {
return spectatorAuthToken;
}
public Game getGame() {
return theGame;
}
public List<List<GdlSentence>> getMoveHistory() {
return moveHistory;
}
public List<Set<GdlSentence>> getStateHistory() {
return stateHistory;
}
public List<Date> getStateTimeHistory() {
return stateTimeHistory;
}
public int getPlayClock() {
return playClock;
}
public int getStartClock() {
return startClock;
}
public Date getStartTime() {
return startTime;
}
public List<String> getRoleNames() {
return theRoleNames;
}
public boolean isCompleted() {
return isCompleted;
}
public List<Integer> getGoalValues() {
return goalValues;
}
/* Static methods */
public static String getRandomString(int nLength) {
Random theGenerator = new Random();
String theString = "";
for (int i = 0; i < nLength; i++) {
int nVal = theGenerator.nextInt(62);
if (nVal < 26) theString += (char)('a' + nVal);
else if (nVal < 52) theString += (char)('A' + (nVal-26));
else if (nVal < 62) theString += (char)('0' + (nVal-52));
}
return theString;
}
private static String renderArrayAsJSON(List<?> theList, boolean useQuotes) {
String s = "[";
for (int i = 0; i < theList.size(); i++) {
Object o = theList.get(i);
// AppEngine-specific, not needed yet: if (o instanceof Text) o = ((Text)o).getValue();
if (o instanceof Date) o = ((Date)o).getTime();
if (useQuotes) s += "\"";
s += o.toString();
if (useQuotes) s += "\"";
if (i < theList.size() - 1)
s += ", ";
}
return s + "]";
}
private static List<String> renderStateHistory(List<Set<GdlSentence>> stateHistory) {
List<String> renderedStates = new ArrayList<String>();
for (Set<GdlSentence> aState : stateHistory) {
renderedStates.add(renderStateAsSymbolList(aState));
}
return renderedStates;
}
private static List<String> renderMoveHistory(List<List<GdlSentence>> moveHistory) {
List<String> renderedMoves = new ArrayList<String>();
for (List<GdlSentence> aMove : moveHistory) {
renderedMoves.add(renderArrayAsJSON(aMove, true));
}
return renderedMoves;
}
private static String renderStateAsSymbolList(Set<GdlSentence> theState) {
// Strip out the TRUE proposition, since those are implied for states.
String s = "( ";
for (GdlSentence sent : theState) {
String sentString = sent.toString();
s += sentString.substring(6, sentString.length()-2).trim() + " ";
}
return s + ")";
}
}