package net.sf.colossus.game;
/**
* Class Movement has the masterboard move logic - or that part that could
* already be unified and pulled up from server/client sides.
* There are still some methods that need pulling up, but they need more
* refactoring before that can be done.
*
* @author Clemens Katzer (created the new combined game.Movement class)
* @author David Ripton (e.g. original client.Movement class)
* @author possibly: Bruce Sherrod, Romain Dolbeau (old server.Game class)
*/
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.common.Constants;
import net.sf.colossus.common.Options;
import net.sf.colossus.server.LegionServerSide;
import net.sf.colossus.util.Split;
import net.sf.colossus.variant.MasterHex;
abstract public class Movement
{
private static final Logger LOGGER = Logger.getLogger(Movement.class
.getName());
protected final Game game;
protected final Options options;
public Movement(Game game, Options options)
{
// just here so that LOGGER is used :)
LOGGER.finest("Movement instantiated");
this.game = game;
this.options = options;
}
/** Set the entry side relative to the hex label. */
protected static EntrySide findEntrySide(MasterHex hex, int cameFrom)
{
int entrySide = -1;
if (cameFrom != -1)
{
if (hex.getTerrain().hasStartList())
{
entrySide = 3;
}
else
{
entrySide = (6 + cameFrom - hex.getLabelSide()) % 6;
}
}
return EntrySide.values()[entrySide];
}
protected static int findBlock(MasterHex hex)
{
int block = Constants.ARCHES_AND_ARROWS;
for (int j = 0; j < 6; j++)
{
if (hex.getExitType(j) == Constants.HexsideGates.BLOCK)
{
// Only this path is allowed.
block = j;
}
}
return block;
}
/** Recursively find all unoccupied hexes within roll hexes, for
* tower teleport. */
protected Set<MasterHex> findNearbyUnoccupiedHexes(MasterHex hex,
Legion legion, int roll, int cameFrom)
{
// This hex is the final destination. Mark it as legal if
// it is unoccupied.
Set<MasterHex> result = new HashSet<MasterHex>();
if (!game.isOccupied(hex))
{
result.add(hex);
}
if (roll > 0)
{
for (int i = 0; i < 6; i++)
{
if (i != cameFrom
&& (hex.getExitType(i) != Constants.HexsideGates.NONE || hex
.getEntranceType(i) != Constants.HexsideGates.NONE))
{
result.addAll(findNearbyUnoccupiedHexes(
hex.getNeighbor(i), legion, roll - 1, (i + 3) % 6));
}
}
}
return result;
}
public boolean titanTeleportAllowed()
{
if (options.getOption(Options.noTitanTeleport))
{
return false;
}
if (game.getTurnNumber() == 1
&& options.getOption(Options.noFirstTurnTeleport))
{
return false;
}
return true;
}
protected boolean towerTeleportAllowed()
{
if (options.getOption(Options.noTowerTeleport))
{
return false;
}
if (game.getTurnNumber() == 1
&& options.getOption(Options.noFirstTurnTeleport))
{
return false;
}
return true;
}
protected boolean towerToTowerTeleportAllowed()
{
if (!towerTeleportAllowed())
{
return false;
}
if (game.getTurnNumber() == 1
&& options.getOption(Options.noFirstTurnT2TTeleport))
{
return false;
}
return true;
}
protected boolean towerToNonTowerTeleportAllowed()
{
if (!towerTeleportAllowed())
{
return false;
}
if (options.getOption(Options.towerToTowerTeleportOnly))
{
return false;
}
return true;
}
/** Return set of hexLabels describing where this legion can teleport.
* @return set of hexlabels
*/
protected Set<MasterHex> listTeleportMoves(Legion legion, MasterHex hex,
int movementRoll, boolean inAdvance)
{
Player player = legion.getPlayer();
Set<MasterHex> result = new HashSet<MasterHex>();
if (hex == null)
{
LOGGER.warning("listTeleportMoves called with null hex!");
return result;
}
if (((movementRoll != 6 || legion.hasMoved() || player.hasTeleported()) && !inAdvance))
{
return result;
}
// Tower teleport
if (hex.getTerrain().isTower() && legion.numLords() > 0
&& towerTeleportAllowed())
{
// Mark every unoccupied hex within 6 hexes.
if (towerToNonTowerTeleportAllowed())
{
result.addAll(findNearbyUnoccupiedHexes(hex, legion, 6,
Constants.NOWHERE));
}
if (towerToTowerTeleportAllowed())
{
// Mark every unoccupied tower.
for (MasterHex tower : game.getVariant().getMasterBoard()
.getTowerSet())
{
if (!game.isOccupied(tower) && !(tower.equals(hex)))
{
result.add(tower);
}
}
}
else
{
// Remove nearby towers from set.
result.removeAll(game.getVariant().getMasterBoard()
.getTowerSet());
}
}
// Titan teleport
if (player.canTitanTeleport() && legion.hasTitan()
&& titanTeleportAllowed())
{
// Mark every hex containing an enemy stack that does not
// already contain a friendly stack.
for (Legion other : game.getEnemyLegions(player))
{
MasterHex otherHex = other.getCurrentHex();
if (!game.containsOpposingLegions(otherHex))
{
result.add(otherHex);
}
}
}
result.remove(null);
return result;
}
/** Return set of hexLabels describing where this legion can teleport. */
public Set<MasterHex> listTeleportMoves(Legion legion, MasterHex hex,
int movementRoll)
{
// Call with inAdvance=false
return listTeleportMoves(legion, hex, movementRoll, false);
}
/** Return a Set of Strings "Left" "Right" or "Bottom" describing
* possible entry sides. If the hex is unoccupied, just return
* one entry side since it doesn't matter. */
public Set<EntrySide> listPossibleEntrySides(Legion legion,
MasterHex targetHex, boolean teleport)
{
Set<EntrySide> entrySides = new HashSet<EntrySide>();
int movementRoll = game.getMovementRoll();
MasterHex currentHex = legion.getCurrentHex();
if (teleport)
{
if (isValidTeleportMove(legion, targetHex, movementRoll) == null)
{
// Startlisted terrain only have bottom entry side.
// Don't bother finding more than one entry side if unoccupied.
if (!game.isOccupied(targetHex)
|| targetHex.getTerrain().hasStartList())
{
entrySides.add(EntrySide.BOTTOM);
return entrySides;
}
else
{
entrySides.add(EntrySide.BOTTOM);
entrySides.add(EntrySide.LEFT);
entrySides.add(EntrySide.RIGHT);
return entrySides;
}
}
else
{
return entrySides;
}
}
// Normal moves.
Set<String> tuples = findNormalMoves(currentHex, legion, movementRoll,
findBlock(currentHex), Constants.NOWHERE, null, false);
Iterator<String> it = tuples.iterator();
while (it.hasNext())
{
String tuple = it.next();
List<String> parts = Split.split(':', tuple);
String hl = parts.get(0);
if (hl.equals(targetHex.getLabel()))
{
String buf = parts.get(1);
entrySides.add(EntrySide.fromLabel(buf));
}
}
return entrySides;
}
/** Return set of hexLabels describing where this legion can move. */
public Set<MasterHex> listAllMoves(Legion legion, MasterHex hex,
int movementRoll)
{
return listAllMoves(legion, hex, movementRoll, false);
}
/** Return set of hexLabels describing where this legion can move. */
public Set<MasterHex> listAllMoves(Legion legion, MasterHex hex,
int movementRoll, boolean inAdvance)
{
Set<MasterHex> set = listNormalMoves(legion, hex, movementRoll, false,
null, inAdvance);
set.addAll(listTeleportMoves(legion, hex, movementRoll, inAdvance));
return set;
}
public Set<MasterHex> listNormalMoves(Legion legion, MasterHex hex,
int movementRoll)
{
return listNormalMoves(legion, hex, movementRoll, false, null, false);
}
/** Return set of hexLabels describing where this legion can move
* without teleporting.
* Include moves currently blocked by friendly legions
* if ignoreFriends is true.
* @return set of hexlabels
*/
public Set<MasterHex> listNormalMoves(Legion legion, MasterHex hex,
int movementRoll, boolean ignoreFriends, MasterHex fromHex,
boolean inAdvance)
{
if (hex == null || (legion.hasMoved() && !inAdvance))
{
return new HashSet<MasterHex>();
}
Set<String> tuples = findNormalMoves(hex, legion, movementRoll,
findBlock(hex), Constants.NOWHERE, fromHex, ignoreFriends);
// Extract just the hexLabels from the hexLabel:entrySide tuples.
Set<MasterHex> result = new HashSet<MasterHex>();
Iterator<String> it = tuples.iterator();
while (it.hasNext())
{
String tuple = it.next();
List<String> parts = Split.split(':', tuple);
String hexLabel = parts.get(0);
result.add(game.getVariant().getMasterBoard()
.getHexByLabel(hexLabel));
}
return result;
}
/** Recursively find conventional moves from this hex.
* If block >= 0, go only that way. If block == -1, use arches and
* arrows. If block == -2, use only arrows. Do not double back in
* the direction you just came from.
*
* TODO get rid of this String serialization and return a proper data
* structure
*
* @return a set of hexLabel:entrySide tuples.
*/
public Set<String> findNormalMoves(MasterHex hex, Legion legion, int roll,
int block, int cameFrom, MasterHex fromHex, boolean ignoreFriends)
{
Set<String> result = new HashSet<String>();
Player player = legion.getPlayer();
// TODO is fromHex actually useful?
// if (game.getNumEnemyLegions(hex, player) > 0 && !hex.equals(fromHex))
// Server side didn't have it at all, and in client side it was not
// passed on to the recursive calls. So it's set in first hex anyway,
// but in first hex there can't be an enemy legion, or can there?
//
// If there are enemy legions in this hex, mark it
// as a legal move and stop recursing. If there is
// also a friendly legion there, just stop recursing.
// Do a check versus fromHexLabel if we are evaluating
// passing through this hex
if (game.getNumEnemyLegions(hex, player) > 0 && !hex.equals(fromHex))
{
if (game.getNumFriendlyLegions(hex, player) == 0 || ignoreFriends)
{
// Set the entry side relative to the hex label.
if (cameFrom != -1)
{
result.add(hex.getLabel() + ":"
+ findEntrySide(hex, cameFrom).getLabel());
}
}
return result;
}
if (roll == 0)
{
// This is the originally server side functionality:
// XXX fix
// This hex is the final destination. Mark it as legal if
// it is unoccupied by friendly legions.
List<? extends Legion> legions = player.getLegions();
for (Legion otherLegion : legions)
{
if (!ignoreFriends && otherLegion != legion
&& hex.equals(otherLegion.getCurrentHex()))
{
return result;
}
}
/* The part below is how it was in MovementClientSide.
* When I tried to use that, the cycle/spin case with split
* legions produces NAKs - server thinks the player has valid
* conventional moves.
*/
/*
// This hex is the final destination. Mark it as legal if
// it is unoccupied by friendly legions that have already moved.
// Account for spin cycles.
List<Legion> legions = game.getFriendlyLegions(hex, player);
if (!legions.isEmpty() && !ignoreFriends)
{
// it's enough to check one; there can never be a moved one
// and a not moved one in same hex
if (legions.get(0).hasMoved())
{
return result;
}
}
*/
if (cameFrom != -1)
{
result.add(hex.getLabel() + ":"
+ findEntrySide(hex, cameFrom).getLabel());
return result;
}
}
else if (roll < 0)
{
LOGGER.log(
Level.SEVERE,
"Movement.findNormalMoves() was called with negative roll number "
+ roll + "; legion " + legion.getMarkerId()
+ ", fromHex = " + fromHex.getLabel() + ", hex="
+ hex.getLabel());
return result;
}
if (block >= 0)
{
result.addAll(findNormalMoves(hex.getNeighbor(block), legion,
roll - 1, Constants.ARROWS_ONLY, (block + 3) % 6, null,
ignoreFriends));
}
else if (block == Constants.ARCHES_AND_ARROWS)
{
for (int i = 0; i < 6; i++)
{
if (hex.getExitType(i).ordinal() >= Constants.HexsideGates.ARCH
.ordinal() && i != cameFrom)
{
result.addAll(findNormalMoves(hex.getNeighbor(i), legion,
roll - 1, Constants.ARROWS_ONLY, (i + 3) % 6, null,
ignoreFriends));
}
}
}
else if (block == Constants.ARROWS_ONLY)
{
for (int i = 0; i < 6; i++)
{
if (hex.getExitType(i).ordinal() >= Constants.HexsideGates.ARROW
.ordinal() && i != cameFrom)
{
result.addAll(findNormalMoves(hex.getNeighbor(i), legion,
roll - 1, Constants.ARROWS_ONLY, (i + 3) % 6, null,
ignoreFriends));
}
}
}
return result;
}
/** Verify whether this is a valid teleport move.
*
* @param legion
* @param hex
* @param roll
* @return Reason why it is not a valid teleport move, null if valid move
*/
public String isValidTeleportMove(Legion legion, MasterHex hex, int roll)
{
Set<MasterHex> set = listTeleportMoves(legion, legion.getCurrentHex(),
roll, false);
if (!set.contains(hex))
{
String marker = legion.getMarkerId() + " "
+ ((LegionServerSide)legion).getMarkerName();
return "List for teleport moves " + set + " of " + marker
+ " from " + legion.getCurrentHex() + " does not contain '"
+ hex + "'";
}
return null;
}
/** Verify whether this is a valid normal move.
*
* @param legion
* @param hex
* @param player
* @return Reason why it is not a normal move, null if valid move
*/
public String isValidNormalMove(Legion legion, MasterHex hex,
Player player, int roll)
{
Set<MasterHex> set = listNormalMoves(legion, legion.getCurrentHex(),
roll, false, null, false);
if (!set.contains(hex))
{
String marker = legion.getMarkerId() + " "
+ ((LegionServerSide)legion).getMarkerName();
return "List for normal moves " + set + " + of " + marker
+ " from " + legion.getCurrentHex() + " does not contain '"
+ hex + "'";
}
return null;
}
/** Verify whether this is a valid entry side.
*
* @param legion The moving legion
* @param hex The aimed target hex
* @param teleport Whether this is a teleported move
* @return Reason why it is not a valid entry side, null if valid
*/
public String isValidEntrySide(Legion legion, MasterHex hex,
boolean teleport, EntrySide entrySide)
{
Set<EntrySide> legalSides = listPossibleEntrySides(legion, hex,
teleport);
if (!legalSides.contains(entrySide))
{
return "EntrySide '" + entrySide + "' is not valid, valid are: "
+ legalSides.toString();
}
return null;
}
}