//
// @(#)WorldFactory.java 4/2002
//
// Copyright 2002 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/
//
package dip.world;
import dip.order.*;
import dip.world.variant.data.*;
import java.util.*;
import java.io.*;
import java.net.*;
import dip.misc.Utils;
/**
* A WorldFactory creates World objects from XML map data.
*
*/
public class WorldFactory
{
// il8n
private static final String WF_PROV_NON_UNIQUE = "WF_PROV_NON_UNIQUE";
private static final String WF_PROV_MISMATCH = "WF_PROV_MISMATCH";
//private static final String WF_VARIANT_NOTFOUND = "WF_VARIANT_NOTFOUND";
private static final String WF_BAD_STARTINGTIME = "WF_BAD_STARTINGTIME";
private static final String WF_BAD_SC_PROVINCE = "WF_BAD_SC_PROVINCE";
private static final String WF_BAD_SC_HOMEPOWER = "WF_BAD_SC_HOMEPOWER";
private static final String WF_BAD_SC_OWNER = "WF_BAD_SC_OWNER";
private static final String WF_BAD_IS_POWER = "WF_BAD_IS_POWER";
private static final String WF_BAD_IS_PROVINCE = "WF_BAD_IS_PROVINCE";
private static final String WF_BAD_IS_UNIT_LOC = "WF_BAD_IS_UNIT_LOC";
private static final String WF_BAD_IS_UNIT = "WF_BAD_IS_UNIT";
private static final String WF_BAD_VC = "WF_BAD_VC";
private static final String WF_ADJ_BAD_TYPE = "WF_ADJ_BAD_TYPE";
private static final String WF_ADJ_BAD_PROVINCE = "WF_ADJ_BAD_PROVINCE";
private static final String WF_ADJ_INVALID = "WF_ADJ_INVALID";
//private static final String WF_BAD_BUILDOPTION = "WF_BADBUILDOPTION";
private static final String WF_BAD_BORDER_NAME = "WF_BAD_BORDER_NAME";
private static final String WF_BAD_BORDER_LOCATION = "WF_BAD_BORDER_LOCATION";
// class variables
private static WorldFactory instance = null;
//cached Map Objects
private java.util.Map<Variant,dip.world.Map> mapscache = new HashMap<Variant,Map>();
private WorldFactory()
{
}// WorldFactory()
/** Get an instance of the WorldFactory */
public synchronized static WorldFactory getInstance()
{
if(instance == null)
{
instance = new WorldFactory();
}
return instance;
}// getInstance()
/*
* Create a map, or retrieve from cache
*/
public Map createMap(Variant variant) throws InvalidWorldException{
dip.world.Map map = null;
if (mapscache.containsKey(variant)){
map = mapscache.get(variant);
}else{
List provinces = new ArrayList(100);
HashMap provNameMap = new HashMap(); // mapping of names->provinces
// gather all province data, and create provinces
ProvinceData[] provinceDataArray = variant.getProvinceData();
for(int i=0; i<provinceDataArray.length; i++)
{
ProvinceData provinceData = provinceDataArray[i];
// get short names
String[] shortNames = provinceData.getShortNames();
// verify uniqueness of names
if( !isUnique(provNameMap, provinceData.getFullName(), shortNames) )
{
throw new InvalidWorldException(Utils.getLocalString(WF_PROV_NON_UNIQUE, provinceData.getFullName()));
}
// create Province object
Province province = new Province(provinceData.getFullName(), shortNames, i, provinceData.getConvoyableCoast());
// add Province data to list
provinces.add(province);
// add Province names (all) to our name->province map
provNameMap.put(province.getFullName().toLowerCase(), province);
String[] lcProvNames = province.getShortNames();
for(int pnIdx=0; pnIdx<lcProvNames.length; pnIdx++)
{
provNameMap.put(lcProvNames[pnIdx].toLowerCase(), province);
}
}
// gather all adjacency data
// parse adjacency data for all provinces
// keep a list of the locations parsed below
ArrayList locationList = new ArrayList(16);
for(int i=0; i<provinceDataArray.length; i++)
{
ProvinceData provinceData = provinceDataArray[i];
String[] adjProvinceTypes = provinceData.getAdjacentProvinceTypes();
String[] adjProvinceNames = provinceData.getAdjacentProvinceNames();
if(adjProvinceTypes.length != adjProvinceNames.length)
{
throw new InvalidWorldException(Utils.getLocalString(WF_PROV_MISMATCH));
}
// get the Province to which this adjacency data refers
Province province = (Province) provNameMap.get(provinceData.getFullName().toLowerCase());
// get the Adjacency data structure from the Province
Province.Adjacency adjacency = province.getAdjacency();
// parse adjacency data, then set it for this province
for(int adjIdx=0; adjIdx<adjProvinceTypes.length; adjIdx++)
{
// get the coast type.
Coast coast = Coast.parse(adjProvinceTypes[adjIdx]);
// clear the location list (we re-use it)
locationList.clear();
// parse provinces, making locations for each
// provinces must be seperated by " " or "," or ";" or ":"
String input = adjProvinceNames[adjIdx].trim().toLowerCase();
StringTokenizer st = new StringTokenizer(input, " ,;:\t\n\r", false);
while(st.hasMoreTokens())
{
// makeLocation() will change the coast, as needed, and verify the province
Location location = makeLocation(provNameMap, st.nextToken(), coast);
locationList.add(location);
}
// add data to adjacency table after unwrapping collection
Location[] locations = (Location[]) locationList.toArray(new Location[locationList.size()]);
adjacency.setLocations(coast, locations);
}
// validate adjacency data
if(!adjacency.validate(province))
{
throw new InvalidWorldException(Utils.getLocalString(WF_ADJ_INVALID, provinceData.getFullName()));
}
// create wing coast
adjacency.createWingCoasts();
}
// Process BorderData. This requires the Provinces to be known and
// successfully parsed. They are mapped to the ID name, stored in the borderMap.
HashMap borderMap = new HashMap(11);
try
{
BorderData[] borderDataArray = variant.getBorderData();
for(int i=0; i<borderDataArray.length; i++)
{
BorderData bd = borderDataArray[i];
Location fromLocs[] = makeBorderLocations(bd.getFrom(), provNameMap);
Border border = new Border( bd.getID(), bd.getDescription(),
bd.getUnitTypes(), fromLocs, bd.getOrderTypes(),
bd.getBaseMoveModifier(), bd.getSeason(),
bd.getPhase(),bd.getYear() );
borderMap.put(bd.getID(), border);
}
}
catch(InvalidBorderException ibe)
{
throw new InvalidWorldException(ibe.getMessage());
}
// set the Border data (if any) for each province.
{
ArrayList list = new ArrayList(10);
for(int i=0; i<provinceDataArray.length; i++)
{
list.clear();
ProvinceData provinceData = provinceDataArray[i];
Province province = (Province) provNameMap.get(provinceData.getFullName().toLowerCase());
String[] borderNames = provinceData.getBorders();
for(int bIdx=0; bIdx<borderNames.length; bIdx++)
{
Border border = (Border) borderMap.get( borderNames[bIdx] );
if(border == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_BORDER_NAME, province.getShortName(), borderNames[bIdx]));
}
list.add(border);
}
if( !list.isEmpty() )
{
province.setBorders( (Border[]) list.toArray(new Border[list.size()]) );
}
}
}
// Now that we know the variant, we know the powers, and can
// create the Map.
map = new dip.world.Map(
variant.getPowers(),
(Province[]) provinces.toArray(new Province[provinces.size()]) );
SupplyCenter[] supplyCenters = variant.getSupplyCenters();
for(int i=0; i<supplyCenters.length; i++)
{
Province province = map.getProvince(supplyCenters[i].getProvinceName());
if(province == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_SC_PROVINCE, supplyCenters[i].getProvinceName()));
}
province.setSupplyCenter(true);
}
mapscache.put(variant, map);
}
return map;
}
/** Generates a World given the supplied Variant information */
public World createWorld(Variant variant)
throws InvalidWorldException
{
if(variant == null)
{
throw new IllegalArgumentException();
}
Map map = createMap(variant);
// create the World object as well, now that we have the Map
World world = new World(map);
// set variables to null that we don't need (just a safety check)
// locationList = null;
// provinces = null;
// provNameMap = null;
// borderMap.clear();
// borderMap = null;
// create initial turn state based on starting game time
Phase phase = variant.getStartingPhase();
if(phase == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_STARTINGTIME));
}
// create the Position object, as we will need it for various game state
Position pos = new Position(map);
// define supply centers
SupplyCenter[] supplyCenters = variant.getSupplyCenters();
for(int i=0; i<supplyCenters.length; i++)
{
Province province = map.getProvince(supplyCenters[i].getProvinceName());
if(province == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_SC_PROVINCE, supplyCenters[i].getProvinceName()));
}
String hpName = supplyCenters[i].getHomePowerName();
if(!"none".equalsIgnoreCase(hpName))
{
Power power = map.getPower(hpName);
if(power == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_SC_HOMEPOWER, hpName));
}
pos.setSupplyCenterHomePower(province, power);
}
// define current owner of supply center, if any
String scOwner = supplyCenters[i].getOwnerName();
if(!"none".equalsIgnoreCase(scOwner))
{
Power power = map.getPower(scOwner);
if(power == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_SC_OWNER, scOwner));
}
pos.setSupplyCenterOwner(province, power);
}
}
// set initial state [derived from INITIALSTATE elements in XML file]
InitialState[] initStates = variant.getInitialStates();
for(int i=0; i<initStates.length; i++)
{
// a province and power is required, no matter what, unless
// we are ONLY setting the supply center (which we do above)
Power power = map.getPowerMatching(initStates[i].getPowerName());
Province province = map.getProvinceMatching(initStates[i].getProvinceName());
// n/a if we use a validating parser
if(power == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_IS_POWER));
}
// n/a if we use a validating parser
if(province == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_IS_PROVINCE));
}
Unit.Type unitType = initStates[i].getUnitType();
if(unitType != null)
{
// create unit in province, if location is valid
Coast coast = initStates[i].getCoast();
Unit unit = new Unit(power, unitType);
Location location = new Location(province, coast);
try
{
location = location.getValidatedSetup(unitType);
unit.setCoast(location.getCoast());
pos.setUnit(province, unit);
// set 'lastOccupier' for unit
pos.setLastOccupier(province, unit.getPower());
}
catch(OrderException e)
{
throw new InvalidWorldException( Utils.getLocalString(WF_BAD_IS_UNIT_LOC,
initStates[i].getProvinceName(), e.getMessage()) );
}
}
else
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_IS_UNIT, initStates[i].getProvinceName()));
}
}
// set the victory conditions
// make sure we have at least one victory condition!
if( variant.getNumSCForVictory() <= 0
&& variant.getMaxYearsNoSCChange() <= 0
&& variant.getMaxGameTimeYears() <= 0 )
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_VC));
}
VictoryConditions vc = new VictoryConditions( variant.getNumSCForVictory(), variant.getMaxYearsNoSCChange(),
variant.getMaxGameTimeYears(), phase);
world.setVictoryConditions(vc);
// set TurnState / Map / complete World creation.
TurnState turnState = new TurnState(phase);
turnState.setPosition(pos);
turnState.setWorld(world);
world.setTurnState(turnState);
return world;
}// makeWorld()
/**
* Parses the Adjacency data and converts it into the Location objects
* e.g.:
* <p>
* The default coast is corrected as follows:
* <br> 'mv' : stays 'mv' (Coast.LAND)
* <br> 'xc' : stays 'xc' (Coast.SINGLE)
* <br> 'nc', 'wc', 'ec', 'sc' : converted to 'xc' (Coast.SINGLE)
* <p>
* The default coast is over-ridden if a coast is specified
* after a ref. thus given: <ADJACENCY type="xc" refs="xxx yyy/sc"/>
* "yyy" will have a coast of "sc" (Coast.SOUTH)
* <p>
* The reason is because look at the following (for bulgaria)
* <pre>
* <PROVINCE shortname="bul" fullname="Bulgaria">
* <ADJACENCY type="mv" refs="gre con ser rum" />
* <ADJACENCY type="ec" refs="con bla rum" />
* <ADJACENCY type="sc" refs="gre aeg con" />
* </PROVINCE>
* </pre>
* If we do not convert directional coasts (nc/ec/wc/sc) to (xc),
* bul/ec would then be linked to con/ec, bla/ec, rum/ec which
* do not even exist.
*
*
*/
private Location makeLocation(HashMap provNameMap, String name, Coast theDefaultCoast)
throws InvalidWorldException
{
Coast defaultCoast = theDefaultCoast;
if(defaultCoast.equals(Coast.UNDEFINED))
{
throw new InvalidWorldException(Utils.getLocalString(WF_ADJ_BAD_TYPE, name));
}
else if( defaultCoast.equals(Coast.NORTH) || defaultCoast.equals(Coast.WEST)
|| defaultCoast.equals(Coast.SOUTH) || defaultCoast.equals(Coast.EAST) )
{
defaultCoast = Coast.SINGLE;
}
Coast coast = Coast.parse(name);
String provinceName = Coast.getProvinceName(name);
if(coast.equals(Coast.UNDEFINED))
{
coast = defaultCoast;
}
// name lookup
Province province = (Province) provNameMap.get(provinceName.toLowerCase());
if(province == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_ADJ_BAD_PROVINCE,
name, provinceName, defaultCoast));
}
// create Location
return new Location(province, coast);
}// makeLocation()
/**
* Makes a Border location. This uses the already-generated Provinces and Adjacency data,
* which help error checking. It also will create "undefined" coasts by default. If the
* coast does not exist for a Province (but is not Undefined) then this will create an
* Exception.
* <p>
* Input is a space and/or comma-seperated list.
* <p>
* This will return null if there are no border locations, instead of
* a zero-length array.
*/
private Location[] makeBorderLocations(String in, HashMap provNameMap)
throws InvalidWorldException
{
ArrayList al = new ArrayList(6);
StringTokenizer st = new StringTokenizer(in.trim(), ";, ");
while(st.hasMoreTokens())
{
String tok = st.nextToken();
Coast coast = Coast.parse(tok);
Province province = (Province) provNameMap.get( Coast.getProvinceName(tok).toLowerCase());
if(province == null)
{
throw new InvalidWorldException(Utils.getLocalString(WF_BAD_BORDER_LOCATION, tok));
}
al.add(new Location(province, coast));
}
if(al.isEmpty())
{
return null;
}
else
{
return (Location[]) al.toArray(new Location[al.size()]);
}
}// makeBorderLocation()
// verify all names are unique. (hasn't yet been added to the map)
private boolean isUnique(HashMap provNameMap, String fullname, String[] shortnames)
{
if(provNameMap.get(fullname.toLowerCase()) != null)
{
return false;
}
for(int i=0; i<shortnames.length; i++)
{
if(provNameMap.get(shortnames[i].toLowerCase()) != null)
{
return false;
}
}
return true;
}// isUnique()
}// class MapFactory