package org.ggp.base.server; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.ggp.base.server.event.ServerAbortedMatchEvent; import org.ggp.base.server.event.ServerCompletedMatchEvent; import org.ggp.base.server.event.ServerConnectionErrorEvent; import org.ggp.base.server.event.ServerIllegalMoveEvent; import org.ggp.base.server.event.ServerMatchUpdatedEvent; import org.ggp.base.server.event.ServerNewGameStateEvent; import org.ggp.base.server.event.ServerNewMatchEvent; import org.ggp.base.server.event.ServerNewMovesEvent; import org.ggp.base.server.event.ServerTimeEvent; import org.ggp.base.server.event.ServerTimeoutEvent; import org.ggp.base.server.threads.AbortRequestThread; import org.ggp.base.server.threads.PlayRequestThread; import org.ggp.base.server.threads.PreviewRequestThread; import org.ggp.base.server.threads.RandomPlayRequestThread; import org.ggp.base.server.threads.StartRequestThread; import org.ggp.base.server.threads.StopRequestThread; import org.ggp.base.util.gdl.grammar.GdlPool; import org.ggp.base.util.match.Match; import org.ggp.base.util.match.MatchPublisher; import org.ggp.base.util.observer.Event; import org.ggp.base.util.observer.Observer; import org.ggp.base.util.observer.Subject; import org.ggp.base.util.statemachine.MachineState; import org.ggp.base.util.statemachine.Move; import org.ggp.base.util.statemachine.Role; import org.ggp.base.util.statemachine.StateMachine; import org.ggp.base.util.statemachine.exceptions.GoalDefinitionException; import org.ggp.base.util.statemachine.exceptions.MoveDefinitionException; import org.ggp.base.util.statemachine.implementation.prover.ProverStateMachine; public final class GameServer extends Thread implements Subject { private final Match match; private final StateMachine stateMachine; private MachineState currentState; private final List<String> hosts; private final List<Integer> ports; private final boolean[] playerGetsUnlimitedTime; private final boolean[] playerPlaysRandomly; private final List<Observer> observers; private List<Move> previousMoves; private Map<Role,String> mostRecentErrors; private String saveToFilename; private String spectatorServerURL; private String spectatorServerKey; private boolean forceUsingEntireClock; public GameServer(Match match, List<String> hosts, List<Integer> ports) { this.match = match; this.hosts = hosts; this.ports = ports; playerGetsUnlimitedTime = new boolean[hosts.size()]; playerPlaysRandomly = new boolean[hosts.size()]; List<Role> roles = Role.computeRoles(match.getGame().getRules()); for (int r = 0; r < roles.size(); r++) { if (roles.get(r).getName() == GdlPool.RANDOM) { playerPlaysRandomly[r] = true; } } stateMachine = new ProverStateMachine(); stateMachine.initialize(match.getGame().getRules()); currentState = stateMachine.getInitialState(); previousMoves = null; mostRecentErrors = new HashMap<Role,String>(); match.appendState(currentState.getContents()); observers = new ArrayList<Observer>(); spectatorServerURL = null; forceUsingEntireClock = false; } public void startSavingToFilename(String theFilename) { saveToFilename = theFilename; } public String startPublishingToSpectatorServer(String theURL) { spectatorServerURL = theURL; return publishWhenNecessary(); } @Override public void addObserver(Observer observer) { observers.add(observer); } public List<Integer> getGoals() throws GoalDefinitionException { List<Integer> goals = new ArrayList<Integer>(); for (Role role : stateMachine.getRoles()) { goals.add(stateMachine.getGoal(currentState, role)); } return goals; } public StateMachine getStateMachine() { return stateMachine; } @Override public void notifyObservers(Event event) { for (Observer observer : observers) { observer.observe(event); } // Add error events to mostRecentErrors for recording. if (event instanceof ServerIllegalMoveEvent) { ServerIllegalMoveEvent sEvent = (ServerIllegalMoveEvent)event; mostRecentErrors.put(sEvent.getRole(), "IL " + sEvent.getMove()); } else if (event instanceof ServerTimeoutEvent) { ServerTimeoutEvent sEvent = (ServerTimeoutEvent)event; mostRecentErrors.put(sEvent.getRole(), "TO"); } else if (event instanceof ServerConnectionErrorEvent) { ServerConnectionErrorEvent sEvent = (ServerConnectionErrorEvent)event; mostRecentErrors.put(sEvent.getRole(), "CE"); } } // Should be called after each move, to collect all of the errors // caused by players and write them into the match description. private void appendErrorsToMatchDescription() { List<String> theErrors = new ArrayList<String>(); for (int i = 0; i < stateMachine.getRoles().size(); i++) { Role r = stateMachine.getRoles().get(i); if (mostRecentErrors.containsKey(r)) { theErrors.add(mostRecentErrors.get(r)); } else { theErrors.add(""); } } match.appendErrors(theErrors); mostRecentErrors.clear(); } @Override public void run() { try { if (match.getPreviewClock() >= 0) { sendPreviewRequests(); } notifyObservers(new ServerNewMatchEvent(stateMachine.getRoles(), currentState)); notifyObservers(new ServerTimeEvent(match.getStartClock() * 1000)); sendStartRequests(); appendErrorsToMatchDescription(); while (!stateMachine.isTerminal(currentState)) { publishWhenNecessary(); saveWhenNecessary(); notifyObservers(new ServerNewGameStateEvent(currentState)); notifyObservers(new ServerTimeEvent(match.getPlayClock() * 1000)); notifyObservers(new ServerMatchUpdatedEvent(match, spectatorServerKey, saveToFilename)); previousMoves = sendPlayRequests(); notifyObservers(new ServerNewMovesEvent(previousMoves)); currentState = stateMachine.getNextState(currentState, previousMoves); match.appendMoves2(previousMoves); match.appendState(currentState.getContents()); appendErrorsToMatchDescription(); if (match.isAborted()) { return; } } match.markCompleted(stateMachine.getGoals(currentState)); publishWhenNecessary(); saveWhenNecessary(); notifyObservers(new ServerNewGameStateEvent(currentState)); notifyObservers(new ServerCompletedMatchEvent(getGoals())); notifyObservers(new ServerMatchUpdatedEvent(match, spectatorServerKey, saveToFilename)); sendStopRequests(previousMoves); } catch (InterruptedException ie) { if (match.isAborted()) { return; } else { ie.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } } public void abort() { try { match.markAborted(); sendAbortRequests(); saveWhenNecessary(); publishWhenNecessary(); notifyObservers(new ServerAbortedMatchEvent()); notifyObservers(new ServerMatchUpdatedEvent(match, spectatorServerKey, saveToFilename)); } catch (Exception e) { e.printStackTrace(); } } private void saveWhenNecessary() { if (saveToFilename == null) { return; } try { File file = new File(saveToFilename); if (!file.exists()) { file.createNewFile(); } FileWriter fw = new FileWriter(file); BufferedWriter bw = new BufferedWriter(fw); bw.write(match.toJSON().toString()); bw.close(); fw.close(); } catch (IOException ie) { ie.printStackTrace(); } } private String publishWhenNecessary() { if (spectatorServerURL == null) { return null; } int nAttempt = 0; while (true) { try { spectatorServerKey = MatchPublisher.publishToSpectatorServer(spectatorServerURL, match); return spectatorServerKey; } catch (IOException e) { if (nAttempt > 9) { e.printStackTrace(); return null; } } nAttempt++; } } public String getSpectatorServerKey() { return spectatorServerKey; } private synchronized List<Move> sendPlayRequests() throws InterruptedException, MoveDefinitionException { List<PlayRequestThread> threads = new ArrayList<PlayRequestThread>(hosts.size()); for (int i = 0; i < hosts.size(); i++) { List<Move> legalMoves = stateMachine.getLegalMoves(currentState, stateMachine.getRoles().get(i)); if (playerPlaysRandomly[i]) { threads.add(new RandomPlayRequestThread(match, legalMoves)); } else { threads.add(new PlayRequestThread(this, match, previousMoves, legalMoves, stateMachine.getRoles().get(i), hosts.get(i), ports.get(i), getPlayerNameFromMatchForRequest(i), playerGetsUnlimitedTime[i])); } } for (PlayRequestThread thread : threads) { thread.start(); } if (forceUsingEntireClock) { Thread.sleep(match.getPlayClock() * 1000); } List<Move> moves = new ArrayList<Move>(); for (PlayRequestThread thread : threads) { thread.join(); moves.add(thread.getMove()); } return moves; } private synchronized void sendPreviewRequests() throws InterruptedException { List<PreviewRequestThread> threads = new ArrayList<PreviewRequestThread>(hosts.size()); for (int i = 0; i < hosts.size(); i++) { if (!playerPlaysRandomly[i]) { threads.add(new PreviewRequestThread(this, match, stateMachine.getRoles().get(i), hosts.get(i), ports.get(i), getPlayerNameFromMatchForRequest(i))); } } for (PreviewRequestThread thread : threads) { thread.start(); } if (forceUsingEntireClock) { Thread.sleep(match.getStartClock() * 1000); } for (PreviewRequestThread thread : threads) { thread.join(); } } private synchronized void sendStartRequests() throws InterruptedException { List<StartRequestThread> threads = new ArrayList<StartRequestThread>(hosts.size()); for (int i = 0; i < hosts.size(); i++) { if (!playerPlaysRandomly[i]) { threads.add(new StartRequestThread(this, match, stateMachine.getRoles().get(i), hosts.get(i), ports.get(i), getPlayerNameFromMatchForRequest(i))); } } for (StartRequestThread thread : threads) { thread.start(); } if (forceUsingEntireClock) { Thread.sleep(match.getStartClock() * 1000); } for (StartRequestThread thread : threads) { thread.join(); } } private synchronized void sendStopRequests(List<Move> previousMoves) throws InterruptedException { List<StopRequestThread> threads = new ArrayList<StopRequestThread>(hosts.size()); for (int i = 0; i < hosts.size(); i++) { if (!playerPlaysRandomly[i]) { threads.add(new StopRequestThread(this, match, previousMoves, stateMachine.getRoles().get(i), hosts.get(i), ports.get(i), getPlayerNameFromMatchForRequest(i))); } } for (StopRequestThread thread : threads) { thread.start(); } for (StopRequestThread thread : threads) { thread.join(); } } private void sendAbortRequests() throws InterruptedException { List<AbortRequestThread> threads = new ArrayList<AbortRequestThread>(hosts.size()); for (int i = 0; i < hosts.size(); i++) { if (!playerPlaysRandomly[i]) { threads.add(new AbortRequestThread(this, match, stateMachine.getRoles().get(i), hosts.get(i), ports.get(i), getPlayerNameFromMatchForRequest(i))); } } for (AbortRequestThread thread : threads) { thread.start(); } for (AbortRequestThread thread : threads) { thread.join(); } interrupt(); } public void givePlayerUnlimitedTime(int i) { playerGetsUnlimitedTime[i] = true; } public void makePlayerPlayRandomly(int i) { playerPlaysRandomly[i] = true; } // Why would you want to force the game server to wait for the entire clock? // This can be used to rate-limit matches, so that you don't overload supporting // services like the repository server, spectator server, players, etc. public void setForceUsingEntireClock() { forceUsingEntireClock = true; } public Match getMatch() { return match; } private String getPlayerNameFromMatchForRequest(int i) { if (match.getPlayerNamesFromHost() != null) { return match.getPlayerNamesFromHost().get(i); } else { return ""; } } }