package models; import com.fasterxml.jackson.databind.JsonNode; import models.event.BoardState; import models.event.GameOver; import models.event.IllegalMove; import models.event.ReadyToStart; import models.player.ConnectedPlayer; import models.player.PairedPlayer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import play.libs.F; import play.mvc.WebSocket; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.util.*; /** * Notifies players of the game start and performs coordination of the moves between peers. * * The caller is expected to call {@link Game#start} to notify peers of the * game start and install {@link WebSocket} message handlers. Next, each peer * move is directed to {@link Game#onMove(String, com.fasterxml.jackson.databind.JsonNode)}, * where the move is validated, board state is updated, game completion is checked, * and finally the board state update is sent back to the peers. * * Client-server messaging is performed in JSON messages described by * {@link models.event.Event} classes. * * @see models.event.Event */ @ThreadSafe public class Game { private final static Logger log = LoggerFactory.getLogger(Game.class); protected final String id = UUID.randomUUID().toString(); protected final Map<String, PairedPlayer> players; protected final ShutdownListener shutdownListener; @GuardedBy("this") protected String nextPlayerId; @GuardedBy("this") protected boolean started; public interface ShutdownListener { public void onGameShutdown(String gameId); } public Game( ConnectedPlayer upperPlayer, ConnectedPlayer lowerPlayer, ShutdownListener shutdownListener) { // Initialize players. PairedPlayer upperPairedPlayer = upperPlayer.upgrade(lowerPlayer.getId()); PairedPlayer lowerPairedPlayer = lowerPlayer.upgrade(upperPlayer.getId()); Map<String, PairedPlayer> players = new HashMap<>(); players.put(upperPairedPlayer.getId(), upperPairedPlayer); players.put(lowerPairedPlayer.getId(), lowerPairedPlayer); this.players = Collections.unmodifiableMap(players); // Set shutdown listener. this.shutdownListener = shutdownListener; // Initialize the next player id. this.nextPlayerId = upperPairedPlayer.getId(); // Turn started flag off. this.started = false; } public String getId() { return id; } public Collection<PairedPlayer> getPlayers() { return players.values(); } /** * Notifies peers of the game start and installs {@link WebSocket} message handlers. */ public synchronized void start() { if (!started) { for (final PairedPlayer player : players.values()) { new ReadyToStart(player.getOpponentId(), nextPlayerId) .write(player.getOutputSocket()); player.getInputSocket().onMessage(new F.Callback<JsonNode>() { @Override public void invoke(JsonNode move) throws Throwable { onMove(player.getId(), move); } }); player.getInputSocket().onClose(new F.Callback0() { @Override public void invoke() throws Throwable { shutdownListener.onGameShutdown(id); } }); } started = true; log.trace("{} is started. (Pair information is pushed.)", this); } } /** * Checks if game is over and invokes {@link Game#shutdownListener} on success. */ private synchronized void complete() { boolean over = false; for (PairedPlayer player : players.values()) if (player.isOver()) { over = true; break; } if (over) { int winnerScore = 0; String winnerId = null; for (PairedPlayer player : players.values()) { int score = player.score(); if (winnerScore < score) { winnerScore = score; winnerId = player.getId(); } } GameOver go = new GameOver(winnerId); for (PairedPlayer player : players.values()) { go.write(player.getOutputSocket()); } log.trace("{} is completed. Calling shutdown listener...", this); shutdownListener.onGameShutdown(id); } } /** * Validates the given move, updates board state, checks if game is over, and notifies peers. */ private synchronized void onMove(String playerId, int pos) { PairedPlayer player = players.get(playerId); WebSocket.Out<JsonNode> out = player.getOutputSocket(); int[] pits = player.getPits(); if (pos < 0 || pos > pits.length - 2) new IllegalMove("Invalid pit index: %d", pos).write(out); else if (pits[pos] < 1) new IllegalMove("No stones available at pit %d.", pos).write(out); else { int size = pits[pos]; pits[pos] = 0; for (int i = 0; i < size; i++) pits[(pos + i + 1) % pits.length]++; int lastPos = (pos + size) % pits.length; if (lastPos != pits.length - 1) { nextPlayerId = player.getOpponentId(); if (pits[lastPos] == 1) { int[] opponentsPits = players.get(player.getOpponentId()).getPits(); int opponentsPos = pits.length - lastPos - 2; int opponentsSize = opponentsPits[opponentsPos]; opponentsPits[opponentsPos] = 0; pits[lastPos] = 0; pits[pits.length - 1] += opponentsSize + 1; } } BoardState boardState = new BoardState(players.values(), nextPlayerId); for (PairedPlayer pairedPlayer : players.values()) boardState.write(pairedPlayer.getOutputSocket()); } complete(); } /** * Validates the given move and passes the control to {@link Game#onMove(String, int).} */ private synchronized void onMove(String playerId, JsonNode move) { ConnectedPlayer player = players.get(playerId); log.trace("New move from {}: {}", player, move); WebSocket.Out<JsonNode> out = player.getOutputSocket(); if (!nextPlayerId.equals(playerId)) new IllegalMove("It is opponent's turn.").write(out); else try { onMove(playerId, Integer.parseInt(move.asText())); } catch (NumberFormatException nfe) { new IllegalMove("Invalid pit index: %s", move).write(out); } } @Override public String toString() { return String.format("Game[%s]", id); } }