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) {