package net.sf.colossus.variant; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.logging.Logger; import javax.swing.text.Document; import net.sf.colossus.game.Game; import net.sf.colossus.util.CollectionHelper; import net.sf.colossus.util.Predicate; /** * Hub for all variant-specific information. * * This class is meant to give access to all the information about a Colossus * game in the static sense: the master board layout, the battle board layouts, * available creatures, rules, etc. The information about a game in progress is * in the {@link Game} class. * * Instances of this class are immutable. * * TODO add access to the markers by having a class for them * TODO same thing for the colors/markersets */ public class Variant { private static final Logger LOGGER = Logger.getLogger(Variant.class .getName()); private final AllCreatureType creatureTypes; private final List<CreatureType> summonableCreatureTypes; private final Collection<MasterBoardTerrain> terrains; private final List<AcquirableData> acquirableList; private final MasterBoard masterBoard; private final Document readme; private final String variantName; private final int titanImprove; private final int titanTeleport; /** * A map for fast lookup of creatures by their name. * * This is a cache to find creatures by their case-insensitive name quickly. */ private final Map<String, CreatureType> creatureTypeByNameCache = new HashMap<String, CreatureType>(); public Variant(IVariantInitializer variantInitializer, AllCreatureType creatureTypes, MasterBoard masterBoard, Document readme, String name) { this.creatureTypes = creatureTypes; // defensive copies to ensure immutability this.acquirableList = variantInitializer.getAcquirablesList(); this.titanTeleport = variantInitializer.getTitanTeleportValue(); this.titanImprove = variantInitializer.getTitanImprovementValue(); // create some caches for faster lookups -- by name and by the "summonable" attribute initCreatureNameCache(); this.summonableCreatureTypes = new ArrayList<CreatureType>(); CollectionHelper.copySelective(this.creatureTypes.getCreatureTypes(), this.summonableCreatureTypes, new Predicate<CreatureType>() { public boolean matches(CreatureType creatureType) { return creatureType.isSummonable(); } }); this.terrains = variantInitializer.getTerrains(); this.masterBoard = masterBoard; this.readme = readme; this.variantName = name; } public List<CreatureType> getCreatureTypesAsList() { return this.creatureTypes.getCreatureTypesAsList(); } public SortedSet<CreatureType> getCreatureTypes() { return this.creatureTypes.getCreatureTypes(); } public Collection<MasterBoardTerrain> getTerrains() { return this.terrains; } /** * Retrieves the terrain with the given identifier. * * @param id The identifier for the terrain. Must be a valid for this variant. * @return The matching terrain. * @throws IllegalArgumentException iff the identifier does not refer to an * existing terrain in this variant. */ public MasterBoardTerrain getTerrainById(String id) { for (MasterBoardTerrain terrain : this.terrains) { if (terrain.getId().equals(id)) { return terrain; } } throw new IllegalArgumentException("No terrain with the ID " + id); } public MasterBoard getMasterBoard() { return masterBoard; } public Document getReadme() { return readme; } public String getName() { return variantName; } /** * Look up a creature type by its name. * * The lookup is case-insensitive at the moment (TODO: check if that makes * sense at all). * * TODO in the long run noone should really need this since the names shouldn't * be passed around by themselves * ... except for resolving name that came over network... (Clemens) * * @param name Name of a creature type. Not null. * @return CreatureType with the given name, null no such creature type. */ public CreatureType getCreatureByName(final String name) { assert name != null; if (name.equals("null")) { LOGGER.warning("Attempt to resolve String 'null' to CreatureType"); Thread.dumpStack(); } if (name.equals("Anything")) { // TODO they should be properly handled LOGGER.info("Attempt to resolve 'Anything' to CreatureType"); // Thread.dumpStack(); /* WARNING: Could not find creature with name Anything java.lang.Exception: Stack trace at java.lang.Thread.dumpStack(Thread.java:1158) at net.sf.colossus.variant.Variant.getCreatureByName(Variant.java:163) at net.sf.colossus.game.RecruitGraph.numberOfRecruiterNeeded(RecruitGraph.java:412) at net.sf.colossus.xmlparser.TerrainRecruitLoader.anonymousRecruitLegal(TerrainRecruitLoader.java:977) at net.sf.colossus.server.GameServerSide.anonymousRecruitLegal(GameServerSide.java:1991) at net.sf.colossus.server.GameServerSide.doRecruit(GameServerSide.java:2010) at net.sf.colossus.server.Server.doRecruit(Server.java:1613) at net.sf.colossus.server.ClientHandler.callMethod(ClientHandler.java:384) at net.sf.colossus.server.ClientHandler.doCallMethodInTryBlock(ClientHandler.java:267) at net.sf.colossus.server.ClientHandler.processInput(ClientHandler.java:155) at net.sf.colossus.server.Server.waitOnSelector(Server.java:427) at net.sf.colossus.server.Server.run(Server.java:179) */ return null; } String lowerCaseName = name.toLowerCase(); // XXX Why do I need to special-case angels but not titans? if (lowerCaseName.startsWith("angel")) { lowerCaseName = "angel"; } CreatureType result = creatureTypeByNameCache.get(lowerCaseName); if (result == null) { // TODO find every case where this happens and get rid of it, // we should be able to assert a result LOGGER.warning("Could not find creature with name " + name); } return result; } private void initCreatureNameCache() { // find it the slow way and add to cache. for (CreatureType creatureType : this.creatureTypes.getCreatureTypes()) { creatureTypeByNameCache.put(creatureType.getName().toLowerCase(), creatureType); } // "null" (not a null pointer...) is used for recruiter // when it is anonymous, so it is known and legal, // mapped to null (a null pointer, this time). // TODO avoid using nulls altogether creatureTypeByNameCache.put("null", null); } /** * Checks if a creature with the given name exists. * * @param name (case insensitive) name of a creature, must not be null. * @return true if this names represents a creature */ public boolean isCreature(final String name) { return creatureTypeByNameCache.containsKey(name.toLowerCase()); } public List<CreatureType> getSummonableCreatureTypes() { return summonableCreatureTypes; } /** * Used internally to record the Acquirable name, points needed for * recruiting, and the list of terrains in which the Acquirable dwells. * @author Romain Dolbeau */ public static class AcquirableData { // TODO should be CreatureType private final String name; private final int value; private final List<MasterBoardTerrain> where; public AcquirableData(String n, int v, List<MasterBoardTerrain> terrains) { name = n; value = v; where = terrains; } String getName() { return name; } int getValue() { return value; } /** * Tell if the Acquirable can be Acquired in the terrain. * @param t The terrain in which the Acquirements occurs. * @return True if the Acquirable can be acquired here, * false otherwise. */ boolean isAvailable(MasterBoardTerrain t) { if (where.isEmpty() || ((where.indexOf(t)) != -1)) { return true; } else { return false; } } @Override public String toString() { return ("Acquirable by name of " + name + ", available every " + value + (where.isEmpty() ? "" : ", in terrain " + where)); } } /** * To obtain all the Creature that can be Acquired. * @return The list of name (as String) that can be Acquired */ public List<String> getAcquirableList() { List<String> al = new ArrayList<String>(); Iterator<AcquirableData> it = acquirableList.iterator(); while (it.hasNext()) { AcquirableData ad = it.next(); al.add(ad.getName()); } return al; } /** * To obtain the base amount of points needed for Acquirement. * All Acquirements must occur at integer multiple of this. * @return The base amount of points needed for Acquirement. */ public int getAcquirableRecruitmentsValue() { AcquirableData ad = acquirableList.get(0); return ad.getValue(); } /** * To obtain the first Acquirable (aka 'primary') Creature name. * This one is the starting Lord with the Titan. * @return The name of the primary Acquirable Creature. */ public String getPrimaryAcquirable() { AcquirableData ad = acquirableList.get(0); return ad.getName(); } /** * To obtain all the Creature that can be acquired at the given amount of * points in the given terrain. * * TODO should return List<CreatureType> * * @param t The Terrain in which the recruitment occurs. * @param value The number of points at which the recruitment occurs. * Valid values are constrained. * @return The list of name (as String) that can be acquired in this * terrain, for this amount of points. * @see #getAcquirableRecruitmentsValue() */ public List<String> getRecruitableAcquirableList(MasterBoardTerrain t, int value) { List<String> al = new ArrayList<String>(); if ((value % getAcquirableRecruitmentsValue()) != 0) { return al; } Iterator<AcquirableData> it = acquirableList.iterator(); while (it.hasNext()) { AcquirableData ad = it.next(); if (ad.isAvailable(t) && ((value % ad.getValue()) == 0)) { al.add(ad.getName()); } } return al; } /** * Check if the Creature whose name is in parameter is an Acquirable * creature or not. * @param name The name of the Creature inquired. * @return If the creature is Acquirable. */ private boolean isAcquirable(String name) { Iterator<AcquirableData> it = acquirableList.iterator(); while (it.hasNext()) { AcquirableData ad = it.next(); if (name.equals(ad.getName())) { return true; } } return false; } /** * Check if the Creature in parameter is an Acquirable creature or not. * @param c The Creature inquired. * @return If the creature is Acquirable. */ public boolean isAcquirable(CreatureType c) { return isAcquirable(c.getName()); } /** * To obtain the base amount of points needed for Titan improvement. * @return The base amount of points needed for Titan improvement. */ public int getTitanImprovementValue() { return titanImprove; } /** * To obtain the amount of points needed for Titan teleport. * @return The amount of points needed for Titan teleport. */ public int getTitanTeleportValue() { return titanTeleport; } public int[] getReinforcementTurns() { int[] reinforcementTurns = { 4 }; return reinforcementTurns; } public int getMaxBattleTurns() { return 7; } }