/**
* Copyright (C) 2002-2012 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol 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.
*
* FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.server.generator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.logging.Logger;
import org.freecolandroid.repackaged.java.awt.Rectangle;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Map.Direction;
import net.sf.freecol.common.model.Map.Position;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Region.RegionType;
import net.sf.freecol.common.model.Resource;
import net.sf.freecol.common.model.ResourceType;
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.TileItemContainer;
import net.sf.freecol.common.model.TileType;
import net.sf.freecol.common.option.MapGeneratorOptions;
import net.sf.freecol.common.option.OptionGroup;
import net.sf.freecol.server.model.ServerRegion;
import net.sf.freecol.common.util.RandomChoice;
/**
* Class for making a <code>Map</code> based upon a land map.
*/
public class TerrainGenerator {
private static final Logger logger = Logger.getLogger(TerrainGenerator.class.getName());
public static final int LAND_REGIONS_SCORE_VALUE = 1000;
public static final int LAND_REGION_MIN_SCORE = 5;
public static final int PACIFIC_SCORE_VALUE = 100;
public static final int LAND_REGION_MAX_SIZE = 75;
private final OptionGroup mapGeneratorOptions;
private final Random random;
private TileType lake;
private TileImprovementType riverType;
private TileImprovementType fishBonusLandType;
private TileImprovementType fishBonusRiverType;
private ArrayList<TileType> terrainTileTypes;
private ArrayList<TileType> oceanTileTypes;
// TODO dynamic lakes, mountains and hills
/**
* Creates a new <code>TerrainGenerator</code>.
*
* @param mapGeneratorOptions The options.
* @param random A <code>Random</code> number source.
* @see #createMap
*/
public TerrainGenerator(OptionGroup mapGeneratorOptions, Random random) {
this.mapGeneratorOptions = mapGeneratorOptions;
this.random = random;
}
// TODO: this might be useful elsewhere, too
private int limitToRange(int value, int lower, int upper) {
return Math.max(lower, Math.min(value, upper));
}
/**
* Select a random land position on the map.
*
* <b>Warning:</b> This method should not be used by any model
* object unless we have completed restructuring the model
* (making all model changes at the server). The reason is
* the use of random numbers in this method.
*
* @param map The <code>Map</code> to search in.
* @param random A <code>Random</code> number source.
* @return Position selected
*/
public Position getRandomLandPosition(Map map, Random random) {
int x = (map.getWidth() < 10) ? random.nextInt(map.getWidth())
: random.nextInt(map.getWidth() - 10) + 5;
int y = (map.getHeight() < 10) ? random.nextInt(map.getHeight())
: random.nextInt(map.getHeight() - 10) + 5;
Position centerPosition = new Position(x, y);
Iterator<Position> it = map.getFloodFillIterator(centerPosition);
while (it.hasNext()) {
Position p = it.next();
if (map.getTile(p).isLand()) return p;
}
return null;
}
/**
* Creates a <code>Map</code> for the given <code>Game</code>.
*
* The <code>Map</code> is added to the <code>Game</code> after
* it is created.
*
* @param game The game.
* @param landMap Determines whether there should be land
* or ocean on a given tile. This array also
* specifies the size of the map that is going
* to be created.
* @see Map
*/
public void createMap(Game game, boolean[][] landMap) {
createMap(game, null, landMap);
}
/**
* Creates a <code>Map</code> for the given <code>Game</code>.
*
* The <code>Map</code> is added to the <code>Game</code> after
* it is created.
*
* @param game The game.
* @param importGame The game to import information form.
* @param landMap Determines whether there should be land
* or ocean on a given tile. This array also
* specifies the size of the map that is going
* to be created.
* @see Map
*/
public void createMap(Game game, Game importGame, boolean[][] landMap) {
Specification spec = game.getSpecification();
lake = spec.getTileType("model.tile.lake");
riverType = spec.getTileImprovementType("model.improvement.river");
fishBonusLandType = spec.getTileImprovementType("model.improvement.fishBonusLand");
fishBonusRiverType = spec.getTileImprovementType("model.improvement.fishBonusRiver");
final int width = landMap.length;
final int height = landMap[0].length;
final boolean importTerrain = (importGame != null)
&& getMapGeneratorOptions().getBoolean(MapGeneratorOptions.IMPORT_TERRAIN);
final boolean importBonuses = (importGame != null)
&& getMapGeneratorOptions().getBoolean(MapGeneratorOptions.IMPORT_BONUSES);
boolean mapHasLand = false;
Tile[][] tiles = new Tile[width][height];
Map map = new Map(game, tiles);
int minimumLatitude = getMapGeneratorOptions()
.getInteger(MapGeneratorOptions.MINIMUM_LATITUDE);
int maximumLatitude = getMapGeneratorOptions()
.getInteger(MapGeneratorOptions.MAXIMUM_LATITUDE);
// make sure the values are in range
minimumLatitude = limitToRange(minimumLatitude, -90, 90);
maximumLatitude = limitToRange(maximumLatitude, -90, 90);
map.setMinimumLatitude(Math.min(minimumLatitude, maximumLatitude));
map.setMaximumLatitude(Math.max(minimumLatitude, maximumLatitude));
for (int y = 0; y < height; y++) {
int latitude = map.getLatitude(y);
for (int x = 0; x < width; x++) {
if (landMap[x][y]) {
mapHasLand = true;
}
Tile t;
if (importTerrain && importGame.getMap().isValid(x, y)) {
Tile importTile = importGame.getMap().getTile(x, y);
if (importTile.isLand() == landMap[x][y]) {
t = new Tile(game, spec.getTileType(importTile.getType().getId()), x, y);
if (importTile.getMoveToEurope() != null) {
t.setMoveToEurope(importTile.getMoveToEurope());
}
if (importTile.getTileItemContainer() != null) {
TileItemContainer container = new TileItemContainer(game, t);
// TileItemContainer copies every natural item
//including Resource unless importBonuses == false
// Rumors and roads are not copied
container.copyFrom(importTile.getTileItemContainer(), importBonuses, true);
t.setTileItemContainer(container);
}
} else {
t = createTile(game, x, y, landMap, latitude);
}
} else {
t = createTile(game, x, y, landMap, latitude);
}
tiles[x][y] = t;
}
}
game.setMap(map);
if (!importTerrain) {
createOceanRegions(map);
createHighSeas(map);
if (mapHasLand) {
createMountains(map);
findLakes(map);
// Create the rivers *after* the land regions as
// rivers are child regions of the surrounding lands.
createLandRegions(map);
createRivers(map);
}
}
// Add the bonuses only after the map is completed.
// Otherwise we risk creating resources on fields where they
// don't belong (like sugar in large rivers or tobaco on hills).
for (Tile tile: map.getAllTiles()) {
perhapsAddBonus(tile, !importBonuses);
if (!tile.isLand()) {
encodeStyle(tile);
}
}
}
public static void encodeStyle(Tile ocean) {
EnumMap<Direction, Boolean> connections =
new EnumMap<Direction, Boolean>(Direction.class);
// corners
for (Direction d : Direction.corners) {
Tile tile = ocean.getNeighbourOrNull(d);
connections.put(d, (tile != null && tile.isLand()));
}
// edges
for (Direction d : Direction.longSides) {
Tile tile = ocean.getNeighbourOrNull(d);
if (tile != null && tile.isLand()) {
connections.put(d, true);
// ignore adjacent corners
connections.put(d.getNextDirection(), false);
connections.put(d.getPreviousDirection(), false);
} else {
connections.put(d, false);
}
}
int result = 0;
int index = 0;
for (Direction d : Direction.corners) {
if (connections.get(d)) {
result += (int) Math.pow(2, index);
}
index++;
}
for (Direction d : Direction.longSides) {
if (connections.get(d)) {
result += (int) Math.pow(2, index);
}
index++;
}
ocean.setStyle(result);
}
private Tile createTile(Game game, int x, int y, boolean[][] landMap, int latitude) {
Tile t;
if (landMap[x][y]) {
t = new Tile(game, getRandomLandTileType(game, latitude), x, y);
} else {
t = new Tile(game, getRandomOceanTileType(game, latitude), x, y);
}
return t;
}
/**
* Adds a terrain bonus with a probability determined by the
* <code>MapGeneratorOptions</code>.
*
* @param t a <code>Tile</code> value
* @param generateBonus a <code>boolean</code> value
*/
private void perhapsAddBonus(Tile t, boolean generateBonus) {
if (t.isLand()) {
if (generateBonus
&& random.nextInt(100) < getMapGeneratorOptions().getInteger("model.option.bonusNumber")) {
// Create random Bonus Resource
t.addResource(createResource(t));
}
} else {
int adjacentLand = 0;
boolean adjacentRiver = false;
for (Direction direction : Direction.values()) {
Tile otherTile = t.getNeighbourOrNull(direction);
if (otherTile != null && otherTile.isLand()) {
adjacentLand++;
if (otherTile.hasRiver()) {
adjacentRiver = true;
}
}
}
//In Col1, ocean tiles with less than 3 land neighbours produce 2 fish,
//all others produce 4 fish
if (adjacentLand > 2) {
t.add(new TileImprovement(t.getGame(), t, fishBonusLandType));
}
//In Col1, the ocean tile in front of a river mouth would
//get an additional +1 bonus
//TODO: This probably has some false positives, means river tiles
//that are NOT a river mouth next to this tile!
if (!t.hasRiver() && adjacentRiver) {
t.add(new TileImprovement(t.getGame(), t, fishBonusRiverType));
}
if (t.getType().isConnected()) {
if (generateBonus && adjacentLand > 1 && random.nextInt(10 - adjacentLand) == 0) {
t.addResource(createResource(t));
}
} else {
if (random.nextInt(100) < getMapGeneratorOptions().getInteger("model.option.bonusNumber")) {
// Create random Bonus Resource
t.addResource(createResource(t));
}
}
}
}
public Resource createResource(Tile tile) {
if (tile == null) {
return null;
}
ResourceType resourceType = RandomChoice.getWeightedRandom(null, null,
random, tile.getType().getWeightedResources());
if (resourceType == null) {
return null;
}
int minValue = resourceType.getMinValue();
int maxValue = resourceType.getMaxValue();
int quantity = (minValue == maxValue) ? maxValue :
(minValue + random.nextInt(maxValue - minValue + 1));
return new Resource(tile.getGame(), tile, resourceType, quantity);
}
/**
* Gets the <code>MapGeneratorOptions</code>.
* @return The <code>MapGeneratorOptions</code> being used
* when creating terrain.
*/
private OptionGroup getMapGeneratorOptions() {
return mapGeneratorOptions;
}
/**
*
* @param game
* @param latitude
* @return
*/
private TileType getRandomOceanTileType(Game game, int latitude) {
// create the main list of TileTypes the first time, and reuse it afterwards
if (oceanTileTypes==null) {
oceanTileTypes = new ArrayList<TileType>();
for (TileType tileType : game.getSpecification().getTileTypeList()) {
if (tileType.isWater() && tileType.isConnected()
&& !tileType.hasAbility("model.ability.moveToEurope")) {
oceanTileTypes.add(tileType);
}
}
}
return getRandomTileType(game, oceanTileTypes, latitude);
}
/**
* Gets a random land tile type based on the given percentage.
*
* @param game the Game
* @param latitude The location of the tile relative to the north/south
* poles and equator:
* 0 is the mid-section of the map (equator)
* +/-90 is on the bottom/top of the map (poles).
*/
private TileType getRandomLandTileType(Game game, int latitude) {
// create the main list of TileTypes the first time, and reuse it afterwards
if (terrainTileTypes==null) {
terrainTileTypes = new ArrayList<TileType>();
for (TileType tileType : game.getSpecification().getTileTypeList()) {
if (tileType.isElevation() || tileType.isWater()) {
// do not generate elevated and water tiles at this time
// they are created separately
continue;
}
terrainTileTypes.add(tileType);
}
}
return getRandomTileType(game, terrainTileTypes, latitude);
}
/**
* Returns a TileType, that fits to the regional requirements.
* <br/><br/>
* TODO: Can be used for mountains and rivers too.
*
* @param game The game.
* @param candidates A list of <tt>TileType</tt>s to use for calculations.
* @param latitude The latitude.
* @return A <tt>TileType</tt> that fits to the regional requirements.
*/
private TileType getRandomTileType(Game game, List<TileType> candidates, int latitude) {
// decode options
final int forestChance = getMapGeneratorOptions().getInteger("model.option.forestNumber");
final int temperaturePreference = getMapGeneratorOptions().getInteger("model.option.temperature");
// temperature calculation
int poleTemperature = -20;
int equatorTemperature= 40;
switch(temperaturePreference) {
case MapGeneratorOptions.TEMPERATURE_COLD:
poleTemperature = -20;
equatorTemperature = 25;
break;
case MapGeneratorOptions.TEMPERATURE_CHILLY:
poleTemperature = -20;
equatorTemperature = 30;
break;
case MapGeneratorOptions.TEMPERATURE_TEMPERATE:
poleTemperature = -10;
equatorTemperature = 35;
break;
case MapGeneratorOptions.TEMPERATURE_WARM:
poleTemperature = -5;
equatorTemperature = 40;
break;
case MapGeneratorOptions.TEMPERATURE_HOT:
poleTemperature = 0;
equatorTemperature = 40;
break;
}
int temperatureRange = equatorTemperature-poleTemperature;
int localeTemperature = poleTemperature + (90 - Math.abs(latitude))
* temperatureRange/90;
int temperatureDeviation = 7; // +/- 7 degrees randomization
localeTemperature += random.nextInt(temperatureDeviation*2)-temperatureDeviation;
localeTemperature = limitToRange(localeTemperature, -20, 40);
// humidity calculation
int localeHumidity = game.getSpecification().getRangeOption(MapGeneratorOptions.HUMIDITY).getValue();
int humidityDeviation = 20; // +/- 20% randomization
localeHumidity += random.nextInt(humidityDeviation*2) - humidityDeviation;
localeHumidity = limitToRange(localeHumidity, 0, 100);
/*
* Make and use a backup of the specified list, because we are modifying the list here.
*/
List<TileType> candidateTileTypes = new ArrayList<TileType>(candidates);
/*
* There will be a check for forested later, but there might be tiles like
* oceans/mountains that do not support forests. Therefore shuffle now,
* because in this case, we will just use the last one standing.
*/
Collections.shuffle(candidateTileTypes, this.random);
// remove those that do not match the current temperature
Iterator<TileType> it = candidateTileTypes.iterator();
while (it.hasNext()) {
TileType t = it.next();
if (!t.withinRange(TileType.RangeType.TEMPERATURE, localeTemperature)) {
it.remove();
}
}
if (candidateTileTypes.size() == 1) {
return candidateTileTypes.get(0);
} else if (candidateTileTypes.size()==0) {
throw new RuntimeException("No TileType for temperature==" + localeTemperature );
}
// remove those that do not match the current humidity
it = candidateTileTypes.iterator();
while (it.hasNext()) {
TileType t = it.next();
if (!t.withinRange(TileType.RangeType.HUMIDITY, localeHumidity)) {
it.remove();
}
}
if (candidateTileTypes.size() == 1) {
return candidateTileTypes.get(0);
} else if (candidateTileTypes.size()==0) {
throw new RuntimeException("No TileType for temperature==" + localeTemperature
+" and humidity==" + localeHumidity);
}
// Choose based on forested/unforested if possible
boolean forested = random.nextInt(100) < forestChance;
it = candidateTileTypes.iterator();
while (it.hasNext()) {
TileType t = it.next();
if (t.isForested() != forested) {
/*
* Fix for land/ocean/mountain tiles that do not support forests. In this case
* return the last one standing. (shuffled before to get random tiles here)
*/
if (it.hasNext()) {
it.remove();
}
}
}
if (candidateTileTypes.size() == 1) {
return candidateTileTypes.get(0);
} else if (candidateTileTypes.size()==0) {
throw new RuntimeException("No TileType for temperature==" + localeTemperature
+" and humidity==" + localeHumidity + " and forested=="+forested);
}
/*
* Return the first one, if more than one is left after termination.
* Note, that we shuffled the list already.
*/
return candidateTileTypes.get(0);
}
/**
* Creates ocean map regions in the given Map. At the moment, the ocean
* is divided into two by two regions.
*
* @param map a <code>Map</code> value
*/
void createOceanRegions(Map map) {
Game game = map.getGame();
ServerRegion pacific =
new ServerRegion(game, "model.region.pacific", RegionType.OCEAN);
ServerRegion northPacific =
new ServerRegion(game, "model.region.northPacific", RegionType.OCEAN, pacific);
ServerRegion southPacific =
new ServerRegion(game, "model.region.southPacific", RegionType.OCEAN, pacific);
ServerRegion atlantic =
new ServerRegion(game, "model.region.atlantic", RegionType.OCEAN);
ServerRegion northAtlantic =
new ServerRegion(game, "model.region.northAtlantic", RegionType.OCEAN, atlantic);
ServerRegion southAtlantic =
new ServerRegion(game, "model.region.southAtlantic", RegionType.OCEAN, atlantic);
for (ServerRegion region : new ServerRegion[] {
northPacific, southPacific,
atlantic, northAtlantic, southAtlantic }) {
region.setPrediscovered(true);
map.setRegion(region);
}
map.setRegion(pacific);
pacific.setDiscoverable(true);
pacific.setScoreValue(PACIFIC_SCORE_VALUE);
// Fill the ocean regions by first filling the quadrants individually,
// then allow the quadrants to overflow into their horizontally
// opposite quadrant, then finally into the whole map.
// This correctly handles cases like:
//
// NP NP NP NA NA NA NP NP NP NA NA NA
// NP L L L L NA NP L L NA L NA
// NP L NA NA NA NA or NP L NA NA L NA
// SP L SA SA SA SA SP L NA L L SA
// SP L L L L SA SP L L L L SA
// SP SP SP SA SA SA SP SP SP SA SA SA
//
// or multiple such incursions across the nominal quadrant divisions.
//
int maxx = map.getWidth();
int midx = maxx / 2;
int maxy = map.getHeight();
int midy = maxy / 2;
Position pNP = new Position(0, midy-1);
Position pSP = new Position(0, midy+1);
Position pNA = new Position(maxx-1, midy-1);
Position pSA = new Position(maxx-1, midy+1);
Rectangle rNP = new Rectangle(0,0, midx,midy);
Rectangle rSP = new Rectangle(0,midy, midx,maxy);
Rectangle rNA = new Rectangle(midx,0, maxx,midy);
Rectangle rSA = new Rectangle(midx,midy, maxx,maxy);
fillOcean(map, pNP, northPacific, rNP);
fillOcean(map, pSP, southPacific, rSP);
fillOcean(map, pNA, northAtlantic, rNA);
fillOcean(map, pSA, southAtlantic, rSA);
Rectangle rN = new Rectangle(0,0, maxx,midy);
Rectangle rS = new Rectangle(0,midy, maxx,maxy);
fillOcean(map, pNP, northPacific, rN);
fillOcean(map, pSP, southPacific, rS);
fillOcean(map, pNA, northAtlantic, rN);
fillOcean(map, pSA, southAtlantic, rS);
Rectangle rAll = new Rectangle(0,0, maxx,maxy);
fillOcean(map, pNP, northPacific, rAll);
fillOcean(map, pSP, southPacific, rAll);
fillOcean(map, pNA, northAtlantic, rAll);
fillOcean(map, pSA, southAtlantic, rAll);
}
/**
* Flood fill ocean regions.
*
* @param map The <code>Map</code> to fill in.
* @param p A valid starting <code>Position</code>.
* @param region A <code>Region</code> to fill with.
* @param bounds A <code>Rectangle</code> that bounds the filling.
*/
private void fillOcean(Map map, Position p, Region region,
Rectangle bounds) {
Queue<Position> q = new LinkedList<Position>();
boolean[][] visited = new boolean[map.getWidth()][map.getHeight()];
visited[p.getX()][p.getY()] = true;
q.add(p);
while ((p = q.poll()) != null) {
Tile tile = map.getTile(p);
tile.setRegion(region);
for (Direction direction : Direction.values()) {
Position n = p.getAdjacent(direction);
if (map.isValid(n)
&& !visited[n.getX()][n.getY()]
&& bounds.contains(n.getX(), n.getY())) {
visited[n.getX()][n.getY()] = true;
Tile next = map.getTile(n);
if ((next.getRegion() == null || next.getRegion() == region)
&& !next.isLand()) {
q.add(n);
}
}
}
}
}
/**
* Creates land map regions in the given Map.
* First, the arctic/antarctic regions are defined, based on <code>LandGenerator.POLAR_HEIGHT</code>
* For the remaining land tiles, one region per contiguous landmass is created.
* @param map a <code>Map</code> value
*/
void createLandRegions(Map map) {
Game game = map.getGame();
//Create arctic/antarctic regions first
ServerRegion arctic =
new ServerRegion(game, "model.region.arctic", RegionType.LAND);
ServerRegion antarctic =
new ServerRegion(game, "model.region.antarctic", RegionType.LAND);
map.setRegion(arctic);
arctic.setPrediscovered(true);
map.setRegion(antarctic);
antarctic.setPrediscovered(true);
int arcticHeight = Map.POLAR_HEIGHT;
int antarcticHeight = map.getHeight() - Map.POLAR_HEIGHT - 1;
for (int x = 0; x < map.getWidth(); x++) {
for (int y = 0; y < arcticHeight; y++) {
if (map.isValid(x, y)) {
Tile tile = map.getTile(x, y);
if (tile.isLand()) {
arctic.addTile(tile);
}
}
}
for (int y = antarcticHeight; y < map.getHeight(); y++) {
if (map.isValid(x, y)) {
Tile tile = map.getTile(x, y);
if (tile.isLand()) {
antarctic.addTile(tile);
}
}
}
}
//Then, create "geographic" land regions
//These regions are used by the MapGenerator to place Indian Settlements
//Note that these regions are "virtual", i.e. having a bounding box,
//but containing no tiles
int thirdWidth = map.getWidth()/3;
int twoThirdWidth = 2 * thirdWidth;
int thirdHeight = map.getHeight()/3;
int twoThirdHeight = 2 * thirdHeight;
ServerRegion northWest =
new ServerRegion(game, "model.region.northWest", RegionType.LAND);
northWest.setBounds(new Rectangle(0,0,thirdWidth,thirdHeight));
ServerRegion north =
new ServerRegion(game, "model.region.north", RegionType.LAND);
north.setBounds(new Rectangle(thirdWidth,0,thirdWidth,thirdHeight));
ServerRegion northEast =
new ServerRegion(game, "model.region.northEast", RegionType.LAND);
northEast.setBounds(new Rectangle(twoThirdWidth,0,map.getWidth()-twoThirdWidth,thirdHeight));
ServerRegion west =
new ServerRegion(game, "model.region.west", RegionType.LAND);
west.setBounds(new Rectangle(0,thirdHeight,thirdWidth,thirdHeight));
ServerRegion center =
new ServerRegion(game, "model.region.center", RegionType.LAND);
center.setBounds(new Rectangle(thirdWidth,thirdHeight,thirdWidth,thirdHeight));
ServerRegion east =
new ServerRegion(game, "model.region.east", RegionType.LAND);
east.setBounds(new Rectangle(twoThirdWidth,thirdHeight,map.getWidth()-twoThirdWidth,thirdHeight));
ServerRegion southWest =
new ServerRegion(game, "model.region.southWest", RegionType.LAND);
southWest.setBounds(new Rectangle(0,twoThirdHeight,thirdWidth,map.getHeight()-twoThirdHeight));
ServerRegion south =
new ServerRegion(game, "model.region.south", RegionType.LAND);
south.setBounds(new Rectangle(thirdWidth,twoThirdHeight,thirdWidth,map.getHeight()-twoThirdHeight));
ServerRegion southEast =
new ServerRegion(game, "model.region.southEast", RegionType.LAND);
southEast.setBounds(new Rectangle(twoThirdWidth,twoThirdHeight,map.getWidth()-twoThirdWidth,map.getHeight()-twoThirdHeight));
ServerRegion[] geographicRegions = new ServerRegion[] {
northWest, north, northEast, west, center,
east, southWest, south, southEast
};
for (ServerRegion region : geographicRegions) {
region.setDiscoverable(false);
map.setRegion(region);
}
//last, create "explorable" land regions
int continents = 0;
boolean[][] landmap = new boolean[map.getWidth()][map.getHeight()];
int[][] continentmap = new int[map.getWidth()][map.getHeight()];
//initialize both maps
for (int x = 0; x < map.getWidth(); x++) {
for (int y = 0; y < map.getHeight(); y++) {
continentmap[x][y] = 0;
if (map.isValid(x, y)) {
Tile tile = map.getTile(x, y);
boolean isMountainRange = false;
if (tile.getRegion() != null) {
isMountainRange = (tile.getRegion().getType() == RegionType.MOUNTAIN);
}
if (tile.isLand()) {
//exclude arctic/antarctic tiles and Mountain Ranges
//Note: Great Rivers are excluded by tile.isLand()
if ((y<arcticHeight) || (y>=antarcticHeight) || isMountainRange) {
landmap[x][y] = false;
} else {
landmap[x][y] = true;
}
} else {
landmap[x][y] = false;
}
}
}
}
//floodfill, so that we end up with individual landmasses numbered in continentmap[][]
for (int y = 0; y < map.getHeight(); y++) {
for (int x = 0; x < map.getWidth(); x++) {
if (landmap[x][y]) { //found a new continent/island
continents++;
boolean[][] continent = floodFill(landmap,new Position(x,y));
for (int yy = 0; yy < map.getHeight(); yy++) {
for (int xx = 0; xx < map.getWidth(); xx++) {
if (continent[xx][yy]) {
continentmap[xx][yy] = continents;
landmap[xx][yy] = false;
}
}
}
}
}
}
logger.info("Number of individual landmasses is " + continents);
//get landmass sizes
int[] continentsize = new int[continents+1];
int landsize = 0;
for (int y = 0; y < map.getHeight(); y++) {
for (int x = 0; x < map.getWidth(); x++) {
continentsize[continentmap[x][y]]++;
if (continentmap[x][y]>0) {
landsize++;
}
}
}
// Go through landmasses, split up those too big
int oldcontinents = continents;
for (int c = 1; c <= oldcontinents; c++) {
//c starting at 1, c=0 is all water tiles
if (continentsize[c]>LAND_REGION_MAX_SIZE) {
boolean[][] splitcontinent = new boolean[map.getWidth()][map.getHeight()];
Position splitposition = new Position(0,0);
for (int x = 0; x < map.getWidth(); x++) {
for (int y = 0; y < map.getHeight(); y++) {
if (continentmap[x][y]==c) {
splitcontinent[x][y] = true;
splitposition = new Position(x,y);
} else {
splitcontinent[x][y] = false;
}
}
}
while (continentsize[c]>LAND_REGION_MAX_SIZE) {
int targetsize = LAND_REGION_MAX_SIZE;
if (continentsize[c] < 2*LAND_REGION_MAX_SIZE) {
targetsize = continentsize[c]/2;
}
continents++; //index of the new region in continentmap[][]
boolean[][] newregion = floodFill(splitcontinent, splitposition, targetsize);
for (int x = 0; x < map.getWidth(); x++) {
for (int y = 0; y < map.getHeight(); y++) {
if (newregion[x][y]) {
continentmap[x][y] = continents;
splitcontinent[x][y] = false;
continentsize[c]--;
}
if (splitcontinent[x][y]) {
splitposition = new Position(x,y);
}
}
}
}
}
}
logger.info("Number of land regions being created: " + continents);
// Create ServerRegions for all land regions
ServerRegion[] landregions = new ServerRegion[continents+1];
for (int c = 1; c <= continents; c++) {
//c starting at 1, c=0 is all water tiles
landregions[c]
= new ServerRegion(map.getGame(), "model.region.land" + c,
Region.RegionType.LAND);
landregions[c].setDiscoverable(true);
map.setRegion(landregions[c]);
}
// Add tiles to ServerRegions
landsize = 0;
for (int y = 0; y < map.getHeight(); y++) {
for (int x = 0; x < map.getWidth(); x++) {
if (continentmap[x][y] > 0) {
Tile tile = map.getTile(x, y);
landregions[continentmap[x][y]].addTile(tile);
landsize++;
}
}
}
for (int c = 1; c <= continents; c++) {
ServerRegion sr = landregions[c];
// Set exploration points for land regions based on size
int score = Math.max((int)(((float) sr.getSize() / landsize)
* LAND_REGIONS_SCORE_VALUE),
LAND_REGION_MIN_SCORE);
sr.setScoreValue(score);
// Set a parent region from the geographic regions now we
// know where each region is.
for (ServerRegion gr : geographicRegions) {
Position cen = sr.getCenter();
if (gr.getBounds().contains(cen.getX(), cen.getY())) {
sr.setParent(gr);
gr.addChild(sr);
gr.setSize(gr.getSize() + sr.getSize());
break;
}
}
logger.fine("Created land region " + sr.getNameKey()
+ " (size " + sr.getSize()
+ ", score " + sr.getScoreValue()
+ ", parent " + ((sr.getParent() == null) ? "(null)"
: sr.getParent().getNameKey())
+ ")");
}
for (ServerRegion gr : geographicRegions) {
logger.fine("Geographic region " + gr.getNameKey()
+ " (size " + gr.getSize()
+ ", children " + gr.getChildren().size()
+ ")");
}
}
/**
* Places "high seas"-tiles on the border of the given map.
* @param map The <code>Map</code> to create high seas on.
*/
private void createHighSeas(Map map) {
createHighSeas(map, getMapGeneratorOptions().getInteger("model.option.distanceToHighSea"),
getMapGeneratorOptions().getInteger("model.option.maximumDistanceToEdge"));
}
/**
* Places "high seas"-tiles on the border of the given map.
*
* All other tiles previously of type High Seas
* will be set to Ocean.
*
* @param map The <code>Map</code> to create high seas on.
* @param distToLandFromHighSeas The distance between the land
* and the high seas (given in tiles).
* @param maxDistanceToEdge The maximum distance a high sea tile
* can have from the edge of the map.
*/
public static void determineHighSeas(Map map,
int distToLandFromHighSeas,
int maxDistanceToEdge) {
// lookup ocean TileTypes from xml specification
TileType ocean = null, highSeas = null;
for (TileType t : map.getSpecification().getTileTypeList()) {
if (t.isWater()) {
if (t.hasAbility("model.ability.moveToEurope")) {
if (highSeas == null) {
highSeas = t;
if (ocean != null) {
break;
}
}
} else {
if (ocean == null) {
ocean = t;
if (highSeas != null) {
break;
}
}
}
}
}
if (highSeas == null || ocean == null) {
throw new RuntimeException("Both Ocean and HighSeas TileTypes must be defined");
}
// reset all water tiles to default ocean type, and remove regions
for (Tile t : map.getAllTiles()) {
t.setRegion(null);
if (t.getType() == highSeas) {
t.setType(ocean);
}
}
// recompute the new high seas layout
createHighSeas(map, distToLandFromHighSeas, maxDistanceToEdge);
}
/**
* Places "high seas"-tiles on the border of the given map.
*
* @param map The <code>Map</code> to create high seas on.
* @param distToLandFromHighSeas The distance between the land
* and the high seas (given in tiles).
* @param maxDistanceToEdge The maximum distance a high sea tile
* can have from the edge of the map.
*/
private static void createHighSeas(Map map,
int distToLandFromHighSeas,
int maxDistanceToEdge) {
if (distToLandFromHighSeas < 0
|| maxDistanceToEdge < 0) {
throw new IllegalArgumentException("The integer arguments cannot be negative.");
}
TileType highSeas = null;
for (TileType t : map.getSpecification().getTileTypeList()) {
if (t.isWater()) {
if (t.hasAbility("model.ability.moveToEurope")) {
highSeas = t;
break;
}
}
}
if (highSeas == null) {
throw new RuntimeException("HighSeas TileType is defined by the 'sail-to-europe' attribute");
}
for (int y = 0; y < map.getHeight(); y++) {
for (int x=0; x<maxDistanceToEdge &&
x<map.getWidth() &&
!map.isLandWithinDistance(x, y, distToLandFromHighSeas); x++) {
if (map.isValid(x, y)) {
map.getTile(x, y).setType(highSeas);
}
}
for (int x=1; x<=maxDistanceToEdge &&
x<=map.getWidth()-1 &&
!map.isLandWithinDistance(map.getWidth()-x, y, distToLandFromHighSeas); x++) {
if (map.isValid(map.getWidth()-x, y)) {
map.getTile(map.getWidth()-x, y).setType(highSeas);
}
}
}
}
/**
* Returns the approximate number of land tiles.
*
* @return the approximate number of land tiles
*/
private int getLand() {
return mapGeneratorOptions.getInteger("model.option.mapWidth")
* mapGeneratorOptions.getInteger("model.option.mapHeight")
* mapGeneratorOptions.getInteger("model.option.landMass")
/ 100;
}
/**
* Creates mountain ranges on the given map. The number and size
* of mountain ranges depends on the map size.
*
* @param map The map to use.
*/
private void createMountains(Map map) {
float randomHillsRatio = 0.5f;
// 50% of user settings will be allocated for random hills here and there
// the rest will be allocated for large mountain ranges
int maximumLength = Math.max(getMapGeneratorOptions().getInteger("model.option.mapWidth"),
getMapGeneratorOptions().getInteger("model.option.mapHeight")) / 10;
int number = (int) ((getLand() / getMapGeneratorOptions().getInteger("model.option.mountainNumber"))
* (1 - randomHillsRatio));
logger.info("Number of mountain tiles is " + number);
logger.fine("Maximum length of mountain ranges is " + maximumLength);
// lookup the resources from specification
TileType hills = map.getSpecification().getTileType("model.tile.hills");
TileType mountains = map.getSpecification().getTileType("model.tile.mountains");
if (hills == null || mountains == null) {
throw new RuntimeException("Both Hills and Mountains TileTypes must be defined");
}
// generate the mountain ranges
int counter = 0;
nextTry: for (int tries = 0; tries < 100; tries++) {
if (counter < number) {
Position p = getRandomLandPosition(map, random);
if (p == null) {
// this can only happen if the map contains no land
return;
}
Tile startTile = map.getTile(p);
if (startTile.getType() == hills || startTile.getType() == mountains) {
// already a high ground
continue;
}
// do not start a mountain range too close to another
for (Tile t: startTile.getSurroundingTiles(3)) {
if (t.getType() == mountains) {
continue nextTry;
}
}
// do not add a mountain range too close to the ocean/lake
// this helps with good locations for building colonies on shore
for (Tile t: startTile.getSurroundingTiles(2)) {
if (!t.isLand()) {
continue nextTry;
}
}
ServerRegion mountainRegion = new ServerRegion(map.getGame(),
"model.region.mountain" + tries,
Region.RegionType.MOUNTAIN,
startTile.getRegion());
mountainRegion.setDiscoverable(true);
mountainRegion.setClaimable(true);
map.setRegion(mountainRegion);
Direction direction = Direction.getRandomDirection("getLand", random);
int length = maximumLength - random.nextInt(maximumLength/2);
for (int index = 0; index < length; index++) {
p = p.getAdjacent(direction);
Tile nextTile = map.getTile(p);
if (nextTile == null || !nextTile.isLand())
continue;
nextTile.setType(mountains);
mountainRegion.addTile(nextTile);
counter++;
Iterator<Position> it = map.getCircleIterator(p, false, 1);
while (it.hasNext()) {
Tile neighborTile = map.getTile(it.next());
if (neighborTile==null || !neighborTile.isLand() || neighborTile.getType()==mountains)
continue;
int r = random.nextInt(8);
if (r == 0) {
neighborTile.setType(mountains);
mountainRegion.addTile(neighborTile);
counter++;
} else if (r > 2) {
neighborTile.setType(hills);
mountainRegion.addTile(neighborTile);
}
}
}
int scoreValue = 2 * mountainRegion.getSize();
mountainRegion.setScoreValue(scoreValue);
logger.fine("Created mountain region (direction " + direction +
", length " + length + ", size " + mountainRegion.getSize() +
", score value " + scoreValue + ").");
}
}
logger.info("Added " + counter + " mountain range tiles.");
// and sprinkle a few random hills/mountains here and there
number = (int) (getLand() * randomHillsRatio)
/ getMapGeneratorOptions().getInteger("model.option.mountainNumber");
counter = 0;
nextTry: for (int tries = 0; tries < 1000; tries++) {
if (counter < number) {
Position p = getRandomLandPosition(map, random);
Tile t = map.getTile(p);
if (t.getType() == hills || t.getType() == mountains) {
// already a high ground
continue;
}
// do not add hills too close to a mountain range
// this would defeat the purpose of adding random hills
for (Tile tile: t.getSurroundingTiles(3)) {
if (tile.getType() == mountains) {
continue nextTry;
}
}
// do not add hills too close to the ocean/lake
// this helps with good locations for building colonies on shore
for (Tile tile: t.getSurroundingTiles(1)) {
if (!tile.isLand()) {
continue nextTry;
}
}
int k = random.nextInt(4);
if (k == 0) {
// 25% chance of mountain
t.setType(mountains);
} else {
// 75% chance of hill
t.setType(hills);
}
counter++;
}
}
logger.info("Added " + counter + " random hills tiles.");
}
/**
* Creates rivers on the given map. The number of rivers depends
* on the map size.
*
* @param map The map to create rivers on.
*/
private void createRivers(Map map) {
int number = getLand() / getMapGeneratorOptions().getInteger("model.option.riverNumber");
int counter = 0;
HashMap<Position, River> riverMap = new HashMap<Position, River>();
List<River> rivers = new ArrayList<River>();
for (int i = 0; i < number; i++) {
nextTry: for (int tries = 0; tries < 100; tries++) {
Position position = getRandomLandPosition(map, random);
if (!map.getTile(position).getType().canHaveImprovement(riverType)) {
continue;
}
// check the river source/spring is not too close to the ocean
for (Tile neighborTile: map.getTile(position).getSurroundingTiles(2)) {
if (!neighborTile.isLand()) {
continue nextTry;
}
}
if (riverMap.get(position) == null) {
// no river here yet
ServerRegion riverRegion = new ServerRegion(map.getGame(),
"model.region.river" + i,
Region.RegionType.RIVER,
map.getTile(position).getRegion());
riverRegion.setDiscoverable(true);
riverRegion.setClaimable(true);
River river = new River(map, riverMap, riverRegion, random);
if (river.flowFromSource(position)) {
logger.fine("Created new river with length " + river.getLength());
map.setRegion(riverRegion);
rivers.add(river);
counter++;
break;
} else {
logger.fine("Failed to generate river.");
}
}
}
}
logger.info("Created " + counter + " rivers of maximum " + number + ".");
for (River river : rivers) {
ServerRegion region = river.getRegion();
int scoreValue = 0;
for (RiverSection section : river.getSections()) {
scoreValue += section.getSize();
}
scoreValue *= 2;
region.setScoreValue(scoreValue);
logger.fine("Created river region (length " + river.getLength() +
", score value " + scoreValue + ").");
}
}
private void findLakes(Map map) {
Game game = map.getGame();
//Currently, all inland lakes belong to one prediscovered region
//TODO: Create separate, discoverable lake regions
ServerRegion inlandlakes =
new ServerRegion(game, "model.region.inlandlakes", RegionType.LAKE);
map.setRegion(inlandlakes);
inlandlakes.setPrediscovered(true);
Position p = null;
// Search for a reachable water tile on the vertical edge.
// No need to search on the horizental edges as they will
// be covered by the polar regions.
for (int x : new int[] {0, map.getWidth() - 1}) {
for (int y = 0; y < map.getHeight(); y++) {
Tile tile = map.getTile(x, y);
if (tile != null && tile.getType() != null && !tile.isLand()) {
p = new Position(x, y);
break;
}
}
}
if (p == null) {
// This should not happen!
logger.warning("Find lakes: unable to find entry point.");
return;
}
boolean[][] watermap = new boolean[map.getWidth()][map.getHeight()];
for (int y = 0; y < map.getHeight(); y++) {
for (int x = 0; x < map.getWidth(); x++) {
watermap[x][y] = !map.getTile(x,y).isLand();
}
}
boolean[][] visited = floodFill(watermap,p);
// Every water tile we did not reach is a lake.
for (int y=0; y < map.getHeight(); y++) {
for (int x=0; x < map.getWidth(); x++) {
if (watermap[x][y] && !visited[x][y]) {
Tile tile = map.getTile(x, y);
if (tile != null) {
tile.setType(lake);
inlandlakes.addTile(tile);
}
}
}
}
}
/**
* Floodfills from a given <code>Position</code> p,
* based on connectivity information encoded in boolmap
*
* @param boolmap The connectivity information for this floodfill
* @param p The starting position
* @param limit Limit to stop floodfill at
* @return A boolean[][] of the same size as boolmap, where "true" means was floodfilled
*/
private boolean[][] floodFill(boolean[][] boolmap, Position p, int limit) {
// Starting from position p, find all tiles connected to it on boolmap
// using a floodfill algorithm and BFS.
Queue<Position>q = new LinkedList<Position>();
boolean[][] visited = new boolean[boolmap.length][boolmap[0].length];
visited[p.getX()][p.getY()] = true;
limit--;
do {
for (Direction direction : Direction.values()) {
Position n = p.getAdjacent(direction);
if (Map.isValid(n,boolmap.length,boolmap[0].length)
&& boolmap[n.getX()][n.getY()]
&& !visited[n.getX()][n.getY()] && limit > 0) {
visited[n.getX()][n.getY()] = true;
limit--;
q.add(n);
}
}
p = q.poll();
} while ((p != null) && (limit > 0));
return visited;
}
private boolean[][] floodFill(boolean[][] boolmap, Position p) {
return floodFill (boolmap, p, java.lang.Integer.MAX_VALUE);
}
}