package net.sf.colossus.game; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.common.Constants; import net.sf.colossus.util.CompareDoubles; import net.sf.colossus.variant.BattleHex; import net.sf.colossus.variant.HazardTerrain; import net.sf.colossus.variant.MasterHex; /** * An ongoing battle. */ abstract public class Battle { private static final Logger LOGGER = Logger.getLogger(Battle.class .getName()); protected final Game game; protected final Legion attacker; protected final Legion defender; private final MasterHex location; protected int battleTurnNumber; public Battle(Game game, Legion attacker, Legion defender, MasterHex location) { this.game = game; this.attacker = attacker; this.defender = defender; this.location = location; this.battleTurnNumber = 1; } public Game getGame() { return game; } public Legion getAttackingLegion() { return attacker; } public Legion getDefendingLegion() { return defender; } /** * Caller must ensure that yDist != 0 * * TODO Temporarily public because n.s.c.client.Strike needs it */ public static boolean toLeft(double xDist, double yDist) { double ratio = xDist / yDist; if (ratio >= 1.5 || (ratio >= 0 && ratio <= 0.75) || (ratio >= -1.5 && ratio <= -0.75)) { return true; } else { return false; } } /** * Return the hexside direction of the path from hex1 to hex2. * Sometimes two directions are possible. If the left parameter * is set, the direction further left will be given. Otherwise, * the direction further right will be given. */ public static int getDirection(BattleHex hex1, BattleHex hex2, boolean left) { if (hex1 == hex2) { return -1; } int x1 = hex1.getXCoord(); double y1 = hex1.getYCoord(); int x2 = hex2.getXCoord(); double y2 = hex2.getYCoord(); // Offboard creatures are not allowed. if (hex1.isEntrance() || hex2.isEntrance()) { return -1; } // Hexes with odd X coordinates are pushed down half a hex. if ((x1 & 1) == 1) { y1 += 0.5; } if ((x2 & 1) == 1) { y2 += 0.5; } int xDist = x2 - x1; double yDist = y2 - y1; double xDistAndAHalf = 1.5 * xDist; if (xDist >= 0) { if (yDist > xDistAndAHalf) { return 3; } else if (CompareDoubles.almostEqual(yDist, xDistAndAHalf)) { if (left) { return 2; } else { return 3; } } else if (yDist < -xDistAndAHalf) { return 0; } else if (CompareDoubles.almostEqual(yDist, -xDistAndAHalf)) { if (left) { return 0; } else { return 1; } } else if (yDist > 0) { return 2; } else if (yDist < 0) { return 1; } else { if (left) { return 1; } else { return 2; } } } else { if (yDist < xDistAndAHalf) { return 0; } else if (CompareDoubles.almostEqual(yDist, xDistAndAHalf)) { if (left) { return 5; } else { return 0; } } else if (yDist > -xDistAndAHalf) { return 3; } else if (CompareDoubles.almostEqual(yDist, -xDistAndAHalf)) { if (left) { return 3; } else { return 4; } } else if (yDist > 0) { return 4; } else if (yDist < 0) { return 5; } else { if (left) { return 4; } else { return 5; } } } } /** Return the number of intervening bramble hexes. If LOS is along a * hexspine, go left if argument left is true, right otherwise. If * LOS is blocked, return a large number. * @deprecated another function with explicit reference to Bramble * that should be fixed. */ @Deprecated private int countBrambleHexesDir(BattleHex hex1, BattleHex hex2, boolean left, int previousCount) { int count = previousCount; // Offboard hexes are not allowed. if (hex1.isEntrance() || hex2.isEntrance()) { return Constants.BIGNUM; } int direction = getDirection(hex1, hex2, left); BattleHex nextHex = hex1.getNeighbor(direction); if (nextHex == null) { return Constants.BIGNUM; } if (nextHex == hex2) { // Success! return count; } // Add one if it's bramble. if (nextHex.getTerrain().equals(HazardTerrain.BRAMBLES)) { count++; } return countBrambleHexesDir(nextHex, hex2, left, count); } /** Return the number of intervening bramble hexes. If LOS is along a * hexspine and there are two choices, pick the lower one. * @deprecated another function with explicit reference to Bramble * that should be fixed. */ @Deprecated public int countBrambleHexes(BattleHex hex1, BattleHex hex2) { if (hex1 == hex2) { return 0; } int x1 = hex1.getXCoord(); double y1 = hex1.getYCoord(); int x2 = hex2.getXCoord(); double y2 = hex2.getYCoord(); // Offboard hexes are not allowed. if (hex1.isEntrance() || hex2.isEntrance()) { return Constants.BIGNUM; } // Hexes with odd X coordinates are pushed down half a hex. if ((x1 & 1) == 1) { y1 += 0.5; } if ((x2 & 1) == 1) { y2 += 0.5; } double xDist = x2 - x1; double yDist = y2 - y1; if (CompareDoubles.almostEqual(yDist, 0.0) || CompareDoubles.almostEqual(Math.abs(yDist), 1.5 * Math.abs(xDist))) { int strikeElevation = Math.min(hex1.getElevation(), hex2.getElevation()); // Hexspine; try unblocked side(s). if (isLOSBlockedDir(hex1, hex1, hex2, true, strikeElevation, false, false, false, false, false, false, 0, 0)) { return countBrambleHexesDir(hex1, hex2, false, 0); } else if (isLOSBlockedDir(hex1, hex1, hex2, false, strikeElevation, false, false, false, false, false, false, 0, 0)) { return countBrambleHexesDir(hex1, hex2, true, 0); } else { return Math.min(countBrambleHexesDir(hex1, hex2, true, 0), countBrambleHexesDir(hex1, hex2, false, 0)); } } else { return countBrambleHexesDir(hex1, hex2, toLeft(xDist, yDist), 0); } } /** * @deprecated This is the realm of HazardEdge, not direct use of hexside */ @Deprecated protected static boolean isObstacle(char hexside) { return (hexside != ' ') && (hexside != 'r'); } /** * Return the range in hexes from hex1 to hex2. Titan ranges are * inclusive at both ends. */ public static int getRange(BattleHex hex1, BattleHex hex2, boolean allowEntrance) { if (hex1 == null || hex2 == null) { LOGGER.log(Level.WARNING, "passed null hex to getRange()"); return Constants.OUT_OF_RANGE; } if (hex1.isEntrance() || hex2.isEntrance()) { if (allowEntrance) { if (hex1.isEntrance()) { return 1 + minRangeToNeighbor(hex1, hex2); } else { return 1 + minRangeToNeighbor(hex2, hex1); } } else { // It's out of range. No need to do the math. return Constants.OUT_OF_RANGE; } } int x1 = hex1.getXCoord(); double y1 = hex1.getYCoord(); int x2 = hex2.getXCoord(); double y2 = hex2.getYCoord(); // Hexes with odd X coordinates are pushed down half a hex. if ((x1 & 1) == 1) { y1 += 0.5; } if ((x2 & 1) == 1) { y2 += 0.5; } double xDist = Math.abs(x2 - x1); double yDist = Math.abs(y2 - y1); if (xDist >= 2 * yDist) { return (int)Math.ceil(xDist + 1); } else if (xDist >= yDist) { return (int)Math.floor(xDist + 2); } else if (yDist >= 2 * xDist) { return (int)Math.ceil(yDist + 1); } else { return (int)Math.floor(yDist + 2); } } /** * Return the minimum range from any neighbor of hex1 to hex2. */ private static int minRangeToNeighbor(BattleHex hex1, BattleHex hex2) { int min = Constants.OUT_OF_RANGE; for (int i = 0; i < 6; i++) { BattleHex hex = hex1.getNeighbor(i); if (hex != null) { int range = getRange(hex, hex2, false); if (range < min) { min = range; } } } return min; } /** * Check to see if the LOS from hex1 to hex2 is blocked. If the LOS * lies along a hexspine, check both and return true only if both are * blocked. */ public boolean isLOSBlocked(BattleHex hex1, BattleHex hex2) { if (hex1 == hex2) { return false; } int x1 = hex1.getXCoord(); double y1 = hex1.getYCoord(); int x2 = hex2.getXCoord(); double y2 = hex2.getYCoord(); // Offboard hexes are not allowed. if (hex1.isEntrance() || hex2.isEntrance()) { return true; } // Hexes with odd X coordinates are pushed down half a hex. if ((x1 & 1) == 1) { y1 += 0.5; } if ((x2 & 1) == 1) { y2 += 0.5; } double xDist = x2 - x1; double yDist = y2 - y1; // Creatures below the level of the strike do not block LOS. int strikeElevation = Math.min(hex1.getElevation(), hex2.getElevation()); if (CompareDoubles.almostEqual(yDist, 0.0) || CompareDoubles.almostEqual(Math.abs(yDist), 1.5 * Math.abs(xDist))) { return isLOSBlockedDir(hex1, hex1, hex2, true, strikeElevation, false, false, false, false, false, false, 0, 0) && isLOSBlockedDir(hex1, hex1, hex2, false, strikeElevation, false, false, false, false, false, false, 0, 0); } else { return isLOSBlockedDir(hex1, hex1, hex2, toLeft(xDist, yDist), strikeElevation, false, false, false, false, false, false, 0, 0); } } /** * Check LOS, going to the left of hexspines if argument left is true, or * to the right if it is false. */ protected boolean isLOSBlockedDir(BattleHex initialHex, BattleHex currentHex, BattleHex finalHex, boolean left, int strikeElevation, boolean strikerAtop, boolean strikerAtopCliff, boolean strikerAtopWall, boolean midObstacle, boolean midCliff, boolean midChit, int totalObstacles, int totalWalls) { boolean targetAtop = false; boolean targetAtopCliff = false; boolean targetAtopWall = false; if (currentHex == finalHex) { return false; } // Offboard hexes are not allowed. if (currentHex.isEntrance() || finalHex.isEntrance()) { return true; } int direction = getDirection(currentHex, finalHex, left); BattleHex nextHex = currentHex.getNeighbor(direction); if (nextHex == null) { return true; } char hexside = currentHex.getHexsideHazard(direction).getCode(); char hexside2 = currentHex.getOppositeHazard(direction).getCode(); if (currentHex == initialHex) { if (isObstacle(hexside)) { strikerAtop = true; totalObstacles++; if (hexside == 'c') { strikerAtopCliff = true; } else if (hexside == 'w') { strikerAtopWall = true; totalWalls++; } } if (isObstacle(hexside2)) { midObstacle = true; totalObstacles++; if (hexside2 == 'c' || hexside2 == 'd') { midCliff = true; } else if (hexside2 == 'w') { return true; } } } else if (nextHex == finalHex) { if (isObstacle(hexside)) { midObstacle = true; totalObstacles++; if (hexside == 'c' || hexside == 'd') { midCliff = true; } else if (hexside == 'w') { return true; } } if (isObstacle(hexside2)) { targetAtop = true; totalObstacles++; if (hexside2 == 'c') { targetAtopCliff = true; } else if (hexside2 == 'w') { totalWalls++; targetAtopWall = true; } } if (midChit && !targetAtopCliff) { return true; } if (midCliff && (!strikerAtopCliff || !targetAtopCliff)) { return true; } if (midObstacle && !strikerAtop && !targetAtop) { return true; } // If there are three slopes, striker and target must each // be atop one. if (totalObstacles >= 3 && (!strikerAtop || !targetAtop) && (!strikerAtopCliff && !targetAtopCliff)) { return true; } if (totalWalls >= 2) { if (!(strikerAtopWall || targetAtopWall)) { return true; } } // Success! return false; } else // not leaving first or entering last hex { if (midChit) { // We're not in the initial or final hex, and we have already // marked a mid chit, so it's not adjacent to the base of a // cliff that the target is atop. return true; } if (isObstacle(hexside) || isObstacle(hexside2)) { midObstacle = true; totalObstacles++; if (hexside == 'c' || hexside2 == 'c' || hexside == 'd' || hexside2 == 'd') { midCliff = true; } } } if (nextHex.blocksLineOfSight()) { return true; } // Creatures block LOS, unless both striker and target are at higher // elevation than the creature, or unless the creature is at // the base of a cliff and the striker or target is atop it. if (isOccupied(nextHex) && nextHex.getElevation() >= strikeElevation && (!strikerAtopCliff || currentHex != initialHex)) { midChit = true; } return isLOSBlockedDir(initialHex, nextHex, finalHex, left, strikeElevation, strikerAtop, strikerAtopCliff, strikerAtopWall, midObstacle, midCliff, midChit, totalObstacles, totalWalls); } /** * Return true if the rangestrike is possible. */ protected boolean isRangestrikePossible(Creature critter, Creature target, BattleHex currentHex, BattleHex targetHex) { int range = getRange(currentHex, targetHex, false); int skill = critter.getType().getSkill(); if (range > skill) { return false; } else if (!critter.getType().useMagicMissile() && (range < 3 || target.getType().isLord() || isLOSBlocked( currentHex, targetHex))) { return false; } return true; } private int computeSkillPenaltyRangestrikeThroughDir(BattleHex hex1, BattleHex hex2, Creature c, boolean left, int previousCount) { int count = previousCount; // Offboard hexes are not allowed. if (hex1.isEntrance() || hex2.isEntrance()) { return Constants.BIGNUM; } int direction = getDirection(hex1, hex2, left); BattleHex nextHex = hex1.getNeighbor(direction); if (nextHex == null) { return Constants.BIGNUM; } if (nextHex == hex2) { // Success! return count; } HazardTerrain terrain = nextHex.getTerrain(); count += terrain.getSkillPenaltyRangestrikeThrough(c.getType() .isNativeIn(terrain)); return computeSkillPenaltyRangestrikeThroughDir(nextHex, hex2, c, left, count); } /** Compute the minimum Skill penalty that the creature will endure to * rangestrike from hex1 to a creature in hex2 from the intervening hex. * @param hex1 The hex in which the rangestriker sit * @param hex2 The hex in which the rangestruck sit * @param c The rangestriker * @return The penalty to the Skill Factor of the rangestriker from intervening hex. */ public int computeSkillPenaltyRangestrikeThrough(BattleHex hex1, BattleHex hex2, Creature c) { if (hex1 == hex2) { return 0; } int x1 = hex1.getXCoord(); double y1 = hex1.getYCoord(); int x2 = hex2.getXCoord(); double y2 = hex2.getYCoord(); // Offboard hexes are not allowed. if (hex1.isEntrance() || hex2.isEntrance()) { return Constants.BIGNUM; } // Hexes with odd X coordinates are pushed down half a hex. if ((x1 & 1) == 1) { y1 += 0.5; } if ((x2 & 1) == 1) { y2 += 0.5; } double xDist = x2 - x1; double yDist = y2 - y1; if (CompareDoubles.almostEqual(yDist, 0.0) || CompareDoubles.almostEqual(Math.abs(yDist), 1.5 * Math.abs(xDist))) { int strikeElevation = Math.min(hex1.getElevation(), hex2.getElevation()); // Hexspine; try unblocked side(s) if (isLOSBlockedDir(hex1, hex1, hex2, true, strikeElevation, false, false, false, false, false, false, 0, 0)) { return computeSkillPenaltyRangestrikeThroughDir(hex1, hex2, c, false, 0); } else if (isLOSBlockedDir(hex1, hex1, hex2, false, strikeElevation, false, false, false, false, false, false, 0, 0)) { return computeSkillPenaltyRangestrikeThroughDir(hex1, hex2, c, true, 0); } else { return Math.min( computeSkillPenaltyRangestrikeThroughDir(hex1, hex2, c, true, 0), computeSkillPenaltyRangestrikeThroughDir(hex1, hex2, c, false, 0)); } } else { return computeSkillPenaltyRangestrikeThroughDir(hex1, hex2, c, toLeft(xDist, yDist), 0); } } protected Legion getLegionByPlayer(Player player) { Legion lattacker = getAttackingLegion(); if (lattacker != null && lattacker.getPlayer().equals(player)) { return lattacker; } Legion ldefender = getDefendingLegion(); if (ldefender != null && ldefender.getPlayer().equals(player)) { return ldefender; } return null; } public MasterHex getLocation() { return location; } public void setBattleTurnNumber(int battleTurnNumber) { this.battleTurnNumber = battleTurnNumber; } public int getBattleTurnNumber() { return battleTurnNumber; } public BattleCritter getCritter(BattleHex hex) { assert hex != null; for (BattleCritter critter : getAllCritters()) { if (hex.equals(critter.getCurrentHex())) { return critter; } } // TODO check if this is feasible, otherwise assert false here return null; } public boolean isOccupied(BattleHex hex) { for (BattleCritter critter : getAllCritters()) { if (hex.equals(critter.getCurrentHex())) { return true; } } return false; } abstract public Legion getBattleActiveLegion(); /** Get all BattleCritters / BattleUnits * Abstract because currently implementation is different, but needed * on both side, e.g. for BattleMovement */ abstract protected List<BattleCritter> getAllCritters(); abstract public boolean isInContact(BattleCritter striker, boolean countDead); }