freecol/src/net/sf/freecol/server/generator/EuropeanStartingPositionsGe...

611 lines
27 KiB
Java

package net.sf.freecol.server.generator;
import static net.sf.freecol.common.model.Constants.INFINITY;
import static net.sf.freecol.common.util.CollectionUtils.find;
import static net.sf.freecol.common.util.CollectionUtils.minimize;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import static net.sf.freecol.common.util.RandomUtils.getRandomMember;
import static net.sf.freecol.common.util.RandomUtils.randomShuffle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.function.ToIntFunction;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import net.sf.freecol.common.debug.FreeColDebugger;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.model.AbstractUnit;
import net.sf.freecol.common.model.Area;
import net.sf.freecol.common.model.Building;
import net.sf.freecol.common.model.BuildingType;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.EuropeanNationType;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Role;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.TileImprovement;
import net.sf.freecol.common.model.TileImprovementType;
import net.sf.freecol.common.model.TileType;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.option.GameOptions;
import net.sf.freecol.server.model.ServerBuilding;
import net.sf.freecol.server.model.ServerColony;
import net.sf.freecol.server.model.ServerPlayer;
import net.sf.freecol.server.model.ServerUnit;
/**
* Handles the starting placement of European units.
*/
class EuropeanStartingPositionsGenerator {
private static final Logger logger = Logger.getLogger(EuropeanStartingPositionsGenerator.class.getName());
private final Random random;
EuropeanStartingPositionsGenerator(Random random) {
this.random = random;
}
/**
* Creates and places the European units as determined by the map settings.
*
* The non-naval are placed on land if there are no starting naval unit able to
* carry them, and in Europe if there's a starting naval unit but no more room.
*
* Naval units are placed in Europe if non-naval units are placed on land.
*
* @param map The {@code Map} to place the european units on.
* @param players The players in the game. Starting units are still only created
* for non-REF European players.
*/
void createEuropeanUnits(Map map, List<Player> players) {
final List<Player> europeanPlayers = players.stream()
.filter(p -> p.isEuropean() && !p.isREF())
.collect(Collectors.toList());
if (europeanPlayers.isEmpty()) {
throw new RuntimeException("No players to generate units for!");
}
Collections.shuffle(europeanPlayers, random);
final java.util.Map<Player, StartingUnits> playerStartingUnits = determineStartingUnits(europeanPlayers);
final java.util.Map<Player, Tile> playerStartingTiles = determineStartingTiles(map, europeanPlayers, playerStartingUnits);
for (Player player : europeanPlayers) {
final Tile start = playerStartingTiles.get(player);
final StartingUnits startingUnits = playerStartingUnits.get(player);
placeUnitsAtStartingLocation(player, start, startingUnits);
}
}
private java.util.Map<Player, StartingUnits> determineStartingUnits(List<Player> europeanPlayers) {
final java.util.Map<Player, StartingUnits> playerStartingUnits = new HashMap<>();
for (Player player : europeanPlayers) {
playerStartingUnits.put(player, new StartingUnits(player, random));
}
return playerStartingUnits;
}
private java.util.Map<Player, Tile> determineStartingTiles(Map map, List<Player> europeanPlayers, java.util.Map<Player, StartingUnits> playerStartingUnits) {
final Specification spec = map.getSpecification();
final Game game = map.getGame();
final boolean mapDefinedStartingPositionsAvailable = isMapDefinedStartingPositionsAvailableFor(europeanPlayers, game);
if (!mapDefinedStartingPositionsAvailable) {
return determineStartingTilesWithoutUsingPredeterminedPositions(map, europeanPlayers, playerStartingUnits);
}
final java.util.Map<Player, Tile> startingPositions = determineStartingTilesUsingMapDefinedPositions(europeanPlayers, playerStartingUnits, game);
if (startingPositions.size() < playerStartingUnits.size()) {
logger.info("Unable to use map defined starting positions - possibly caused by overlapping nation areas.");
return determineStartingTilesWithoutUsingPredeterminedPositions(map, europeanPlayers, playerStartingUnits);
}
return startingPositions;
}
private java.util.Map<Player, Tile> determineStartingTilesUsingMapDefinedPositions(List<Player> europeanPlayers, java.util.Map<Player, StartingUnits> playerStartingUnits, Game game) {
final Specification spec = game.getSpecification();
final int positionType = spec.getInteger(GameOptions.STARTING_POSITIONS);
switch (positionType) {
case GameOptions.STARTING_POSITIONS_CLASSIC:
return playerMapStartingAreaAndEnsureDistance(europeanPlayers, playerStartingUnits, game);
case GameOptions.STARTING_POSITIONS_RANDOM:
return randomMapStartingArea(europeanPlayers, playerStartingUnits, game);
case GameOptions.STARTING_POSITIONS_HISTORICAL:
return playerMapStartingArea(europeanPlayers, playerStartingUnits, game);
default:
throw new IllegalStateException("Unknown positionType=" + positionType);
}
}
private java.util.Map<Player, Tile> playerMapStartingAreaAndEnsureDistance(List<Player> europeanPlayers,
java.util.Map<Player, StartingUnits> playerStartingUnits, final Game game) {
final int MINIMUM_DISTANCE_BETWEEN_PLAYERS = 10;
final int MAX_TRIES = 10;
for (int i=0; i<MAX_TRIES; i++) {
final java.util.Map<Player, Tile> startingTiles = playerMapStartingArea(europeanPlayers, playerStartingUnits, game);
final boolean satisfiesMinimumDistance = startingTiles.values().stream()
.noneMatch(t -> startingTiles.values().stream().anyMatch(t2 -> t != t2 && t.getDistanceTo(t2) < MINIMUM_DISTANCE_BETWEEN_PLAYERS));
if (satisfiesMinimumDistance) {
return startingTiles;
}
}
logger.info("Not able to secure minimum starting distance between European players.");
return playerMapStartingArea(europeanPlayers, playerStartingUnits, game);
}
private java.util.Map<Player, Tile> playerMapStartingArea(List<Player> europeanPlayers, java.util.Map<Player, StartingUnits> playerStartingUnits, final Game game) {
final java.util.Map<Player, Tile> startingTiles = new HashMap<>();
for (Player player : europeanPlayers) {
final StartingUnits startingUnits = playerStartingUnits.get(player);
final boolean prefersLand = startingUnits.getCarriers().isEmpty();
final Area startingArea = game.getNationStartingArea(player.getNation());
List<Tile> possibleStartingTiles = startingArea.getTiles().stream()
.filter(t -> t.isLand() == prefersLand)
.collect(Collectors.toList());
if (possibleStartingTiles.isEmpty()) {
possibleStartingTiles = startingArea.getTiles();
}
while (!possibleStartingTiles.isEmpty()) {
final int randomIndex = random.nextInt(possibleStartingTiles.size());
final Tile startingTile = possibleStartingTiles.get(randomIndex);
if (!startingTiles.values().contains(startingTile)) {
startingTiles.put(player, startingTile);
break;
}
possibleStartingTiles.remove(randomIndex);
}
}
return startingTiles;
}
private java.util.Map<Player, Tile> randomMapStartingArea(List<Player> europeanPlayers, java.util.Map<Player, StartingUnits> playerStartingUnits, final Game game) {
final java.util.Map<Player, Tile> startingTiles = new HashMap<>();
final Set<Tile> tilesToChooseFrom = new HashSet<>();
for (Player player : europeanPlayers) {
final Area startingArea = game.getNationStartingArea(player.getNation());
if (startingArea != null) {
tilesToChooseFrom.addAll(startingArea.getTiles());
}
}
final List<Tile> randomLandTilesToChooseFrom = tilesToChooseFrom.stream()
.filter(t -> t.isLand())
.collect(Collectors.toList());
Collections.shuffle(randomLandTilesToChooseFrom, random);
int indexLand = 0;
final List<Tile> randomOceanTilesToChooseFrom = tilesToChooseFrom.stream()
.filter(t -> !t.isLand())
.collect(Collectors.toList());
Collections.shuffle(randomOceanTilesToChooseFrom, random);
int indexOcean = 0;
for (Player player : europeanPlayers) {
final StartingUnits startingUnits = playerStartingUnits.get(player);
final boolean prefersLand = startingUnits.getCarriers().isEmpty();
final Tile startingTile;
if (prefersLand && indexLand < randomLandTilesToChooseFrom.size()) {
startingTile = randomLandTilesToChooseFrom.get(indexLand);
indexLand++;
} else if (!prefersLand && indexOcean < randomOceanTilesToChooseFrom.size()) {
startingTile = randomOceanTilesToChooseFrom.get(indexOcean);
indexOcean++;
} else if (indexLand < randomLandTilesToChooseFrom.size()) {
startingTile = randomLandTilesToChooseFrom.get(indexLand);
indexLand++;
} else {
startingTile = randomOceanTilesToChooseFrom.get(indexOcean);
indexOcean++;
}
startingTiles.put(player, startingTile);
}
return startingTiles;
}
private boolean isMapDefinedStartingPositionsAvailableFor(List<Player> europeanPlayers, Game game) {
final Specification spec = game.getSpecification();
boolean mapDefinedStartingPositions = spec.getBoolean(GameOptions.MAP_DEFINED_STARTING_POSITIONS);
final int positionType = spec.getInteger(GameOptions.STARTING_POSITIONS);
int numAreas = 0;
if (mapDefinedStartingPositions) {
for (Player player : europeanPlayers) {
final Area area = game.getNationStartingArea(player.getNation());
if (area == null || area.getTiles().isEmpty()) {
logger.info("No map defined starting area for: " + player.getNationId());
if (positionType == GameOptions.STARTING_POSITIONS_CLASSIC
|| positionType == GameOptions.STARTING_POSITIONS_HISTORICAL) {
break;
}
continue;
}
numAreas++;
}
}
if (numAreas < europeanPlayers.size()) {
mapDefinedStartingPositions = false;
}
return mapDefinedStartingPositions;
}
private java.util.Map<Player, Tile> determineStartingTilesWithoutUsingPredeterminedPositions(Map map, List<Player> europeanPlayers, java.util.Map<Player, StartingUnits> playerStartingUnits) {
final Specification spec = map.getSpecification();
final int positionType = spec.getInteger(GameOptions.STARTING_POSITIONS);
final int number = europeanPlayers.size();
// Make lists of candidate starting tiles on the east and west
// of the map, then break them up by land and "sea" (revisit
// if we get a map with a lake on the edge)
List<Tile> eastTiles = new ArrayList<>();
List<Tile> westTiles = new ArrayList<>();
List<Tile> eastLandTiles = new ArrayList<>();
List<Tile> westLandTiles = new ArrayList<>();
List<Tile> eastSeaTiles = new ArrayList<>();
List<Tile> westSeaTiles = new ArrayList<>();
map.collectStartingTiles(eastTiles, westTiles);
for (Tile t : eastTiles) {
if (t.isLand()) eastLandTiles.add(t); else eastSeaTiles.add(t);
}
for (Tile t : westTiles) {
if (t.isLand()) westLandTiles.add(t); else westSeaTiles.add(t);
}
// Now consider what type of positions we are selecting from
switch (positionType) {
case GameOptions.STARTING_POSITIONS_CLASSIC:
// Break the lists up into at least <number> candidate
// positions and shuffle. Empty the relevant list on failure.
sampleTiles(eastLandTiles, number);
sampleTiles(eastSeaTiles, number);
sampleTiles(westLandTiles, number);
sampleTiles(westSeaTiles, number);
break;
case GameOptions.STARTING_POSITIONS_RANDOM:
// Random starts are the same as classic but do not
// distinguish between east and west
eastLandTiles.addAll(westLandTiles);
eastSeaTiles.addAll(westSeaTiles);
sampleTiles(eastLandTiles, number);
sampleTiles(eastSeaTiles, number);
break;
case GameOptions.STARTING_POSITIONS_HISTORICAL:
break; // Historic positions retain all the possible tiles
}
final java.util.Map<Player, Tile> playerStartingTiles = new HashMap<>();
for (Player player : europeanPlayers) {
boolean startEast = player.getNation().getStartsOnEastCoast();
boolean startAtSea = !playerStartingUnits.get(player).getCarriers().isEmpty();
// Select a starting position from the available tiles
Tile start = null;
switch (positionType) {
case GameOptions.STARTING_POSITIONS_CLASSIC:
// Classic mode respects coast preference, the lists
// are pre-sampled and shuffled
start = ((startAtSea)
? ((startEast) ? eastSeaTiles : westSeaTiles)
: ((startEast) ? eastLandTiles : westLandTiles)).remove(0);
break;
case GameOptions.STARTING_POSITIONS_RANDOM:
// Random mode is as classic but ignores coast
// preference, the east lists already contain the west
start = ((startAtSea) ? eastSeaTiles : eastLandTiles).remove(0);
break;
case GameOptions.STARTING_POSITIONS_HISTORICAL:
start = (startAtSea)
? findHistoricalStartingPosition(player, map,
eastSeaTiles, westSeaTiles)
: findHistoricalStartingPosition(player, map,
eastLandTiles, westLandTiles);
break;
}
if (start == null) {
throw new RuntimeException("Failed to find start tile "
+ ((startAtSea) ? "at sea" : "on land")
+ " for player " + player);
}
playerStartingTiles.put(player, start);
}
return playerStartingTiles;
}
private void placeUnitsAtStartingLocation(Player player, Tile start, StartingUnits startingUnits) {
final Map map = player.getGame().getMap();
final Specification spec = map.getSpecification();
player.setEntryTile(start);
final Europe europe = player.getEurope();
final ServerPlayer serverPlayer = (ServerPlayer) player;
if (!start.isLand()) { // All aboard!
for (Unit u : startingUnits.getCarriers()) {
u.setLocation(start);
serverPlayer.exploreForUnit(u);
}
passengers: for (Unit unit : startingUnits.getPassengers()) {
for (Unit carrier : startingUnits.getCarriers()) {
if (carrier.canAdd(unit)) {
unit.setLocation(carrier);
continue passengers;
}
}
// no space left on carriers
unit.setLocation(europe);
}
for (Unit u : startingUnits.getOtherNaval()) {
u.setLocation(start);
serverPlayer.exploreForUnit(u);
}
} else { // Land ho!
for (Unit u : startingUnits.getPassengers()) {
u.setLocation(start);
serverPlayer.exploreForUnit(u);
}
for (Unit u : startingUnits.getOtherNaval()) {
u.setLocation(europe);
}
}
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.INIT)) {
createDebugUnits(map, player, start);
spec.setInteger(GameOptions.STARTING_MONEY, 10000);
}
// Start our REF player entry tile somewhere near to our
// starting tile, but do not place their units.
// They are assumed to always be on naval transport.
final Player ourREF = player.getREFPlayer();
if (ourREF == null) {
return;
}
final int distance = 10;
final List<Tile> refTiles = StreamSupport.stream(map.getCircleTiles(start, true, distance).spliterator(), false)
.filter(t -> !t.isLand())
.collect(Collectors.toList());
final Tile startRef = getRandomMember(logger, ourREF + " start", refTiles, random);
ourREF.setEntryTile(startRef);
}
/**
* Find the best historical starting position for a player from lists
* of tiles.
*
* @param player The {@code Player} to find a tile for.
* @param map The {@code Map} to search.
* @param east A list of starting {@code Tile}s on the east of the map.
* @param west A list of starting {@code Tile}s on the west of the map.
* @return The best {@code Tile} found, or null if none suitable.
*/
private Tile findHistoricalStartingPosition(final Player player,
final Map map,
List<Tile> east,
List<Tile> west) {
final Nation nation = player.getNation();
final int latY = map.getRow(nation.getPreferredLatitude());
List<Tile> tiles = (nation.getStartsOnEastCoast()) ? east : west;
if (tiles.isEmpty()) return null;
final ToIntFunction<Tile> dist = t -> Math.abs(t.getY() - latY);
final Comparator<Tile> closest = Comparator.comparingInt(dist);
Tile ret = minimize(tiles, closest);
tiles.remove(ret); // chosen tile is no longer a candidate
return ret;
}
/**
* Sample a list of tiles to pick spread out starting positions.
* Shuffle the result or clear it if there were too few tiles.
*
* @param tiles The list of {@code Tile}s to sample.
* @param number The number of players, which determines the spacing.
* @return True if there were enough tiles in the list.
*/
private boolean sampleTiles(List<Tile> tiles, int number) {
final int n = tiles.size();
final int step = n / number;
if (step <= 1) {
tiles.clear();
return false;
}
List<Tile> samples = new ArrayList<>();
// The offset start prevents selecting right on the poles
for (int i = step/2; i < n; i += step) {
samples.add(tiles.get(i));
}
tiles.clear();
tiles.addAll(samples);
randomShuffle(logger, "Starting tiles", tiles, random);
return true;
}
private List<Unit> createDebugUnits(Map map, Player player, Tile startTile) {
final Game game = map.getGame();
final Specification spec = game.getSpecification();
List<Unit> ret = new ArrayList<>(20);
// In debug mode give each player a few more units and a colony.
UnitType unitType = spec.getUnitType("model.unit.galleon");
Unit galleon = new ServerUnit(game, startTile, player, unitType);
galleon.setName(player.getNameForUnit(unitType, random));
ret.add(galleon);
unitType = spec.getUnitType("model.unit.privateer");
Unit privateer = new ServerUnit(game, startTile, player, unitType);
ret.add(privateer);
privateer.setName(player.getNameForUnit(unitType, random));
((ServerPlayer)player).exploreForUnit(privateer);
unitType = spec.getUnitType("model.unit.freeColonist");
ret.add(new ServerUnit(game, galleon, player, unitType));
unitType = spec.getUnitType("model.unit.veteranSoldier");
ret.add(new ServerUnit(game, galleon, player, unitType));
unitType = spec.getUnitType("model.unit.jesuitMissionary");
ret.add(new ServerUnit(game, galleon, player, unitType));
Tile colonyTile = null;
for (Tile tempTile : map.getCircleTiles(startTile, true, INFINITY)) {
if (tempTile.isPolar()) continue; // No initial polar colonies
if (player.canClaimToFoundSettlement(tempTile)) {
colonyTile = tempTile;
break;
}
}
if (colonyTile == null) {
return ret;
}
colonyTile.setType(find(spec.getTileTypeList(), t -> !t.isWater()));
unitType = spec.getUnitType("model.unit.expertFarmer");
Unit buildColonyUnit = new ServerUnit(game, colonyTile,
player, unitType);
ret.add(buildColonyUnit);
String colonyName = Messages.message(player.getNationLabel())
+ " " + Messages.message("Colony");
Colony colony = new ServerColony(game, player, colonyName, colonyTile);
player.addSettlement(colony);
colony.placeSettlement(true);
for (Tile tile : transform(colonyTile.getSurroundingTiles(1,1), t ->
(!t.hasSettlement()
&& (t.getOwner() == null || !t.getOwner().isEuropean())))) {
tile.changeOwnership(player, colony);
if (tile.hasLostCityRumour()) tile.removeLostCityRumour();
}
buildColonyUnit.setLocation(colony);
Tile ct = buildColonyUnit.getWorkTile();
if (ct != null) {
for (TileType t : transform(spec.getTileTypeList(),
tt -> !tt.isWater())) {
ct.setType(t);
TileImprovementType plowType = map.getSpecification()
.getTileImprovementType("model.improvement.plow");
TileImprovement plow = new TileImprovement(game, ct, plowType, null);
plow.setTurnsToComplete(0);
ct.add(plow);
break;
}
}
BuildingType schoolType = spec.getBuildingType("model.building.schoolhouse");
Building schoolhouse = new ServerBuilding(game, colony, schoolType);
colony.addBuilding(schoolhouse);
unitType = spec.getUnitType("model.unit.elderStatesman");
Unit statesman = new ServerUnit(game, colonyTile, player, unitType);
ret.add(statesman);
statesman.setLocation(colony.getWorkLocationFor(statesman,
statesman.getType().getExpertProduction()));
unitType = spec.getUnitType("model.unit.expertLumberJack");
Unit lumberjack = new ServerUnit(game, colony, player, unitType);
ret.add(lumberjack);
Tile lt = lumberjack.getWorkTile();
if (lt != null) {
TileType tt = find(spec.getTileTypeList(), TileType::isForested);
if (tt != null) lt.setType(tt);
lumberjack.changeWorkType(lumberjack.getType()
.getExpertProduction());
}
unitType = spec.getUnitType("model.unit.masterCarpenter");
ret.add(new ServerUnit(game, colony, player, unitType));
unitType = spec.getUnitType("model.unit.seasonedScout");
Unit scout = new ServerUnit(game, colonyTile, player, unitType);
ret.add(scout);
((ServerPlayer)player).exploreForUnit(scout);
unitType = spec.getUnitType("model.unit.veteranSoldier");
ret.add(new ServerUnit(game, colonyTile, player, unitType));
ret.add(new ServerUnit(game, colonyTile, player, unitType));
unitType = spec.getUnitType("model.unit.artillery");
ret.add(new ServerUnit(game, colonyTile, player, unitType));
ret.add(new ServerUnit(game, colonyTile, player, unitType));
ret.add(new ServerUnit(game, colonyTile, player, unitType));
unitType = spec.getUnitType("model.unit.treasureTrain");
Unit train = new ServerUnit(game, colonyTile, player, unitType);
ret.add(train);
train.setTreasureAmount(10000);
unitType = spec.getUnitType("model.unit.wagonTrain");
Unit wagon = new ServerUnit(game, colonyTile, player, unitType);
ret.add(wagon);
GoodsType cigarsType = spec.getGoodsType("model.goods.cigars");
Goods cigards = new Goods(game, wagon, cigarsType, 5);
wagon.add(cigards);
unitType = spec.getUnitType("model.unit.jesuitMissionary");
ret.add(new ServerUnit(game, colonyTile, player, unitType));
ret.add(new ServerUnit(game, colonyTile, player, unitType));
((ServerPlayer)player).exploreForSettlement(colony);
return ret;
}
private static final class StartingUnits {
private final List<Unit> carriers = new ArrayList<>();
private final List<Unit> passengers = new ArrayList<>();
private final List<Unit> otherNaval = new ArrayList<>();
StartingUnits(Player player, Random random) {
final Game game = player.getGame();
final Specification spec = player.getSpecification();
List<AbstractUnit> unitList = ((EuropeanNationType) player.getNationType()).getStartingUnits();
for (AbstractUnit startingUnit : unitList) {
UnitType type = startingUnit.getType(spec);
Role role = startingUnit.getRole(spec);
Unit newUnit = new ServerUnit(game, null, player, type, role);
newUnit.setName(player.getNameForUnit(type, random));
if (newUnit.isNaval()) {
if (newUnit.canCarryUnits()) {
newUnit.setState(Unit.UnitState.ACTIVE);
carriers.add(newUnit);
} else {
otherNaval.add(newUnit);
}
} else {
newUnit.setState(Unit.UnitState.SENTRY);
passengers.add(newUnit);
}
}
}
List<Unit> getCarriers() {
return carriers;
}
List<Unit> getPassengers() {
return passengers;
}
List<Unit> getOtherNaval() {
return otherNaval;
}
}
}