package net.sf.colossus.server; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.client.PlayerClientSide; import net.sf.colossus.common.Constants; import net.sf.colossus.common.Options; import net.sf.colossus.game.Dice; import net.sf.colossus.game.Game; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Player; import net.sf.colossus.util.Glob; import net.sf.colossus.util.InstanceTracker; import net.sf.colossus.variant.CreatureType; /** * Class Player holds the data for one player in a Titan game. * * @author David Ripton */ public final class PlayerServerSide extends Player implements Comparable<PlayerServerSide> { private static final Logger LOGGER = Logger .getLogger(PlayerServerSide.class.getName()); private static boolean REDUCE_DUPLICATE_MOVEMENT_ROLLS = false; // TODO the half-points are really used only in the die(..) method, // they could be summed up there and then added all in one go. That // would save us from storing a double and truncating things later // and the getScore/setScore overrides could go. private double score; // track half-points, then round private boolean summoned; /** * TODO {@link PlayerClientSide} just checks if any legion has teleported. * Pick one version and move up into {@link Player}. */ private boolean teleported; /** * TODO this might be better as a state in {@link Game} since there is * always only one per game, not per player * OR NOT TODO anymore... * Nowadays (11/2015) it's more feasible to have them player * specific, so that we can easier do "special stuff" that * involves comparing a roll to the previous one of same player. */ private int movementRoll; // 0 if movement has not been rolled. private int preExtraRollRequestMovementRoll; // set ONLY for requestExtraRoll use private int previousTurnMovementRoll = 0; private final PlayerSpecificDice myDice; private boolean titanEliminated; /** * The legion which gave a summonable creature. */ private LegionServerSide donor; private String firstMarker; /* Needed during loading a game: */ private String playersEliminatedBackup = ""; private final List<Legion> legionsBackup = new ArrayList<Legion>(); PlayerServerSide(String name, GameServerSide game, String shortTypeName) { // TODO why are the players on the client side numbered but not here? super(game, name, 0); this.myDice = new PlayerSpecificDice(); // add package path to AI names and choose random type for "anyAI": setType(shortTypeName); InstanceTracker.register(this, name); } /** * Overridden to return specific flavor of Game until the upper class is sufficient. */ @Override public GameServerSide getGame() { return (GameServerSide)super.getGame(); } // TODO strong redundancy with Client.setType(String) @Override public void setType(final String shortTypeName) { String type = shortTypeName; LOGGER.log(Level.FINEST, "Called Player.setType() for " + getName() + " " + type); if (type.endsWith(Constants.anyAI)) { int aiCount = Constants.numAITypes; // Do not choose an ExperimentalAI as "A Random AI" in user games, // but in stresstest we DO want to test them. if (!Options.isStresstest()) { aiCount--; } int whichAI = Dice.rollDie(aiCount) - 1; type = Constants.aiArray[whichAI]; } if (!type.startsWith(Constants.aiPackage)) { type = Constants.aiPackage + type; } super.setType(type); } void initMarkersAvailable() { initMarkersAvailable(getShortColor()); } void initMarkersAvailable(String shortColor) { for (int i = 1; i <= 9; i++) { addMarkerAvailable(shortColor + '0' + Integer.toString(i)); } for (int i = 10; i <= 12; i++) { addMarkerAvailable(shortColor + Integer.toString(i)); } } /** Set markersAvailable based on other available information. * NOTE: to be used only during loading a Game! */ void computeMarkersAvailable() { if (isDead()) { clearMarkersAvailable(); } else { initMarkersAvailable(); StringBuilder allVictims = new StringBuilder(getPlayersElim()); for (int i = 0; i < allVictims.length(); i += 2) { String shortColor = allVictims.substring(i, i + 2); initMarkersAvailable(shortColor); Player victim = getGame().getPlayerByShortColor(shortColor); allVictims.append(victim.getPlayersElim()); } for (Legion legion : getLegions()) { removeMarkerAvailable(legion.getMarkerId()); } } } void setFirstMarker(String firstMarker) { this.firstMarker = firstMarker; } String getFirstMarker() { return firstMarker; } /** Players are sorted in order of decreasing starting tower. This is inconsistent with equals(). */ public int compareTo(PlayerServerSide other) { return other.getStartingTower().getLabel() .compareTo(this.getStartingTower().getLabel()); } @Override public boolean hasTeleported() { return teleported; } void setTeleported(boolean teleported) { this.teleported = teleported; } boolean hasSummoned() { return summoned; } void setSummoned(boolean summoned) { this.summoned = summoned; } LegionServerSide getDonor() { return donor; } void setDonor(LegionServerSide donor) { this.donor = donor; } /** Remove all of this player's zero-height legions. */ void removeEmptyLegions() { Iterator<LegionServerSide> it = getLegions().iterator(); while (it.hasNext()) { LegionServerSide legion = it.next(); if (legion.getHeight() == 0) { if (legion.equals(donor)) { donor = null; } legion.prepareToRemove(true, true); it.remove(); } } } /** * TODO remove once noone needs the specific version anymore */ @SuppressWarnings("unchecked") @Override public List<LegionServerSide> getLegions() { return (List<LegionServerSide>)super.getLegions(); } /** Return the number of this player's legions that have moved. */ int legionsMoved() { int count = 0; for (Legion legion : getLegions()) { if (legion.hasMoved()) { count++; } } return count; } /** Return the number of this player's legions that have legal non-teleport moves remaining. */ int countMobileLegions() { int count = 0; for (LegionServerSide legion : getLegions()) { if ((legion).hasConventionalMove()) { count++; } } return count; } void commitMoves() { for (LegionServerSide legion : getLegions()) { (legion).commitMove(); } } // TODO temporary for Movement class; move up to game.Player? public int getMovementRollSS() { return getMovementRoll(); } int getMovementRoll() { return movementRoll; } void setMovementRoll(int movementRoll) { this.movementRoll = movementRoll; } void resetTurnState() { summoned = false; donor = null; setTeleported(false); movementRoll = 0; preExtraRollRequestMovementRoll = 0; // Make sure that all legions are allowed to move and recruit. commitMoves(); } int rollMovement(String reason) { boolean forExtraRequest = reason.equals(Constants.reasonExtraRoll); // Only roll if it hasn't already been done. if (movementRoll != 0) { LOGGER.warning("Called rollMovement() more than once"); } else { movementRoll = makeMovementRoll(); if (forExtraRequest) { // for the request extra roll, explicitly prevent a duplicate roll while (movementRoll == preExtraRollRequestMovementRoll) { LOGGER.finer("Extra roll is " + movementRoll + ", same as previous roll! Rolling again."); movementRoll = makeMovementRoll(); } } else { // For normal rolls, reduce the likelihood of duplicates by // re-rolling once; but this way it can still happen if (REDUCE_DUPLICATE_MOVEMENT_ROLLS) { if (movementRoll == previousTurnMovementRoll) { LOGGER.finer("Normal roll is " + movementRoll + ", same as roll from previous turn! " + "Rolling again ONCE."); movementRoll = makeMovementRoll(); if (movementRoll == previousTurnMovementRoll) { LOGGER.finest("He he, same roll, so it can still happen!"); } } } } LOGGER.info("Player " + getName() + " rolls a " + movementRoll + " for movement [" + reason + "]"); previousTurnMovementRoll = movementRoll; } return movementRoll; } void takeMulligan() { int mulligans = getMulligansLeft(); if (mulligans > 0) { undoAllMoves(); LOGGER.finer("Player " + getName() + " takes a mulligan"); if (!getGame().getOption(Options.unlimitedMulligans)) { mulligans--; setMulligansLeft(mulligans); } movementRoll = 0; } } void prepareExtraRoll() { undoAllMoves(); LOGGER.finer("Player " + getName() + " gets the requested extra roll"); preExtraRollRequestMovementRoll = movementRoll; movementRoll = 0; } void undoMove(Legion legion) { if (legion != null) { ((LegionServerSide)legion).undoMove(); } } void undoAllMoves() { for (LegionServerSide legion : getLegions()) { legion.undoMove(); } } /** Return true if two or more of this player's legions share * a hex and they have a legal non-teleport move. */ boolean splitLegionHasForcedMove() { for (LegionServerSide legion : getLegions()) { if (getGame().getNumFriendlyLegions(legion.getCurrentHex(), this) > 1 && (legion).hasConventionalMove()) { LOGGER.finest("Found unseparated split legions at hex " + legion.getCurrentHex()); return true; } } return false; } /** Return true if any legion can recruit. */ boolean canRecruit() { for (LegionServerSide legion : getLegions()) { if (legion.hasMoved() && legion.canRecruit()) { return true; } } return false; } // private Legion previousUndoRecruitLegion = null; /** * Tell legion to do undo the recruiting and trigger needed messages * to be sent to clients * @param legion The legion which undoes the recruiting */ void undoRecruit(Legion legion) { assert legion != null : "Player.undoRecruit: legion must not be null"; // This is now permanently fixed in Player.java, so this should // never happen again. Still, leaving this in place, just to be sure... CreatureType recruit = ((LegionServerSide)legion).getRecruit(); if (recruit == null) { if (previousUndoRecruitLegion != null && previousUndoRecruitLegion.equals(legion)) { LOGGER .info("Player.undoRecruit: " + "Nothing to unrecruit for legion " + legion.getMarkerId() + " but that's probably just a duplicate click - ignoring it."); LOGGER.info(getGame().getServer().processingCH .dumpLastProcessedLines()); } else { LOGGER.log(Level.WARNING, "Player.undoRecruit: Nothing to unrecruit for legion " + legion.getMarkerId()); LOGGER.info(getGame().getServer().processingCH .dumpLastProcessedLines()); } return; } previousUndoRecruitLegion = legion; ((LegionServerSide)legion).undoRecruit(); // Update number of creatures in status window. getGame().getServer().allUpdatePlayerInfo("UndoRecruit"); getGame().getServer().undidRecruit(legion, recruit, false); } /** * Tell legion to do undo the reinforcement and trigger needed messages * to be sent to clients * (quite similar to undorecuit, but not exactly the same) * @param legion The legion which cancels the reinforcement */ void undoReinforcement(Legion legion) { // This is now permanently fixed in Player.java, so this should // never happen again. Still, leaving this in place, just to be sure... CreatureType recruit = ((LegionServerSide)legion).getRecruit(); if (recruit == null) { LOGGER.log(Level.SEVERE, "Player.undoReinforcement: Nothing to unreinforce for marker " + legion); return; } ((LegionServerSide)legion).undoReinforcement(); // We don't do the allUpdatePlayerInfo() here, because the remove // is done later by iterator (so amounts are not even changed yet) getGame().getServer().undidRecruit(legion, recruit, true); } void undoSplit(Legion splitoff) { Legion parent = ((LegionServerSide)splitoff).getParent(); ((LegionServerSide)splitoff).recombine(parent, true); getGame().getServer().allUpdatePlayerInfo("UndoSplit"); } void recombineIllegalSplits() { Iterator<LegionServerSide> it = getLegions().iterator(); while (it.hasNext()) { Legion legion = it.next(); // Don't use the legion's real parent, as there could have been // a 3-way split and the parent could be gone. Legion parent = getGame().getFirstFriendlyLegion( legion.getCurrentHex(), this); if (legion != parent) { ((LegionServerSide)legion).recombine(parent, false); it.remove(); } } getGame().getServer().allUpdatePlayerInfo("recombineIllegalSplits"); } @Override public void setScore(int score) { this.score = score; } @Override public int getScore() { return (int)score; } /** Add points to this player's score. Update the status window * to reflect the addition. */ void addPoints(double points, boolean halfPoints) { if (points > 0) { score += points; if (getGame() != null) { getGame().getServer().allUpdatePlayerInfo("AddPoints"); } LOGGER.info(getName() + " earns " + points + " " + (halfPoints ? "half-points" : "points") + " (" + (score - points) + " + " + points + " => " + score + ")"); } } /** Remove half-points. */ void truncScore() { score = Math.floor(score); } /** * Award points and handle all acquiring related issues. * * Note that this is not used for adding points for cleaning up legions * of a dead player! * * @param points the points to award * @param legion the legion which is entitled to acquire due to that * @param halfPoints this are already halfPoints (from fleeing) */ void awardPoints(int points, LegionServerSide legion, boolean halfPoints) { int scoreBeforeAdd = getScore(); // 375 addPoints(points, halfPoints); // 375 + 150 = 525 getGame().acquireMaybe(legion, scoreBeforeAdd, points); } /** * Turns the player dead. * * This method calculates the points other players get, adds them to their score and * then cleans up this player and marks him dead. * * TODO is it really the Player's role to assign points? I'd rather see that responsibility * with the Game object * * TODO the slayer could be non-null if we introduce a null object (some object called * e.g. "NOONE" that behaves like a Player as far as possible, giving a name and swallowing * points) * * @param slayer The player who killed us. May be null if we just gave up or it is a draw. */ void die(Player slayer) { LOGGER.info("Player '" + getName() + "' is dying, killed by " + (slayer == null ? "nobody" : slayer.getName())); // Engaged legions give half points to the player they're // engaged with. All others give half points to slayer, // if non-null. for (Iterator<LegionServerSide> itLeg = getLegions().iterator(); itLeg .hasNext();) { LegionServerSide legion = itLeg.next(); Legion enemyLegion = getGame().getFirstEnemyLegion( legion.getCurrentHex(), this); double halfPoints = legion.getPointValue() / 2.0; Player scorer; if (enemyLegion != null) { scorer = enemyLegion.getPlayer(); } else { scorer = slayer; } if (scorer != null) { ((PlayerServerSide)scorer).addPoints(halfPoints, true); } // Call the iterator's remove() method rather than // removeLegion() to avoid concurrent modification problems. legion.prepareToRemove(true, true); itLeg.remove(); } // Truncate every player's score to an integer value. for (Player player : getGame().getPlayers()) { ((PlayerServerSide)player).truncScore(); } // Mark this player as dead. setDead(true); // Record the slayer and give him this player's legion markers. handleSlaying(slayer); getGame().getServer().allUpdatePlayerInfo("Die"); LOGGER.info(getName() + " is dead, telling everyone about it"); getGame().getServer().allTellPlayerElim(this, slayer, true); } // Record the slayer and give him this player's legion markers. public void handleSlaying(Player slayer) { if (slayer != null) { slayer.addPlayerElim(this); for (String markerId : getMarkersAvailable()) { slayer.addMarkerAvailable(markerId); } clearMarkersAvailable(); } } void eliminateTitan() { titanEliminated = true; } boolean isTitanEliminated() { return titanEliminated; } /** Return a colon-separated string with a bunch of info for * the status screen. */ String getStatusInfo(boolean treatDeadAsAlive) { List<String> li = new ArrayList<String>(); li.add(Boolean.toString(!treatDeadAsAlive && isDead())); li.add(getName()); li.add(getStartingTower() != null ? getStartingTower().getLabel() : null); li.add((getColor() != null) ? getColor().getName() : null); li.add(getType()); li.add(getPlayersElim()); li.add(Integer.toString(getLegions().size())); li.add(Integer.toString(getNumCreatures())); li.add(Integer.toString(getTitanPower())); li.add(Integer.toString(getScore())); li.add(Integer.toString(getMulligansLeft())); li.addAll(getMarkersAvailable()); String info = Glob.glob(":", li); return info; } private String lastFetchedValues = ""; /** * Return a string with current values for this player, if anything is * different than the data provided on last call. * String is in form: * name:isDead:eliminatedPlayers:score:mulligans:mk01,mk01,...mk12 * If info is identical, returns empty string "". * * @return New info or "" if all is same as last time */ String getValuesIfChanged() { List<String> li = new ArrayList<String>(); li.add(getName()); li.add(Boolean.toString(isDead())); li.add(getPlayersElim()); li.add(Integer.toString(getScore())); li.add(Integer.toString(getMulligansLeft())); String markers = Glob.glob(",", getMarkersAvailable()); li.add(markers); li.add("dummy"); String values = Glob.glob(":", li); if (values.equals(lastFetchedValues)) { return ""; } else { lastFetchedValues = values; return values; } } /* After legions are loaded, put them aside (legions and who owns them * might be totally mismatching esp. after player elimination. * Reconstruct creation, deceasing, surviving legions, player elimination * and slayer-setting and markerTransfer from history; * After that, synchronize state info like location, hasMoved etc * from the backup. */ public void backupLoadedData() { playersEliminatedBackup = getPlayersElim(); setPlayersElim(""); for (LegionServerSide l : getLegions()) { legionsBackup.add(l); } removeAllLegions(); } public boolean resyncBackupData() { if (!(playersEliminatedBackup != null && getPlayersElim() != null && getPlayersElim() .equals(playersEliminatedBackup))) { LOGGER.severe("PlayersEliminated strings NOT in sync: replayed " + " is '" + getPlayersElim() + "' but loaded is '" + playersEliminatedBackup + "'!"); return false; } if (getLegions().size() != legionsBackup.size()) { LOGGER.severe("Loaded player data, legion lists different size!" + " replay: " + getLegions().size() + ", loaded: " + legionsBackup.size()); return false; } boolean ok = true; for (Legion bl : legionsBackup) { Legion l = getLegionByMarkerId(bl.getMarkerId()); List<CreatureType> ourCreatures = new ArrayList<CreatureType>( l.getCreatureTypes()); ourCreatures.removeAll(bl.getCreatureTypes()); if ((l.getHeight() != bl.getHeight()) || !ourCreatures.isEmpty()) { LOGGER .severe("Loaded legion data differs from replay result: " + "Replay-Legion content for " + l.getMarkerId() + " is " + Glob.glob(l.getCreatureTypes()) + ", Loaded-Legion content is " + Glob.glob(bl.getCreatureTypes())); ok = false; } } if (ok) { // discard the replay results and get the backup data back into // place which has the right legion state: removeAllLegions(); for (Legion l : legionsBackup) { addLegion(l); } } return ok; } @Override public int makeBattleRoll() { return myDice.rollBattleDie(); } @Override public int makeMovementRoll() { return myDice.rollMovementDie(); } }