diff --git a/data/rules/classic/specification.xml b/data/rules/classic/specification.xml index e03dbae75..a33a27ca0 100644 --- a/data/rules/classic/specification.xml +++ b/data/rules/classic/specification.xml @@ -4624,6 +4624,10 @@ sail in? --> + + + + diff --git a/data/strings/FreeColMessages.properties b/data/strings/FreeColMessages.properties index 2e12f4665..1181c20e1 100644 --- a/data/strings/FreeColMessages.properties +++ b/data/strings/FreeColMessages.properties @@ -727,6 +727,8 @@ model.option.continueFoundingFatherRecruitment.name=Continue recruiting Founding model.option.continueFoundingFatherRecruitment.shortDescription=Continue recruiting Founding Fathers after independence is granted. model.option.teleportREF.name=Teleport REF model.option.teleportREF.shortDescription=REF appears at the landing site for its first target. +model.option.mapDefinedStartingPositions.name=Use starting positions defined by map (if available). +model.option.mapDefinedStartingPositions.shortDescription=Starting positions defined by the map author is used if available. model.option.startingPositions.name=Starting Positions model.option.startingPositions.shortDescription=Determines the starting positions of the European players. model.option.startingPositions.classic.name=Classic diff --git a/src/net/sf/freecol/common/model/Specification.java b/src/net/sf/freecol/common/model/Specification.java index 35be25d2d..c2267b2aa 100644 --- a/src/net/sf/freecol/common/model/Specification.java +++ b/src/net/sf/freecol/common/model/Specification.java @@ -3088,6 +3088,12 @@ public final class Specification implements OptionContainer { Boolean.FALSE, BooleanOption.class); // end @compat 0.11.6 + // @compat 1.1.0 + ret |= checkOp(GameOptions.MAP_DEFINED_STARTING_POSITIONS, + GameOptions.GAMEOPTIONS_MAP, + Boolean.TRUE, BooleanOption.class); + // end @compat 1.1.0 + // SAVEGAME_VERSION == 14 return ret; } diff --git a/src/net/sf/freecol/common/option/GameOptions.java b/src/net/sf/freecol/common/option/GameOptions.java index 2dbe18c1a..4cefdd960 100644 --- a/src/net/sf/freecol/common/option/GameOptions.java +++ b/src/net/sf/freecol/common/option/GameOptions.java @@ -100,6 +100,11 @@ public class GameOptions { public static final String TELEPORT_REF = "model.option.teleportREF"; + /** + * Use map defined starting positions (if available). + */ + public static final String MAP_DEFINED_STARTING_POSITIONS = "model.option.mapDefinedStartingPositions"; + /** How to determine the starting positions of European players. */ public static final String STARTING_POSITIONS = "model.option.startingPositions"; diff --git a/src/net/sf/freecol/server/generator/EuropeanStartingPositionsGenerator.java b/src/net/sf/freecol/server/generator/EuropeanStartingPositionsGenerator.java index b35a643ba..4ac7d52ff 100644 --- a/src/net/sf/freecol/server/generator/EuropeanStartingPositionsGenerator.java +++ b/src/net/sf/freecol/server/generator/EuropeanStartingPositionsGenerator.java @@ -8,10 +8,13 @@ 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; @@ -20,6 +23,7 @@ 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; @@ -78,6 +82,8 @@ class EuropeanStartingPositionsGenerator { if (europeanPlayers.isEmpty()) { throw new RuntimeException("No players to generate units for!"); } + + Collections.shuffle(europeanPlayers, random); final java.util.Map playerStartingUnits = determineStartingUnits(europeanPlayers); final java.util.Map playerStartingTiles = determineStartingTiles(map, europeanPlayers, playerStartingUnits); @@ -100,7 +106,138 @@ class EuropeanStartingPositionsGenerator { } private java.util.Map determineStartingTiles(Map map, List europeanPlayers, java.util.Map playerStartingUnits) { - return determineStartingTilesWithoutUsingPredeterminedPositions(map, europeanPlayers, 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 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 playerMapStartingAreaAndEnsureDistance(List europeanPlayers, + java.util.Map playerStartingUnits, final Game game) { + final int MINIMUM_DISTANCE_BETWEEN_PLAYERS = 10; + final int MAX_TRIES = 10; + for (int i=0; i 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 playerMapStartingArea(List europeanPlayers, java.util.Map playerStartingUnits, final Game game) { + final java.util.Map 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 possibleStartingTiles = startingArea.getTiles().stream() + .filter(t -> t.isLand() == prefersLand) + .collect(Collectors.toList()); + if (possibleStartingTiles.isEmpty()) { + possibleStartingTiles = startingArea.getTiles(); + } + + final int randomIndex = random.nextInt(possibleStartingTiles.size()); + final Tile startingTile = possibleStartingTiles.get(randomIndex); + startingTiles.put(player, startingTile); + } + return startingTiles; + } + + private java.util.Map randomMapStartingArea(List europeanPlayers, java.util.Map playerStartingUnits, final Game game) { + final java.util.Map startingTiles = new HashMap<>(); + + final Set tilesToChooseFrom = new HashSet<>(); + for (Player player : europeanPlayers) { + final Area startingArea = game.getNationStartingArea(player.getNation()); + if (startingArea != null) { + tilesToChooseFrom.addAll(startingArea.getTiles()); + } + } + + final List randomLandTilesToChooseFrom = tilesToChooseFrom.stream() + .filter(t -> t.isLand()) + .collect(Collectors.toList()); + Collections.shuffle(randomLandTilesToChooseFrom, random); + int indexLand = 0; + + final List 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 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 determineStartingTilesWithoutUsingPredeterminedPositions(Map map, List europeanPlayers, java.util.Map playerStartingUnits) {