/** * 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.awt.Rectangle; import java.util.ArrayList; 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 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.common.util.RandomChoice; import net.sf.freecol.server.model.ServerRegion; /** * Class for making a <code>Map</code> based upon a land map. * * TODO dynamic lakes, mountains and hills */ 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; /** * The map options group. */ private final OptionGroup mapOptions; /** * The pseudo-random number source to use. * Uses of the PRNG are usually logged in FreeCol, but the use is * so intense here and this code is called pre-game, so we intentionally * skip the logging here. */ private final Random random; // Cache of land and ocean tile types. private ArrayList<TileType> landTileTypes = null; private ArrayList<TileType> oceanTileTypes = null; // Cache of geographic regions private ServerRegion[] geographicRegions = null; /** * Creates a new <code>TerrainGenerator</code>. * * @param mapGeneratorOptions The <code>OptionGroup</code> * containing the options for the map generator. * @param random A <code>Random</code> number source. * @see #createMap */ public TerrainGenerator(OptionGroup mapGeneratorOptions, Random random) { this.mapOptions = mapGeneratorOptions; this.random = random; } // Utilities // TODO: this might be useful elsewhere, too private int limitToRange(int value, int lower, int upper) { return Math.max(lower, Math.min(value, upper)); } /** * Gets the approximate number of land tiles. * * @return The approximate number of land tiles */ private int getApproximateLandCount() { return mapOptions.getInteger("model.option.mapWidth") * mapOptions.getInteger("model.option.mapHeight") * mapOptions.getInteger("model.option.landMass") / 100; } /** * Set a parent region from the geographic regions now we * know where each region is. * * @param sr The <code>ServerRegion</code> to find a parent for. */ private void setGeographicRegion(ServerRegion sr) { if (geographicRegions == null) return; 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; } } } /** * Creates a random tile for the specified position. * * @param game The <code>Game</code> to create the tile in. * @param x The tile x coordinate. * @param y The tile y coordinate. * @param landMap A boolean array defining where the land is. * @param latitude The tile latitude. * @return The created tile. */ private Tile createTile(Game game, int x, int y, boolean[][] landMap, int latitude) { return (landMap[x][y]) ? new Tile(game, getRandomLandTileType(game, latitude), x, y) : new Tile(game, getRandomOceanTileType(game, latitude), x, y); } /** * Gets a random land tile type based on the latitude. * * @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). * @return A suitable random land tile type. */ private TileType getRandomLandTileType(Game game, int latitude) { if (landTileTypes == null) { landTileTypes = new ArrayList<TileType>(); for (TileType type : game.getSpecification().getTileTypeList()) { if (type.isElevation() || type.isWater()) { // do not generate elevated and water tiles at this time // they are created separately continue; } landTileTypes.add(type); } } return getRandomTileType(game, landTileTypes, latitude); } /** * Gets a random ocean tile type. * * @param game The <code>Game</code> to query for tile types. * @param latitude The latitude of the proposed tile. * @return A suitable random ocean tile type. */ private TileType getRandomOceanTileType(Game game, int latitude) { if (oceanTileTypes == null) { oceanTileTypes = new ArrayList<TileType>(); for (TileType type : game.getSpecification().getTileTypeList()) { if (type.isWater() && type.isHighSeasConnected() && !type.isDirectlyHighSeasConnected()) { oceanTileTypes.add(type); } } } return getRandomTileType(game, oceanTileTypes, latitude); } /** * Gets a tile type fitted to the regional requirements. * * TODO: Can be used for mountains and rivers too. * * @param game The <code>Game</code> to find the type in. * @param candidates A list of <code>TileType</code>s to use for * calculations. * @param latitude The tile latitude. * @return A suitable <code>TileType</code>. */ private TileType getRandomTileType(Game game, List<TileType> candidates, int latitude) { // decode options final int forestChance = mapOptions.getInteger("model.option.forestNumber"); final int temperaturePreference = mapOptions.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; } final Specification spec = game.getSpecification(); 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 = spec.getRangeOption(MapGeneratorOptions.HUMIDITY) .getValue(); int humidityDeviation = 20; // +/- 20% randomization localeHumidity += random.nextInt(humidityDeviation * 2) - humidityDeviation; localeHumidity = limitToRange(localeHumidity, 0, 100); List<TileType> candidateTileTypes = new ArrayList<TileType>(candidates); // Filter the candidates by temperature. int i = 0; while (i < candidateTileTypes.size()) { TileType type = candidateTileTypes.get(i); if (!type.withinRange(TileType.RangeType.TEMPERATURE, localeTemperature)) { candidateTileTypes.remove(i); continue; } i++; } // Need to continue? switch (candidateTileTypes.size()) { case 0: throw new RuntimeException("No TileType for" + " temperature==" + localeTemperature); case 1: return candidateTileTypes.get(0); default: break; } // Filter the candidates by humidity. i = 0; while (i < candidateTileTypes.size()) { TileType type = candidateTileTypes.get(i); if (!type.withinRange(TileType.RangeType.HUMIDITY, localeHumidity)) { candidateTileTypes.remove(i); continue; } i++; } // Need to continue? switch (candidateTileTypes.size()) { case 0: throw new RuntimeException("No TileType for" + " humidity==" + localeHumidity); case 1: return candidateTileTypes.get(0); default: break; } // Filter the candidates by forest presence. boolean forested = random.nextInt(100) < forestChance; i = 0; while (i < candidateTileTypes.size()) { TileType type = candidateTileTypes.get(i); if (type.isForested() != forested) { candidateTileTypes.remove(i); continue; } i++; } // Done switch (i = candidateTileTypes.size()) { case 0: throw new RuntimeException("No TileType for" + " forested==" + forested); case 1: return candidateTileTypes.get(0); default: return candidateTileTypes.get(random.nextInt(i)); } } /** * Select a random land position on the map. * * Public so that map generators can also use it. However: * <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 A random land position, or null if none found. */ 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; } // Main functionality, create the map. /** * 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 <code>Game</code> to add the map to. * @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 <code>Game</code> to add the map to. * @param importGame The <code>Game</code> 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) { final Specification spec = game.getSpecification(); final int width = landMap.length; final int height = landMap[0].length; final boolean importTerrain = (importGame != null) && mapOptions.getBoolean(MapGeneratorOptions.IMPORT_TERRAIN); final boolean importBonuses = (importGame != null) && mapOptions.getBoolean(MapGeneratorOptions.IMPORT_BONUSES); boolean mapHasLand = false; Tile[][] tiles = new Tile[width][height]; Map map = new Map(game, tiles); int minimumLatitude = mapOptions .getInteger(MapGeneratorOptions.MINIMUM_LATITUDE); int maximumLatitude = mapOptions .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)); java.util.Map<String, ServerRegion> regionMap = new HashMap<String, ServerRegion>(); if (importTerrain) { // Import the regions String ids = ""; for (Region r : importGame.getMap().getRegions()) { ServerRegion region = new ServerRegion(game, r); map.setRegion(region); regionMap.put(r.getId(), region); ids += " " + region.getNameKey(); } for (Region r : importGame.getMap().getRegions()) { ServerRegion region = regionMap.get(r.getId()); Region x = r.getParent(); if (x != null) x = regionMap.get(x.getId()); region.setParent(x); for (Region c : r.getChildren()) { x = regionMap.get(c.getId()); if (x != null) region.addChild(x); } } logger.info("Imported regions: " + ids); } List<Tile> fixRegions = new ArrayList<Tile>(); 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, importTile = null; if (importTerrain && importGame.getMap().isValid(x, y) && (importTile = importGame.getMap().getTile(x, y)) != null && importTile.isLand() == landMap[x][y]) { String id = importTile.getType().getId(); t = new Tile(game, spec.getTileType(id), 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); } Region r = importTile.getRegion(); if (r == null) { fixRegions.add(t); } else { ServerRegion ours = regionMap.get(r.getId()); if (ours == null) { logger.warning("Could not set tile region " + r.getId() + " for tile: " + t); fixRegions.add(t); } else { ours.addTile(t); } } } else { t = createTile(game, x, y, landMap, latitude); } tiles[x][y] = t; } } game.setMap(map); geographicRegions = getStandardRegions(map); if (importTerrain) { if (!fixRegions.isEmpty()) { // Fix the tiles missing regions. createOceanRegions(map); createLakeRegions(map); createLandRegions(map); } } else { createOceanRegions(map); createHighSeas(map); if (mapHasLand) { createMountains(map); createRivers(map); createLakeRegions(map); createLandRegions(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); } } map.resetContiguity(); map.resetHighSeasCount(); } /** * Creates ocean map regions in the given Map. * * Be careful to tolerate regions pre-existing from an imported game. * At the moment, the ocean is divided into two by two regions. * * @param map The <code>Map</code> to work on. */ private void createOceanRegions(Map map) { Game game = map.getGame(); ServerRegion pacific = (ServerRegion)map .getRegion("model.region.pacific"); ServerRegion northPacific = (ServerRegion)map .getRegion("model.region.northPacific"); ServerRegion southPacific = (ServerRegion)map .getRegion("model.region.southPacific"); ServerRegion atlantic = (ServerRegion)map .getRegion("model.region.atlantic"); ServerRegion northAtlantic = (ServerRegion)map .getRegion("model.region.northAtlantic"); ServerRegion southAtlantic = (ServerRegion)map .getRegion("model.region.southAtlantic"); int present = 0; if (pacific == null) { pacific = new ServerRegion(game, "model.region.pacific", RegionType.OCEAN, null); pacific.setDiscoverable(true); map.setRegion(pacific); pacific.setScoreValue(PACIFIC_SCORE_VALUE); } if (northPacific == null) { northPacific = new ServerRegion(game, "model.region.northPacific", RegionType.OCEAN, pacific); northPacific.setDiscoverable(false); map.setRegion(northPacific); } else present++; if (southPacific == null) { southPacific = new ServerRegion(game, "model.region.southPacific", RegionType.OCEAN, pacific); southPacific.setDiscoverable(false); map.setRegion(southPacific); } else present++; if (atlantic == null) { atlantic = new ServerRegion(game, "model.region.atlantic", RegionType.OCEAN, null); atlantic.setPrediscovered(true); atlantic.setDiscoverable(false); map.setRegion(atlantic); } if (northAtlantic == null) { northAtlantic = new ServerRegion(game, "model.region.northAtlantic", RegionType.OCEAN, atlantic); northAtlantic.setPrediscovered(true); northAtlantic.setDiscoverable(false); map.setRegion(northAtlantic); } else present++; if (southAtlantic == null) { southAtlantic = new ServerRegion(game, "model.region.southAtlantic", RegionType.OCEAN, atlantic); southAtlantic.setPrediscovered(true); southAtlantic.setDiscoverable(false); map.setRegion(southAtlantic); } else present++; if (present == 4) { // All the ocean regions were defined, no need to regenerate. return; } // 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. // final int maxx = map.getWidth(); final int midx = maxx / 2; final int maxy = map.getHeight(); final int midy = maxy / 2; Position pNP = null, pSP = null, pNA = null, pSA = null; for (int y = midy-1; y >= 0; y--) { if (pNP == null && !map.getTile(0, y).isLand()) { pNP = new Position(0, y); } if (pNA == null && !map.getTile(maxx-1, y).isLand()) { pNA = new Position(maxx-1, y); } if (pNP != null && pNA != null) break; } for (int y = midy; y < maxy; y++) { if (pSP == null && !map.getTile(0, y).isLand()) { pSP = new Position(0, y); } if (pSA == null && !map.getTile(maxx-1, y).isLand()) { pSA = new Position(maxx-1, y); } if (pSP != null && pSA != null) break; } int nNP = 0, nSP = 0, nNA = 0, nSA = 0; 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); if (pNP != null) nNP += fillOcean(map, pNP, northPacific, rNP); if (pSP != null) nSP += fillOcean(map, pSP, southPacific, rSP); if (pNA != null) nNA += fillOcean(map, pNA, northAtlantic, rNA); if (pSA != null) nSA += fillOcean(map, pSA, southAtlantic, rSA); Rectangle rN = new Rectangle(0,0, maxx,midy); Rectangle rS = new Rectangle(0,midy, maxx,maxy); if (pNP != null) nNP += fillOcean(map, pNP, northPacific, rN); if (pSP != null) nSP += fillOcean(map, pSP, southPacific, rS); if (pNA != null) nNA += fillOcean(map, pNA, northAtlantic, rN); if (pSA != null) nSA += fillOcean(map, pSA, southAtlantic, rS); Rectangle rAll = new Rectangle(0,0, maxx,maxy); if (pNP != null) nNP += fillOcean(map, pNP, northPacific, rAll); if (pSP != null) nSP += fillOcean(map, pSP, southPacific, rAll); if (pNA != null) nNA += fillOcean(map, pNA, northAtlantic, rAll); if (pSA != null) nSA += fillOcean(map, pSA, southAtlantic, rAll); if (nNP <= 0) logger.warning("No North Pacific tiles found"); if (nSP <= 0) logger.warning("No South Pacific tiles found"); if (nNA <= 0) logger.warning("No North Atlantic tiles found"); if (nSA <= 0) logger.warning("No South Atlantic tiles found"); logger.info("Ocean regions complete: " + nNP + " North Pacific, " + nSP + " South Pacific, " + nNA + " North Atlantic, " + nSP + " South Atlantic"); } /** * 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>ServerRegion</code> to fill with. * @param bounds A <code>Rectangle</code> that bounds the filling. * @return The number of tiles filled. */ private int fillOcean(Map map, Position p, ServerRegion region, Rectangle bounds) { Queue<Position> q = new LinkedList<Position>(); int n = 0; 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); region.addTile(tile); n++; for (Direction direction : Direction.values()) { Position next = p.getAdjacent(direction); if (map.isValid(next) && !visited[next.getX()][next.getY()] && bounds.contains(next.getX(), next.getY())) { visited[next.getX()][next.getY()] = true; Tile t = map.getTile(next); if ((t.getRegion() == null || t.getRegion() == region) && !t.isLand()) { q.add(next); } } } } return n; } /** * Makes sure we have the standard regions. * * @param map The <code>Map</code> to work on. * @return An array of the standard geographic regions. */ private ServerRegion[] getStandardRegions(Map map) { final Game game = map.getGame(); // Create arctic/antarctic regions first, but only if they do // not exist in on the map already. This allows for example // the imported Caribbean map to have arctic/antarctic regions // defined but with no tiles assigned to them, thus they will // not be seen on the map. Generated games though will not have // the region defined, and so will create it here. final int arcticHeight = Map.POLAR_HEIGHT; final int antarcticHeight = map.getHeight() - Map.POLAR_HEIGHT - 1; ServerRegion arctic = (ServerRegion)map .getRegion("model.region.arctic"); if (arctic == null) { arctic = new ServerRegion(game, "model.region.arctic", RegionType.LAND, null); arctic.setPrediscovered(true); map.setRegion(arctic); 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); } } } } ServerRegion antarctic = (ServerRegion)map .getRegion("model.region.antarctic"); if (antarctic == null) { antarctic = new ServerRegion(game, "model.region.antarctic", RegionType.LAND, null); antarctic.setPrediscovered(true); map.setRegion(antarctic); for (int x = 0; x < map.getWidth(); x++) { 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 directly. final int thirdWidth = map.getWidth()/3; final int twoThirdWidth = 2 * thirdWidth; final int thirdHeight = map.getHeight()/3; final int twoThirdHeight = 2 * thirdHeight; ServerRegion northWest = (ServerRegion)map .getRegion("model.region.northWest"); if (northWest == null) { northWest = new ServerRegion(game, "model.region.northWest", RegionType.LAND, null); map.setRegion(northWest); } northWest.setBounds(new Rectangle(0,0, thirdWidth,thirdHeight)); northWest.setPrediscovered(true); ServerRegion north = (ServerRegion)map .getRegion("model.region.north"); if (north == null) { north = new ServerRegion(game, "model.region.north", RegionType.LAND, null); map.setRegion(north); } north.setBounds(new Rectangle(thirdWidth,0, twoThirdWidth,thirdHeight)); north.setPrediscovered(true); ServerRegion northEast = (ServerRegion)map .getRegion("model.region.northEast"); if (northEast == null) { northEast = new ServerRegion(game, "model.region.northEast", RegionType.LAND, null); map.setRegion(northEast); } northEast.setBounds(new Rectangle(twoThirdWidth,0, map.getWidth(),thirdHeight)); northEast.setPrediscovered(true); ServerRegion west = (ServerRegion)map .getRegion("model.region.west"); if (west == null) { west = new ServerRegion(game, "model.region.west", RegionType.LAND, null); map.setRegion(west); } west.setBounds(new Rectangle(0,thirdHeight, thirdWidth,twoThirdHeight)); west.setPrediscovered(true); ServerRegion center = (ServerRegion)map .getRegion("model.region.center"); if (center == null) { center = new ServerRegion(game, "model.region.center", RegionType.LAND, null); map.setRegion(center); } center.setBounds(new Rectangle(thirdWidth,thirdHeight, twoThirdWidth,twoThirdHeight)); center.setPrediscovered(true); ServerRegion east = (ServerRegion)map .getRegion("model.region.east"); if (east == null) { east = new ServerRegion(game, "model.region.east", RegionType.LAND, null); map.setRegion(east); } east.setBounds(new Rectangle(twoThirdWidth,thirdHeight, map.getWidth(),twoThirdHeight)); east.setPrediscovered(true); ServerRegion southWest = (ServerRegion)map .getRegion("model.region.southWest"); if (southWest == null) { southWest = new ServerRegion(game, "model.region.southWest", RegionType.LAND, null); map.setRegion(southWest); } southWest.setBounds(new Rectangle(0,twoThirdHeight, thirdWidth,map.getHeight())); southWest.setPrediscovered(true); ServerRegion south = (ServerRegion)map .getRegion("model.region.south"); if (south == null) { south = new ServerRegion(game, "model.region.south", RegionType.LAND, null); map.setRegion(south); } south.setBounds(new Rectangle(thirdWidth,twoThirdHeight, twoThirdWidth,map.getHeight())); south.setPrediscovered(true); ServerRegion southEast = (ServerRegion)map .getRegion("model.region.southEast"); if (southEast == null) { southEast = new ServerRegion(game, "model.region.southEast", RegionType.LAND, null); map.setRegion(southEast); } southEast.setBounds(new Rectangle(twoThirdWidth,twoThirdHeight, map.getWidth(),map.getHeight())); southEast.setPrediscovered(true); return new ServerRegion[] { northWest, north, northEast, west, center, east, southWest, south, southEast }; } /** * 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 The <code>Map</code> to work on. */ private void createLandRegions(Map map) { Game game = map.getGame(); // Create "explorable" land regions int continents = 0; boolean[][] landmap = new boolean[map.getWidth()][map.getHeight()]; int[][] continentmap = new int[map.getWidth()][map.getHeight()]; int landsize = 0; // Initialize both maps for (int x = 0; x < map.getWidth(); x++) { for (int y = 0; y < map.getHeight(); y++) { continentmap[x][y] = 0; landmap[x][y] = false; if (map.isValid(x, y)) { Tile tile = map.getTile(x, y); // Exclude existing regions (arctic/antarctic, mountains, // rivers). landmap[x][y] = tile.isLand() && tile.getRegion() == null; if (tile.isLand()) landsize++; } } } // Flood fill, 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 region. continents++; boolean[][] continent = Map.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]; for (int y = 0; y < map.getHeight(); y++) { for (int x = 0; x < map.getWidth(); x++) { continentsize[continentmap[x][y]]++; } } // 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 excluded 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 = Map.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]; int landIndex = 1; for (int c = 1; c <= continents; c++) { // c starting at 1, c=0 is all water tiles String id; do { id = "model.region.land" + Integer.toString(landIndex++); } while (map.getRegion(id) != null); landregions[c] = new ServerRegion(map.getGame(), id, Region.RegionType.LAND, null); landregions[c].setDiscoverable(true); map.setRegion(landregions[c]); } // Add tiles to ServerRegions 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); } } } 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); setGeographicRegion(sr); 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) { OptionGroup opt = mapOptions; createHighSeas(map, opt.getInteger("model.option.distanceToHighSea"), opt.getInteger("model.option.maximumDistanceToEdge")); } /** * Places "high seas"-tiles on the border of the given map. * * Public so it can be called from an action when the distance * parameters are changed. * * 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) { final Specification spec = map.getSpecification(); final TileType ocean = spec.getTileType("model.tile.ocean"); final TileType highSeas = spec.getTileType("model.tile.highSeas"); if (highSeas == null || ocean == null) { throw new RuntimeException("Both Ocean and HighSeas TileTypes must be defined"); } // Reset all highSeas tiles to the default ocean type. for (Tile t : map.getAllTiles()) { 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."); } final Specification spec = map.getSpecification(); final TileType ocean = spec.getTileType("model.tile.ocean"); final TileType highSeas = spec.getTileType("model.tile.highSeas"); if (highSeas == null) { throw new RuntimeException("TileType highSeas must be defined."); } Tile t, seaL = null, seaR = null; int totalL = 0, totalR = 0, distanceL = -1, distanceR = -1; for (int y = 0; y < map.getHeight(); y++) { for (int x = 0; x < maxDistanceToEdge && x < map.getWidth() && map.isValid(x, y) && (t = map.getTile(x, y)).getType() == ocean; x++) { Tile other = map.getLandWithinDistance(x, y, distToLandFromHighSeas); if (other == null) { t.setType(highSeas); totalL++; } else { int distance = t.getDistanceTo(other); if (distanceL < distance) { distanceL = distance; seaL = t; } } } for (int x = 0; x < maxDistanceToEdge && x < map.getWidth() && map.isValid(map.getWidth()-1-x, y) && (t = map.getTile(map.getWidth()-1-x, y)) .getType() == ocean; x++) { Tile other = map.getLandWithinDistance(map.getWidth()-1-x, y, distToLandFromHighSeas); if (other == null) { t.setType(highSeas); totalR++; } else { int distance = t.getDistanceTo(other); if (distanceR < distance) { distanceR = distance; seaR = t; } } } } if (totalL <= 0 && seaL != null) { seaL.setType(highSeas); totalL++; } if (totalR <= 0 && seaR != null) { seaR.setType(highSeas); totalR++; } if (totalL <= 0 || totalR <= 0) { logger.warning("No high seas on " + ((totalL <= 0 && totalR <= 0) ? "either" : (totalL <= 0) ? "left" : (totalR <= 0) ? "right" : "BOGUS") + " side of the map." + " This can cause failures on small test maps."); } } /** * 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(mapOptions.getInteger("model.option.mapWidth"), mapOptions.getInteger("model.option.mapHeight")) / 10; int number = (int)((getApproximateLandCount() / mapOptions.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 final Specification spec = map.getSpecification(); TileType hills = spec.getTileType("model.tile.hills"); TileType mountains = spec.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++; for (Tile neighbour : nextTile.getSurroundingTiles(1)) { if (!neighbour.isLand() || neighbour.getType() == mountains) continue; int r = random.nextInt(8); if (r == 0) { neighbour.setType(mountains); mountainRegion.addTile(neighbour); counter++; } else if (r > 2) { neighbour.setType(hills); mountainRegion.addTile(neighbour); } } } 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) (getApproximateLandCount() * randomHillsRatio) / mapOptions.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) { continue; // Already on high ground } // 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; } // 25% mountains, 75% hills t.setType((random.nextInt(4) == 0) ? mountains : 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 <code>Map</code> to create rivers on. */ private void createRivers(Map map) { final Specification spec = map.getSpecification(); final TileImprovementType riverType = spec.getTileImprovementType("model.improvement.river"); final int number = getApproximateLandCount() / mapOptions.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++; } else { logger.fine("Failed to generate river."); } break; } } } 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 + ")."); } } /** * Finds all the lake regions. * * @param map The <code>Map</code> to work on. */ private void createLakeRegions(Map map) { Game game = map.getGame(); final TileType lakeType = map.getSpecification() .getTileType("model.tile.lake"); // Create the water map, and find any tiles that are water but // not part of any region (such as the oceans). These are // lake tiles. List<Position> lakes = new ArrayList<Position>(); 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.isValid(x, y) && !map.getTile(x,y).isLand(); if (watermap[x][y] && map.getTile(x, y).getRegion() == null) { lakes.add(new Position(x, y)); } } } // Make lake regions from unassigned lake tiles. int lakeCount = 0; while (!lakes.isEmpty()) { Position p = lakes.remove(0); Tile t = map.getTile(p); if (t.getRegion() != null) continue; String id; while (game.getFreeColGameObject(id = "model.region.inlandLake" + lakeCount) != null) lakeCount++; ServerRegion lakeRegion = new ServerRegion(game, id, RegionType.LAKE, null); setGeographicRegion(lakeRegion); map.setRegion(lakeRegion); // Pretend lakes are discovered with the surrounding terrain? lakeRegion.setPrediscovered(false); boolean[][] found = Map.floodFill(watermap, p); for (int y = 0; y < map.getHeight(); y++) { for (int x = 0; x < map.getWidth(); x++) { if (found[x][y]) { Tile tt = map.getTile(x, y); tt.setType(lakeType); tt.setRegion(lakeRegion); } } } } } /** * Adds a terrain bonus with a probability determined by the * <code>MapGeneratorOptions</code>. * * @param t The <code>Tile</code> to add bonuses to. * @param generateBonus Generate the bonus or not. */ private void perhapsAddBonus(Tile t, boolean generateBonus) { final Specification spec = t.getSpecification(); TileImprovementType fishBonusLandType = spec.getTileImprovementType("model.improvement.fishBonusLand"); TileImprovementType fishBonusRiverType = spec.getTileImprovementType("model.improvement.fishBonusRiver"); final int bonusNumber = mapOptions.getInteger("model.option.bonusNumber"); if (t.isLand()) { if (generateBonus && random.nextInt(100) < 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().isHighSeasConnected()) { if (generateBonus && adjacentLand > 1 && random.nextInt(10 - adjacentLand) == 0) { t.addResource(createResource(t)); } } else { if (random.nextInt(100) < bonusNumber) { // Create random Bonus Resource t.addResource(createResource(t)); } } } } /** * Create a random resource on a tile. * * @param tile The <code>Tile</code> to create the resource on. * @return The created resource, or null if it is not possible. */ private 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); } /** * Sets the style of the tiles. * Only relevant to water tiles for now. * Public because it is used in the river generator. * * @param tile The <code>Tile</code> to set the style of. */ public static void encodeStyle(Tile tile) { EnumMap<Direction, Boolean> connections = new EnumMap<Direction, Boolean>(Direction.class); // corners for (Direction d : Direction.corners) { Tile t = tile.getNeighbourOrNull(d); connections.put(d, t != null && t.isLand()); } // edges for (Direction d : Direction.longSides) { Tile t = tile.getNeighbourOrNull(d); if (t != null && t.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++; } tile.setStyle(result); } }