package net.sf.colossus.server; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.client.Client; import net.sf.colossus.client.Client.ConnectionInitException; import net.sf.colossus.common.Constants; import net.sf.colossus.common.IOptions; import net.sf.colossus.common.Options; import net.sf.colossus.common.WhatNextManager; import net.sf.colossus.common.WhatNextManager.WhatToDoNext; import net.sf.colossus.game.BattlePhase; import net.sf.colossus.game.Caretaker; import net.sf.colossus.game.Creature; import net.sf.colossus.game.Dice; import net.sf.colossus.game.DiceStatistics; import net.sf.colossus.game.EntrySide; import net.sf.colossus.game.Game; import net.sf.colossus.game.Legion; import net.sf.colossus.game.MovementServerSide; import net.sf.colossus.game.Phase; import net.sf.colossus.game.Player; import net.sf.colossus.game.PlayerColor; import net.sf.colossus.game.Proposal; import net.sf.colossus.game.actions.AddCreatureAction; import net.sf.colossus.game.actions.EditAddCreature; import net.sf.colossus.game.actions.Summoning; import net.sf.colossus.server.BattleServerSide.AngelSummoningStates; import net.sf.colossus.util.Glob; import net.sf.colossus.util.InstanceTracker; import net.sf.colossus.util.ViableEntityManager; import net.sf.colossus.variant.BattleHex; import net.sf.colossus.variant.CreatureType; import net.sf.colossus.variant.MasterBoardTerrain; import net.sf.colossus.variant.MasterHex; import net.sf.colossus.variant.Variant; import net.sf.colossus.webclient.RunGameInSameJVM; import net.sf.colossus.webclient.WebClient; import net.sf.colossus.xmlparser.TerrainRecruitLoader; import org.jdom.DataConversionException; import org.jdom.Element; /** * Class Game gets and holds high-level data about a Titan game. * * This is the old design with the game information in the server. Some * of the functionality here is supposed to be moved into the {@link net.sf.colossus.game.Game} * class which then can be shared between server and clients (the class, not the instances). * Other parts should be moved into the {@link Server} class or elsewhere. * * @author David Ripton * @author Bruce Sherrod * @author Romain Dolbeau */ public class GameServerSide extends Game { private static final Logger LOGGER = Logger.getLogger(GameServerSide.class .getName()); private int activePlayerNum; private int lastRecruitTurnNumber; private boolean battleInProgress; private boolean summoning; private boolean reinforcing; private boolean acquiring; private int pointsScored; private int turnCombatFinished; private Legion winner; private String engagementResult; private boolean pendingAdvancePhase; private boolean loadingGame; private boolean replayOngoing = false; private Server server; private boolean wasLoaded = false; // Negotiation private final Set<Proposal> attackerProposals = new HashSet<Proposal>(); private final Set<Proposal> defenderProposals = new HashSet<Proposal>(); private final LinkedList<Player> colorPickOrder = new LinkedList<Player>(); private List<PlayerColor> colorsLeft; private final PhaseAdvancer phaseAdvancer = new GamePhaseAdvancer(); private final Options options; private String hostingPlayerName = null; private String flagFilename = null; private INotifyWebServer notifyWebServer = null; private WebClient startingWebClient = null; private final WhatNextManager whatNextManager; private History history; private final BattleStrikeServerSide battleStrikeSS; private final MovementServerSide movementSS; /** * The object that handles the Game Saving procedure */ private final GameSaving gameSaver; /** * From the very first autosave file name, we derive the file * where to store all the messages that are sent to the internal * spectator client. */ private boolean isFirstAutoSave = true; /** * The file where to send the spectator messages. * Might/will be null when autosave disabled. */ private PrintWriter iscMessages = null; private static int gameCounter = 1; private final String gameId; private boolean hotSeatMode = false; // currently visible board-player (for hotSeatMode) private Player cvbPlayer = null; private final DiceStatistics diceStatCollector; /** Shortcut for UnitTests, * to create a Game with dummy input objects on the fly. */ static GameServerSide makeNewGameServerSide(Variant variant) { Options startOptions = new Options(Constants.OPTIONS_START); WhatNextManager whatNextManager = new WhatNextManager(startOptions); Options serverOptions = new Options("UnitTest", true); return new GameServerSide(whatNextManager, serverOptions, variant); } /** * For more complicated functional tests * @param whatNextMgr * @param serverOptions * @param variant * @return */ static public GameServerSide newGameServerSide( WhatNextManager whatNextMgr, Options serverOptions, Variant variant) { if (Options.isFunctionalTest()) { return new GameServerSideTestAccess(whatNextMgr, serverOptions, variant); } else { return new GameServerSide(whatNextMgr, serverOptions, variant); } } /** * The normal constructor to be used everywhere * @param whatNextMgr A WhatNextManager object which manages the main * control flow which thing to do 'next' when this game is over. * @param serverOptions The server side options, initialized from the * GetPlayers dialog and/or command line options. * @param variant Variant of this game */ public GameServerSide(WhatNextManager whatNextMgr, Options serverOptions, Variant variant) { super(variant, new String[0]); // later perhaps from command line, GUI, or WebServer set it? gameId = "#" + (gameCounter++); this.whatNextManager = whatNextMgr; this.options = serverOptions; this.gameSaver = new GameSaving(this, options); this.battleStrikeSS = new BattleStrikeServerSide(this); this.movementSS = new MovementServerSide(this, options); String statisticsFileName = options .getStringOption(Options.diceStatisticsFile); if (statisticsFileName != null) { this.diceStatCollector = new DiceStatistics(statisticsFileName); } else { this.diceStatCollector = null; } InstanceTracker.register(this, "Game at port " + getPort()); // The caretaker object was created by super(...) getCaretaker().addListener(new Caretaker.ChangeListener() { public void creatureTypeAvailabilityUpdated(CreatureType type, int availableCount) { updateCaretakerDisplaysFor(type); } public void creatureTypeDeadCountUpdated(CreatureType type, int deadCount) { updateCaretakerDisplaysFor(type); } public void creatureTypeCountsUpdated(CreatureType type) { updateCaretakerDisplaysFor(type); } public void fullUpdate() { updateCaretakerDisplays(); } }); } public void setFlagFilename(String flagFilename) { this.flagFilename = flagFilename; } public void setWasLoaded(boolean value) { this.wasLoaded = value; } public boolean wasLoaded() { return this.wasLoaded; } public String getHostingPlayer() { return hostingPlayerName; } private int getPort() { int port = options.getIntOption(Options.serveAtPort); if (port < 0) { port = Constants.defaultPort; } return port; } public DiceStatistics getDiceStatCollector() { return this.diceStatCollector; } private void initServer() { // create it even if not needed (=no web server). // This way we can have all the "if <there is a webserver>" // wrappers inside the notify Class, instead spread over the code... if (startingWebClient != null) { notifyWebServer = startingWebClient.getWhomToNotify(); } else { notifyWebServer = new NotifyWebServerViaFile(flagFilename); } if (server != null) { server.setObsolete(); server.disposeAllClients(); } server = new Server(this, whatNextManager, getPort()); if (startingWebClient != null) { // TODO get rid of this: // WebClient needs this to start the players local Client. startingWebClient.setLocalServer(server); } try { server.initFileServer(); server.initSocketServer(); boolean gotAll = startServerAndWaitUntilNotifiedThatWaitForClientsCompleted(); if (gotAll) { ViableEntityManager.register(this, "Server/Game " + gameId); // Tell WebClient to inform the WebServer that the game // was started (on this player's PC) and all clients have // connected (WebClients updates the status in bottom row // then). notifyWebServer.allClientsConnected(); if (startingWebClient != null) { // Tell WebClient to inform the WebServer that the game // was started (on this player's PC) and all clients have // connected (WebClients updates the status in bottom row // then). startingWebClient.informGameStartedLocally(); } } else { LOGGER.warning("waitForClients returned false, " + "indicating that not all clients connected."); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Server initialization got Exception " + e.getMessage(), e); } } Object waitForClientsMutex = new Object(); boolean serverGotAll = false; public void actOnWaitForClientsCompleted(boolean gotAll) { synchronized (waitForClientsMutex) { serverGotAll = gotAll; waitForClientsMutex.notify(); } } /* All between start the server and waiting for being notified is done * inside one synchronized block, so that server cannot notify us * before we are waiting on the mutex. */ private boolean startServerAndWaitUntilNotifiedThatWaitForClientsCompleted() { Runnable doCreateClients = new Runnable() { public void run() { server.createClientHandlerStub(); createLocalClients(); if (Constants._CREATE_LOCAL_DUMMY_CLIENT) { createInternalDummyClient(); } } }; Thread doCreateClientsThread = new Thread(doCreateClients); synchronized (waitForClientsMutex) { server.start(); notifyWebServer.readyToAcceptClients(); doCreateClientsThread.start(); try { waitForClientsMutex.wait(); } catch (InterruptedException e) { LOGGER.warning("main thread waiting for all clients to " + "connect got InterruptedException: " + e); } } return serverGotAll; } public void createLocalClients() { boolean atLeastOneBoardNeeded = whatNextManager.getStartOptions() .getOption(Options.FORCE_BOARD); // if there is at least one human alive, no need to create board for // an AI (makes difference if first player is set to AI and a later // one as human...) for (Player player : getPlayers()) { // Note: endsWith needed, because the constant is only "Human", // but the type is is the fully qualified class name! if (player.getType().endsWith(Constants.human) && !((PlayerServerSide)player).getDeadBeforeSave()) { atLeastOneBoardNeeded = false; break; } } for (Player player : getPlayers()) { String type = player.getType(); // getDeadBeforeSave to Game instead? if (!((PlayerServerSide)player).getDeadBeforeSave() && !type.endsWith(Constants.network)) { if (player.getName().equals(getHostingPlayer())) { LOGGER.info("Skipping creation of local client for " + "hosting player " + player.getName()); } else { boolean createGUI = !player.isAI(); if (atLeastOneBoardNeeded) { // Yes, only depending on the atLeast..., no matter // whether for this player true or not! // This relies on the fact that for cmdline setup // Human players are created before AI players. createGUI = true; atLeastOneBoardNeeded = false; } LOGGER.info("Creating local client for player " + player.getName() + ", type " + type); createLocalClient((PlayerServerSide)player, createGUI, type); } } } } private void createLocalClient(PlayerServerSide player, boolean createGUI, String type) { String playerName = player.getName(); boolean dontUseOptionsFile = player.isAI(); LOGGER.finest("Called Server.createLocalClient() for " + playerName); try { Client c = Client.createClient("127.0.0.1", getPort(), playerName, type, whatNextManager, server, false, dontUseOptionsFile, createGUI, false); storeLocalClient(playerName, c); } catch (ConnectionInitException e) { LOGGER.warning("Creating local client for player " + playerName + " failed, reason " + e.getMessage()); } } private void createInternalDummyClient() { String playerName = Constants.INTERNAL_DUMMY_CLIENT_NAME; LOGGER.finest("Called Server.createLocalClient() for " + playerName); try { Client c = Client.createClient("127.0.0.1", getPort(), playerName, Constants.aiPackage + Constants.human, whatNextManager, server, false, true, false, true); storeLocalClient(playerName, c); } catch (ConnectionInitException e) { LOGGER.warning("Creating local _internal_dummy_ client" + " failed, reason " + e.getMessage()); } } protected void storeLocalClient(String playerName, Client c) { LOGGER.finest("Created local client with name " + playerName + ", isNull: " + (c == null)); // Dummy in here, does something in GameServerSideTestAccess } /** * Update the dead and available counts for a creature type on all clients. */ private void updateCaretakerDisplaysFor(CreatureType type) { if (replayOngoing) { return; } if (server != null) { server.allUpdateCreatureCount(type, getCaretaker() .getAvailableCount(type), getCaretaker().getDeadCount(type)); } } /** * Update the dead and available counts for all creature types on all clients. */ public void updateCaretakerDisplays() { if (replayOngoing) { return; } for (CreatureType type : getVariant().getCreatureTypes()) { updateCaretakerDisplaysFor(type); } } protected void waitUntilGameFinishes() { server.waitUntilGameFinishes(); } private void cleanupWhenGameOver() { server.waitUntilGameFinishes(); if (diceStatCollector != null) { for (Player p : getPlayers()) { diceStatCollector.printStatistics(p); } } server.doCleanup(); server = null; ViableEntityManager.unregister(this); } private void clearFlags() { clearEngagementData(); battleInProgress = false; summoning = false; reinforcing = false; acquiring = false; pendingAdvancePhase = false; loadingGame = false; engagementResult = null; } private void addPlayersFromOptions() { for (int i = 0; i < VariantSupport.getMaxPlayers(); i++) { String name = options.getStringOption(Options.playerName + i); String type = options.getStringOption(Options.playerType + i); if (name != null && type != null && !type.equals(Constants.none)) { createAndAddPlayer(name, type); LOGGER.info("Added " + type + " player " + name); } } // No longer need the player name and type options // - except if needed for next stresstest round. if (!Options.isStresstest()) { options.clearPlayerInfo(); } getVariant().getCreatureByName("Titan").setMaxCount(getNumPlayers()); } public boolean startNewGameAndWaitUntilOver(String hostingPlayer) { boolean ok = newGame(hostingPlayer); if (ok) { // Main thread (or the Runnable when started by WebClient locally?) // has now nothing to do any more, can wait until game finishes. // TODO if runnable by WebClient, is there even need to wait ? cleanupWhenGameOver(); } return ok; } /** Start a new game. */ boolean newGame(String hostingPlayer) { hostingPlayerName = hostingPlayer; // In case game was started by WebClient locally on user's // computer, WebClient cannot be passed into the GameServerSide // (to do so, the IStartHandler interface would need to import the // WebClient class, and then we would have a cyclic dependency). // So, the initiating WebClient stores itself into a static // variable which we query here. startingWebClient = RunGameInSameJVM.getInitiatingWebClient(); clearFlags(); turnNumber = 1; lastRecruitTurnNumber = -1; setPhase(Phase.SPLIT); players.clear(); VariantSupport.loadVariantByName( options.getStringOption(Options.variant), true); LOGGER.info("Starting new game"); CustomRecruitBase.resetAllInstances(); CustomRecruitBase.setGame(this); addPlayersFromOptions(); // reset the caretaker after we have the players to get the right Titan counts getCaretaker().resetAllCounts(); hotSeatMode = options.getOption(Options.hotSeatMode); history = new History(); // initServer returns after all clients have connected // (or if server startup failed for some reason) initServer(); // Some more stuff is done from newGame2() when the last // expected client has connected. if (!server.isServerRunning()) { LOGGER.warning("Server startup failed: doing cleanup!"); server.doCleanup(); server = null; return false; } // Inform caller that startup went ok return true; } /* Called from the last ClientHandler connecting * ( = when expected nr. of clients has connected). */ void newGame2() { syncOptions(); server.allInitBoard(); assignTowers(); // Renumber players in descending tower order. sortPlayersDescendingTower(); activePlayerNum = 0; assignColors(); } /** * Temporary solution ... I do not know a better way how to do the * sorting on players (type List<Player>) itself. * I don't want to pull up the Comparator predicate, because * ClientSide might have totally different idea of the right "order"... */ private void sortPlayersDescendingTower() { List<PlayerServerSide> playersSS = new ArrayList<PlayerServerSide>(); for (Player p : getPlayers()) { playersSS.add((PlayerServerSide)p); } Collections.sort(playersSS); players.clear(); for (PlayerServerSide p : playersSS) { players.add(p); } } private boolean nameIsTaken(String name, Player checkedPlayer) { for (int i = 0; i < getNumPlayers(); i++) { Player player = players.get(i); if (player.getName().equals(name) && !player.equals(checkedPlayer)) { return true; } } return false; } /** If the name is taken, add random digits to the end. */ String getUniqueName(final String name, Player player) { if (!nameIsTaken(name, player)) { return name; } return getUniqueName(name + Dice.rollDie(), player); } /** Find a Player for a new remote client. * If loading a game then this is the network player with a matching * player name. mustAlreadyExist is set for that case. * If a new game, it's the first network player whose name is still * set to <By client> (mustAlreadyExist is given as false) * If it's spectator, it won't find anything and simply return null. * * @param mustAlreadyExist Do not consider <By client> matching. */ Player findNetworkPlayer(final String playerName, boolean mustAlreadyExist) { for (int i = 0; i < getNumPlayers(); i++) { Player curPlayer = players.get(i); if (curPlayer.getType().endsWith(Constants.network)) { // during loading or reconnect, real name must already be set if (mustAlreadyExist) { if (curPlayer.getName().equals(playerName)) { return curPlayer; } } else { if (curPlayer.getName().startsWith(Constants.byClient)) { curPlayer.setName(playerName); return curPlayer; } } } } return null; } /** Send all current game option values to all clients. */ private void syncOptions() { Enumeration<String> en = options.propertyNames(); while (en.hasMoreElements()) { String name = en.nextElement(); String value = options.getStringOption(name); server.allSyncOption(name, value); } // Synchronize also some global ones (as 'false') if they are not // defined, to override them on client // (client cfg file might still contain them e.g. from earlier games) for (String optName : Options.globalGameOptions) { if (options.isOptionUndefined(optName)) { server.allSyncOption(optName, false); } } } private void assignColors() { List<PlayerColor> cli = new ArrayList<PlayerColor>( Arrays.asList(PlayerColor.values())); colorsLeft = new ArrayList<PlayerColor>(); /* Add the first 6 colors in random order, ... */ for (int i = 0; i < Constants.DEFAULT_MAX_PLAYERS; i++) { colorsLeft.add(cli.remove(Dice .rollDie(Constants.DEFAULT_MAX_PLAYERS - i) - 1)); } /* ... and finish with the newer ones, also in random order */ int newer = cli.size(); for (int i = 0; i < newer; i++) { colorsLeft.add(cli.remove(Dice.rollDie(newer - i) - 1)); } // Let human players pick colors first, followed by AI players. // Within each group, players pick colors in ascending tower order. colorPickOrder.clear(); for (int i = getNumPlayers() - 1; i >= 0; i--) { Player player = players.get(i); if (player.isHuman()) { colorPickOrder.add(player); } } for (int i = getNumPlayers() - 1; i >= 0; i--) { Player player = players.get(i); if (player.isAI()) { colorPickOrder.add(player); } } nextPickColor(); } void nextPickColor() { if (colorPickOrder.size() >= 1) { Player playerName = colorPickOrder.getFirst(); server.askPickColor(playerName, colorsLeft); } else { // All players are done picking colors; continue. newGame3(); } } private String makeNameByType(String templateName, String type) { String number = templateName.substring(Constants.byType.length()); // type is the full class name of client, e.g. // "net.sf.colossus.ai.SimpleAI" String prefix = Constants.aiPackage; int len = prefix.length(); String shortName = type.substring(len); String newName; if (shortName.equals("Human")) { newName = "Human" + number; } else if (shortName.equals("SimpleAI")) { newName = "Simple" + number; } else if (shortName.equals("CowardSimpleAI")) { newName = "Coward" + number; } else if (shortName.equals("RationalAI")) { newName = "Rational" + number; } else if (shortName.equals("HumanHaterRationalAI")) { newName = "Hater" + number; } else if (shortName.equals("MilvangAI")) { newName = "Milvang" + number; } else if (shortName.equals("ExperimentalAI")) { newName = "Experimental" + number; } else if (shortName.equals("ParallelEvaluatorAI")) { newName = "ParallelEvaluator" + number; } else { newName = null; } return newName; } void assignColor(Player player, PlayerColor color) { colorPickOrder.remove(player); colorsLeft.remove(color); ((PlayerServerSide)player).setColor(color); String type = ((PlayerServerSide)player).getType(); String gotName = player.getName(); if (gotName.startsWith(Constants.byType)) { String newName = makeNameByType(gotName, type); if (newName != null) { LOGGER.info("Setting for \"" + gotName + "\" new name: " + newName); server.setPlayerName(player, newName); player.setName(newName); } else { LOGGER.log(Level.WARNING, "Type " + type + " not recognized" + ". Giving name by color instead (" + color + ")"); gotName = Constants.byColor; } } if (gotName.startsWith(Constants.byColor)) { server.setPlayerName(player, color.getName()); player.setName(color.getName()); } LOGGER.info(player + " chooses color " + color); ((PlayerServerSide)player).initMarkersAvailable(); server.allUpdatePlayerInfo(false, "AssignColor"); server.askPickFirstMarker(player); } Player getNextColorPicker() { return colorPickOrder.getFirst(); } /** Done picking player colors; proceed to start game. */ private void newGame3() { server.allUpdatePlayerInfo("NewGame3.1"); for (Player p : getPlayers()) { PlayerServerSide player = (PlayerServerSide)p; placeInitialLegion(player, player.getFirstMarker()); server.allRevealLegion(player.getLegions().get(0), Constants.reasonInitial); server .allUpdatePlayerInfo("NewGame3.2-loop-player-" + p.getName()); } server.allTellAllLegionLocations(); server.allSetupTurnState(); updateCaretakerDisplays(); setupSplit(); gameSaver.commitPointReached(); autoSave(); server.allRequestConfirmCatchup("KickstartGame", false); } /** Randomize towers by rolling dice and rerolling ties. */ private void assignTowers() { int numPlayers = getNumPlayers(); MasterHex[] playerTower = new MasterHex[numPlayers]; Set<MasterHex> towerSet = getVariant().getMasterBoard().getTowerSet(); // first create a list with all tower hexes List<MasterHex> towerList = new ArrayList<MasterHex>(towerSet); if (getOption(Options.balancedTowers)) { towerList = getBalancedTowers(numPlayers, towerList); } int playersLeft = numPlayers - 1; while ((playersLeft >= 0) && (!towerList.isEmpty())) { int which = Dice.rollDie(towerList.size()); playerTower[playersLeft] = towerList.remove(which - 1); playersLeft--; } for (int i = 0; i < numPlayers; i++) { Player player = players.get(i); LOGGER.info(player + " gets tower " + playerTower[i]); player.setStartingTower(playerTower[i]); } } /** Return a list with a balanced order of numPlayer towers chosen from towerList, which must hold numeric strings. */ static List<MasterHex> getBalancedTowers(int numPlayers, final List<MasterHex> towerList) { int numTowers = towerList.size(); if (numPlayers > numTowers) { LOGGER.log(Level.SEVERE, "More players than towers!"); return towerList; } // Make a sorted copy, converting String to Integer. ArrayList<Integer> numericList = new ArrayList<Integer>(); for (MasterHex tower : towerList) { Integer i = new Integer(tower.getLabel()); numericList.add(i); } Collections.sort(numericList); double towersPerPlayer = (double)numTowers / numPlayers; // First just find a balanced sequence starting at zero. double counter = 0.0; int numDone = 0; List<Integer> sequence = new ArrayList<Integer>(); // Prevent floating-point roundoff error. double epsilon = 0.0000001; while (numDone < numPlayers) { sequence.add(Integer.valueOf((int)Math.floor(counter + epsilon))); numDone++; counter += towersPerPlayer; } // Pick a random starting point. (Zero-based) int startingTower = Dice.rollDie(numTowers) - 1; // Offset the sequence by the starting point, and get only // the number of starting towers we need. List<MasterHex> returnList = new ArrayList<MasterHex>(); Iterator<Integer> it = sequence.iterator(); numDone = 0; while (it.hasNext() && numDone < numPlayers) { Integer raw = it.next(); int cooked = (raw.intValue() + startingTower) % numTowers; Integer numericLabel = numericList.get(cooked); returnList.add(VariantSupport.getCurrentVariant().getMasterBoard() .getHexByLabel(numericLabel.toString())); numDone++; } return returnList; } Server getServer() { return server; } PlayerServerSide createAndAddPlayer(String name, String shortTypeName) { PlayerServerSide player = new PlayerServerSide(name, this, shortTypeName); addPlayer(player); return player; } // Only meant for GameSaving! int getActivePlayerNum() { return activePlayerNum; } Player getActivePlayer() { // Sanity check in case called before all players are loaded. if (activePlayerNum < players.size()) { return players.get(activePlayerNum); } else { return null; } } // TODO store only in GameSS, not in PlayerSS private void makeMovementRoll(String reason) { Player player = getActivePlayer(); int roll = ((PlayerServerSide)player).rollMovement(reason); // update in game.Game: setMovementRoll(roll); getServer().allTellMovementRoll(roll, reason); movementRollEvent(player, roll); } @Override public int getMovementRoll() { return ((PlayerServerSide)getActivePlayer()).getMovementRoll(); } /** * Resolve playerName into Player object. Name might be null, * then returns null. * @param playerName * @return The player object for given player name, null if name was null */ Player getPlayerByNameIgnoreNull(String playerName) { if (playerName == null) { return null; } else { return getPlayerByName(playerName); } } /** * Resolve playerName into Player object. Name must not be null. * * If no player for given name found, it would throw IllegalArgumentException * (well, did earlier, at the moment (03/2012) replace with SEVERE error; * exception would totally throw server thread out of the orbit...) * @param playerName * @return Player object for given name. */ Player getPlayerByName(String playerName) { assert playerName != null : "Name for player to find must not be null!"; for (Player player : players) { if (player.getName().equals(playerName)) { return player; } } LOGGER.severe("No player object found for name '" + playerName + "'"); return null; /* throw new IllegalArgumentException("No player object found for name '" + playerName + "'"); */ } /** * NOTE: to be used only during loading a Game! * Client side has a more sophisticated version that takes * slain players and their inherited markers into account. */ Player getPlayerByShortColor(String shortColor) { if (shortColor != null) { for (Player player : getPlayers()) { if (shortColor.equals(player.getShortColor())) { return player; } } } return null; } /** * A player requested he wants to withdraw (or connection was lost, and * server socket handling does withdraw then). * * @param player The player that wishes to withdraw from the game * * TODO Notify all players. */ void handlePlayerWithdrawal(Player player) { String name = player.getName(); if (player.isDead()) { LOGGER.log(Level.FINE, "Nothing to do for withdrawal of player " + name + " - is already dead."); return; } LOGGER.log(Level.FINE, "Player " + name + " withdraws from the game."); // If player quits while engaged, set slayer. Player slayer = null; Legion legion = player.getTitanLegion(); if (legion != null && containsOpposingLegions(legion.getCurrentHex())) { slayer = getFirstEnemyLegion(legion.getCurrentHex(), player) .getPlayer(); } ((PlayerServerSide)player).die(slayer); checkForVictory(); // checks if game over state is reached, and if yes, announces so; // and returns false. // Otherwise it returns true and that means game shall go on. if (gameShouldContinue()) { if (player == getActivePlayer()) { advancePhase(getPhase(), player); } } } private Player getWinner() { int remaining = 0; Player result = null; for (Player player : getPlayers()) { if (!player.isDead()) { remaining++; if (remaining > 1) { return null; } else { result = player; } } } return result; } // TODO Up to game.Game or not? // In practice it is only needed in server side. void checkForVictory() { if (isGameOver()) { LOGGER .severe("checkForVictory called although game is already over!!"); return; } int remaining = getNumLivingPlayers(); switch (remaining) { case 0: LOGGER.info("Reached game over state -- Draw at " + new Date().getTime()); setGameOver(true, "Draw"); break; case 1: String winnerName = getWinner().getName(); LOGGER.info("Reached game over state -- " + winnerName + " wins at " + new Date().getTime()); setGameOver(true, winnerName + " wins"); break; default: break; } } private void announceGameOver(boolean disposeFollows, boolean suspended) { server.allFullyUpdateAllLegionContents(Constants.reasonGameOver); LOGGER.info("Announcing: Game over -- " + getGameOverMessage()); server .allTellGameOver(getGameOverMessage(), disposeFollows, suspended); } boolean isLoadingGame() { return loadingGame; } boolean isReplayOngoing() { return replayOngoing; } public void stopAllDueToFunctionalTestCompleted() { server.doSetWhatToDoNext(WhatToDoNext.QUIT_ALL, true); LOGGER.info("kickstartGame - before stop server running"); server.stopServerRunning(); server.cleanupStartlog(); } public void kickstartGame() { LOGGER.info("All clients have caught up with loading/replay or " + "pickColor, now kicking off the Game!"); if (Options.isStartupTest()) { LOGGER.info("IS ONLY A STARTUP TEST!"); LOGGER.info("Game will exit after 2 seconds again."); WhatNextManager.sleepFor(2000); stopAllDueToFunctionalTestCompleted(); } else { notifyTestCaseGameIsUpNow(); LOGGER.finer("NOT A STARTUP TEST - kickstarting game!"); server.kickPhase(); } } protected void notifyTestCaseGameIsUpNow() { // dummy in here, overriden in Test Access } /** * Advance to the next phase, only if the passed oldPhase and playerName * are current. */ void advancePhase(final Phase oldPhase, final Player player) { if (oldPhase != phase) { LOGGER.severe("Player " + player + " called advancePhase illegally (reason: " + "oldPhase (" + oldPhase + ") != phase (" + phase + "))"); return; } if (pendingAdvancePhase) { LOGGER .severe("Player " + player + " called advancePhase illegally (reason: pendingAdvancePhase is true)"); return; } if (!player.equals(getActivePlayer())) { LOGGER.severe("Player " + player + " called advancePhase illegally (reason: " + "wrong player [" + player + " vs. " + getActivePlayer() + "])"); return; } if (getOption(Options.autoStop) && onlyAIsRemain() && !isGameOver()) { LOGGER.info("Not advancing because no humans remain"); setGameOver(true, "All humans eliminated"); } if (gameShouldContinue()) { phaseAdvancer.advancePhase(); } } public void handleSuspend() { setGameOver(true, "Game suspended! Please close your board."); announceGameOver(true, true); } @Override public void setGameOver(boolean gameOver, String message) { super.setGameOver(gameOver, message); if (startingWebClient != null) { startingWebClient.informLocallyGameOver(); } } /** Wrap the complexity of phase advancing. */ class GamePhaseAdvancer implements PhaseAdvancer { /** Advance to the next phase, only if the passed oldPhase and * playerName are current. */ public void advancePhase() { pendingAdvancePhase = true; advancePhaseInternal(); } /** Advance to the next phase, with no error checking. */ public void advancePhaseInternal() { Phase oldPhase = phase; if (oldPhase == Phase.SPLIT) { setPhase(Phase.MOVE); } else if (oldPhase == Phase.MOVE) { // skip phase totally if there aren't any engagements. if (findEngagements().size() > 0) { setPhase(Phase.FIGHT); } else { setPhase(Phase.MUSTER); } } else if (oldPhase == Phase.FIGHT) { setPhase(Phase.MUSTER); } if (oldPhase == Phase.MUSTER || (getActivePlayer().isDead() && getNumLivingPlayers() > 0)) { advanceTurn(); } else { LOGGER.info("Phase advances to " + phase); } pendingAdvancePhase = false; if (isPhase(Phase.SPLIT)) { server.allSetupTurnState(); } // A new phase starts. // First, set it up and create commit point (take snapshot) setupPhase(); gameSaver.commitPointReached(); // Next, initial actions in that phase. So far, only for move. if (isPhase(Phase.MOVE)) { makeMovementRoll(Constants.reasonNormalRoll); } if (isAutoSavePoint()) { autoSave(); } // Inform Client now it's his time to act. server.kickPhase(); } /** * Make the next player being the activePlayer, and set phase to Split. * If that next player is dead, advance again (recursively). */ public void advanceTurn() { clearFlags(); activePlayerNum++; if (activePlayerNum == getNumPlayers()) { activePlayerNum = 0; turnNumber++; if (turnNumber - lastRecruitTurnNumber > 100 && Options.isStresstest()) { LOGGER.info("\nLast recruiting is 100 turns ago - " + "exiting to prevent AIs from endlessly " + "running around...\n"); System.exit(0); } } // TODO Used only by Balrog, and for Balrog it should rather be // done directly whenever score of a player changes /* notify all CustomRecruitBase objects that we change the * active player, for bookkeeping purpose */ CustomRecruitBase.everyoneAdvanceTurn(activePlayerNum); setPhase(Phase.SPLIT); if (getActivePlayer().isDead() && getNumLivingPlayers() > 0) { advanceTurn(); } else { LOGGER.info(getActivePlayer() + "'s turn, number " + turnNumber); } } } private void setupPhase() { if (isPhase(Phase.SPLIT)) { setupSplit(); } else if (isPhase(Phase.MOVE)) { setupMove(); } else if (isPhase(Phase.FIGHT)) { setupFight(); } else if (isPhase(Phase.MUSTER)) { setupMuster(); } else { LOGGER.log(Level.SEVERE, "Bogus phase"); } } private void setupSplit() { Player player = getActivePlayer(); if (player == null) { LOGGER.log(Level.SEVERE, "No players"); dispose(); return; } ((PlayerServerSide)player).resetTurnState(); server.allSetupSplit(); if (hotSeatMode) { hotSeatModeChangeBoards(); } } private void hotSeatModeChangeBoards() { Player activePlayer = getActivePlayer(); // game just started - find the local player which shall // get the board first, and hide all other local ones. if (cvbPlayer == null) { int i; for (i = 0; i < getNumPlayers(); i++) { Player iPlayer = players.get(i); if (iPlayer.isLocalHuman() && !server.isClientGone(iPlayer)) { // This is a local alive player. if (cvbPlayer == null) { cvbPlayer = iPlayer; server.setBoardVisibility(iPlayer, true); } else { server.setBoardVisibility(iPlayer, false); } } } return; } // otherwise, switch board to next, then and only then // if activePlayer is now the next local human which is // still connected ( = has not closed his board). if (activePlayer.isLocalHuman() && !server.isClientGone(activePlayer)) { server.setBoardVisibility(cvbPlayer, false); server.setBoardVisibility(activePlayer, true); cvbPlayer = activePlayer; } } private void setupMove() { server.allSetupMove(); } private void setupFight() { server.allSetupFight(); server.nextEngagement(); } private void setupMuster() { Player player = getActivePlayer(); ((PlayerServerSide)player).removeEmptyLegions(); // If a player has been eliminated we can't count on his client // still being around to advance the turn. if (player.isDead()) { advancePhase(Phase.MUSTER, player); } else { server.allSetupMuster(); } } /** * 11/2015: Save now at begin of each phase * @return Whether now is a time when autosave is due, * in practice returns ATM always true. */ private boolean isAutoSavePoint() { return true; } void autoSave() { if (getOption(Options.autosave) && !isGameOver()) { gameSaver.saveGameWithErrorHandling(null, true); if (isFirstAutoSave) { isFirstAutoSave = false; iscMessages = gameSaver.createIscmFile(); } } } public PrintWriter getIscMessageFile() { return iscMessages; } void saveGameWithErrorHandling(String filename, boolean autoSave) { gameSaver.saveGameWithErrorHandling(filename, autoSave); } public boolean loadGameAndWaitUntilOver(Element root) { boolean ok = loadGame(root); if (ok) { // Main thread (or the Runnable when started by WebClient locally?) // has now nothing to do any more, can wait until game finishes. // TODO if runnable by WebClient, is there even need to wait ? cleanupWhenGameOver(); } return ok; } // JDOM lacks generics, so we need casts @SuppressWarnings("unchecked") public boolean loadGame(Element root) { CustomRecruitBase.resetAllInstances(); CustomRecruitBase.setGame(this); try { // Reset flags that are not in the savegame file. clearFlags(); loadingGame = true; Element el = root.getChild("TurnNumber"); turnNumber = Integer.parseInt(el.getTextTrim()); // not quite the same as it was when saved, but the idea of lastRTN // is only to prevent stresstest games from hanging forever... lastRecruitTurnNumber = turnNumber; el = root.getChild("CurrentPlayer"); activePlayerNum = Integer.parseInt(el.getTextTrim()); el = root.getChild("CurrentPhase"); setPhase(Phase.fromInt(Integer.parseInt(el.getTextTrim()))); Element ct = root.getChild("Caretaker"); List<Element> kids = ct.getChildren(); Iterator<Element> it = kids.iterator(); while (it.hasNext()) { el = it.next(); String creatureName = el.getAttribute("name").getValue(); int remaining = el.getAttribute("remaining").getIntValue(); int dead = el.getAttribute("dead").getIntValue(); CreatureType type = getVariant().getCreatureByName( creatureName); getCaretaker().setAvailableCount(type, remaining); getCaretaker().setDeadCount(type, dead); } players.clear(); if (battle != null) { server.allCleanupBattle(); } // Players List<Element> playerElements = root.getChildren("Player"); for (Element pla : playerElements) { String name = pla.getAttribute("name").getValue(); String type = pla.getAttribute("type").getValue(); PlayerServerSide player = createAndAddPlayer(name, type); String colorName = pla.getAttribute("color").getValue(); player.setColor(PlayerColor.getByName(colorName)); String towerLabel = pla.getAttribute("startingTower") .getValue(); player.setStartingTower(getVariant().getMasterBoard() .getHexByLabel(towerLabel)); int score = pla.getAttribute("score").getIntValue(); player.setScore(score); // Don't use the normal "dead" attribute - will be set // during replay... boolean dead = pla.getAttribute("dead").getBooleanValue(); player.setDeadBeforeSave(dead); int mulligansLeft = pla.getAttribute("mulligansLeft") .getIntValue(); player.setMulligansLeft(mulligansLeft); // TODO what about the donor value? Just summoned is // good enough, so that at least one cannot summon // twice in same engagements-phase, // but not good enough to save a game in mid-battle // in particular in battle turn 4. player.setSummoned(pla.getAttribute("summoned") .getBooleanValue()); String playersElim = pla.getAttribute("colorsElim").getValue(); if ("null".contains(playersElim)) { playersElim = ""; } player.setPlayersElim(playersElim); List<Element> legionElements = pla.getChildren("Legion"); Iterator<Element> it2 = legionElements.iterator(); while (it2.hasNext()) { Element leg = it2.next(); readLegion(leg, player); } } // Battle stuff // TODO if the loading of a battle would be moved into the BattleServerSide class, then // the AngelSummoningStates and LegionTags enums could be private (possibly with some holes // for testing) Element bat = root.getChild("Battle"); if (bat != null) { String engagementHexLabel = bat.getAttribute("masterHexLabel") .getValue(); MasterHex engagementHex = getVariant().getMasterBoard() .getHexByLabel(engagementHexLabel); int battleTurnNum = bat.getAttribute("turnNumber") .getIntValue(); String battleActivePlayerName = bat.getAttribute( "activePlayer").getValue(); BattlePhase battlePhase = BattlePhase.values()[bat .getAttribute("phase").getIntValue()]; AngelSummoningStates summonState = AngelSummoningStates .valueOf(bat.getAttribute("summonState").getValue()); int carryDamage = bat.getAttribute("carryDamage") .getIntValue(); boolean preStrikeEffectsApplied = bat.getAttribute( "preStrikeEffectsApplied").getBooleanValue(); List<Element> cts = bat.getChildren("CarryTarget"); Set<BattleHex> carryTargets = new HashSet<BattleHex>(); Iterator<Element> it2 = cts.iterator(); while (it2.hasNext()) { Element cart = it2.next(); carryTargets.add(engagementHex.getTerrain().getHexByLabel( cart.getTextTrim())); } Player attackingPlayer = getActivePlayer(); Legion attacker = getFirstFriendlyLegion(engagementHex, attackingPlayer); Legion defender = getFirstEnemyLegion(engagementHex, attackingPlayer); BattleServerSide.LegionTags activeLegionTag; if (battleActivePlayerName.equals(attackingPlayer.getName())) { activeLegionTag = BattleServerSide.LegionTags.ATTACKER; } else { activeLegionTag = BattleServerSide.LegionTags.DEFENDER; } createBattle(attacker, defender, activeLegionTag, engagementHex, battlePhase); getBattleSS().setBattleTurnNumber(battleTurnNum); getBattleSS().setSummonState(summonState); getBattleSS().setCarryDamage(carryDamage); getBattleSS().setPreStrikeEffectsApplied( preStrikeEffectsApplied); getBattleSS().setCarryTargets(carryTargets); } // Backup Legion data and wipe it out, so that history // starts from a clean table. After history reply, we compare // whether the replay result matches this "loaded data". for (Player buPlayer : getPlayers()) { ((PlayerServerSide)buPlayer).backupLoadedData(); } // Load history (RedoLog stuff is handled later) history = new History(root); initServer(); // Remaining stuff has been moved to loadGame2() if (!server.isServerRunning()) { LOGGER.warning("Server startup failed: doing cleanup!"); server.doCleanup(); server = null; return false; } // Some more stuff is done from loadGame2() when the last // expected client has connected. // Inform caller that loading and starting game went ok: return true; } catch (Exception ex) { LOGGER.log(Level.SEVERE, ">>> Whoaah! Exception while " + "tried to load savegame:", ex); dispose(); return false; } } public void createBattle(Legion attacker, Legion defender, BattleServerSide.LegionTags activeLegionTag, MasterHex engagementHex, BattlePhase battlePhase) { battle = new BattleServerSide(this, attacker, defender, activeLegionTag, engagementHex, battlePhase); } // JDOM lacks generics, so we need casts @SuppressWarnings("unchecked") private void readLegion(Element leg, PlayerServerSide player) throws DataConversionException { String markerId = leg.getAttribute("name").getValue(); String currentHexLabel = leg.getAttribute("currentHex").getValue(); MasterHex currentHex = getVariant().getMasterBoard().getHexByLabel( currentHexLabel); String startingHexLabel = leg.getAttribute("startingHex").getValue(); MasterHex startingHex = getVariant().getMasterBoard().getHexByLabel( startingHexLabel); boolean moved = leg.getAttribute("moved").getBooleanValue(); EntrySide entrySide = EntrySide.values()[leg.getAttribute("entrySide") .getIntValue()]; String parentId = leg.getAttribute("parent").getValue(); if (parentId.equals("null")) { parentId = null; } CreatureType recruit = null; String recruitName = leg.getAttribute("recruitName").getValue(); if (recruitName != null && !recruitName.equals("null")) { recruit = getVariant().getCreatureByName(recruitName); } int battleTally = leg.getAttribute("battleTally").getIntValue(); // Critters // find legion for them, if it doesn't exist create one LegionServerSide legion = (LegionServerSide)player .getLegionByMarkerId(markerId); if (legion == null) { // TODO can there ever be a legion before? If not: collect all data // first (including critters) and then create the legion in one go Legion parentLegion = player.getLegionByMarkerId(parentId); legion = new LegionServerSide(markerId, parentLegion, currentHex, startingHex, player, this); player.addLegion(legion); } else { LOGGER.warning("Legion for marker does already exist?"); } List<Element> creatureElements = leg.getChildren("Creature"); for (Element cre : creatureElements) { String name = cre.getAttribute("name").getValue(); CreatureServerSide critter = new CreatureServerSide(getVariant() .getCreatureByName(name), null, this); // Battle stuff if (cre.getAttribute("hits") != null) { int hits = cre.getAttribute("hits").getIntValue(); critter.setHits(hits); MasterBoardTerrain terrain = getVariant().getMasterBoard() .getHexByLabel(currentHexLabel).getTerrain(); String currentBattleHexLabel = cre.getAttribute("currentHex") .getValue(); BattleHex currentBattleHex = terrain .getHexByLabel(currentBattleHexLabel); critter.setCurrentHex(currentBattleHex); String startingBattleHexLabel = cre .getAttribute("startingHex").getValue(); BattleHex startingBattleHex = terrain .getHexByLabel(startingBattleHexLabel); critter.setStartingHex(startingBattleHex); boolean struck = cre.getAttribute("struck").getBooleanValue(); critter.setStruck(struck); } legion.addCritter(critter); } legion.setMoved(moved); legion.setRecruit(recruit); legion.setEntrySide(entrySide); legion.addToBattleTally(battleTally); } /* Called from the last ClientHandler connecting * ( = when expected nr. of clients has connected). */ boolean loadGame2() { server.allSetColor(); syncOptions(); server.allUpdatePlayerInfo(true, "LoadGameStart"); replayOngoing = true; server.allTellReplay(true, turnNumber); server.allInitBoard(); // XXX // System.out // .println("\n################\nFiring history events from XML"); history.fireEventsFromXML(server); // System.out // .println("################\nCompleted Firing history events from XML\n"); boolean ok = resyncBackupData(); LOGGER.info("Loading and resync result: " + ok); if (!ok) { LOGGER.severe("Loading and resync failed - Aborting!!"); return false; } for (Player player : getPlayers()) { ((PlayerServerSide)player).computeMarkersAvailable(); } server.allFullyUpdateLegionStatus(); server.allUpdatePlayerInfo(false, "LoadGameCompleted"); // to initialize the 'lastFecthed' values server.allUpdateChangedPlayerValues("LoadGameCompleted"); server.allTellAllLegionLocations(); updateCaretakerDisplays(); server.allSetupTurnState(); setupPhase(); // Create an initial snapshot gameSaver.commitPointReached(); server.allTellRedo(true); // XXX // System.out.println("\n################\nProcessing redo log "); history.processRedoLog(server); // XXX // System.out // .println("################\nCompleted Processing redo log\n"); server.allTellRedo(false); server.allTellReplay(false, 0); replayOngoing = false; if (battle != null) { getBattleSS().setServer(getServer()); getBattleSS().init(); } CustomRecruitBase.initCustomVariantForAllCRBs(); server.allRequestConfirmCatchup("KickstartGame", false); updateCaretakerDisplays(); return ok; } private boolean resyncBackupData() { boolean allOk = true; for (Player player : getPlayers()) { allOk = allOk && ((PlayerServerSide)player).resyncBackupData(); } return allOk; } /** * Return a list of eligible recruits, as Creatures. * * TODO second parameter is probably superfluous */ List<CreatureType> findEligibleRecruits(Legion legion, MasterHex hex) { List<CreatureType> recruits; MasterBoardTerrain terrain = hex.getTerrain(); recruits = new ArrayList<CreatureType>(); List<CreatureType> tempRecruits = TerrainRecruitLoader .getPossibleRecruits(terrain, hex); List<CreatureType> recruiters = TerrainRecruitLoader .getPossibleRecruiters(terrain, hex); for (CreatureType creature : tempRecruits) { for (CreatureType lesser : recruiters) { if ((TerrainRecruitLoader.numberOfRecruiterNeeded(lesser, creature, terrain, hex) <= ((LegionServerSide)legion) .numCreature(lesser)) && (recruits.indexOf(creature) == -1)) { recruits.add(creature); } } } // Make sure that the potential recruits are available. Iterator<CreatureType> it = recruits.iterator(); while (it.hasNext()) { CreatureType recruit = it.next(); if (getCaretaker().getAvailableCount(recruit) < 1) { it.remove(); } } return recruits; } /** Return a list of eligible recruiter creatures. */ private List<CreatureType> findEligibleRecruiters(Legion legion, String recruitName) { List<CreatureType> recruiters; CreatureType recruit = getVariant().getCreatureByName(recruitName); if (recruit == null) { return new ArrayList<CreatureType>(); } MasterHex hex = legion.getCurrentHex(); MasterBoardTerrain terrain = hex.getTerrain(); recruiters = TerrainRecruitLoader.getPossibleRecruiters(terrain, hex); Iterator<CreatureType> it = recruiters.iterator(); while (it.hasNext()) { CreatureType possibleRecruiter = it.next(); int needed = TerrainRecruitLoader.numberOfRecruiterNeeded( possibleRecruiter, recruit, terrain, hex); if (needed < 1 || needed > ((LegionServerSide)legion) .numCreature(possibleRecruiter)) { // Zap this possible recruiter. it.remove(); } } return recruiters; } /** * Return true if this legion can recruit this recruit * without disclosing a recruiter. */ private boolean anonymousRecruitLegal(Legion legion, CreatureType recruit) { return TerrainRecruitLoader.anonymousRecruitLegal(recruit, legion .getCurrentHex().getTerrain(), legion.getCurrentHex()); } /** Add recruit to legion. */ void doRecruit(Legion legion, CreatureType recruit, CreatureType recruiter) { if (recruit == null) { LOGGER.log(Level.SEVERE, "null recruit in Game.doRecruit()"); return; } // Check for recruiter legality. List<CreatureType> recruiters = findEligibleRecruiters(legion, recruit.getName()); if (recruiter == null) { // If recruiter can be anonymous, then this is okay. if (!anonymousRecruitLegal(legion, recruit)) { LOGGER.log(Level.SEVERE, "null recruiter in Game.doRecruit()"); // XXX Let it go for now Should return later } else { LOGGER.finest("null recruiter okay"); } } else if (!recruiters.contains(recruiter)) { LOGGER.log(Level.SEVERE, "Illegal recruiter " + recruiter.getName() + " for recruit " + recruit.getName()); return; } lastRecruitTurnNumber = turnNumber; if (((LegionServerSide)legion).addCreature(recruit, true)) { MasterHex hex = legion.getCurrentHex(); int numRecruiters = 0; if (recruiter != null) { // Mark the recruiter(s) as visible. numRecruiters = TerrainRecruitLoader.numberOfRecruiterNeeded( recruiter, recruit, hex.getTerrain(), hex); } LOGGER.info("Legion " + legion + " in " + hex.getDescription() + " recruited " + recruit.getName() + " with " + (recruiter == null ? "nothing" : numRecruiters + " " + (numRecruiters > 1 ? recruiter.getPluralName() : recruiter.getName()))); recruitEvent(legion, recruit, recruiter); // Recruits are one to a customer. legion.setRecruit(recruit); reinforcing = false; } else { LOGGER.finest("Did not add creature " + recruit.getName() + " - none left in caretaker to take!"); } } public void editModeAddCreature(String markerId, String creatureName) { CreatureType creature = getVariant().getCreatureByName(creatureName); Player player = getPlayerByMarkerId(markerId); LegionServerSide legion = (LegionServerSide)player .getLegionByMarkerId(markerId); AddCreatureAction event = new EditAddCreature(legion, creature); legion.addCreature(creature, true); server.allTellAddCreature(event, true, Constants.reasonEdit); } public void editModeRemoveCreature(String markerId, String creatureName) { CreatureType creature = getVariant().getCreatureByName(creatureName); Player player = getPlayerByMarkerId(markerId); LegionServerSide legion = (LegionServerSide)player .getLegionByMarkerId(markerId); legion.editRemoveCreature(creature); server.allTellRemoveCreature(legion, creature, true, Constants.reasonEdit); } public void editModeRelocateLegion(String markerId, String hexLabel) { Player player = getPlayerByMarkerId(markerId); LegionServerSide legion = (LegionServerSide)player .getLegionByMarkerId(markerId); MasterHex hex = getVariant().getMasterBoard().getHexByLabel(hexLabel); legion.setCurrentHex(hex); server.allTellLegionLocation(legion); history.relocateLegionEvent(legion); } /** * If the legion can acquire (height < 7), find out which acquirable it * might get for the pointsToAdd, and fire off the askAcquirable messages. * @param legion Legion which earned the points and thus is entitled to * get the acqirable * @param scoreBeforeAdd Score from which to start * @param pointsToAdd How many points were earned */ public void acquireMaybe(LegionServerSide legion, int scoreBeforeAdd, int pointsToAdd) { if (legion.getHeight() < 7) { // calculate and set them as pending legion.setupAcquirableDecisions(scoreBeforeAdd, pointsToAdd); // make the server send the ask... to the client legion.askAcquirablesDecisions(); } } private final Object disposeMutex = new Object(); private boolean disposeOngoing = false; void dispose() { synchronized (disposeMutex) { if (disposeOngoing) { LOGGER.warning("Thread " + Thread.currentThread().getName() + ": Trying to dispose game a 2nd time?"); return; } disposeOngoing = true; } LOGGER.info("GSS: Disposing game (thread " + Thread.currentThread().getName() + ")"); if (server != null) { LOGGER.info("GSS: Stop Server running"); server.stopServerRunning(); } if (notifyWebServer != null) { LOGGER .info("GSS: Notfifying Web Server that server stopped running."); notifyWebServer.serverStoppedRunning(); notifyWebServer = null; } } private void placeInitialLegion(PlayerServerSide player, String markerId) { String name = player.getName(); player.selectMarkerId(markerId); LOGGER.info(name + " selects initial marker"); // Lookup coords for chit starting from player[i].getTower() MasterHex hex = player.getStartingTower(); LegionServerSide legion = getStartingLegion(markerId, hex, player); player.addLegion(legion); } public boolean hasConventionalMove(LegionServerSide legion, MasterHex hex, int roll, boolean ignoreFriends) { return !movementSS.listNormalMoves(legion, hex, roll, ignoreFriends, null, false).isEmpty(); } void createSummonAngel(Legion attacker) { if (!isGameOver()) { summoning = true; server.createSummonAngel(attacker); } } /** Called locally and from Battle. */ void reinforce(Legion legion) { reinforcing = true; server.reinforce(legion); } void doneReinforcing() { reinforcing = false; checkEngagementDone(); } /** * Handles summoning of a creature. * * @param event The summoning event (or null if summoning is to be skipped) * * TODO get rid of downcasts */ void doSummon(Summoning event) { PlayerServerSide player = (PlayerServerSide)getActivePlayer(); if (event != null && ((LegionServerSide)event.getLegion()).canSummonAngel()) { Legion legion = event.getLegion(); // Only one angel can be summoned per turn. player.setSummoned(true); Legion donor = event.getDonor(); player.setDonor(((LegionServerSide)donor)); // Move the angel or archangel. CreatureType angel = event.getAddedCreatureType(); ((LegionServerSide)donor).removeCreature(angel, false, false); ((LegionServerSide)legion).addCreature(angel, false); server.allTellRemoveCreature(donor, angel, true, Constants.reasonSummon); server.allTellAddCreature(event, true, Constants.reasonSummon); server.allTellDidSummon(legion, donor, angel); LOGGER.info("One " + angel + " is summoned from legion " + donor + " into legion " + legion); } // Need to call this regardless to advance past the summon phase. if (battle != null) { getBattleSS().finishSummoningAngel(player.hasSummoned()); summoning = false; } else { summoning = false; checkEngagementDone(); } } BattleServerSide getBattleSS() { return (BattleServerSide)battle; } boolean isBattleInProgress() { return battleInProgress; } History getHistory() { return history; } void finishBattle(MasterHex masterHex, boolean attackerEntered, int points, int turnDone) { getBattleSS().cleanRefs(); battle = null; server.allCleanupBattle(); LegionServerSide battleWinner = null; // Handle any after-battle angel summoning or recruiting. if (getNumLegions(masterHex) == 1) { battleWinner = (LegionServerSide)getFirstLegion(masterHex); // Make all creatures in the victorious legion visible. server.allRevealLegion(battleWinner, Constants.reasonWinner); // Remove battle info from winning legion and its creatures. battleWinner.clearBattleInfo(); if (battleWinner.getPlayer() == getActivePlayer()) { // Attacker won, so possibly summon angel. if (battleWinner.canSummonAngel()) { createSummonAngel(battleWinner); } } else { // Defender won, so possibly recruit reinforcement. if (attackerEntered && battleWinner.canRecruit()) { LOGGER .finest("Calling Game.reinforce() from Game.finishBattle()"); reinforce(battleWinner); } } } battleInProgress = false; getCaretaker().resurrectImmortals(); updateCaretakerDisplays(); setEngagementResult(Constants.erMethodFight, battleWinner, points, turnDone); checkEngagementDone(); } /** Return true and call Server.didSplit() if the split succeeded. * Return false if it failed. */ boolean doSplit(Legion parent, String childId, List<CreatureType> creaturesToSplit) { PlayerServerSide player = (PlayerServerSide)parent.getPlayer(); // Need a legion marker to split. if (!player.isMarkerAvailable(childId)) { LOGGER.log(Level.SEVERE, "Marker " + childId + " is not available."); String available = Glob.glob(",", player.getMarkersAvailable()); String notAvail = Glob.glob(",", player.getMarkersUsed()); LOGGER.warning("Available: " + available + ": not available " + notAvail); return false; } // Pre-split legion must have 4+ creatures. if (((LegionServerSide)parent).getHeight() < 4) { LOGGER.log(Level.SEVERE, "Legion " + parent + " is too short to split."); return false; } if (creaturesToSplit == null) { LOGGER .finest("Empty split list (" + parent + ", " + childId + ")"); return false; } // Each legion must have 2+ creatures after the split. if (creaturesToSplit.size() < 2 || ((LegionServerSide)parent).getHeight() - creaturesToSplit.size() < 2) { LOGGER.finest("Too small/big split list (" + parent + ", " + childId + ")"); return false; } List<CreatureType> tempCreatureTypes = new ArrayList<CreatureType>(); for (Creature creature : parent.getCreatures()) { tempCreatureTypes.add(creature.getType()); } for (CreatureType creatureType : creaturesToSplit) { if (!tempCreatureTypes.remove(creatureType)) { LOGGER.finest("Unavailable creature in split list (" + parent + ", " + childId + ") : " + creatureType.getName()); return false; } } if (getTurnNumber() == 1) { // Only allow a single split on turn 1. if (player.getLegions().size() > 1) { LOGGER.log(Level.SEVERE, "Cannot split twice on Turn 1."); return false; } // Each stack must contain exactly 4 creatures. if (creaturesToSplit.size() != 4) { return false; } // Each stack must contain exactly 1 lord. int numLords = 0; for (CreatureType creature : creaturesToSplit) { if (creature.isLord()) { numLords++; } } if (numLords != 1) { return false; } } Legion newLegion = ((LegionServerSide)parent).split(creaturesToSplit, childId); if (newLegion == null) { return false; } server.allTellDidSplit(parent, newLegion, getTurnNumber(), true); // viewableAll depends on the splitPrediction to tell then true contents, // and viewableOwn it does not harm; it only helps the AIs :) String viewModeOpt = options.getStringOption(Options.viewMode); int viewModeOptNum = options.getNumberForViewMode(viewModeOpt); if (viewModeOptNum == Options.viewableAllNum || viewModeOptNum == Options.viewableOwnNum) { server.allRevealLegion(parent, Constants.reasonSplit); server.allRevealLegion(newLegion, Constants.reasonSplit); } else { server.oneRevealLegion(parent, player, Constants.reasonSplit); server.oneRevealLegion(newLegion, player, Constants.reasonSplit); } return true; } /** Move the legion to the hex if legal. Return a string telling * the reason why it is illegal, or null if ok and move was done. */ String doMove(Legion legion, MasterHex hex, EntrySide entrySide, boolean teleport, CreatureType teleportingLord) { assert legion != null : "Legion must not be null"; String reason = null; Player player = legion.getPlayer(); int roll = ((PlayerServerSide)player).getMovementRoll(); if (roll < 0) { LOGGER.severe("getMovementRoll returned negative roll number: " + roll); return "Internal Error!"; } // Verify that the move is legal. if (teleport) { if ((reason = movementSS.isValidTeleportMove(legion, hex, roll)) != null) { return reason; } } else { if ((reason = movementSS.isValidNormalMove(legion, hex, player, roll)) != null) { return reason; } } // Verify that the entry side is legal. if ((reason = movementSS.isValidEntrySide(legion, hex, teleport, entrySide)) != null) { return reason; } // If this is a tower hex, the only entry side is the bottom. if (hex.getTerrain().hasStartList() && !entrySide.equals(EntrySide.BOTTOM)) { LOGGER.log(Level.WARNING, "Tried to enter invalid side of tower"); entrySide = EntrySide.BOTTOM; } // If the legion teleported, reveal a lord. if (teleport) { // Verify teleporting lord. if (teleportingLord == null || !((LegionServerSide)legion).listTeleportingLords(hex) .contains(teleportingLord)) { // boil out if assertions are enabled -- gives a stacktrace assert false : "Illegal teleport"; if (teleportingLord == null) { return "teleportingLord null"; } return "list of telep. lords " + ((LegionServerSide)legion).listTeleportingLords(hex) .toString() + " does not contain '" + teleportingLord + "'"; } List<CreatureType> creatures = new ArrayList<CreatureType>(); creatures.add(teleportingLord); server.allRevealCreatures(legion, creatures, Constants.reasonTeleport); } ((LegionServerSide)legion).moveToHex(hex, entrySide, teleport, teleportingLord); legionMoveEvent(legion, hex, entrySide, teleport, teleportingLord); return null; } void undoMove(Legion legion) { MasterHex formerHex = legion.getCurrentHex(); PlayerServerSide activePlayer = (PlayerServerSide)getActivePlayer(); activePlayer.undoMove(legion); MasterHex currentHex = legion.getCurrentHex(); // TODO calculate on client side instead where needed // needed in undidMove to decide whether to dis/enable button boolean splitLegionHasForcedMove = activePlayer .splitLegionHasForcedMove(); legionUndoMoveEvent(legion); server.allTellUndidMove(legion, formerHex, currentHex, splitLegionHasForcedMove); } void engage(MasterHex hex) { // Do not allow clicking on engagements if one is // already being resolved. if (containsOpposingLegions(hex) && !isEngagementOngoing()) { Player player = getActivePlayer(); Legion attacker = getFirstFriendlyLegion(hex, player); Legion defender = getFirstEnemyLegion(hex, player); createEngagement(hex, attacker, defender); server.allTellEngagement(hex, attacker, defender); ((LegionServerSide)attacker).sortCritters(); ((LegionServerSide)defender).sortCritters(); server.oneRevealLegion(attacker, defender.getPlayer(), Constants.reasonEngaged); server.oneRevealLegion(defender, attacker.getPlayer(), Constants.reasonEngaged); if (defender.canFlee()) { // Fleeing gives half points and denies the // attacker the chance to summon an angel. server.askFlee(defender, attacker); } else { engage2(hex); } } else { if (getEngagement() != null) { LOGGER.warning("illegal call to Game.engage() for " + hex.getDescription() + ": engagement " + "ongoing: " + getEngagement().toString()); } else { LOGGER.warning("illegal call to Game.engage(): " + "no opponent legion in hex " + hex.getDescription() + "; hex contains: " + getLegionsByHex(hex).toString()); } } } // Defender did not flee; attacker may concede early. private void engage2(MasterHex hex) { Player player = getActivePlayer(); Legion attacker = getFirstFriendlyLegion(hex, player); Legion defender = getFirstEnemyLegion(hex, player); server.askConcede(attacker, defender); } // Attacker did not concede early; negotiate. private void engage3(MasterHex hex) { Player player = getActivePlayer(); Legion attacker = getFirstFriendlyLegion(hex, player); Legion defender = getFirstEnemyLegion(hex, player); attackerProposals.clear(); defenderProposals.clear(); server.twoNegotiate(attacker, defender); } void flee(Legion legion) { Legion attacker = getFirstEnemyLegion(legion.getCurrentHex(), legion.getPlayer()); handleConcession(legion, attacker, true); } void concede(Legion attacker) { if (battleInProgress) { getBattleSS().concede(attacker.getPlayer()); } else { Legion defender = getFirstEnemyLegion(attacker.getCurrentHex(), attacker.getPlayer()); handleConcession(attacker, defender, false); } } void doNotFlee(Legion legion) { engage2(legion.getCurrentHex()); } /** Used only for pre-battle attacker concession. */ void doNotConcede(Legion legion) { if (isGameOver()) { return; } engage3(legion.getCurrentHex()); } /** playerName offers proposal. */ void makeProposal(String playerName, String proposalString) { // If it's too late to negotiate, just throw this away. if (battleInProgress) { return; } Proposal proposal = Proposal.makeFromString(proposalString, this); final Set<Proposal> ourProposals; final Set<Proposal> opponentProposals; if (playerName.equals(getActivePlayer().getName())) { ourProposals = attackerProposals; opponentProposals = defenderProposals; } else { ourProposals = defenderProposals; opponentProposals = attackerProposals; } // If this player wants to fight, cancel negotiations. if (proposal.isFight()) { Legion attacker = proposal.getAttacker(); fight(attacker.getCurrentHex()); } // If this proposal matches an earlier one from the other player, // settle the engagement. else if (opponentProposals.contains(proposal)) { handleNegotiation(proposal); } // Otherwise remember this proposal and continue. else { ourProposals.add(proposal); Player other = null; if (playerName.equals(getActivePlayer().getName())) { Legion defender = proposal.getDefender(); other = defender.getPlayer(); } else { other = getActivePlayer(); } // Tell the other player about the proposal. server.tellProposal(other, proposal); } } void fight(MasterHex hex) { if (!battleInProgress) { Player player = getActivePlayer(); Legion attacker = getFirstFriendlyLegion(hex, player); Legion defender = getFirstEnemyLegion(hex, player); // If the second player clicks Fight from the negotiate // dialog late, just exit. if (attacker == null || defender == null) { return; } battleInProgress = true; // Reveal both legions to all players. server.allRevealEngagedLegion(attacker, true, Constants.reasonBattleStarts); server.allRevealEngagedLegion(defender, false, Constants.reasonBattleStarts); createBattle(attacker, defender, BattleServerSide.LegionTags.DEFENDER, hex, BattlePhase.MOVE); getBattleSS().init(); } } private void handleConcession(Legion loser, Legion winner, boolean fled) { // Figure how many points the victor receives. int points = ((LegionServerSide)loser).getPointValue(); if (fled) { points /= 2; LOGGER.info("Legion " + loser + " flees from legion " + winner); } else { LOGGER.info("Legion " + loser + " concedes to legion " + winner); } // Add points, and angels if necessary. ((PlayerServerSide)winner.getPlayer()).awardPoints(points, (LegionServerSide)winner, fled); // @TODO: probably the truncating is not needed at all here? // Remove any fractional points. ((PlayerServerSide)winner.getPlayer()).truncScore(); // Need to grab the player reference before the legion is // removed. Player losingPlayer = loser.getPlayer(); String reason = fled ? Constants.reasonFled : Constants.reasonConcession; server.allRevealEngagedLegion(loser, losingPlayer.equals(getActivePlayer()), reason); // server.allRemoveLegion(loser.getMarkerId()); // If this was the titan stack, its owner dies and gives half // points to the victor. if (((LegionServerSide)loser).hasTitan()) { // first remove dead legion, then the rest. Cannot do the // loser.remove outside/before the if (or would need to store // the hasTitan information as extra boolean) ((LegionServerSide)loser).remove(); ((PlayerServerSide)losingPlayer).die(winner.getPlayer()); checkForVictory(); } else { // simply remove the dead legion. ((LegionServerSide)loser).remove(); } // No recruiting or angel summoning is allowed after the // defender flees or the attacker concedes before entering // the battle. String method = fled ? Constants.erMethodFlee : Constants.erMethodConcede; setEngagementResult(method, winner, points, 0); checkEngagementDone(); } private void handleNegotiation(Proposal results) { LegionServerSide attacker = (LegionServerSide)results.getAttacker(); LegionServerSide defender = (LegionServerSide)results.getDefender(); LegionServerSide negotiatedWinner = null; int points = 0; if (results.isMutual()) { boolean attackerHasTitan = attacker.hasTitan(); boolean defenderHasTitan = defender.hasTitan(); // Remove both legions and give no points. attacker.remove(); defender.remove(); LOGGER.info(attacker + " and " + defender + " agree to mutual elimination"); // If both Titans died, eliminate both players. if (attackerHasTitan && defenderHasTitan) { // Make defender die first, to simplify turn advancing. defender.getPlayer().die(null); attacker.getPlayer().die(null); checkForVictory(); } // If either was the titan stack, its owner dies and gives // half points to the victor. else if (attackerHasTitan) { attacker.getPlayer().die(defender.getPlayer()); checkForVictory(); } else if (defenderHasTitan) { defender.getPlayer().die(attacker.getPlayer()); checkForVictory(); } } else { // One legion was eliminated during negotiations. negotiatedWinner = (LegionServerSide)results.getWinner(); LegionServerSide loser; if (negotiatedWinner == defender) { loser = attacker; } else { loser = defender; } StringBuilder log = new StringBuilder("Winning legion "); log.append(negotiatedWinner.getLongMarkerName()); log.append(" loses creatures "); // Remove all dead creatures from the winning legion. List<String> winnerLosses = results.getWinnerLosses(); Iterator<String> it = winnerLosses.iterator(); while (it.hasNext()) { String creatureName = it.next(); log.append(creatureName); if (it.hasNext()) { log.append(", "); } CreatureType creature = getVariant().getCreatureByName( creatureName); negotiatedWinner.removeCreature(creature, true, true); server.allTellRemoveCreature(negotiatedWinner, creature, true, Constants.reasonNegotiated); } LOGGER.info(log.toString()); server.oneRevealLegion(negotiatedWinner, attacker.getPlayer(), Constants.reasonNegotiated); server.oneRevealLegion(negotiatedWinner, defender.getPlayer(), Constants.reasonNegotiated); points = loser.getPointValue(); PlayerServerSide losingPlayer = loser.getPlayer(); // Need to check and remember this before removing the legion boolean loserHasTitan = loser.hasTitan(); // Remove the losing legion. loser.remove(); // Add points, and angels if necessary. negotiatedWinner.getPlayer().awardPoints(points, negotiatedWinner, false); LOGGER.info("Legion " + loser + " is eliminated by legion " + negotiatedWinner + " via negotiation"); // If this was the titan stack, its owner dies and gives half // points to the victor. if (loserHasTitan) { losingPlayer.die(negotiatedWinner.getPlayer()); checkForVictory(); } if (isGameOver()) { LOGGER.info("Negotiation (non-mutual) causes Game Over - " + "skipping summon/reinforce procedures."); } else if (negotiatedWinner == defender) { if (defender.canRecruit()) { // If the defender won the battle by agreement, // he may recruit. reinforce(defender); } } else { if (attacker.canSummonAngel()) { // If the attacker won the battle by agreement, // he may summon an angel. createSummonAngel(attacker); } } } setEngagementResult(Constants.erMethodNegotiate, negotiatedWinner, points, 0); checkEngagementDone(); } void askAcquireAngel(PlayerServerSide player, Legion legion, List<CreatureType> recruits) { acquiring = true; server.askAcquireAngel(player, legion, recruits); } void doneAcquiringAngels() { acquiring = false; checkEngagementDone(); } private void setEngagementResult(String aResult, Legion winner, int aPoints, int aTurn) { engagementResult = aResult; this.winner = winner; pointsScored = aPoints; turnCombatFinished = aTurn; } private void checkEngagementDone() { if (summoning || reinforcing || acquiring || engagementResult == null) { return; } clearEngagementData(); server.allUpdatePlayerInfo("CheckEngagementDone"); server.allTellEngagementResults(winner, engagementResult, pointsScored, turnCombatFinished); engagementResult = null; // This output is produced for optimizing AI battle functionality: String result = "not set"; if (winner == null) { result = "draw"; } else { result = "winner is " + winner.getMarkerId() + " (player " + winner.getPlayer().getName() + ")"; } LOGGER.info("Battle completed, result: " + result); // This comes from a system property: if (Constants.END_AFTER_FIRST_BATTLE) { LOGGER.info("endAfterFirstBattle is set, terminating game."); server.doSetWhatToDoNext(WhatToDoNext.QUIT_ALL, true); server.triggerDispose(); return; } if (gameShouldContinue()) { gameSaver.commitPointReached(); if (findEngagements().size() < 1) { LOGGER .info("No more engagements to resolve - advanding phase."); advancePhase(Phase.FIGHT, getActivePlayer()); } else { autoSave(); server.nextEngagement(); } } } /* * returns true if game should go on. */ public boolean gameShouldContinue() { if (isGameOver()) { if (getOption(Options.autoQuit)) { LOGGER.info("Reached Game Over - announce and quit"); announceGameOver(true, false); server.doSetWhatToDoNext(WhatToDoNext.QUIT_ALL, true); LOGGER .info("Reached Game Over, AutoQuit - trigger Game Dispose"); server.triggerDispose(); } else { LOGGER.info("Reached Game Over - just announce"); announceGameOver(false, false); } LOGGER.info("Game is now over - returning false"); return false; } else { LOGGER.finest("Game is NOT over yet - returning true"); return true; } } @Override public LegionServerSide getLegionByMarkerId(String markerId) { for (Player player : players) { LegionServerSide legion = (LegionServerSide)player .getLegionByMarkerId(markerId); if (legion != null) { return legion; } } LOGGER.warning("Can't find legion for markerId '" + markerId + "'"); assert false : "Request for unknown legion '" + markerId + "'"; return null; } // TODO copy and paste from Client class public Player getPlayerByMarkerId(String markerId) { assert markerId != null : "Parameter must not be null"; String shortColor = markerId.substring(0, 2); return getPlayerUsingColor(shortColor); } // TODO copy and paste from Client class private Player getPlayerUsingColor(String shortColor) { assert players != null : "Game not yet initialized"; assert shortColor != null : "Parameter must not be null"; // Stage 1: See if the player who started with this color is alive. for (Player player : players) { if (shortColor.equals(player.getShortColor()) && !player.isDead()) { return player; } } // Stage 2: He's dead. Find who killed him and see if he's alive. for (Player player : players) { if (player.getPlayersElim().indexOf(shortColor) != -1) { // We have the killer. if (!player.isDead()) { return player; } else { return getPlayerUsingColor(player.getShortColor()); } } } return null; } private LegionServerSide getStartingLegion(String markerId, MasterHex hex, Player player) { CreatureType[] startCre = TerrainRecruitLoader .getStartingCreatures(hex); LegionServerSide legion = new LegionServerSide(markerId, null, hex, hex, player, this, VariantSupport.getCurrentVariant() .getCreatureByName(Constants.titan), VariantSupport .getCurrentVariant().getCreatureByName( getVariant().getPrimaryAcquirable()), startCre[2], startCre[2], startCre[0], startCre[0], startCre[1], startCre[1]); for (Creature critter : legion.getCreatures()) { getCaretaker().takeOne(critter.getType()); } return legion; } int mulligan() { if (getPhase() != Phase.MOVE) { return -1; } PlayerServerSide player = (PlayerServerSide)getActivePlayer(); // mark mulligan as taken and set roll to 0 player.takeMulligan(); server.allUpdatePlayerInfo("Mulligan"); setupMove(); makeMovementRoll(Constants.reasonMulligan); gameSaver.commitPointReached(); autoSave(); server.kickPhase(); return player.getMovementRoll(); } int makeExtraRoll() { if (getPhase() != Phase.MOVE) { return -1; } PlayerServerSide player = (PlayerServerSide)getActivePlayer(); // mark mulligan as taken and set roll to 0 player.prepareExtraRoll(); server.allUpdatePlayerInfo("ExtraRoll"); setupMove(); makeMovementRoll(Constants.reasonExtraRoll); gameSaver.commitPointReached(); autoSave(); server.kickPhase(); return player.getMovementRoll(); } IOptions getOptions() { return options; } boolean getOption(String optname) { return options.getOption(optname); } int getIntOption(String optname) { return options.getIntOption(optname); } // History wrappers. Time to start obeying the Law of Demeter. void addCreatureEvent(AddCreatureAction event, String reason) { lastRecruitTurnNumber = turnNumber; history.addCreatureEvent(event, turnNumber, reason); } void removeCreatureEvent(Legion legion, CreatureType creature, String reason) { history.removeCreatureEvent(legion, creature, turnNumber, reason); } void splitEvent(Legion parent, Legion child, List<CreatureType> splitoffs) { history.splitEvent(parent, child, splitoffs, turnNumber); } void mergeEvent(String splitoffId, String survivorId) { history.mergeEvent(splitoffId, survivorId, turnNumber); } void revealEvent(boolean allPlayers, List<Player> players, Legion legion, List<CreatureType> creatureNames, String reason) { history.revealEvent(allPlayers, players, legion, creatureNames, turnNumber, reason); } void playerElimEvent(Player player, Player slayer) { history.playerElimEvent(player, slayer, turnNumber); } void movementRollEvent(Player player, int roll) { history.movementRollEvent(player, roll); } void legionMoveEvent(Legion legion, MasterHex newHex, EntrySide entrySide, boolean teleport, CreatureType lord) { history.legionMoveEvent(legion, newHex, entrySide, teleport, lord); } void legionUndoMoveEvent(Legion legion) { history.legionUndoMoveEvent(legion); } void recruitEvent(Legion legion, CreatureType recruit, CreatureType recruiter) { history.recruitEvent(legion, recruit, recruiter); } void undoRecruitEvent(Legion legion) { history.undoRecruitEvent(legion); } INotifyWebServer getNotifyWebServer() { return this.notifyWebServer; } public BattleStrikeServerSide getBattleStrikeSS() { return battleStrikeSS; } }