package net.sf.colossus.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.client.Client;
import net.sf.colossus.client.LegionClientSide;
import net.sf.colossus.common.Constants;
import net.sf.colossus.game.Creature;
import net.sf.colossus.game.Legion;
import net.sf.colossus.game.Player;
import net.sf.colossus.util.Glob;
import net.sf.colossus.util.MultiSet;
import net.sf.colossus.variant.CreatureType;
import net.sf.colossus.variant.MasterBoardTerrain;
import net.sf.colossus.variant.MasterHex;
import net.sf.colossus.xmlparser.TerrainRecruitLoader;
/**
* Simple implementation of a Titan AI
*
* @author Bruce Sherrod, David Ripton
* @author Romain Dolbeau
* @author Corwin Joy, extensively rewritten on 02-Oct-2003
*/
public class RationalAI extends SimpleAI
{
private static final Logger logger = Logger.getLogger(RationalAI.class
.getName());
boolean I_HATE_HUMANS = false;
private final List<Legion> legionsToSplit = new LinkedList<Legion>();
private Map<MasterHex, List<Legion>>[] enemyAttackMap;
private final Map<String, Integer> evaluateMoveMap = new HashMap<String, Integer>();
private List<LegionBoardMove> bestMoveList;
private Iterator<LegionBoardMove> bestMoveListIter;
public RationalAI(Client client)
{
super(client);
}
private static double r3(double value)
{
return Math.round(1000. * value) / 1000.;
}
@Override
public boolean split()
{
// Refresh these once per turn.
enemyAttackMap = buildEnemyAttackMap(client.getOwningPlayer());
evaluateMoveMap.clear();
legionsToSplit.clear();
Player player = client.getOwningPlayer();
for (Legion legion : player.getLegions())
{
legionsToSplit.add(legion);
}
return fireSplits();
}
/** Return true if done with all splits and callbacks */
private boolean fireSplits()
{
logger.log(Level.FINEST, "RationalAI.fireSplits " + legionsToSplit);
//safe to cache the player out of the loop, ref to mutable object
Player player = client.getOwningPlayer();
while (!legionsToSplit.isEmpty())
{
if (player.getNumMarkersAvailable() == 0)
{
logger.log(Level.FINEST, "No more splits. No markers left.");
return true; //early exit, out of markers
}
Legion legion = legionsToSplit.remove(0);
if (!splitOneLegion(player, legion))
{
return false; //early exit, we've decided we won't finish
}
}
return true; //successfully looked at all splits
}
// TODO Undoing a split could release the marker needed to do another
// split, so we need to synchronize access.
/** If parentId and childId are null, this is a callback to
* an undo split */
@Override
public boolean splitCallback(Legion parent, Legion child)
{
logger.log(Level.FINEST, "RationalAI.splitCallback " + parent + " "
+ child);
return splitOneLegionCallback((LegionClientSide)parent,
(LegionClientSide)child) && fireSplits();
}
// Compute the expected value of a split legion
// If we want to compute just a single legion, pass null for
// the child_legion
private double expectedValueSplitLegion(LegionClientSide legion,
LegionClientSide child_legion)
{
double split_value = 0.0;
// Compute value of staying for each split stack
double stay_here_value1 = hexRisk(legion, legion.getCurrentHex(),
false);
double stay_here_value2;
if (child_legion != null)
{
stay_here_value2 = hexRisk(child_legion, legion.getCurrentHex(),
false);
logger.log(Level.FINEST, "expectedValueSplitLegion(), value of "
+ "staying here for split legion1 " + stay_here_value1);
logger.log(Level.FINEST, "expectedValueSplitLegion(), value of "
+ "staying here for split legion1 " + stay_here_value2);
}
else
{
logger.log(Level.FINEST, "expectedValueSplitLegion(), value of "
+ "staying here for unsplit legion " + stay_here_value1);
stay_here_value2 = 0.0;
}
for (int roll = 1; roll <= 6; roll++)
{
logger.log(Level.FINEST, "expectedValueSplitLegion: Roll " + roll);
Set<MasterHex> moves = client.getMovement().listAllMoves(legion,
legion.getCurrentHex(), roll);
int size1 = moves.size() + 1;
int size2;
if (child_legion != null)
{
size2 = moves.size() + 1;
}
else
{
size2 = 2;
}
double[] valueStack1 = new double[size1];
double[] valueStack2 = new double[size2];
int move_i = 0;
for (MasterHex hex : moves)
{
double risk_payoff1 = evaluateMove(legion, hex, RECRUIT_TRUE,
1, true);
valueStack1[move_i] = risk_payoff1;
if (child_legion != null)
{
double risk_payoff2 = evaluateMove(child_legion, hex,
RECRUIT_TRUE, 1, true);
valueStack2[move_i] = risk_payoff2;
}
move_i++;
}
// add no-move as an option for each stack
valueStack1[move_i] = stay_here_value1;
if (child_legion != null)
{
valueStack2[move_i] = stay_here_value2;
}
else
{
// for loop below we need 2 available "stay here" values
valueStack2[0] = 0.0;
valueStack2[0] = 0.0;
}
// find optimal move for this roll
// iterate through move combinations of stack1 and stack2 to
// find max
double max_split;
max_split = Integer.MIN_VALUE;
for (int i = 0; i < size1; i++)
{
double val_i = valueStack1[i];
for (int j = 0; j < size2; j++)
{
if (i == j)
{
continue;
} // split stacks can't move to same hex
double val_j = valueStack2[j];
double val = val_i + val_j;
if (val > max_split)
{
max_split = val;
}
}
}
split_value += max_split;
} // end for roll
return split_value;
}
/** Return true if done, false if waiting for callback. */
boolean splitOneLegion(Player player, Legion legion)
{
logger.log(Level.FINEST, "splitOneLegion()");
// Allow aggressive splits - especially early in the game it is better
// to split more often -- this should get toned down later in the
// game by the scooby snack factor
if (legion.getHeight() < 6)
{
logger.log(Level.FINEST, "No split: height < 6");
return true;
}
double stay_here_risk = hexRisk(legion, legion.getCurrentHex(), false);
boolean at_risk = false;
if (stay_here_risk > 0 || legion.hasTitan())
{
at_risk = true;
}
if (at_risk && legion.getHeight() < 7)
{
logger
.log(Level.FINEST, "No split: height < 7 and legion at risk");
return true;
}
List<CreatureType> results = new ArrayList<CreatureType>();
boolean hasMustered = false;
MusteredCreatures mc = chooseCreaturesToSplitOut(legion, at_risk);
List<CreatureType> creatures = mc.creatures;
hasMustered = mc.mustered;
Iterator<CreatureType> it = creatures.iterator();
// int child_value = 0;
while (it.hasNext())
{
CreatureType creature = it.next();
// child_value += (creature).getPointValue();
results.add(creature);
}
// don't split a stack if it has not mustered
if (legion.getHeight() < 7 && !hasMustered)
{
logger.log(Level.FINEST, "No split: height < 7 and not mustered");
return true;
}
// Do the split. If we don't like the result we will undo it.
logger.log(Level.FINER, "RemainingMarkers: "
+ Glob.glob(",", player.getMarkersAvailable())
+ "; UsedMarkers: " + Glob.glob(",", player.getMarkersUsed()));
if (player.getMarkersAvailable().size() < 1)
{
// should never happen now that we check in fireSplits
logger.log(Level.FINEST, "No split. No markers available.");
return true;
}
String newMarkerId = pickMarker(player.getMarkersAvailable(),
player.getShortColor());
if (newMarkerId == null || "".equals(newMarkerId))
{
logger.log(Level.WARNING, "No split: new marker is empty ('"
+ newMarkerId + "') - how is that possible?");
return true;
}
logger.log(Level.FINEST, "New marker id is '" + newMarkerId + "'");
client.sendDoSplitToServer(legion, newMarkerId, results);
return false;
}
/** Return true if done, false if waiting for undo split */
private boolean splitOneLegionCallback(LegionClientSide parent,
LegionClientSide child)
{
if (parent == null && child == null)
{
return true; //nothing to do. splitCallback() depends on this exit
}
logger.log(Level.FINEST, "Split complete");
if (client.getTurnNumber() == 1)
{ // first turn
logger.log(Level.FINEST, "First turn split");
return true;
}
// Compute split value
logger
.log(Level.FINEST, "splitOneLegion(): Expected value with split");
double split_value = expectedValueSplitLegion(parent, child);
// expected value of split
// find expected value of no split
logger.log(Level.FINEST,
"splitOneLegionCallback(): Expected value with no split");
double no_split_value = 0.0;
if (client.getTurnNumber() > 1)
{
no_split_value = expectedValueSplitLegion(parent, null);
}
// For Titan group, try to only split when at 7
// The only exception should be if we are under
// severe attack and splitting can save us
// For now, just don't split titans under 7 tall no matter what
/*
if (legion.hasTitan() &&
(legion.getHeight() + child_legion.getHeight()) < 7)
{
split_value -= 10000;
no_split_value += 10000;
}
*/
// If expected value of split + 5 <= no split, do not split.
// This gives tendency to split if not under attack.
// If no_split_value is < -20 , we are under attack and trapped.
// Do not split.
logger.log(Level.FINEST, "no split value: " + no_split_value);
logger.log(Level.FINEST, "split value: " + split_value);
// Inequality needs to be < here. I_HATE_HUMANS will causes
// split_value = 0 on both sides.
if (split_value * 1.02 < no_split_value || split_value < -20)
{
// Undo the split
client.undoSplit(child);
logger.log(Level.FINEST,
"undo split - better to keep stack together");
return false;
}
else
{
logger.log(Level.FINEST, "keep the split");
return true;
}
}
/** Find value of recruiting, including possibly attacking an enemy
set enemy = null to indicate no enemy */
private double recruitValue(Legion legion, MasterHex hex, Legion enemy,
MasterBoardTerrain terrain)
{
int value = 0;
// Allow recruits even at 7 high for purposes of calculating
// the mobility value of a particular hex in evaluateMove
// Consider recruiting.
List<CreatureType> recruits = client.findEligibleRecruits(legion, hex);
if (!recruits.isEmpty())
{
CreatureType bestRecruit = recruits.get(recruits.size() - 1);
value = getHintedRecruitmentValue(bestRecruit, legion,
hintSectionUsed);
}
// Consider acquiring angels.
if (enemy != null)
{
int pointValue = ((LegionClientSide)enemy).getPointValue();
boolean wouldFlee = flee(enemy, legion);
if (wouldFlee)
{
pointValue /= 2;
}
// should work with all variants
int currentScore = legion.getPlayer().getScore();
int arv = getAcqStepValue();
int nextScore = ((currentScore / arv) + 1) * arv;
CreatureType bestRecruit = null;
while ((currentScore + pointValue) >= nextScore)
{
List<String> ral = variant.getRecruitableAcquirableList(
terrain, nextScore);
Iterator<String> it = ral.iterator();
while (it.hasNext())
{
CreatureType tempRecruit = variant.getCreatureByName(it
.next());
if ((bestRecruit == null)
|| (getHintedRecruitmentValue(tempRecruit, legion,
hintSectionUsed) >= getHintedRecruitmentValue(
bestRecruit, legion, hintSectionUsed)))
{
bestRecruit = tempRecruit;
}
}
nextScore += arv;
}
// add value of any angels
if (bestRecruit != null)
{
value += getHintedRecruitmentValue(bestRecruit, legion,
hintSectionUsed);
}
}
return value;
}
// Given a list of creatures sorted by value, figure out which ones are
// redundant / have mustered.
// Removes any creatures that have mustered from the original sorted list
private List<CreatureType> removeMustered(
List<CreatureType> sortedCreatures)
{
// Look at 4 lowest valued creatures
// Try to pull out pair that has already mustered.
List<CreatureType> creaturesThatHaveMustered = new ArrayList<CreatureType>();
outer: for (int index1 = 0; index1 < 4
&& index1 < sortedCreatures.size(); index1++)
{
CreatureType critter1 = sortedCreatures.get(index1);
for (int index2 = index1 + 1; index2 < sortedCreatures.size(); index2++)
{
CreatureType critter2 = sortedCreatures.get(index2);
if (critter1.equals(critter2))
{ // mustering yourself does not count
continue;
}
if (TerrainRecruitLoader.getRecruitGraph()
.isRecruitDistanceLessThan(critter1.getName(),
critter2.getName(), 2))
{// this creature has mustered
creaturesThatHaveMustered.add(sortedCreatures.get(index1));
sortedCreatures.remove(index1);
index1--; // adjust index to account for removal
continue outer;
}
}
}
return creaturesThatHaveMustered;
}
public class CompCreaturesByValueName implements Comparator<CreatureType>
{
private final Legion legion;
public CompCreaturesByValueName(Legion legion)
{
this.legion = legion;
}
public final int compare(CreatureType creature1, CreatureType creature2)
{
int val1 = getHintedRecruitmentValue(creature1, legion,
hintSectionUsed);
int val2 = getHintedRecruitmentValue(creature2, legion,
hintSectionUsed);
if (val1 < val2)
{
return -1;
}
if (val1 > val2)
{
return 1;
}
// val1 == val2, compare string
return (creature1).getName().compareTo((creature2).getName());
}
}
// Sort creatures first by value then by name.
// Exclude titan.
private List<CreatureType> sortCreaturesByValueName(Legion legion)
{
List<CreatureType> sortedCreatures = new ArrayList<CreatureType>();
// copy list excluding titan
for (Creature creature : legion.getCreatures())
{
CreatureType critter = creature.getType();
// Never split out the titan.
if (critter.isTitan())
{
continue;
}
sortedCreatures.add(critter);
}
Collections
.sort(sortedCreatures, new CompCreaturesByValueName(legion));
return sortedCreatures;
}
class MusteredCreatures
{
public boolean mustered;
public List<CreatureType> creatures;
MusteredCreatures(boolean m, List<CreatureType> c)
{
mustered = m;
creatures = c;
}
}
/** Decide how to split this legion, and return a list of
* Creatures to remove + status flag indicating if these
creatures have mustered or not*/
MusteredCreatures chooseCreaturesToSplitOut(Legion legion, boolean at_risk)
{
//
// split a 5 to 8 high legion
//
// idea: pick the 2 weakest creatures and kick them
// out. if there are more than 2 weakest creatures,
// try to split out ones that have already mustered.
// return split + status flag to indicate if these
// creatures have mustered or not
//
// Also: when splitting, in the case of cyclops, ogres
// or centaurs try to split out three rather than 2 if
// these have already mustered
//
if (legion.getHeight() == 8)
{
List<CreatureType> creatures = doInitialGameSplit(legion
.getCurrentHex());
return new MusteredCreatures(true, creatures);
}
logger.log(Level.FINEST,
"sortCreaturesByValueName() in chooseCreaturesToSplitOut");
List<CreatureType> sortedCreatures = sortCreaturesByValueName(legion);
logger.log(Level.FINEST, "Sorted stack - minus titan: "
+ sortedCreatures);
// Look at lowest valued creatures
// Try to pull out pair that has already mustered.
logger.log(Level.FINEST,
"removeMustered() in chooseCreaturesToSplitOut");
List<CreatureType> creaturesThatHaveMustered = removeMustered(sortedCreatures);
logger.log(Level.FINEST, "List of mustered creatures: "
+ creaturesThatHaveMustered);
boolean hasMustered = false;
if (creaturesThatHaveMustered.size() < 1)
{
hasMustered = false;
}
else
{
hasMustered = true;
}
List<CreatureType> creaturesToRemove = new ArrayList<CreatureType>();
// Try to pull out pair that has already mustered.
logger.log(Level.FINEST,
"build final split list in chooseCreaturesToSplitOut");
Iterator<CreatureType> sortIt = creaturesThatHaveMustered.iterator();
boolean split_all_mustered = false;
/*
if (!at_risk)
{
split_all_mustered = true;
}
**/
while (sortIt.hasNext()
&& (creaturesToRemove.size() < 2 || split_all_mustered)
&& creaturesToRemove.size() < 4)
{
CreatureType critter = sortIt.next();
creaturesToRemove.add(critter);
}
// If we have 3 mustered creatures, check if we have 3
// Centaur, Ogre, Cyclops, Troll, Lion. If so, try to keep the
// 3 together for maximum mustering potential
// it is a bit aggressive to keep trying to do 3/4 splits
// but it seems to give a better result
if (sortIt.hasNext() && !at_risk)
{
CreatureType first_remove = creaturesToRemove.get(0);
CreatureType critter = sortIt.next();
String s_first = first_remove.getName();
String s_critter = critter.getName();
if (s_first.compareTo(s_critter) == 0) // 3 identical, due to sort
{
if (s_first.compareTo("Centaur") == 0
|| s_first.compareTo("Ogre") == 0
|| s_first.compareTo("Cyclops") == 0
|| s_first.compareTo("Troll") == 0
|| s_first.compareTo("Lion") == 0)
{
// remove the 3rd creature
creaturesToRemove.add(critter);
logger.log(Level.FINEST, "Triple found!");
logger.log(Level.FINEST, "Creatures to remove: "
+ creaturesToRemove);
return new MusteredCreatures(hasMustered,
creaturesToRemove);
}
}
}
// If mustered creatures don't come up to 2
// start pulling out lowest valued un-mustered creatures
sortIt = sortedCreatures.iterator();
while (sortIt.hasNext() && creaturesToRemove.size() < 2)
{
CreatureType critter = sortIt.next();
creaturesToRemove.add(critter);
}
logger.log(Level.FINEST, "Creatures to remove: " + creaturesToRemove);
return new MusteredCreatures(hasMustered, creaturesToRemove);
}
// little helper class to store possible moves by legion
private class LegionBoardMove
{
final Legion legion;
final MasterHex fromHex;
final MasterHex toHex;
final double val;
final boolean noMove;
LegionBoardMove(Legion legion, MasterHex fromHex, MasterHex toHex,
double val, boolean noMove)
{
this.legion = legion;
this.fromHex = fromHex;
this.toHex = toHex;
this.val = val;
this.noMove = noMove;
}
@Override
public String toString()
{
return legion + " to " + toHex;
}
}
/** Return true if we need to run this method again after the server
* updates the client with the results of a move or mulligan. */
@Override
public boolean masterMove()
{
logger.log(Level.FINEST, "This is RationalAI.");
Player player = client.getOwningPlayer();
if (enemyAttackMap == null)
{
// special code to allow game to reload properly if saved
// during AI move
enemyAttackMap = buildEnemyAttackMap(client.getOwningPlayer());
}
// consider mulligans
if (handleMulligans(player))
{
return true;
}
boolean telePort = false;
if (bestMoveList == null)
{
bestMoveList = new ArrayList<LegionBoardMove>();
telePort = handleVoluntaryMoves(player);
bestMoveListIter = bestMoveList.iterator();
}
if (!bestMoveListIter.hasNext())
{
bestMoveList = null;
// ForcedSplit and ForcedSingle implementations here are perhaps
// quite poor solutions, but better than getting NAKs...
boolean moved = handleForcedSplitMoves(player);
if (moved)
{
return true;
}
if (!player.hasMoved())
{
moved = handleForcedSingleMove(player);
// Earlier here was a comment:
// "always need to retry" and hardcoded returned true.
// In [ 1748718 ] Game halt in Abyssal9 this lead to a deadlock;
// - so, if here is returned "false" as for "I won't do any more
// move", that problem does not occur (server recognizes that
// there is no legal move and accepts it)
// -- does this cause negative side effects elsewhere??
// Let's try ;-)
return moved;
}
return false;
}
LegionBoardMove lm = bestMoveListIter.next();
boolean wentOk = client.doMove(lm.legion, lm.toHex);
if (!wentOk)
{
logger.log(Level.WARNING, "RationalAI.masterMove: client.doMove "
+ "returned false!!");
}
if (telePort)
{
bestMoveList = null;
}
return true;
}
private boolean findMoveList(List<? extends Legion> legions,
List<List<LegionBoardMove>> all_legionMoves,
MultiSet<MasterHex> occupiedHexes, boolean teleportsOnly)
{
boolean moved = false;
for (Legion legion : legions)
{
if (legion.hasMoved())
{
moved = true;
continue;
}
logger.finest("consider marker " + legion);
// compute the value of sitting still
List<LegionBoardMove> legionMoves = new ArrayList<LegionBoardMove>();
MasterHex hex = legion.getCurrentHex();
double value = evaluateMove(legion, hex, RECRUIT_FALSE, 2, true);
LegionBoardMove lmove = new LegionBoardMove(legion, hex, hex,
value, true);
if (!teleportsOnly)
{
legionMoves.add(lmove);
occupiedHexes.add(hex);
logger.log(Level.FINEST, "value of sitting still at hex "
+ hex + " : " + value);
}
// find the expected value of all moves for this legion
Set<MasterHex> set;
if (!teleportsOnly)
{
// exclude teleport moves
set = client.getMovement().listNormalMoves(legion, hex,
client.getGame().getMovementRoll());
}
else
{
// only teleport moves
set = client.getMovement().listTeleportMoves(legion, hex,
client.getGame().getMovementRoll());
}
for (MasterHex masterHex : set)
{
value = evaluateMove(legion, masterHex, RECRUIT_TRUE, 2, true);
logger.log(Level.FINEST, "value hex " + masterHex + " value: "
+ r3(value));
lmove = new LegionBoardMove(legion, legion.getCurrentHex(),
masterHex, value, false);
legionMoves.add(lmove);
}
// Sort moves in the order of descending value.
Collections.sort(legionMoves, new Comparator<LegionBoardMove>()
{
public int compare(LegionBoardMove o1, LegionBoardMove o2)
{
return (int)o2.val - (int)o1.val; // want descending order
}
});
all_legionMoves.add(legionMoves);
}
return moved;
}
/** Return true if we moved something and need to be called again. */
private boolean handleVoluntaryMoves(Player player)
{
logger.log(Level.FINEST, "handleVoluntaryMoves()");
boolean moved = false;
List<? extends Legion> legions = player.getLegions();
List<List<LegionBoardMove>> all_legionMoves = new ArrayList<List<LegionBoardMove>>();
MultiSet<MasterHex> occupiedHexes = new MultiSet<MasterHex>();
moved = findMoveList(legions, all_legionMoves, occupiedHexes, false);
logger.log(Level.FINEST, "done computing move values for legions");
logger.log(Level.FINEST, "raw best moves:");
for (List<LegionBoardMove> moves : all_legionMoves)
{
LegionBoardMove lbm = moves.get(0);
logger.log(Level.FINEST, lbm.legion + " to " + lbm.toHex
+ " value " + lbm.val);
}
// handle teleports
// XXX
// just take the best teleport. this is not quite right
// since it may stick the legion that does not get to
// teleport with a really bad move. it is not easy
// to figure this out though.
if (client.getGame().getMovementRoll() == 6)
{
List<List<LegionBoardMove>> teleport_legionMoves = new ArrayList<List<LegionBoardMove>>();
findMoveList(legions, teleport_legionMoves,
new MultiSet<MasterHex>(), true);
Iterator<List<LegionBoardMove>> legit = teleport_legionMoves
.iterator();
LegionBoardMove best_move = new LegionBoardMove(null, null, null,
0, true);
double best_value = 0;
while (legit.hasNext())
{
List<LegionBoardMove> legionMoves = legit.next();
if (legionMoves.isEmpty())
{
continue; // not a teleporting legion
}
LegionBoardMove lm = legionMoves.get(0);
if (lm.val > best_value)
{
logger.log(Level.FINEST, "found new teleport best move "
+ lm.legion + " to " + lm.toHex + " value " + lm.val);
best_value = lm.val;
best_move = lm;
}
}
if (best_value > 0)
{
if (!best_move.noMove)
{
logger.log(Level.FINEST, "found teleport: "
+ best_move.legion + " to " + best_move.toHex
+ " value " + best_move.val);
bestMoveList.add(best_move);
return true;
}
}
}
MoveFinder opt = new MoveFinder();
bestMoveList = opt.findOptimalMove(all_legionMoves, !moved);
return false;
}
/*
* Returns true if it did one move, i.e. client needs to call us again
* (after that move was done), to check whether there is still something
* to do.
* Returns false also if something goes wrong (should never happen, but...)
* so that it does not endlessly redoes that and hangs forever.
* TODO: not very fancy or clever. All it does is:
* if there is a hex remaining with forced moves after split,
* pick the smallest one and move it to the hex with best value.
*
* However, I still prefer this over the SimpleAI approach, which chooses
* based on "Legionvalue * ChangeValue", because I'd rather sacrifice weaker
* legions and spare good ones, and not make "unnecessary" damage to good
* one and spare weak ones...
*/
private boolean handleForcedSplitMoves(Player player)
{
int roll = client.getGame().getMovementRoll();
ArrayList<MasterHex> unsplitHexes = new ArrayList<MasterHex>();
/* Consider one hex after another. It is not necessary to look at
* the individual legions, because
* a) when looking at one hex, either in "this round" there is no valid
* move, or otherwise we move one.
* b) it does not matter whether there are lords or not, because teleport
* moves are not mandatory
* c) Once we did move one, we move, return true, get called again,
* then the list of labels is re-considered again.
*/
for (Legion legion : player.getLegions())
{
List<Legion> friendlyLegions = client.getGameClientSide()
.getFriendlyLegions(legion.getCurrentHex(), player);
if (friendlyLegions.size() > 1)
{
unsplitHexes.add(legion.getCurrentHex());
}
}
for (MasterHex hex : unsplitHexes)
{
List<Legion> friendlyLegions = client.getGameClientSide()
.getFriendlyLegions(hex, player);
// pick just any legion for asking the getMovement
Legion anyLegion = friendlyLegions.get(0);
if (!client.getMovement()
.listNormalMoves(anyLegion, anyLegion.getCurrentHex(), roll)
.isEmpty())
{
// Easiest solution: just move the smallest of them,
// usually that is the one with less valuable stuff split off
//
// If split would be that Titan is in smallest stack, or 2+2+2
// and Titan in one of them (and this gets picked by random)
// well, ... bad luck :-(
// But this happens very rarely, this whole forcedSplitMoves
// in games with 6 AIs perhaps one out of 100 games...
Legion minLegion = anyLegion;
int minSize = minLegion.getHeight();
for (Legion l : friendlyLegions)
{
int size = l.getHeight();
if (size < minSize)
{
minSize = size;
minLegion = l;
}
}
Set<MasterHex> set = client.getMovement().listNormalMoves(
minLegion, minLegion.getCurrentHex(), roll);
if (set.size() == 0)
{
// This should never happen. Most likely we get then a NAK...
logger.log(Level.SEVERE, "Split legion " + minLegion
+ " in hexlabel " + hex
+ " was supposed to have forced moves left, "
+ " but normal moves list is empty?");
// anyway keep on going loop for checking next split legion
}
else
{
int bestValue = -1;
MasterHex bestHex = null;
for (MasterHex targetHex : set)
{
// The set of moves includes still hexes occupied by our own legions.
List<Legion> targetOwnLegions = client
.getGameClientSide().getFriendlyLegions(targetHex,
player);
if (targetOwnLegions.size() == 0)
{
int value = evaluateMove(minLegion, targetHex,
RECRUIT_TRUE, 2, true);
if (value > bestValue || bestValue == -1)
{
bestValue = value;
bestHex = targetHex;
}
}
}
if (bestHex == null)
{
// well, no legal move for this one. So, leave it as it is.
logger.log(Level.FINEST, "Forced split moves remain "
+ "(no legal move for legion " + minLegion + ")");
}
else
{
boolean wentOk = client.doMove(minLegion, bestHex);
if (wentOk)
{
// ok, lets get called again to check if there are more.
return true;
}
else
{
// This should never happen. Most likely we get then a NAK...
logger.log(Level.SEVERE,
"Forced split moves remain, "
+ "but client rejects moving marker "
+ minLegion + " from " + hex + " to "
+ bestHex);
}
}
}
}
}
// No forced move remaining
return false;
}
/*
* Simply move the legion which has the lowest value
* (except Titan legion) to the place which is best for it.
* Moves Titan legion only if no other choice.
*/
private boolean handleForcedSingleMove(Player player)
{
int roll = client.getGame().getMovementRoll();
// first we have to find out those that can move at all:
ArrayList<Legion> movableLegions = new ArrayList<Legion>();
for (Legion legion : player.getLegions())
{
Set<MasterHex> set = client.getMovement().listNormalMoves(legion,
legion.getCurrentHex(), roll);
if (set.size() > 0)
{
boolean couldMove = false;
for (MasterHex targetHex : set)
{
// The set of moves includes still hexes occupied by our own legions.
List<Legion> targetOwnLegions = client.getGameClientSide()
.getFriendlyLegions(targetHex, player);
if (targetOwnLegions.size() == 0)
{
couldMove = true;
}
}
if (couldMove)
{
movableLegions.add(legion);
}
}
}
if (movableLegions.size() == 0)
{
// No valid move for any legion. Fine.
return false;
}
// OK, now decide which of them to move - the smallest one.
int minValue = 0;
Legion minValueLegion = null;
Legion titanLegion = null;
for (Legion legion : movableLegions)
{
int value = legion.getPointValue();
if (legion.hasTitan())
{
titanLegion = legion;
}
else if (value < minValue || minValueLegion == null)
{
Set<MasterHex> set = client.getMovement().listNormalMoves(
legion, legion.getCurrentHex(), roll);
if (set.size() > 0)
{
minValue = value;
minValueLegion = legion;
}
}
}
// Arrrgggh. Have to move Titan legion :-(
if (minValueLegion == null && titanLegion != null)
{
logger.log(Level.FINER, "Rational AI, forced single move: "
+ " have to move Titan legion :-(");
minValueLegion = titanLegion;
}
assert minValueLegion != null : "There should be at least one legion we can move";
// Now decide where we move this unlucky one to:
Set<MasterHex> minValueMoves = client.getMovement().listNormalMoves(
minValueLegion, minValueLegion.getCurrentHex(), roll);
int bestValue = -1;
MasterHex bestHex = null;
for (MasterHex targetHex : minValueMoves)
{
List<Legion> targetOwnLegions = client.getGameClientSide()
.getFriendlyLegions(targetHex, player);
if (targetOwnLegions.size() == 0)
{
int value = evaluateMove(minValueLegion, targetHex,
RECRUIT_TRUE, 2, true);
if (value > bestValue || bestValue == -1)
{
bestValue = value;
bestHex = targetHex;
}
}
}
if (bestHex == null)
{
logger.log(Level.SEVERE, "Forced single moves remain, "
+ "moveIterator left bestHex for minValueLegion"
+ minValueLegion + " null ?");
return false;
}
boolean wentOk = client.doMove(minValueLegion, bestHex);
if (wentOk)
{
return true;
}
else
{
// This should never happen. Most likely we get then a NAK...
logger.log(Level.SEVERE, "Forced single moves remain, "
+ "but client rejects moving legion " + minValueLegion
+ " to " + bestHex);
return false;
}
}
private class MoveFinder
{
private List<LegionBoardMove> bestMove = null;
private double bestScore;
private boolean mustMove;
private long nodesExplored = 0;
// initial score is some value that should be smaller that the
// worst move
private final static double INITIAL_SCORE = -1000000;
private final static double NO_MOVE_EXISTS = 2 * INITIAL_SCORE;
public List<LegionBoardMove> findOptimalMove(
List<List<LegionBoardMove>> all_legionMoves, boolean mustMove)
{
bestMove = new ArrayList<LegionBoardMove>(); // just in case there is no legal move
bestScore = INITIAL_SCORE;
this.mustMove = mustMove;
nodesExplored = 0;
logger.log(Level.FINEST, "Starting computing the best move");
Timer fomTimer = setupTimer();
Collections.shuffle(all_legionMoves, random);
branchAndBound(new ArrayList<LegionBoardMove>(), all_legionMoves,
0);
fomTimer.cancel();
logger
.log(Level.FINEST, "Total nodes explored = " + nodesExplored);
for (Iterator<LegionBoardMove> it = bestMove.iterator(); it
.hasNext();)
{
if (it.next().noMove)
{
it.remove();
}
}
return bestMove;
}
private double moveValueBound(
List<List<LegionBoardMove>> availableMoves)
{
double ret = 0;
for (List<LegionBoardMove> moves : availableMoves)
{
if (moves.isEmpty())
{
// at least one peice has no legal moves
return NO_MOVE_EXISTS;
}
// each move list is assmed t be sorted, so just use the first
ret += (moves.get(0)).val;
}
return ret;
}
/**
* checks if a move is valid, and if so returns the moves in
* an executeable sequence. The legios not moving are not part
* of bestMove. Returns null if the move is not valid.
* @param performedMoves
* @return
*/
private List<LegionBoardMove> getValidMove(
List<LegionBoardMove> performedMoves)
{
if (mustMove)
{
boolean moved = false;
for (LegionBoardMove lm : performedMoves)
{
if (!lm.noMove)
{
moved = true;
break;
}
}
if (!moved)
{
return null;
}
}
Map<MasterHex, List<Legion>> occupiedHexes = new Hashtable<MasterHex, List<Legion>>();
Set<MasterHex> newOccupiedHexes = new HashSet<MasterHex>();
List<LegionBoardMove> newBestMove = new ArrayList<LegionBoardMove>();
for (LegionBoardMove lm : performedMoves)
{
List<Legion> markers = occupiedHexes.get(lm.fromHex);
if (markers == null)
{
markers = new ArrayList<Legion>();
occupiedHexes.put(lm.fromHex, markers);
}
markers.add(lm.legion);
}
boolean moved = true;
while (moved)
{
moved = false;
// move all pieces that has an open move
for (Iterator<LegionBoardMove> it = performedMoves.iterator(); it
.hasNext();)
{
LegionBoardMove lm = it.next();
List<Legion> destConflicts = occupiedHexes.get(lm.toHex);
if (destConflicts == null
|| destConflicts.size() == 0
|| (destConflicts.size() == 1 && lm.legion
.equals(destConflicts.get(0))))
{ // this piece has an open move
List<Legion> markers = occupiedHexes.get(lm.fromHex);
markers.remove(lm.legion);
if (!newOccupiedHexes.add(lm.toHex))
{ // two or more pieces are moving to the same spot.
return null;
}
newBestMove.add(lm);
it.remove();
moved = true;
}
}
}
// if there are moves left in perfornmedMoves
// check if there is a cycle or a split did not separate
for (LegionBoardMove lm : performedMoves)
{
if (!lm.noMove)
{
// A marker that cant move at this point
// means a cycle exists
return null;
}
// now we know we have a split legion not moving since
// that is the only way to have a noMove conflict
Set<MasterHex> moves = client.getMovement().listNormalMoves(
lm.legion, lm.legion.getCurrentHex(),
client.getGame().getMovementRoll());
for (MasterHex dest : moves)
{
if (!(newOccupiedHexes.contains(dest) || occupiedHexes
.containsKey(dest)))
{
// this legion has an open move it should have taken
return null;
}
}
}
return newBestMove;
}
private void branchAndBound(List<LegionBoardMove> performedMoves,
List<List<LegionBoardMove>> availableMoves, double currentValue)
{
nodesExplored++;
if (timeIsUp)
{
if (bestMove == null)
{
// logger.log(Level.FINEST, "no legal move found yet, not able to time out");
}
else
{
logger.log(Level.FINEST,
"handleVoluntaryMoves() time up after "
+ nodesExplored + " Nodes Explored");
return;
}
}
// bounding step
if (currentValue + moveValueBound(availableMoves) <= bestScore)
{
return;
}
if (availableMoves.isEmpty())
{ // this is a leaf check valdity of move
// could be moved to a function
List<LegionBoardMove> newBestMove = getValidMove(performedMoves);
if (newBestMove != null)
{
bestMove = newBestMove;
bestScore = currentValue;
logger.log(Level.FINEST, "New best move found: ("
+ currentValue + ") " + bestMove);
}
else
{
/* logger.log(Level.FINEST,
"Illigal move: ("
+ currentValue
+ " : "
+ bestScore
+ ") "
+ performedMoves);*/
}
return;
}
List<LegionBoardMove> nextMoves = availableMoves.get(0);
for (LegionBoardMove lm : nextMoves)
{
if (!lm.noMove
&& checkNewCycle(lm.fromHex, lm.toHex, performedMoves))
{
continue;
}
List<LegionBoardMove> newPerformedMoves = new ArrayList<LegionBoardMove>(
performedMoves);
newPerformedMoves.add(lm);
branchAndBound(newPerformedMoves,
removeHeadAndConflicts(availableMoves, lm), currentValue
+ lm.val);
}
}
/**
* checkes if there is a path from 'from' to target, using
* the moves in the list. This is used to see if there are cycles
* in the moves, when you ad a move from 'target' to 'from'
* @param target
* @param from
* @param moves
* @return
*/
private boolean checkNewCycle(MasterHex target, MasterHex from,
List<LegionBoardMove> moves)
{
for (LegionBoardMove lm : moves)
{
if (lm.fromHex.equals(from))
{
if (lm.toHex.equals(target))
{
return true;
}
if (checkNewCycle(target, lm.toHex, moves))
{
return true;
}
}
}
return false;
}
private List<List<LegionBoardMove>> removeHeadAndConflicts(
List<List<LegionBoardMove>> availableMoves, LegionBoardMove lm)
{
List<List<LegionBoardMove>> newAvailableMoves = new ArrayList<List<LegionBoardMove>>();
for (Iterator<List<LegionBoardMove>> it = availableMoves
.listIterator(1); it.hasNext();)
{
List<LegionBoardMove> moves = it.next();
List<LegionBoardMove> newMoves = new ArrayList<LegionBoardMove>();
if (lm.noMove && !moves.isEmpty()
&& lm.fromHex.equals((moves.get(0)).fromHex))
{
// special case, these two legions are split, make
// sure to try the move off moves first
LegionBoardMove stayMove = null;
for (LegionBoardMove move : moves)
{
if (move.noMove)
{
stayMove = move;
}
else
{
newMoves.add(move);
}
}
if (stayMove != null)
{ // there should be one, but just checking
newMoves.add(stayMove);
// make sure staymove is explored last
}
}
else
{
for (LegionBoardMove move : moves)
{
if (!lm.toHex.equals(move.toHex))
{
newMoves.add(move);
}
}
}
if (newMoves.size() == 1)
{
// if it only has one possible then consider it first
newAvailableMoves.add(0, newMoves);
}
else
{
newAvailableMoves.add(newMoves);
}
}
return newAvailableMoves;
}
}
final int TITAN_SURVIVAL = 20; // safety margin for Titan
// Compute risk of being attacked
// Value returned is expected point value cost
private double hexRisk(Legion legion, MasterHex hex, boolean invert)
{
double risk = 0.0;
// ignore all fear of attack on first turn
if (client.getTurnNumber() < 2)
{
return 0.0;
}
// logger.log(Level.FINEST, "considering risk of " + legion + " in " + hex);
int roll;
int result = 0;
for (roll = 1; roll <= 6; roll++)
{
List<Legion> enemies = enemyAttackMap[roll].get(hex);
if (enemies == null)
{
continue;
}
double worst_result_this_roll = 0.0;
for (Legion enemy : enemies)
{
result = evaluateCombat(enemy, legion, hex);
if (invert)
{
result = -result;
}
if (result > worst_result_this_roll)
{
worst_result_this_roll = result;
}
}
risk -= worst_result_this_roll;
}
risk /= 6.0;
if (invert)
{
risk = -risk;
}
// logger.log(Level.FINEST, "compute final attack risk as " + r3(risk));
return risk;
}
private int evaluateCombat(Legion attacker, Legion defender, MasterHex hex)
{
if (attacker.getPlayer().equals(defender.getPlayer()))
{
return 0;
}
final int defenderPointValue = ((LegionClientSide)defender)
.getPointValue();
final int attackerPointValue = ((LegionClientSide)attacker)
.getPointValue();
final BattleResults result = estimateBattleResults(attacker, defender,
hex);
int value = (int)result.getExpectedValue();
if (!I_HATE_HUMANS)
{
// In rational AI mode do not reward early titan attacks
if (client.getTurnNumber() < 5)
{
return value - 100;
}
}
boolean defenderTitan = defender.hasTitan();
if (result.getExpectedValue() > 0)
{
if (attacker.hasTitan())
{
// unless we can win the game with this attack
if (defenderTitan)
{
if (I_HATE_HUMANS)
{
// do it and win the game, there is only 1 human
value = 1000 + (int)result.getExpectedValue() * 1000;
}
else if (client.getGameClientSide().getNumLivingPlayers() == 2
&& (attackerPointValue - result.getAttackerDead()) > TITAN_SURVIVAL)
{
// do it and win the game
value = 1000 + (int)result.getExpectedValue() * 1000;
}
else if (result.getAttackerDead() < attackerPointValue / 2
&& (attackerPointValue - result.getAttackerDead()) > TITAN_SURVIVAL)
{
// our titan stack will be badly damaged
// but it is worth it
value = 100 + (int)result.getExpectedValue() * 100;
}
else
{
// ack! we'll mess up our titan group
// use metric below so that if we have no choice
// but to attack we pick the least losing battle
value = result.getAttackerDead() * -100;
}
}
else if (result.getAttackerDead() > attackerPointValue / 2)
// (1/4) will usually be about 3 pieces since titan
// will be large part of value
{
// ack! we'll mess up our titan group
// use metric below so that if we have no choice but to
// attack we pick the least losing battle
value = -100 + result.getAttackerDead() * -100;
}
else
// win with minimal loss
{
// value = result.getExpectedValue(); // default value
}
}
else if (defenderTitan)
{
value = (1000 + (int)result.getExpectedValue() * 1000)
/ client.getGameClientSide().getNumLivingPlayers();
}
}
else
// we expect to lose on this battle.
// but if the enemy is a titan stack it may be worth it
{
if (!attacker.hasTitan() && defenderTitan)
{
// gun for the titan stack if we can knock out
// more than 80% of the value
if (result.getDefenderDead() > defenderPointValue * .5)
{
// value should be proportional to amount of Titan stack
// killed since we may be able to attack with more
// than one legion
value = result.getDefenderDead() * 100
/ client.getGameClientSide().getNumLivingPlayers() / 2;
}
}
else if (attacker.hasTitan())
{
// ack! we'll kill our titan group
// use metric below so that if we have no choice but to attack
// we pick the least losing battle
value = (-1000 + (int)result.getExpectedValue() * 1000)
/ client.getGameClientSide().getNumLivingPlayers();
}
}
// apply penalty to attacks if we have few legions
// Don't reward titan attacks with few stacks
int attackerLegions = attacker.getPlayer().getNumLegions();
if (attackerLegions < 5 && !I_HATE_HUMANS)
{
return value - (result.getAttackerDead() / attackerPointValue)
* 1000;
}
return value;
}
// evaluate the attack payoff of moving into the hex given by 'hex'.
// This will typically be negative to indicate that we might lose,
// zero if the hex is empty, or positive if the hex is occupied
// by a weak legion
static final int RECRUIT_FALSE = 0; // don't allow recruiting by attacker
static final int RECRUIT_TRUE = 1; // allow recruiting by attacker
static final int RECRUIT_AT_7 = 2; // allow recruiting by attacker 7 high
private int evaluateHexAttack(Legion attacker, MasterHex hex,
int canRecruitHere)
{
int value = 0;
// consider making an attack
final Legion defender = client.getGameClientSide()
.getFirstEnemyLegion(hex, attacker.getPlayer());
if (defender != null)
{
if (!attacker.getPlayer().equals(defender.getPlayer()))
{
value = evaluateCombat(attacker, defender, hex);
}
if (I_HATE_HUMANS && !isHumanLegion(defender)
&& !isHumanLegion(attacker))
{
// try not to attack other AIs
if (value > -50)
{
value = -50;
}
if ((attacker).hasTitan())
{
value -= 100;
}
if ((defender).hasTitan())
{
value -= 100;
}
}
return value;
}
if ((canRecruitHere == RECRUIT_TRUE && (attacker).getHeight() < 7)
|| canRecruitHere == RECRUIT_AT_7)
{
if (!(attacker).hasTitan())
{
value += recruitValue(attacker, hex, null, hex.getTerrain());
}
else
{
// prefer recruiting with Titan legion
value += recruitValue(attacker, hex, null, hex.getTerrain()) * 1.1;
}
}
return value;
}
/** Memoizing wrapper for evaluateMoveInner */
private int evaluateMove(Legion legion, MasterHex hex, int canRecruitHere,
int depth, boolean addHexRisk)
{
String sep = "~";
String key = "" + legion + sep + hex + sep + canRecruitHere + sep
+ depth + sep + addHexRisk;
int score;
Integer val;
if (evaluateMoveMap.containsKey(key))
{
val = evaluateMoveMap.get(key);
score = val.intValue();
}
else
{
score = evaluateMoveInner(legion, hex, canRecruitHere, depth,
addHexRisk);
val = Integer.valueOf(score);
evaluateMoveMap.put(key, val);
}
return score;
}
// cheap, inaccurate evaluation function. Returns an expected value for
// moving this legion to this hex. The value defines a distance
// metric over the set of all possible moves.
private int evaluateMoveInner(Legion legion, MasterHex hex,
int canRecruitHere, int depth, boolean normalHexRisk)
{
// evaluateHexAttack includes recruit value
double value = evaluateHexAttack(legion, hex, canRecruitHere);
// if we get killed at this hex there can be no further musters
if (value < 0)
{
return (int)value;
}
// consider what we might be able to recruit next turn, from here
double nextTurnValue = 0.0;
double stay_at_hex = 0.0;
boolean invert = false;
if (!normalHexRisk)
{
// invert = true;
}
// value of staying at hex we move to
// i.e. what is risk we will be attacked if we stay at this hex
// (if invert = true, this becomes value of potentially
// attacking something at this hex)
stay_at_hex = hexRisk(legion, hex, invert);
// when we move to this hex we may get attacked and not have
// a next turn
value += stay_at_hex;
// if we are very likely to be attacked and die here
// then just return value
if (value < -10)
{
return (int)value;
}
if (depth == 0)
{
return (int)value;
}
// squares that are further away are more likely to be blocked
double DISC_FACTOR = 1.0;
double discount = DISC_FACTOR;
// value of next turn
for (int roll = 1; roll <= 6; roll++)
{
Set<MasterHex> normal_moves = client.getMovement()
.listNormalMoves(legion, hex, roll);
Set<MasterHex> tele_moves = client.getMovement()
.listTeleportMoves(legion, hex, roll);
double bestMoveVal = stay_at_hex; // can always stay here
boolean no_attack = false;
for (int i = 0; i < 2; i++)
{
Iterator<MasterHex> nextMoveIt;
if (i == 0)
{
nextMoveIt = normal_moves.iterator();
no_attack = false;
}
else
{
nextMoveIt = tele_moves.iterator();
no_attack = true;
}
while (nextMoveIt.hasNext())
{
MasterHex nextHex = nextMoveIt.next();
double nextMoveVal = evaluateMove(legion, nextHex,
RECRUIT_AT_7, depth - 1, no_attack);
if (nextMoveVal > bestMoveVal)
{
bestMoveVal = nextMoveVal;
}
}
}
bestMoveVal *= discount;
nextTurnValue += bestMoveVal;
// squares that are further away are more likely to be blocked
discount *= DISC_FACTOR;
}
nextTurnValue /= 6.0; // 1/6 chance of each happening
value += 0.9 * nextTurnValue; // discount future moves some
//logger.log(Level.FINEST, "depth " + depth + " EVAL " + legion +
// (canRecruitHere != RECRUIT_FALSE ? " move to " : " stay in ") +
// hex + " = " + r3(value));
return (int)value;
}
static class BattleResults
{
private final double ev; // expected value of attack
private final int att_dead;
private final int def_dead;
private List<String> log = new ArrayList<String>();
public BattleResults(double e, int a, int d)
{
ev = e;
att_dead = a;
def_dead = d;
}
/**
* @param ev
* @param att_dead
* @param def_dead
* @param log
*/
public BattleResults(double ev, int att_dead, int def_dead,
List<String> log)
{
super();
this.ev = ev;
this.att_dead = att_dead;
this.def_dead = def_dead;
this.log = log;
}
/**
* @return Returns the log.
*/
public List<String> getLog()
{
return log;
}
public double getExpectedValue()
{
return ev;
}
public int getAttackerDead()
{
return att_dead;
}
public int getDefenderDead()
{
return def_dead;
}
void log()
{
logger.log(Level.FINEST, "Expected battle log");
for (String string : log)
{
logger.log(Level.FINEST, string);
}
}
}
BattleResults estimateBattleResults(Legion attacker, Legion defender,
MasterHex hex)
{
return estimateBattleResults(attacker, defender, hex, null);
}
// add in value of points received for killing group / 100
// * (fraction of angel value = angel + arch = 24 + 12/5
// + titan value = 10 -- arbitrary, it's worth more than 4)
final double KILLPOINTS = (24.0 + 12.0 / 5.0 + 10.0) / 100.0;
private BattleResults estimateBattleResults(Legion attacker,
Legion defender, MasterHex hex, CreatureType callable)
{
if (I_HATE_HUMANS && !isHumanLegion(attacker)
&& !isHumanLegion(defender))
{
// assume no risk that AIs will attack each other
return new BattleResults(0, 0, 0);
}
MasterBoardTerrain terrain = hex.getTerrain();
// Get list of PowerSkill creatures
List<PowerSkill> attackerCreatures = getCombatList(attacker, terrain,
false);
List<PowerSkill> defenderCreatures = getCombatList(defender, terrain,
true);
ListIterator<PowerSkill> a;
// simulate combat
int attackerKilled = 0;
int defenderKilled = 0;
int defenderMuster = 0;
int round;
List<String> log = new ArrayList<String>(14);
round_loop: for (round = 2; round <= 7; round++)
{
// note start on turn 2, because often there will be no contact in turn 1
log.add("Round " + round + " attacker" + attackerCreatures);
log.add("Round " + round + " defender" + defenderCreatures);
/*
// DO NOT ADD ANGEL!
// If attacker cannot win without angel then this
// will often leave a weak group with an angel in it
// that is a scooby snack. Also, this makes the AI
// too aggressive about attacking and too conservative
// about moving and mustering
if (I_HATE_HUMANS)
{
**/
// angel call
if (callable != null && defenderKilled > 0)
{
PowerSkill angel = getNativeValue(callable, terrain, false);
attackerCreatures.add(angel);
}
//}
// 4th round muster
if (round == 4 && defenderCreatures.size() > 1)
{
// add in enemy's most likely turn 4 recruit
List<CreatureType> recruits = client.findEligibleRecruits(
defender, hex);
if (!recruits.isEmpty())
{
CreatureType bestRecruit = recruits
.get(recruits.size() - 1);
defenderMuster = getHintedRecruitmentValue(bestRecruit,
defender, hintSectionUsed);
defenderCreatures.add(getNativeValue(bestRecruit, terrain,
true));
}
}
for (int move = 0; move < 2; move++)
{
if (attackerCreatures.size() == 0)
{
break round_loop;
}
if (defenderCreatures.size() == 0)
{
break round_loop;
}
for (int phase = 0; phase < 2; phase++)
{
// mutual attacks
a = attackerCreatures.listIterator();
ListIterator<PowerSkill> d = defenderCreatures
.listIterator();
double rollover_power = 0;
int attack_skill = 3;
for (int attacks = 0; attacks < Math.max(
attackerCreatures.size(), defenderCreatures.size()); attacks++)
{
// if we have reached the end of the attackers,
// roll-over
if (!a.hasNext())
{
if (phase == 0)
{ // done attacker strikes
break;
}
a = attackerCreatures.listIterator();
}
if (!d.hasNext())
{
if (phase == 1)
{ // done defender strikes
break;
}
d = defenderCreatures.listIterator();
}
int defend_skill;
int attack_power;
PowerSkill psa;
PowerSkill psd;
if (phase == 0)
{
psa = a.next();
psd = d.next();
}
else
{
psa = d.next();
psd = a.next();
}
double current_carry = 0;
defend_skill = psd.getSkillDefend();
if (rollover_power > 0)
{
rollover_power = Math.max(
rollover_power - psd.getPowerDefend(), 0);
// N.B. use old attack skill & new defend skill
current_carry = rollover_power
* Math.min(attack_skill - defend_skill + 3, 6)
/ 6.0;
}
attack_skill = psa.getSkillAttack();
// when computing attacking power, subtract any
// dice lost due to native defence
attack_power = psa.getPowerAttack()
- psd.getPowerDefend();
double attack_prob = Math.min(
Math.max(attack_skill - defend_skill + 3, 1), 6) / 6.0;
double expected_damage = attack_prob * attack_power
+ current_carry;
psd.addDamage(expected_damage);
if (psd.getHP() < 0)
{
// damage exceeded that needed to kill
rollover_power = -1.0 * psd.getHP();
rollover_power /= attack_prob; // remaining power
psd.setHP(0.0);
}
else
{
rollover_power = 0;
}
// update defender in list
if (phase == 0)
{
d.set(psd);
}
else
{
a.set(psd);
}
}
}
// remove dead bodies
a = attackerCreatures.listIterator();
while (a.hasNext())
{
PowerSkill psa = a.next();
if (psa.getHP() <= 0.2) // we remove both dead and likely dead critters
{
attackerKilled += psa.getPointValue();
a.remove();
}
}
a = defenderCreatures.listIterator();
while (a.hasNext())
{
PowerSkill psa = a.next();
if (psa.getHP() <= 0)
{
defenderKilled += psa.getPointValue();
a.remove();
}
}
}
}
// add in attackers final recruit
double attackerMuster = 0;
if (attackerCreatures.size() > 2)
{
// add in attacker's most likely recruit
List<CreatureType> recruits = client.findEligibleRecruits(
attacker, hex);
if (!recruits.isEmpty())
{
CreatureType bestRecruit = recruits.get(recruits.size() - 1);
attackerMuster = (bestRecruit).getPointValue();
}
}
// add in defender's final recruit, if the combat lasted < 4 rounds
if (round < 4 && defenderCreatures.size() > 1)
{
// add in enemy's most likely turn 4 recruit
List<CreatureType> recruits = client.findEligibleRecruits(
defender, hex);
if (!recruits.isEmpty())
{
CreatureType bestRecruit = recruits.get(recruits.size() - 1);
defenderMuster = getHintedRecruitmentValue(bestRecruit,
defender, hintSectionUsed);
}
}
// tally results
// divide value of enemy killed legions by # of other players
int numOtherPlayers = client.getGameClientSide().getNumLivingPlayers() - 1;
if (I_HATE_HUMANS)
{
numOtherPlayers = 1;
}
double expectedValue;
expectedValue = defenderKilled / (double)numOtherPlayers
- attackerKilled + attackerMuster - defenderMuster
/ (double)numOtherPlayers;
if (attackerCreatures.size() > 1)
{
double pointsValue = defenderKilled * KILLPOINTS;
expectedValue += pointsValue;
}
if (defenderCreatures.size() > 1)
{
double pointsValue = defenderKilled * KILLPOINTS;
expectedValue -= pointsValue / numOtherPlayers;
}
return new BattleResults(expectedValue, attackerKilled, defenderKilled
- defenderMuster, log);
}
@Override
public boolean flee(Legion legion, Legion enemy)
{
logger.log(Level.FINEST, "flee called.");
if ((legion).hasTitan())
{
logger.log(Level.FINEST, "Do not flee. Defender titan.");
return false;
} // Titan never flee !
boolean save_hate = I_HATE_HUMANS;
I_HATE_HUMANS = false; //need true value of battle results here
BattleResults br = estimateBattleResults(enemy, legion,
legion.getCurrentHex());
I_HATE_HUMANS = save_hate;
int result = (int)br.getExpectedValue();
logger.log(Level.FINEST, "flee: attacking legion = " + enemy + ":"
+ Glob.glob(enemy.getCreatureTypes()));
logger.log(Level.FINEST, "flee: defending legion = " + legion + ":"
+ Glob.glob(legion.getCreatureTypes()));
logger.log(Level.FINEST, "flee called. battle results value: "
+ result);
logger.log(Level.FINEST,
"expected value of attacker dead = " + br.getAttackerDead());
logger.log(Level.FINEST,
"expected value of defender dead = " + br.getDefenderDead());
br.log();
// For the first four turns never flee
// Make attacker pay to minimize their future mustering
// capability
if (client.getTurnNumber() < 5)
{
return false;
}
// Don't flee if we win
if (br.getAttackerDead() > br.getDefenderDead())
{
return false;
}
// find attacker's most likely recruit
double deniedMuster = 0;
List<CreatureType> recruits = client.findEligibleRecruits(enemy,
legion.getCurrentHex());
if (!recruits.isEmpty())
{
CreatureType bestRecruit = recruits.get(recruits.size() - 1);
deniedMuster = bestRecruit.getPointValue();
}
int currentScore = enemy.getPlayer().getScore();
int pointValue = ((LegionClientSide)legion).getPointValue();
boolean canAcquireAngel = ((currentScore + pointValue)
/ getAcqStepValue() > (currentScore / getAcqStepValue()));
if (canAcquireAngel)
{
if (deniedMuster > 0)
{
if ((enemy).getHeight() >= 6)
{
deniedMuster += 24;
}
}
else
{
if ((enemy).getHeight() > 6)
{
deniedMuster += 24;
}
}
}
else
{
if ((enemy).getHeight() < 7)
{
deniedMuster = 0;
}
}
logger.log(Level.FINEST, "expected value of denied muster = "
+ deniedMuster);
double do_not_flee_value = br.getAttackerDead() - br.getDefenderDead()
* KILLPOINTS;
double flee_value = deniedMuster - (br.getDefenderDead() * KILLPOINTS)
/ 2.0;
logger.log(Level.FINEST, "do_not_flee_value = " + do_not_flee_value);
logger.log(Level.FINEST, "flee_value = " + flee_value);
if (do_not_flee_value >= flee_value)
{
// defender wins
logger.log(Level.FINEST, "don't flee: defending is worth more");
return false;
}
// defender loses but might not flee if
if ((enemy).hasTitan())
{
if (br.getAttackerDead() > ((LegionClientSide)enemy)
.getPointValue() * 2 / 7)
{
// attacker loses at least 2 significant pieces
// from Titan stack
logger.log(Level.FINEST, "don't flee: hurt titan");
return false;
}
}
// flee
return true;
}
@Override
public boolean concede(Legion legion, Legion enemy)
{
// Never concede titan legion.
if ((legion).hasTitan())
{
return false;
}
// Wimpy legions should concede if it costs the enemy an
// angel or good recruit.
int height = (enemy).getHeight();
boolean save_hate = I_HATE_HUMANS;
I_HATE_HUMANS = false; //need true value of battle results here
BattleResults br = estimateBattleResults(legion, enemy,
legion.getCurrentHex());
I_HATE_HUMANS = save_hate;
logger.log(Level.FINEST, "concede: attacking legion = " + legion + ":"
+ Glob.glob(legion.getCreatureTypes()));
logger.log(Level.FINEST, "concede: defending legion = " + enemy + ":"
+ Glob.glob(enemy.getCreatureTypes()));
logger.log(Level.FINEST,
"concede called. battle results value: " + br.getExpectedValue());
logger.log(Level.FINEST,
"expected value of attacker dead = " + br.getAttackerDead());
logger.log(Level.FINEST,
"expected value of defender dead = " + br.getDefenderDead());
br.log();
if (br.getDefenderDead() < ((LegionClientSide)enemy).getPointValue() * 2 / 7
&& height >= 6)
{
int currentScore = enemy.getPlayer().getScore();
int pointValue = ((LegionClientSide)legion).getPointValue();
boolean canAcquireAngel = ((currentScore + pointValue)
/ getAcqStepValue() > (currentScore / getAcqStepValue()));
// Can't use Legion.getRecruit() because it checks for
// 7-high legions.
boolean canRecruit = !client.findEligibleRecruits(enemy,
enemy.getCurrentHex()).isEmpty();
if (height == 7 && (canAcquireAngel || canRecruit))
{
return true;
}
if (canAcquireAngel && canRecruit) // know height == 6
{
return true;
}
}
return false;
}
public List<PowerSkill> getCombatList(Legion legion,
MasterBoardTerrain terrain, boolean defender)
{
List<PowerSkill> powerskills = new ArrayList<PowerSkill>();
for (CreatureType creature : legion.getCreatureTypes())
{
if (creature.getName().startsWith(Constants.titan))
{
PowerSkill ps;
int titanPower = legion.getPlayer().getTitanPower();
// Assume that Titans
// take only a minimal part in the combat.
// Here we have to include them in the list
// of creatures so that the AI knows to jump
// titan singletons
ps = new PowerSkill("Titan", Math.max(titanPower - 5, 1),
creature.getSkill());
powerskills.add(ps);
}
else
{
PowerSkill ps = getNativeValue(creature, terrain, defender);
powerskills.add(ps);
}
}
return powerskills;
}
}