/** * 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.List; import java.util.Random; 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.option.MapGeneratorOptions; import net.sf.freecol.common.option.OptionGroup; /** * Class for creating a land map. * * <br><br> * * A land map is a two-dimensional array with the boolean * values: * * <ul> * <li><code>true</code>: land</li> * <li><code>false</code>: ocean</li> * </ul> */ public class LandGenerator { private final OptionGroup mapGeneratorOptions; private final Random random; private boolean[][] map; private int width; private int height; private int landMass; private int preferredDistanceToEdge; private int numberOfLandTiles; private int minimumNumberOfTiles; private int genType; /** * Creates a new <code>LandGenerator</code>. * * @param mapGeneratorOptions The options to be * used when creating a land map. * @param random The <code>Random</code> number source to use. * @see #createLandMap */ public LandGenerator(OptionGroup mapGeneratorOptions, Random random) { this.mapGeneratorOptions = mapGeneratorOptions; this.random = random; } /** * Imports the land map from the given <code>Game</code>. * * @param game The <code>Game</code> to get the land map from. * @return An array where <i>true</i> means land * and <i>false</i> means ocean. */ public static boolean[][] importLandMap(Game game) { boolean[][] map = new boolean[game.getMap().getWidth()][game.getMap().getHeight()]; for (int i=0; i<map.length; i++) { for (int j=0; j<map[0].length; j++) { map[i][j] = game.getMap().getTile(i, j).isLand(); } } return map; } /** * Creates a new land map. * * @return An array where <i>true</i> means land * and <i>false</i> means ocean. */ public boolean[][] createLandMap() { //get values from mapGeneratorOptions width = mapGeneratorOptions.getInteger("model.option.mapWidth"); height = mapGeneratorOptions.getInteger("model.option.mapHeight"); preferredDistanceToEdge = mapGeneratorOptions.getInteger("model.option.preferredDistanceToEdge"); landMass = mapGeneratorOptions.getInteger("model.option.landMass"); minimumNumberOfTiles = width * height * landMass / 100; genType = mapGeneratorOptions.getInteger("model.option.landGeneratorType"); //set other internal values map = new boolean[width][height]; numberOfLandTiles = 0; //run one of different land generators, //based on setting in mapGeneratorOptions //"Classic" is the original FreeCol land generator switch (genType) { case MapGeneratorOptions.LAND_GEN_CLASSIC: createClassicLandMap(); break; case MapGeneratorOptions.LAND_GEN_CONTINENT: addPolarRegions(); //create one landmass of 75%, start it somewhere near the center int contsize = (minimumNumberOfTiles*75)/100; addLandmass(contsize,contsize, width/2, random.nextInt(height/2)+height/4); //then create small islands to fill up while (numberOfLandTiles < minimumNumberOfTiles) { addLandmass(15,25); } cleanMap(); break; case MapGeneratorOptions.LAND_GEN_ARCHIPELAGO: addPolarRegions(); //create 5 islands of 10% each int archsize = (minimumNumberOfTiles*10)/100; for (int i=0;i<5;i++) { addLandmass(archsize-10,archsize); } //then, fall into next case to generate small islands case MapGeneratorOptions.LAND_GEN_ISLANDS: addPolarRegions(); //creates only islands of 25..75 tiles while (numberOfLandTiles < minimumNumberOfTiles) { int s=random.nextInt(50) + 25; addLandmass(20,s); } cleanMap(); break; } return map; } private void createClassicLandMap() { int x; int y; while (numberOfLandTiles < minimumNumberOfTiles) { int failCounter=0; do { x=(random.nextInt(width-preferredDistanceToEdge*4)) + preferredDistanceToEdge*2; y=(random.nextInt(height-preferredDistanceToEdge*4)) + preferredDistanceToEdge*2; failCounter++; //if landmass% is set to high, this loop may fail to find a free tile. //decrease necessary minimum over time, so that this process //will eventually come to an end. if (failCounter>100) { failCounter=0; minimumNumberOfTiles--; break; } } while (map[x][y]); setLand(x,y); } addPolarRegions(); cleanMap(); } /** * Tries to create a new land mass (unconnected to existing land masses) * of size=maxsize, and adds it to the current map if it is * at least size=minsize. */ private void addLandmass(int minsize, int maxsize, int x, int y) { int size = 0; boolean[][] newland = new boolean[width][height]; List<Position>l = new ArrayList<Position>(); Position p; //pick a starting position that is sea without neighbouring land if (x<0 || y<0) { do { x=(random.nextInt(width-preferredDistanceToEdge*2)) + preferredDistanceToEdge; y=(random.nextInt(height-preferredDistanceToEdge*2)) + preferredDistanceToEdge; } while (map[x][y] || !isSingleTile(x,y)); } newland[x][y] = true; size++; //add all valid neighbour positions to list p = new Position(x, y); for (Direction direction : Direction.longSides) { Position n = p.getAdjacent(direction); if (Map.isValid(n, width, height) && isSingleTile(n.getX(),n.getY()) && n.getX()>preferredDistanceToEdge && n.getX()<width-preferredDistanceToEdge) { l.add(n); } } //get a random position from the list, //set it to land, //add its valid neighbours to the list while (size < maxsize && l.size()>0) { int i=random.nextInt(l.size()); p = l.remove(i); if (!newland[p.getX()][p.getY()]) { newland[p.getX()][p.getY()] = true; size++; //add all valid neighbour positions to list for (Direction direction : Direction.longSides) { Position n = p.getAdjacent(direction); if (Map.isValid(n, width, height) && isSingleTile(n.getX(),n.getY()) && n.getX()>preferredDistanceToEdge && n.getX()<width-preferredDistanceToEdge) { l.add(n); } } } } //add generated island to map for (x=0; x<width; x++) { for (y=0; y<height; y++) { if (newland[x][y] == true) { map[x][y] = true; numberOfLandTiles++; } } } } private void addLandmass(int minsize, int maxsize) { addLandmass(minsize, maxsize, -1, -1); } /** * Adds land to the first two and last two rows. */ private void addPolarRegions() { for (int x = 0; x < width; x++) { for (int y = 0; y < Map.POLAR_HEIGHT; y++) { if (map[x][y] == false) { map[x][y] = true; numberOfLandTiles++; } } int limit = height - 1 - Map.POLAR_HEIGHT; for (int y = limit; y < height; y++) { if (map[x][y] == false) { map[x][y] = true; numberOfLandTiles++; } } } } /** * Removes any 1x1 islands on the map. */ private void cleanMap() { for (int y=0; y<height; y++) { for (int x=0; x<width; x++) { if (isSingleTile(x, y)) { map[x][y] = false; } } } } /** * Returns <i>true</i> if there are no adjacent land * to the given coordinates. */ private boolean isSingleTile(int x, int y) { Position p = new Position(x, y); for (Direction direction : Direction.values()) { Position n = p.getAdjacent(direction); if (Map.isValid(n, width, height) && map[n.getX()][n.getY()]) { return false; } } return true; } /** * Sets a given map position to land. * Calls #gl(int,int) for all valid adjacent map positions, which may * recursively call setLand for these. */ private void setLand(int x, int y) { if (map[x][y]) { return; } map[x][y] = true; numberOfLandTiles++; Position p = new Position(x, y); for (Direction direction : Direction.longSides) { Position n = p.getAdjacent(direction); if (Map.isValid(n, width, height)) { growLand(n.getX(), n.getY()); } } } /** * Determines, based on position, number of adjacent land tiles and some * random factor, whether a given map position should be set to land. * This is called for all valid map positions adjacent to a position * that has been set to land by #setLand(int,int), and may recursively call * setLand for the current position. */ private void growLand(int i, int j) { if (map[i][j]) { return; } //Generate a comparison value: //Only if the number of adjacent land tiles is bigger than this value, //this tile will be set to land. //This value is part random, part based on position, that is: //-1 in the center of the map, and growing to //preferredDistanceToEdge (*2 for pole ends) at the maps edges. int r = random.nextInt(8) + Math.max(-1, (1+Math.max(preferredDistanceToEdge-Math.min(i,width-i), 2*preferredDistanceToEdge-Math.min(j, height-j)))); int sum = 0; Position p = new Position(i, j); for (Direction direction : Direction.values()) { Position n = p.getAdjacent(direction); if (Map.isValid(n, width, height) && map[n.getX()][n.getY()]) { sum++; } } if (sum > r) { setLand(i,j); } } }