package net.sf.colossus.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import net.sf.colossus.game.Legion;
import net.sf.colossus.util.Combos;
import net.sf.colossus.variant.CreatureType;
import net.sf.colossus.variant.Variant;
/**
* Predicts splits for one enemy player, and adjusts predictions as
* creatures are revealed.
* @author David Ripton
* @author Kim Milvang-Jensen
*
* See docs/SplitPrediction.txt
*/
public class PredictSplitNode implements Comparable<PredictSplitNode>
{
private final String markerId; // Not unique!
private final int turnCreated;
private CreatureInfoList creatures = new CreatureInfoList();
// only if atSplit
private final CreatureInfoList removed = new CreatureInfoList();
private final PredictSplitNode parent;
// Size of child2 at the time this node was split.
private int childSize2;
private PredictSplitNode child1; // child that keeps the marker
private PredictSplitNode child2; // child with the new marker
private final Variant variant;
private final CreatureType titan;
private final CreatureType angel;
private static CreatureInfoComparator cic = new CreatureInfoComparator();
PredictSplitNode(String markerId, int turnCreated, CreatureInfoList cil,
PredictSplitNode parent, Variant variant)
{
this.markerId = markerId;
this.turnCreated = turnCreated;
this.creatures = cil.clone();
this.parent = parent;
this.variant = variant;
this.titan = variant.getCreatureByName("Titan");
this.angel = variant.getCreatureByName("Angel");
clearChildren();
}
private void clearChildren()
{
childSize2 = 0;
child1 = null;
child2 = null;
}
public String getMarkerId()
{
return markerId;
}
public String getFullName()
{
return markerId + '(' + turnCreated + ')';
}
public PredictSplitNode getChild1()
{
return child1;
}
public PredictSplitNode getChild2()
{
return child2;
}
public PredictSplitNode getParent()
{
return parent;
}
public int getTurnCreated()
{
return turnCreated;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder(getFullName() + ":");
for (CreatureInfo ci : getCreatures())
{
sb.append(" " + ci.toString());
}
for (CreatureInfo ci : getRemovedCreatures())
{
sb.append(" " + ci.toString() + "-");
}
return sb.toString();
}
/** Return list of CreatureInfo */
CreatureInfoList getCreatures()
{
boolean success = true;
CreatureInfoList copy;
try
{
copy = creatures.clone();
Collections.sort(copy, cic);
}
catch (Exception e)
{
System.err.println("Exception " + e.toString()
+ " during getCreatures(); trying again");
success = false;
copy = creatures.clone();
Collections.sort(copy, cic);
success = true;
System.err.println("getCreatures() succeeded on 2nd time.");
}
if (!success)
{
System.err.println("getCreatures() failed also on 2nd time.");
}
return copy;
}
void setCreatures(CreatureInfoList creatures)
{
this.creatures = creatures;
}
/** Return list of CreatureInfo */
CreatureInfoList getRemovedCreatures()
{
CreatureInfoList cil = new CreatureInfoList();
cil.addAll(removed);
return cil;
}
/** Return list of CreatureInfo where certain == true. */
CreatureInfoList getCertainCreatures()
{
CreatureInfoList list = new CreatureInfoList();
for (CreatureInfo ci : getCreatures())
{
if (ci.isCertain())
{
list.add(ci);
}
}
return list;
}
int numCertainCreatures()
{
return getCertainCreatures().size();
}
int numUncertainCreatures()
{
return getHeight() - numCertainCreatures();
}
boolean allCertain()
{
for (CreatureInfo ci : getCreatures())
{
if (!ci.isCertain())
{
return false;
}
}
return true;
}
boolean hasSplit()
{
if (child1 == null && child2 == null)
{
return false;
}
assert child1 != null || child2 != null : "One child legion";
return true;
}
List<PredictSplitNode> getChildren()
{
List<PredictSplitNode> li = new ArrayList<PredictSplitNode>();
if (hasSplit())
{
li.add(child1);
li.add(child2);
}
return li;
}
/**
* Return true if all of this node's children, grandchildren, etc. have no
* uncertain creatures
*/
boolean allDescendentsCertain()
{
if (child1 == null)
{
return true;
}
else
{
return child1.allCertain() && child2.allCertain()
&& child1.allDescendentsCertain()
&& child2.allDescendentsCertain();
}
}
/**
* Return list of CreatureInfo where atSplit == true, plus removed
* creatures.
*/
CreatureInfoList getAtSplitOrRemovedCreatures()
{
CreatureInfoList list = new CreatureInfoList();
for (CreatureInfo ci : getCreatures())
{
if (ci.isAtSplit())
{
list.add(ci);
}
}
for (CreatureInfo ci : getRemovedCreatures())
{
list.add(ci);
}
return list;
}
/** Return list of CreatureInfo where atSplit == false. */
CreatureInfoList getAfterSplitCreatures()
{
CreatureInfoList list = new CreatureInfoList();
for (CreatureInfo ci : getCreatures())
{
if (!ci.isAtSplit())
{
list.add(ci);
}
}
return list;
}
/**
* Return list of CreatureInfo where both certain and atSplit are true, plus
* removed creatures.
*/
CreatureInfoList getCertainAtSplitOrRemovedCreatures()
{
CreatureInfoList list = new CreatureInfoList();
for (CreatureInfo ci : getCreatures())
{
if (ci.isCertain() && ci.isAtSplit())
{
list.add(ci);
}
}
for (CreatureInfo ci : getRemovedCreatures())
{
list.add(ci);
}
return list;
}
String getOtherChildMarkerId()
{
if (!markerId.equals(child1.getMarkerId()))
{
return child1.getMarkerId();
}
else
{
return child2.getMarkerId();
}
}
int getHeight()
{
return creatures.size();
}
/**
* Return true if big is a superset of little.
*
* Note that this treats repeated elements as distinct, i.e. if the
* little list contains two copies of something, then the big list has
* to contain two copies, too. It differs in that regard from
* {@linkplain Collection#containsAll(Collection)} which is implemented
* in a fashion where this is not necessary (the specification as of JDK
* 1.5 is actually blurry on the matter).
*/
static <T> boolean superset(List<T> big, List<T> little)
{
List<T> bigclone = new ArrayList<T>(big);
for (T ob : little)
{
if (!bigclone.remove(ob))
{
return false;
}
}
return true;
}
void revealCreatures(List<CreatureType> cnl)
{
if (cnl == null)
{
// this means we are updating the parent, and the info gained is
// computed from children
cnl = new ArrayList<CreatureType>();
cnl.addAll(child1.getCertainAtSplitOrRemovedCreatures()
.getCreatureTypes());
cnl.addAll(child2.getCertainAtSplitOrRemovedCreatures()
.getCreatureTypes());
}
List<CreatureType> certainInfoGained = subtractLists(cnl,
getCertainCreatures().getCreatureTypes());
if (!certainInfoGained.isEmpty())
{
for (CreatureType type : certainInfoGained)
{
this.creatures.add(new CreatureInfo(type, true, true));
}
// TODO : added null guard, because during loading a game it went
// up and up many times (7+) until it hit null.
// Probably caused by incorrect legion contents...
// So null guard here to find the reason for that...
// Probably should never happen any more after loading of saved
// games was fixed in 08/2008... (Clemens)
assert this.parent != null : "Parent in PredictSplitNode is null, but should go up; "
+ "certain info gained is " + certainInfoGained;
// it should never be null, but... in faulty game loading it
// did happen. NullGuard just to avoid exceptions.
// No LOGGER here, too lazy to add it just for this one here...
if (this.parent != null)
{
this.parent.revealCreatures(null);
}
// Note the parent is responsible for updating the CreatureInfo
// for this node when calculating the predicted split.
}
else if (hasSplit())
{
reSplit();
}
else
{
// The reveal didn't contain any actual info; nothing to do.
}
assert this.creatures.size() == getHeight() : "Certainty error in revealCreatures -- size is "
+ this.creatures.size() + " height is " + getHeight();
}
// Hardcoded to default starting legion.
public boolean isLegalInitialSplitoff(List<CreatureType> types)
{
if (types.size() != 4)
{
return false;
}
int count = 0;
if (types.contains(titan))
{
count++;
}
if (types.contains(angel))
{
count++;
}
return count == 1;
}
/**
* Return a list of all legal combinations of splitoffs. Also update
* knownKeep and knownSplit if we conclude that more creatures are certain.
*
* @param childSize
* @param knownKeep
* @param knownSplit
* @return
*/
List<List<CreatureType>> findAllPossibleSplits(int childSize,
List<CreatureType> knownKeep, List<CreatureType> knownSplit)
{
// Sanity checks
assert knownSplit.size() <= childSize : "More known splitoffs than splitoffs";
assert creatures.size() <= 8 : "> 8 creatures in legion";
assert creatures.size() != 8 || childSize == 4 : "Illegal initial split ("
+ childSize + "/" + creatures.size() + ")";
assert creatures.size() != 8
|| creatures.getCreatureTypes().contains(titan) : "No titan in 8-high legion";
assert creatures.size() != 8
|| creatures.getCreatureTypes().contains(angel) : "No angel in 8-high legion";
List<CreatureType> knownCombo = new ArrayList<CreatureType>();
knownCombo.addAll(knownSplit);
knownCombo.addAll(knownKeep);
List<CreatureType> certain = getCertainCreatures().getCreatureTypes();
assert superset(certain, knownCombo) : "knownCombo contains uncertain creatures";
// Now determine by count arguments if we can determine know keepers
// or splits. (If parent contains 3 certain rangers, and we split 5-2
// then the 5 split contains a ranger. By the same argument
// if the 5 stack grows a griffon from 3 lions, then there are only 2
// unkowns in there from the split, so the 2 stack must contain a
// ranger.
List<CreatureType> certainsToSplit = subtractLists(certain, knownCombo);
Collections.sort(certainsToSplit);
// Special code to take into account account the the first split
// must include a lord in each stack
int firstTurnUnknownLord = 0;
if (this.turnCreated == 0)
{
boolean unknownTitan = certainsToSplit.remove(titan);
boolean unknownAngel = certainsToSplit.remove(angel);
if (unknownTitan && unknownAngel)
{
// ei. neither are positioned yet
firstTurnUnknownLord = 1;
}
else if (unknownAngel)
{
// Titan known, set Angel certain
if (knownKeep.contains(titan))
{
knownSplit.add(angel);
}
else
{
knownKeep.add(angel);
}
}
else if (unknownTitan)
{
// Titan known, set Angel certain
if (knownKeep.contains(angel))
{
knownSplit.add(titan);
}
else
{
knownKeep.add(titan);
}
}
}
int numUnknownsToKeep = creatures.size() - childSize
- knownKeep.size();
int numUnknownsToSplit = childSize - knownSplit.size();
if (!certainsToSplit.isEmpty())
{
CreatureType nextCreature = null;
CreatureType currCreature = null;
int count = 0;
Iterator<CreatureType> it = certainsToSplit.iterator();
boolean done = false;
while (!done)
{
currCreature = nextCreature;
if (it.hasNext())
{
nextCreature = it.next();
}
else
{
nextCreature = null;
done = true;
}
if (!safeEquals(currCreature, nextCreature))
{
// Compute how many to keep or split, and update the lists.
int numToKeep = count - numUnknownsToSplit
+ firstTurnUnknownLord;
int numToSplit = count - numUnknownsToKeep
+ firstTurnUnknownLord;
for (int i = 0; i < numToKeep; i++)
{
knownKeep.add(currCreature);
numUnknownsToKeep--;
}
for (int i = 0; i < numToSplit; i++)
{
knownSplit.add(currCreature);
numUnknownsToSplit--;
}
count = 1;
}
else
{
count++;
}
}
}
List<CreatureType> unknowns = creatures.getCreatureTypes();
// update knownCombo because knownKeep or knownSplit may have changed
knownCombo.clear();
knownCombo.addAll(knownSplit);
knownCombo.addAll(knownKeep);
for (CreatureType cre : knownCombo)
{
unknowns.remove(cre);
}
Combos<CreatureType> combos = new Combos<CreatureType>(unknowns,
numUnknownsToSplit);
Set<List<CreatureType>> possibleSplitsSet = new HashSet<List<CreatureType>>();
for (Iterator<List<CreatureType>> it = combos.iterator(); it.hasNext();)
{
List<CreatureType> combo = it.next();
List<CreatureType> pos = new ArrayList<CreatureType>();
pos.addAll(knownSplit);
pos.addAll(combo);
if (getHeight() != 8)
{
possibleSplitsSet.add(pos);
}
else
{
if (isLegalInitialSplitoff(pos))
{
possibleSplitsSet.add(pos);
}
}
}
List<List<CreatureType>> possibleSplits = new ArrayList<List<CreatureType>>(
possibleSplitsSet);
return possibleSplits;
}
private static <T> boolean safeEquals(T obj1, T obj2)
{
if (obj1 == null)
{
return (obj2 == null);
}
if (obj2 == null)
{
// this should happen on equals(null) anyway, but we
// don't trust all equals(..) implementations
return false;
}
return obj1.equals(obj2);
}
// TODO Use SimpleAI version?
/**
* Decide how to split this legion, and return a list of creatures names to
* remove. Return empty list on error.
*/
List<CreatureType> chooseCreaturesToSplitOut(
List<List<CreatureType>> possibleSplits)
{
List<CreatureType> firstElement = possibleSplits.get(0);
boolean maximize = (2 * firstElement.size() > getHeight());
int bestKillValue = -1;
List<CreatureType> creaturesToRemove = new ArrayList<CreatureType>();
for (List<CreatureType> li : possibleSplits)
{
int totalKillValue = 0;
for (CreatureType creature : li)
{
totalKillValue += creature.getKillValue();
}
if ((bestKillValue < 0)
|| (!maximize && totalKillValue < bestKillValue)
|| (maximize && totalKillValue > bestKillValue))
{
bestKillValue = totalKillValue;
creaturesToRemove = li;
}
}
return creaturesToRemove;
}
/** Return the number of times ob is found in li */
int count(List<?> li, Object ob)
{
int num = 0;
for (Object ob2 : li)
{
if (ob.equals(ob2))
{
num++;
}
}
return num;
}
/**
* Computes the predicted split of childsize, given that we may already know
* some pieces that are keept or spilt. Also makes the new
* CreatureInfoLists. Note that knownKeep and knownSplit will be altered,
* and be empty after call
*
* @param childSize
* @param knownKeep
* certain creatures to keep
* @param knownSplit
* certain creatures to split
* @param keepList
* return argument
* @param splitList
* return argument
*/
void computeSplit(int childSize, List<CreatureType> knownKeep,
List<CreatureType> knownSplit, CreatureInfoList keepList,
CreatureInfoList splitList)
{
List<List<CreatureType>> possibleSplits = findAllPossibleSplits(
childSize, knownKeep, knownSplit);
List<CreatureType> splitoffs = chooseCreaturesToSplitOut(possibleSplits);
// We now know how we want to split, caculate certainty and
// make the new creatureInfoLists
for (CreatureInfo ci : creatures)
{
CreatureType type = ci.getType();
CreatureInfo newinfo = new CreatureInfo(ci.getType(), false, true);
if (splitoffs.contains(type))
{
splitList.add(newinfo);
splitoffs.remove(type);
// If in knownSplit, set certain
if (knownSplit.contains(type))
{
knownSplit.remove(type);
newinfo.setCertain(true);
}
}
else
{
keepList.add(newinfo);
// If in knownKeep, set certain
if (knownKeep.contains(type))
{
knownKeep.remove(type);
newinfo.setCertain(true);
}
}
}
}
/**
* Perform the initial split of a stack, and create the children
*
* @param childSize
* @param otherMarkerId
* @param turn
*/
void split(int childSize, Legion otherLegion, int turn)
{
assert creatures.size() <= 8 : "> 8 creatures in legion";
assert !hasSplit() : "use reSplit to recalculate old splits";
List<CreatureType> knownKeep = new ArrayList<CreatureType>();
List<CreatureType> knownSplit = new ArrayList<CreatureType>();
CreatureInfoList keepList = new CreatureInfoList();
CreatureInfoList splitList = new CreatureInfoList();
computeSplit(childSize, knownKeep, knownSplit, keepList, splitList);
// If both children have same height, in 50% of the cases mix it up
// which child gets the "split off" content and which the "to keep".
if (getHeight() == 2 * childSize)
{
// A creative way to produce a random boolean value:
long now = new Date().getTime();
boolean swapKeepAndSplit = ((now % 17) % 2 == 1);
if (swapKeepAndSplit)
{
CreatureInfoList swapTmp = keepList;
keepList = splitList;
splitList = swapTmp;
}
}
child1 = new PredictSplitNode(markerId, turn, keepList, this, variant);
child2 = new PredictSplitNode(otherLegion.getMarkerId(), turn,
splitList, this, variant);
childSize2 = child2.getHeight();
}
/**
* Recompute the split of a stack, taking advantage of any information
* potentially gained from the children
*
*/
void reSplit()
{
assert creatures.size() <= 8 : "> 8 creatures in legion";
List<CreatureType> knownKeep = child1
.getCertainAtSplitOrRemovedCreatures().getCreatureTypes();
List<CreatureType> knownSplit = child2
.getCertainAtSplitOrRemovedCreatures().getCreatureTypes();
CreatureInfoList keepList = new CreatureInfoList();
CreatureInfoList splitList = new CreatureInfoList();
computeSplit(childSize2, knownKeep, knownSplit, keepList, splitList);
// we have now predicted the split we need to inform the children
child1.updateInitialSplitInfo(keepList);
child2.updateInitialSplitInfo(splitList);
}
/**
* This takes potentially new information about the legion's composition at
* split and applies the later changes to the legion to get a new predicton
* of contents. It then recursively resplits.
*
* @param newList
*/
void updateInitialSplitInfo(CreatureInfoList newList)
{
// TODO Check if any new information was gained and stop if not.
newList.addAll(getAfterSplitCreatures());
for (CreatureInfo ci : getRemovedCreatures())
{
newList.remove(ci);
}
setCreatures(newList);
// update children if we have any
if (hasSplit())
{
reSplit();
}
}
/**
* Recombine this legion and other, because it was not possible to move.
* They must share a parent. If either legion has the parent's markerId,
* then that legion will remain. Otherwise this legion will remain. Also
* used to undo splits.
*/
void merge(PredictSplitNode other)
{
if (this.parent == other.parent)
{
assert getMarkerId().equals(parent.getMarkerId())
|| other.getMarkerId().equals(parent.getMarkerId()) : "None of the legions carry the parent maker";
// this is regular merge, cancel split.
parent.clearChildren();
}
else
{
// this must be a merge of a 3-way split
// origNode -- father -- nodeB
// \ nodeA \ third
// this transforms into
// origNode -- (nodeA + nodeB)
// \ third
PredictSplitNode nodeA = null;
PredictSplitNode nodeB = null;
if (this.parent == other.parent.parent)
{
nodeA = this;
nodeB = other;
}
else if (this.parent.parent == other.parent)
{
nodeA = other;
nodeB = this;
}
// check we got a valid combination, otherwise the nodes are not set
assert (nodeA != null) && (nodeB != null) : "Illegal merge";
PredictSplitNode father = nodeB.parent;
PredictSplitNode origNode = nodeA.parent;
PredictSplitNode thirdLegion;
if (nodeB == father.child1)
{
thirdLegion = father.child2;
}
else
{
thirdLegion = father.child1;
}
if (origNode.getMarkerId().equals(thirdLegion.getMarkerId()))
{
// third is carries the original marker and nodeA is then
// the splitoff from the origNode, just add creatures from nodeB
nodeA.creatures.addAll(nodeB.creatures);
origNode.childSize2 = nodeA.getHeight();
origNode.child1 = thirdLegion;
}
else
{
// attach thirdLegion as the split from the node, and
// nodeA+nodeB as the keep
origNode.child2 = thirdLegion;
origNode.childSize2 = thirdLegion.getHeight();
if (origNode.getMarkerId().equals(nodeA.getMarkerId()))
{
nodeA.creatures.addAll(nodeB.creatures);
origNode.child1 = nodeA;
}
else
{
nodeB.creatures.addAll(nodeA.creatures);
origNode.child1 = nodeB;
}
}
}
}
void addCreature(CreatureType type)
{
assert getHeight() < 7 || child1 == null : "Tried adding to 7-high legion";
CreatureInfo ci = new CreatureInfo(type, true, false);
creatures.add(ci);
}
void removeCreature(CreatureType type)
{
assert getHeight() > 0 : "Tried removing from 0-high legion";
List<CreatureType> cnl = Collections.singletonList(type);
revealCreatures(cnl);
// Find the creature to remove
Iterator<CreatureInfo> it = creatures.iterator();
// We have already checked height>0, so taking next is ok.
CreatureInfo ci = it.next();
while (!(ci.isCertain() && ci.getType().equals(type)))
{
assert it.hasNext() : "Tried to remove nonexistant creature";
ci = it.next();
}
// Only need to track the removed creature for future parent split
// predictions if it was here at the time of the split.
if (ci.isAtSplit())
{
removed.add(ci);
}
it.remove();
}
void removeCreatures(List<CreatureType> creatureTypes)
{
revealCreatures(creatureTypes);
for (CreatureType type : creatureTypes)
{
removeCreature(type);
}
}
// TODO Comparable not implemented properly since equals() not
// overridden
public int compareTo(PredictSplitNode other)
{
return toString().compareTo(other.toString());
}
static <T> List<T> subtractLists(List<T> big, List<T> little)
{
ArrayList<T> li = new ArrayList<T>(big);
for (T item : little)
{
li.remove(item);
}
return li;
}
/** Return the number of times name occurs in li */
static int count(List<String> li, String name)
{
int num = 0;
for (String s : li)
{
if (s.equals(name))
{
num++;
}
}
return num;
}
/**
* lili is a list of lists. Return the minimum number of times name appears
* in any of the lists contained in lili.
*/
static int minCount(List<List<String>> lili, String name)
{
int min = Integer.MAX_VALUE;
for (List<String> li : lili)
{
min = Math.min(min, count(li, name));
}
if (min == Integer.MAX_VALUE)
{
min = 0;
}
return min;
}
}