package net.sf.colossus.server;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
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.game.BattleCritter;
import net.sf.colossus.game.Creature;
import net.sf.colossus.game.Game;
import net.sf.colossus.game.Legion;
import net.sf.colossus.variant.BattleHex;
import net.sf.colossus.variant.CreatureType;
import net.sf.colossus.variant.HazardHexside;
/**
* Class Critter represents an individual Titan Character.
*
* TODO this duplicates functionality from the {@link CreatureType} class,
* mostly due to the fact that the latter doesn't handle the Titans
* properly
*
* TODO a lot of the code in here is about the battle rules, often
* implemented in combination with the Battle class. It would be much
* easier if this class was just a dumb critter and the rules of battles
* are all in the Battle class.
*
* @author David Ripton
* @author Romain Dolbeau
*/
public class CreatureServerSide extends Creature implements BattleCritter
{
private static final Logger LOGGER = Logger
.getLogger(CreatureServerSide.class.getName());
// TODO the creature would probably be better off not knowing
// about the battle
private BattleServerSide battle;
/**
* The game this creature belongs to.
*
* Never null.
*/
private final GameServerSide game;
/** Unique identifier for each critter. */
private final int tag;
/** Counter used to assign unique tags. */
private static int tagCounter = -1;
private final SortedSet<PenaltyOption> penaltyOptions = new TreeSet<PenaltyOption>();
private boolean carryPossible;
public CreatureServerSide(CreatureType creature, Legion legion,
GameServerSide game)
{
super(creature, legion);
assert game != null : "No server-side creature without a game";
this.game = game;
tag = ++tagCounter;
}
void setBattleInfo(BattleHex currentHex, BattleHex startingHex,
BattleServerSide battle)
{
setCurrentHex(currentHex);
setStartingHex(startingHex);
this.battle = battle;
}
void setLegion(LegionServerSide legion)
{
this.legion = legion;
}
public Game getGame()
{
return game;
}
public int getTag()
{
return tag;
}
public boolean isDefender()
{
assert legion != null : "Legion must be set when calling isDefender()!";
if (legion.equals(battle.getDefendingLegion()))
{
return true;
}
else if (legion.equals(battle.getAttackingLegion()))
{
return false;
}
else
{
LOGGER
.severe("this creatures legion is neither attacking nor defending legion?");
return false;
}
}
void undoMove()
{
setCurrentHex(getStartingHex());
LOGGER.log(Level.INFO, getName() + " undoes move and returns to "
+ getStartingHex());
}
// TODO change to deal with target critters, instead of hexes?
// Need to check also client side copies of similar functionality ;
// on first glance looks they would be better with creatures as well.
boolean canStrike(Creature target)
{
return battle.findTargetHexes(this, true).contains(
target.getCurrentHex());
}
/** Calculate number of dice and strike number needed to hit target,
* and whether any carries and strike penalties are possible.
* The actual striking is now deferred to strike2(). */
void strike(CreatureServerSide target)
{
battle.leaveCarryMode();
carryPossible = true;
if (battle.numInContact(this, false) < 2)
{
carryPossible = false;
}
int strikeNumber = game.getBattleStrikeSS().getStrikeNumber(this,
target);
int dice = game.getBattleStrikeSS().getDice(this, target);
// Carries are only possible if the striker is rolling more dice than
// the target has hits remaining.
if (carryPossible && (dice <= target.getPower() - target.getHits()))
{
carryPossible = false;
}
if (carryPossible)
{
findCarries(target);
if (!penaltyOptions.isEmpty())
{
game.getServer().askChooseStrikePenalty(penaltyOptions);
return;
}
}
strike2(target, dice, strikeNumber);
}
/** Side effects. */
void assignStrikePenalty(String prompt)
{
if (prompt.equals(Constants.cancelStrike))
{
LOGGER.log(Level.INFO, "Strike cancelled on pickCarryDialog");
penaltyOptions.clear();
battle.clearCarryTargets();
return;
}
PenaltyOption po = matchingPenaltyOption(prompt);
if (po != null)
{
CreatureServerSide target = (CreatureServerSide)po.getTarget();
int dice = po.getDice();
int strikeNumber = po.getStrikeNumber();
carryPossible = (po.numCarryTargets() >= 1);
battle.setCarryTargets(po.getCarryTargets());
strike2(target, dice, strikeNumber);
}
else
{
LOGGER.log(Level.WARNING, "Illegal penalty option " + prompt);
}
}
/** Return true if the passed prompt matches one of the stored
* penalty options. */
private PenaltyOption matchingPenaltyOption(String prompt)
{
if (penaltyOptions == null)
{
return null;
}
Iterator<PenaltyOption> it = penaltyOptions.iterator();
while (it.hasNext())
{
PenaltyOption po = it.next();
if (prompt.equals(po.toString()))
{
return po;
}
}
return null;
}
// XXX Should be able to return penaltyOptions and carry targets
// rather than use side effects.
/** Side effects on penaltyOptions, Battle.carryTargets */
void findCarries(CreatureServerSide target)
{
battle.clearCarryTargets();
penaltyOptions.clear();
// Abort if no carries are possible.
if (game.getBattleStrikeSS().getDice(this, target) <= target
.getPower() - target.getHits())
{
return;
}
// Look for possible carries in each direction.
for (int i = 0; i < 6; i++)
{
if (possibleCarryToDir(target.getCurrentHex(), i))
{
findCarry(target, getCurrentHex().getNeighbor(i));
}
}
if (!penaltyOptions.isEmpty())
{
// Add the non-penalty option as a choice.
PenaltyOption po = new PenaltyOption(game, this, target, game
.getBattleStrikeSS().getDice(this, target), game
.getBattleStrikeSS().getStrikeNumber(this, target));
penaltyOptions.add(po);
// Add all non-penalty carries to every PenaltyOption.
Iterator<PenaltyOption> it = penaltyOptions.iterator();
while (it.hasNext())
{
po = it.next();
po.addCarryTargets(battle.getCarryTargets());
}
}
}
/** Return true if carries are possible to the hex in direction
* dir, considering only terrain. */
private boolean possibleCarryToDir(BattleHex targetHex, int dir)
{
BattleHex hex = getCurrentHex();
BattleHex neighbor = hex.getNeighbor(dir);
if (neighbor == null || neighbor == targetHex)
{
return false;
}
if (hex.isCliff(dir))
{
return false;
}
// Strikes not up across dune hexsides cannot carry up across
// dune hexsides.
int targDir = BattleServerSide.getDirection(targetHex, hex, false);
if (hex.getOppositeHazard(dir) == HazardHexside.DUNE
&& targetHex.getHexsideHazard(targDir) != HazardHexside.DUNE)
{
return false;
}
return true;
}
/** For a strike on target, find any carries (including those
* only allowed via strike penalty) to the creature in neighbor
* Side effects on penaltyOptions, Battle.carryTargets */
private void findCarry(CreatureServerSide target, BattleHex neighbor)
{
final int dice = game.getBattleStrikeSS().getDice(this, target);
final int strikeNumber = game.getBattleStrikeSS().getStrikeNumber(
this, target);
CreatureServerSide victim = battle.getCreatureSS(neighbor);
if (victim == null || victim.getPlayer() == getPlayer()
|| victim.isDead())
{
return;
}
int tmpDice = game.getBattleStrikeSS().getDice(this, victim);
int tmpStrikeNumber = game.getBattleStrikeSS().getStrikeNumber(this,
victim);
// Can't actually get a bonus to carry.
if (tmpDice > dice)
{
tmpDice = dice;
}
if (tmpStrikeNumber < strikeNumber)
{
tmpStrikeNumber = strikeNumber;
}
// Abort if no carries are possible.
if (tmpDice <= target.getPower() - target.getHits())
{
return;
}
if (tmpStrikeNumber == strikeNumber && tmpDice == dice)
{
// Can carry with no need for a penalty.
battle.addCarryTarget(neighbor);
}
else
{
// Add this scenario to the list, reusing an
// existing PenaltyOption if possible.
Iterator<PenaltyOption> it = penaltyOptions.iterator();
while (it.hasNext())
{
PenaltyOption po = it.next();
if (po.getDice() == tmpDice
&& po.getStrikeNumber() == tmpStrikeNumber)
{
po.addCarryTarget(neighbor);
return;
}
}
// No match, so create a new PenaltyOption.
PenaltyOption po = new PenaltyOption(game, this, target, tmpDice,
tmpStrikeNumber);
po.addCarryTarget(neighbor);
penaltyOptions.add(po);
}
}
/** Called after strike penalties are chosen.
* Roll the dice and apply damage. Highlight legal carry targets. */
private void strike2(CreatureServerSide target, int dice, int strikeNumber)
{
List<String> rolls = new ArrayList<String>();
int damage;
if (game.getOption(Options.pbBattleHits))
{
/**
* Probability-based Battle Rolls:
*
* If set to true, give exactly (at least) the amount of hits as one could get
* according to probability. E.g. for 6 dice and strike nr 4, 3 hits,
* 6 dice and strike number 6 gives 1 hit.
* Includes "accumulated wasted luck per creature", i.e. if a centaur
* yields 1.5 wasted luck = 0.5, next time accumulated WL = 1
* => give one hit more and subtract 1.0 from AWL.
*/
damage = game.getBattleStrike().determineProbabilityBasedHits(
this, target, dice, strikeNumber, rolls);
}
else
{
// Whether the rolling should take random number or from the sequence
boolean randomized = !game
.getOption(Options.fixedSequenceBattleDice);
// Roll the dice:
damage = game.getBattleStrike().rollDice(this, target, dice,
strikeNumber, rolls, randomized);
}
int carryDamage = target.adjustHits(damage);
if (!carryPossible)
{
carryDamage = 0;
}
battle.setCarryDamage(carryDamage);
if (damage > 0)
{
// If damaged creature, then may have poisoned
// or slowed it as well.
int poison = getPoison();
if (poison != 0)
{
target.addPoisonDamage(poison);
}
int slow = getSlows();
if (slow != 0)
{
target.addSlowed(slow);
}
}
// Let the attacker choose whether to carry, if applicable.
if (carryDamage > 0)
{
LOGGER.log(Level.INFO, carryDamage
+ (carryDamage == 1 ? " carry available"
: " carries available"));
}
// Record that this attacker has struck.
setStruck(true);
game.getServer().allTellStrikeResults(this, target, strikeNumber,
rolls, damage, carryDamage, battle.getCarryTargetDescriptions());
}
Set<PenaltyOption> getPenaltyOptions()
{
return Collections.unmodifiableSortedSet(penaltyOptions);
}
// TODO noone seems to be calling this, so we might as well remove it
// Check first, though -- maybe by adding an assert false here and then
// run some stresstests and a game or two
@Override
public String toString()
{
return getType().toString();
}
// TODO only hashCode() but not equals() is overridden. This implementation
// makes all Creatures(Critters) of the same CreatureType(Creature) equal,
// which seems highly suspicious
@Override
public int hashCode()
{
return getType().hashCode();
}
}