package org.ggp.base.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 java.util.concurrent.ThreadLocalRandom; import org.ggp.base.util.crypto.BaseCryptography.EncodedKeyPair; import org.ggp.base.util.crypto.SignableJSON; import org.ggp.base.util.game.Game; import org.ggp.base.util.game.RemoteGameRepository; import org.ggp.base.util.gdl.factory.GdlFactory; import org.ggp.base.util.gdl.factory.exceptions.GdlFormatException; import org.ggp.base.util.gdl.grammar.Gdl; import org.ggp.base.util.gdl.grammar.GdlConstant; import org.ggp.base.util.gdl.grammar.GdlFunction; import org.ggp.base.util.gdl.grammar.GdlRelation; import org.ggp.base.util.gdl.grammar.GdlSentence; import org.ggp.base.util.gdl.grammar.GdlTerm; import org.ggp.base.util.gdl.scrambler.GdlScrambler; import org.ggp.base.util.gdl.scrambler.MappingGdlScrambler; import org.ggp.base.util.gdl.scrambler.NoOpGdlScrambler; import org.ggp.base.util.statemachine.Move; import org.ggp.base.util.statemachine.Role; import org.ggp.base.util.symbol.factory.SymbolFactory; import org.ggp.base.util.symbol.factory.exceptions.SymbolFormatException; import org.ggp.base.util.symbol.grammar.SymbolList; import external.JSON.JSONArray; import external.JSON.JSONException; import external.JSON.JSONObject; /** * 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 String tournamentNameFromHost; private final int playClock; private final int startClock; private final int previewClock; private final Date startTime; private final Game theGame; private final List<List<GdlTerm>> moveHistory; private final List<Set<GdlSentence>> stateHistory; private final List<List<String>> errorHistory; private final List<Date> stateTimeHistory; private boolean isCompleted; private boolean isAborted; private final List<Integer> goalValues; private final int numRoles; private EncodedKeyPair theCryptographicKeys; private List<String> thePlayerNamesFromHost; private List<Boolean> isPlayerHuman; private GdlScrambler theGdlScrambler = new NoOpGdlScrambler(); public Match(String matchId, int previewClock, int startClock, int playClock, Game theGame, String tournamentNameFromHost) { this.matchId = matchId; this.tournamentNameFromHost = tournamentNameFromHost; this.previewClock = previewClock; 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.isAborted = false; this.numRoles = Role.computeRoles(theGame.getRules()).size(); this.moveHistory = new ArrayList<List<GdlTerm>>(); this.stateHistory = new ArrayList<Set<GdlSentence>>(); this.stateTimeHistory = new ArrayList<Date>(); this.errorHistory = new ArrayList<List<String>>(); this.goalValues = new ArrayList<Integer>(); } public Match(String theJSON, Game theGame, String authToken) 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; } if (theMatchObject.has("previewClock")) { this.previewClock = theMatchObject.getInt("previewClock"); } else { this.previewClock = -1; } this.startTime = new Date(theMatchObject.getLong("startTime")); this.randomToken = theMatchObject.getString("randomToken"); this.spectatorAuthToken = authToken; this.isCompleted = theMatchObject.getBoolean("isCompleted"); if (theMatchObject.has("isAborted")) { this.isAborted = theMatchObject.getBoolean("isAborted"); } else { this.isAborted = false; } if (theMatchObject.has("tournamentNameFromHost")) { this.tournamentNameFromHost = theMatchObject.getString("tournamentNameFromHost"); } else { this.tournamentNameFromHost = null; } this.numRoles = Role.computeRoles(this.theGame.getRules()).size(); this.moveHistory = new ArrayList<List<GdlTerm>>(); this.stateHistory = new ArrayList<Set<GdlSentence>>(); this.stateTimeHistory = new ArrayList<Date>(); this.errorHistory = new ArrayList<List<String>>(); JSONArray theMoves = theMatchObject.getJSONArray("moves"); for (int i = 0; i < theMoves.length(); i++) { List<GdlTerm> theMove = new ArrayList<GdlTerm>(); JSONArray moveElements = theMoves.getJSONArray(i); for (int j = 0; j < moveElements.length(); j++) { theMove.add(GdlFactory.createTerm(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))); } if (theMatchObject.has("errors")) { JSONArray theErrors = theMatchObject.getJSONArray("errors"); for (int i = 0; i < theErrors.length(); i++) { List<String> theMoveErrors = new ArrayList<String>(); JSONArray errorElements = theErrors.getJSONArray(i); for (int j = 0; j < errorElements.length(); j++) { theMoveErrors.add(errorElements.getString(j)); } errorHistory.add(theMoveErrors); } } 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 e) {} // TODO: Add a way to recover cryptographic public keys and signatures. // Or, perhaps loading a match into memory for editing should strip those? if (theMatchObject.has("playerNamesFromHost")) { thePlayerNamesFromHost = new ArrayList<String>(); JSONArray thePlayerNames = theMatchObject.getJSONArray("playerNamesFromHost"); for (int i = 0; i < thePlayerNames.length(); i++) { thePlayerNamesFromHost.add(thePlayerNames.getString(i)); } } if (theMatchObject.has("isPlayerHuman")) { isPlayerHuman = new ArrayList<Boolean>(); JSONArray isPlayerHumanArray = theMatchObject.getJSONArray("isPlayerHuman"); for (int i = 0; i < isPlayerHumanArray.length(); i++) { isPlayerHuman.add(isPlayerHumanArray.getBoolean(i)); } } } /* Mutators */ public void setCryptographicKeys(EncodedKeyPair k) { this.theCryptographicKeys = k; } public void enableScrambling() { theGdlScrambler = new MappingGdlScrambler(new Random(startTime.getTime())); for (Gdl rule : theGame.getRules()) { theGdlScrambler.scramble(rule); } } public void setPlayerNamesFromHost(List<String> thePlayerNames) { this.thePlayerNamesFromHost = thePlayerNames; } public List<String> getPlayerNamesFromHost() { return thePlayerNamesFromHost; } public void setWhichPlayersAreHuman(List<Boolean> isPlayerHuman) { this.isPlayerHuman = isPlayerHuman; } public void appendMoves(List<GdlTerm> 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. List<GdlTerm> theMoves = new ArrayList<GdlTerm>(); 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 appendErrors(List<String> errors) { errorHistory.add(errors); } public void appendNoErrors() { List<String> theNoErrors = new ArrayList<String>(); for (int i = 0; i < this.numRoles; i++) { theNoErrors.add(""); } errorHistory.add(theNoErrors); } public void markCompleted(List<Integer> theGoalValues) { this.isCompleted = true; if (theGoalValues != null) { this.goalValues.addAll(theGoalValues); } } public void markAborted() { this.isAborted = true; } /* Complex accessors */ public String toJSON() { JSONObject theJSON = new JSONObject(); try { theJSON.put("matchId", matchId); theJSON.put("randomToken", randomToken); theJSON.put("startTime", startTime.getTime()); theJSON.put("gameMetaURL", getGameRepositoryURL()); theJSON.put("isCompleted", isCompleted); theJSON.put("isAborted", isAborted); theJSON.put("states", new JSONArray(renderArrayAsJSON(renderStateHistory(stateHistory), true))); theJSON.put("moves", new JSONArray(renderArrayAsJSON(renderMoveHistory(moveHistory), false))); theJSON.put("stateTimes", new JSONArray(renderArrayAsJSON(stateTimeHistory, false))); if (!errorHistory.isEmpty()) { theJSON.put("errors", new JSONArray(renderArrayAsJSON(renderErrorHistory(errorHistory), false))); } if (!goalValues.isEmpty()) { theJSON.put("goalValues", goalValues); } theJSON.put("previewClock", previewClock); theJSON.put("startClock", startClock); theJSON.put("playClock", playClock); if (thePlayerNamesFromHost != null) { theJSON.put("playerNamesFromHost", thePlayerNamesFromHost); } if (isPlayerHuman != null) { theJSON.put("isPlayerHuman", isPlayerHuman); } if (tournamentNameFromHost != null) { theJSON.put("tournamentNameFromHost", tournamentNameFromHost); } theJSON.put("scrambled", theGdlScrambler != null ? theGdlScrambler.scrambles() : false); } catch (JSONException e) { return null; } if (theCryptographicKeys != null) { try { SignableJSON.signJSON(theJSON, theCryptographicKeys.thePublicKey, theCryptographicKeys.thePrivateKey); if (!SignableJSON.isSignedJSON(theJSON)) { throw new Exception("Could not recognize signed match: " + theJSON); } if (!SignableJSON.verifySignedJSON(theJSON)) { throw new Exception("Could not verify signed match: " + theJSON); } } catch (Exception e) { System.err.println(e); theJSON.remove("matchHostPK"); theJSON.remove("matchHostSignature"); } } return theJSON.toString(); } public String toXML() { try { JSONObject theJSON = new JSONObject(toJSON()); StringBuilder theXML = new StringBuilder(); theXML.append("<match>"); for (String key : JSONObject.getNames(theJSON)) { Object value = theJSON.get(key); if (value instanceof JSONObject) { throw new RuntimeException("Unexpected embedded JSONObject in match JSON with tag " + key + "; could not convert to XML."); } else if (!(value instanceof JSONArray)) { theXML.append(renderLeafXML(key, theJSON.get(key))); } else if (key.equals("states")) { theXML.append(renderStateHistoryXML(stateHistory)); } else if (key.equals("moves")) { theXML.append(renderMoveHistoryXML(moveHistory)); } else if (key.equals("errors")) { theXML.append(renderErrorHistoryXML(errorHistory)); } else { theXML.append(renderArrayXML(key, (JSONArray)value)); } } theXML.append("</match>"); return theXML.toString(); } catch (JSONException je) { return null; } } public List<GdlTerm> getMostRecentMoves() { if (moveHistory.isEmpty()) return null; return moveHistory.get(moveHistory.size()-1); } public Set<GdlSentence> getMostRecentState() { if (stateHistory.isEmpty()) return null; return stateHistory.get(stateHistory.size()-1); } public String getGameRepositoryURL() { return getGame().getRepositoryURL(); } @Override 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<GdlTerm>> getMoveHistory() { return moveHistory; } public List<Set<GdlSentence>> getStateHistory() { return stateHistory; } public List<Date> getStateTimeHistory() { return stateTimeHistory; } public List<List<String>> getErrorHistory() { return errorHistory; } public int getPreviewClock() { return previewClock; } public int getPlayClock() { return playClock; } public int getStartClock() { return startClock; } public Date getStartTime() { return startTime; } public String getTournamentNameFromHost() { return tournamentNameFromHost; } public boolean isCompleted() { return isCompleted; } public boolean isAborted() { return isAborted; } public List<Integer> getGoalValues() { return goalValues; } public GdlScrambler getGdlScrambler() { return theGdlScrambler; } /* Static methods */ public static final String getRandomString(int nLength) { Random theGenerator = ThreadLocalRandom.current(); 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; } /* JSON rendering methods */ private static final 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 final 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 final List<String> renderMoveHistory(List<List<GdlTerm>> moveHistory) { List<String> renderedMoves = new ArrayList<String>(); for (List<GdlTerm> aMove : moveHistory) { renderedMoves.add(renderArrayAsJSON(aMove, true)); } return renderedMoves; } private static final List<String> renderErrorHistory(List<List<String>> errorHistory) { List<String> renderedErrors = new ArrayList<String>(); for (List<String> anError : errorHistory) { renderedErrors.add(renderArrayAsJSON(anError, true)); } return renderedErrors; } private static final 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 + ")"; } /* XML Rendering methods -- these are horribly inefficient and are included only for legacy/standards compatibility */ private static final String renderLeafXML(String tagName, Object value) { return "<" + tagName + ">" + value.toString() + "</" + tagName + ">"; } private static final String renderMoveHistoryXML(List<List<GdlTerm>> moveHistory) { StringBuilder theXML = new StringBuilder(); theXML.append("<history>"); for (List<GdlTerm> move : moveHistory) { theXML.append("<move>"); for (GdlTerm action : move) { theXML.append(renderLeafXML("action", renderGdlToXML(action))); } theXML.append("</move>"); } theXML.append("</history>"); return theXML.toString(); } private static final String renderErrorHistoryXML(List<List<String>> errorHistory) { StringBuilder theXML = new StringBuilder(); theXML.append("<errorHistory>"); for (List<String> errors : errorHistory) { theXML.append("<errors>"); for (String error : errors) { theXML.append(renderLeafXML("error", error)); } theXML.append("</errors>"); } theXML.append("</errorHistory>"); return theXML.toString(); } private static final String renderStateHistoryXML(List<Set<GdlSentence>> stateHistory) { StringBuilder theXML = new StringBuilder(); theXML.append("<herstory>"); for (Set<GdlSentence> state : stateHistory) { theXML.append(renderStateXML(state)); } theXML.append("</herstory>"); return theXML.toString(); } public static final String renderStateXML(Set<GdlSentence> state) { StringBuilder theXML = new StringBuilder(); theXML.append("<state>"); for (GdlSentence sentence : state) { theXML.append(renderGdlToXML(sentence)); } theXML.append("</state>"); return theXML.toString(); } private static final String renderArrayXML(String tag, JSONArray arr) throws JSONException { StringBuilder theXML = new StringBuilder(); for (int i = 0; i < arr.length(); i++) { theXML.append(renderLeafXML(tag, arr.get(i))); } return theXML.toString(); } private static final String renderGdlToXML(Gdl gdl) { String rval = ""; if(gdl instanceof GdlConstant) { GdlConstant c = (GdlConstant)gdl; return c.getValue(); } else if(gdl instanceof GdlFunction) { GdlFunction f = (GdlFunction)gdl; if(f.getName().toString().equals("true")) { return "<fact>"+renderGdlToXML(f.get(0))+"</fact>"; } else { rval += "<relation>"+f.getName()+"</relation>"; for(int i=0; i<f.arity(); i++) rval += "<argument>"+renderGdlToXML(f.get(i))+"</argument>"; return rval; } } else if (gdl instanceof GdlRelation) { GdlRelation relation = (GdlRelation) gdl; if(relation.getName().toString().equals("true")) { for(int i=0; i<relation.arity(); i++) rval+="<fact>"+renderGdlToXML(relation.get(i))+"</fact>"; return rval; } else { rval+="<relation>"+relation.getName()+"</relation>"; for(int i=0; i<relation.arity(); i++) rval+="<argument>"+renderGdlToXML(relation.get(i))+"</argument>"; return rval; } } else { System.err.println("gdlToXML Error: could not handle "+gdl.toString()); return null; } } }