freecol/src/net/sf/freecol/server/model/ServerRegion.java

646 lines
24 KiB
Java

/**
* Copyright (C) 2002-2022 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.model;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import net.sf.freecol.common.model.Direction;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.HistoryEvent;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.networking.ChangeSet;
import net.sf.freecol.common.networking.ChangeSet.See;
import net.sf.freecol.common.option.GameOptions;
import net.sf.freecol.common.util.LogBuilder;
/**
* The server version of a region.
*/
public class ServerRegion extends Region {
/** Score for being first to discover the pacific. */
public static final int PACIFIC_SCORE_VALUE = 100;
/** The size of this Region (number of Tiles). */
private int size = 0;
/** A Rectangle that contains all points of the Region. */
private Rectangle bounds = new Rectangle();
/**
* Trivial constructor for serialization.
*
* @param game The {@code Game} to create in.
* @param id The object identifier.
*/
public ServerRegion(Game game, String id) {
super(game, id);
}
/**
* Constructor for copying in a new region from an imported game.
*
* @param game The {@code Game} to create in.
* @param region A {@code Region} to copy.
*/
public ServerRegion(Game game, Region region) {
super(game);
this.name = region.getName();
this.key = region.getKey();
this.type = region.getType();
this.parent = null; // Has to be fixed up elsewhere
this.claimable = region.getClaimable();
this.discoverable = region.getDiscoverable();
this.discoveredIn = region.getDiscoveredIn();
this.discoveredBy = region.getDiscoveredBy();
this.scoreValue = region.getScoreValue();
}
/**
* Create a new discoverable server region of a given type.
*
* @param game The {@code Game} to create in.
* @param type The {@code RegionType} to use.
*/
public ServerRegion(Game game, RegionType type) {
this(game, null, type, null);
this.setClaimable(type.getClaimable());
this.setDiscoverable(true);
}
/**
* Create a new fixed server region.
*
* @param map The {@code Map} to create in.
* @param key The key for the region.
* @param type The {@code RegionType} to use.
* @param parent The {@code Region} to be the parent of this one.
*/
private ServerRegion(Map map, String key, RegionType type,
Region parent) {
this(map.getGame(), key, type, parent);
map.addRegion(this);
}
/**
* Create a new fixed server region.
*
* @param game The {@code Game} to create in.
* @param key The key for the region.
* @param type The {@code RegionType} to use.
* @param parent The {@code Region} to be the parent of this one.
*/
private ServerRegion(Game game, String key, RegionType type,
Region parent) {
super(game);
this.key = key;
this.name = null;
this.type = type;
this.parent = parent;
if (this.parent != null) this.parent.addChild(this);
this.claimable = false;
this.discoverable = false;
this.discoveredIn = null;
this.discoveredBy = null;
this.scoreValue = 0;
}
/**
* Get the number of tiles in this region.
*
* @return The number of tiles in this region.
*/
public final int getSize() {
return size;
}
/**
* Set the number of tiles in this region.
*
* @param size The new number of tiles.
*/
public final void setSize(final int size) {
this.size = size;
}
/**
* Get the bounding rectangle for this region.
*
* @return The bounding {@code Rectangle}.
*/
public final Rectangle getBounds() {
return bounds;
}
/**
* Is this a geographic region?
*
* @return True if this is a geographic region.
*/
public final boolean isGeographic() {
return this.key != null && this.type == RegionType.LAND;
}
/**
* Get the center of the regions bounds.
*
* @return An two member array [x,y] of the center coordinate.
*/
public int[] getCenter() {
return new int[] { bounds.x + bounds.width/2,
bounds.y + bounds.height/2 };
}
/**
* Does this region contain the center of another?
*
* @param other The other {@code ServerRegion} to check.
* @return True if the center of the other region is within this one.
*/
public boolean containsCenter(ServerRegion other) {
int[] xy = other.getCenter();
return bounds.contains(xy[0], xy[1]);
}
/**
* Add the given tile to this region.
*
* @param tile A {@code Tile} to add.
*/
public void addTile(Tile tile) {
tile.setRegion(this);
size++;
if (bounds.x == 0 && bounds.width == 0
|| bounds.y == 0 && bounds.height == 0) {
bounds.setBounds(tile.getX(), tile.getY(), 0, 0);
} else {
bounds.add(tile.getX(), tile.getY());
}
}
/**
* Discover this region.
*
* @param player The discovering {@code Player}.
* @param unit The discovering {@code Unit}.
* @param turn The discovery {@code Turn}.
* @param newName The name of the region.
* @param cs A {@code ChangeSet} to update.
*/
public void csDiscover(Player player, Unit unit, Turn turn,
String newName, ChangeSet cs) {
if (!getDiscoverable()) return;
final int score = (getSpecification().getBoolean(GameOptions.EXPLORATION_POINTS))
? this.scoreValue
: 0;
if (!hasName()) this.name = newName;
for (Region r : discover(player, unit, turn)) cs.add(See.all(), r);
HistoryEvent h = new HistoryEvent(turn,
HistoryEvent.HistoryEventType.DISCOVER_REGION, player)
.addStringTemplate("%nation%", player.getNationLabel())
.addName("%region%", newName);
h.setScore(score);
cs.addGlobalHistory(getGame(), h);
}
/**
* Determines how to separate the given map into three
* parts (north, center and south) with equal amount of land tiles
* for each.
*
* @param map The map.
* @return The indexes separating the three parts.
*/
private static int[] findYForThirdsOfLand(Map map) {
final int[] landTiles = new int[map.getHeight() - 1];
long sum = 0;
for (Tile t : map) {
if (t.isLand() && !t.isPolar()) {
landTiles[t.getY()]++;
sum++;
}
}
return findThirdsOfValues(landTiles, sum);
}
/**
* Determines how to separate the given submap into three
* parts (west, center and east) with equal amount of land tiles
* for each.
*
* @param map The map.
* @param startY The starting Y-coordinate for the submap (inclusive).
* @param endY The ending Y-coordinate for the submap (inclusive).
* @return The indexes separating the three parts.
*/
private static int[] findXForThirdsOfLand(Map map, int startY, int endY) {
final List<Tile> subMap = map.subMap(0, startY, map.getWidth() - 1, endY - startY + 1);
final int[] landTiles = new int[map.getWidth() - 1];
long sum = 0;
for (Tile t : subMap) {
if (t.isLand() && !t.isPolar()) {
landTiles[t.getX()]++;
sum++;
}
}
return findThirdsOfValues(landTiles, sum);
}
/**
* Finds the indexes for separating the given array into three parts.
* @param timesTheValue An array containing the number of elements for
* each number (as given by the index).
* @param sum The total sum.
* @return The indexes separating the three parts.
*/
private static int[] findThirdsOfValues(int[] timesTheValue, long sum) {
final int[] result = new int[2];
int accumulatedSum = 0;
boolean needFirst = true;
for (int i=0; i<timesTheValue.length; i++) {
accumulatedSum += timesTheValue[i];
if (needFirst && accumulatedSum >= sum / 3) {
result[0] = i;
needFirst = false;
}
if (accumulatedSum >= (2 * sum) / 3) {
result[1] = i;
break;
}
}
return result;
}
private static Rectangle ensureWithinMap(Map map, Rectangle bounds) {
final int capX = Math.max(0, bounds.x);
final int capY = Math.max(0, bounds.y);
final int capWidth = Math.min(map.getWidth() - 1, bounds.x + Math.min(map.getWidth() - 1, bounds.width)) - capX;
final int capHeight = Math.min(map.getHeight() - 1, bounds.y + Math.min(map.getHeight() - 1, bounds.height)) - capY;
return new Rectangle(capX, capY, capWidth, capHeight);
}
/**
* Make the fixed regions if they do not exist in a given map.
*
* @param map The {@code Map} to check.
* @param lb A {@code LogBuilder} to log to.
* @return A list of fixed server regions.
*/
public static List<ServerRegion> requireFixedRegions(Map map, LogBuilder lb) {
final java.util.Map<String, Region> fixed = map.getFixedRegions();
List<ServerRegion> result = new ArrayList<>(16);
lb.add("Add regions ");
// 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;
ServerRegion arctic = (ServerRegion)fixed.get("model.region.arctic");
if (arctic == null) {
arctic = new ServerRegion(map, "model.region.arctic",
RegionType.LAND, null);
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);
}
}
}
lb.add("+arctic");
}
result.add(arctic);
final int antarcticHeight = map.getHeight() - Map.POLAR_HEIGHT - 1;
ServerRegion antarctic = (ServerRegion)fixed.get("model.region.antarctic");
if (antarctic == null) {
antarctic = new ServerRegion(map, "model.region.antarctic",
RegionType.LAND, null);
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);
}
}
}
lb.add("+antarctic");
}
result.add(antarctic);
// 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 contain no tiles directly.
final int[] yDelimiterThirds = findYForThirdsOfLand(map);
int[] xDelimiterThirds = findXForThirdsOfLand(map, 0, yDelimiterThirds[0]);
map.removeRegionsByKey("model.region.northWest");
final ServerRegion northWest = new ServerRegion(map, "model.region.northWest", RegionType.LAND, null);
northWest.bounds.setBounds(ensureWithinMap(map, new Rectangle(
0,
0,
xDelimiterThirds[0],
yDelimiterThirds[0]
)));
map.addRegion(northWest);
map.removeRegionsByKey("model.region.north");
final ServerRegion north = new ServerRegion(map, "model.region.north",RegionType.LAND, null);
north.bounds.setBounds(ensureWithinMap(map, new Rectangle(
xDelimiterThirds[0],
0,
xDelimiterThirds[1] - xDelimiterThirds[0],
yDelimiterThirds[0]
)));
map.addRegion(north);
map.removeRegionsByKey("model.region.northEast");
final ServerRegion northEast = new ServerRegion(map, "model.region.northEast", RegionType.LAND, null);
northEast.bounds.setBounds(ensureWithinMap(map, new Rectangle(
xDelimiterThirds[1],
0,
Integer.MAX_VALUE,
yDelimiterThirds[0]
)));
map.addRegion(northEast);
xDelimiterThirds = findXForThirdsOfLand(map, yDelimiterThirds[0], yDelimiterThirds[1]);
map.removeRegionsByKey("model.region.west");
final ServerRegion west = new ServerRegion(map, "model.region.west", RegionType.LAND, null);
west.bounds.setBounds(ensureWithinMap(map, new Rectangle(
0,
yDelimiterThirds[0],
xDelimiterThirds[0],
yDelimiterThirds[1] - yDelimiterThirds[0]
)));
map.addRegion(west);
map.removeRegionsByKey("model.region.center");
final ServerRegion center = new ServerRegion(map, "model.region.center", RegionType.LAND, null);
center.bounds.setBounds(ensureWithinMap(map, new Rectangle(
xDelimiterThirds[0],
yDelimiterThirds[0],
xDelimiterThirds[1] - xDelimiterThirds[0],
yDelimiterThirds[1] - yDelimiterThirds[0]
)));
map.addRegion(center);
map.removeRegionsByKey("model.region.east");
final ServerRegion east = new ServerRegion(map, "model.region.east", RegionType.LAND, null);
east.bounds.setBounds(ensureWithinMap(map, new Rectangle(
xDelimiterThirds[1],
yDelimiterThirds[0],
Integer.MAX_VALUE,
yDelimiterThirds[1] - yDelimiterThirds[0]
)));
map.addRegion(east);
xDelimiterThirds = findXForThirdsOfLand(map, yDelimiterThirds[1], map.getHeight() - 1);
map.removeRegionsByKey("model.region.southWest");
final ServerRegion southWest = new ServerRegion(map, "model.region.southWest", RegionType.LAND, null);
southWest.bounds.setBounds(ensureWithinMap(map, new Rectangle(
0,
yDelimiterThirds[1],
xDelimiterThirds[0],
Integer.MAX_VALUE
)));
map.addRegion(southWest);
map.removeRegionsByKey("model.region.south");
final ServerRegion south = new ServerRegion(map, "model.region.south", RegionType.LAND, null);
south.bounds.setBounds(ensureWithinMap(map, new Rectangle(
xDelimiterThirds[0],
yDelimiterThirds[1],
xDelimiterThirds[1] - xDelimiterThirds[0],
Integer.MAX_VALUE
)));
map.addRegion(south);
map.removeRegionsByKey("model.region.southEast");
final ServerRegion southEast = new ServerRegion(map, "model.region.southEast", RegionType.LAND, null);
southEast.bounds.setBounds(ensureWithinMap(map, new Rectangle(
xDelimiterThirds[1],
yDelimiterThirds[1],
Integer.MAX_VALUE,
Integer.MAX_VALUE
)));
map.addRegion(southEast);
boolean allOceans = true;
ServerRegion pacific = (ServerRegion)fixed.get("model.region.pacific");
if (pacific == null) {
pacific = new ServerRegion(map, "model.region.pacific",
RegionType.OCEAN, null);
pacific.setDiscoverable(true);
pacific.setScoreValue(PACIFIC_SCORE_VALUE);
allOceans = false;
lb.add("+pacific");
}
result.add(pacific);
ServerRegion northPacific = (ServerRegion)fixed.get("model.region.northPacific");
if (northPacific == null) {
northPacific = new ServerRegion(map, "model.region.northPacific",
RegionType.OCEAN, pacific);
northPacific.setDiscoverable(false); // discovers parent
allOceans = false;
lb.add("+northPacific");
}
result.add(northPacific);
ServerRegion southPacific = (ServerRegion)fixed.get("model.region.southPacific");
if (southPacific == null) {
southPacific = new ServerRegion(map, "model.region.southPacific",
RegionType.OCEAN, pacific);
southPacific.setDiscoverable(false); // discovers parent
allOceans = false;
lb.add("+southPacific");
}
result.add(southPacific);
ServerRegion atlantic = (ServerRegion)fixed.get("model.region.atlantic");
if (atlantic == null) {
atlantic = new ServerRegion(map, "model.region.atlantic",
RegionType.OCEAN, null);
atlantic.setDiscoverable(false);
allOceans = false;
lb.add("+atlantic");
}
result.add(atlantic);
ServerRegion northAtlantic = (ServerRegion)fixed.get("model.region.northAtlantic");
if (northAtlantic == null) {
northAtlantic = new ServerRegion(map, "model.region.northAtlantic",
RegionType.OCEAN, atlantic);
northAtlantic.setDiscoverable(false);
allOceans = false;
lb.add("+northAtlantic");
}
result.add(northAtlantic);
ServerRegion southAtlantic = (ServerRegion)fixed.get("model.region.southAtlantic");
if (southAtlantic == null) {
southAtlantic = new ServerRegion(map, "model.region.southAtlantic",
RegionType.OCEAN, atlantic);
southAtlantic.setDiscoverable(false);
allOceans = false;
lb.add("+southAtlantic");
}
result.add(southAtlantic);
// 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.
//
if (!allOceans) {
final int maxx = map.getWidth();
final int midx = maxx / 2;
final int maxy = map.getHeight();
final int midy = maxy / 2;
Tile tNP = null, tSP = null, tNA = null, tSA = null, t;
for (int y = midy-1; y >= 0; y--) {
if (tNP == null && !(t = map.getTile(0, y)).isLand()) tNP = t;
if (tNA == null && !(t = map.getTile(maxx-1, y)).isLand()) tNA = t;
if (tNP != null && tNA != null) break;
}
for (int y = midy; y < maxy; y++) {
if (tSP == null && !(t = map.getTile(0, y)).isLand()) tSP = t;
if (tSA == null && !(t = map.getTile(maxx-1, y)).isLand()) tSA = t;
if (tSP != null && tSA != 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-midy);
Rectangle rNA = new Rectangle(midx,0, maxx-midx,midy);
Rectangle rSA = new Rectangle(midx,midy, maxx-midx,maxy-midy);
if (tNP != null) nNP += fillOcean(map, tNP, northPacific, rNP);
if (tSP != null) nSP += fillOcean(map, tSP, southPacific, rSP);
if (tNA != null) nNA += fillOcean(map, tNA, northAtlantic, rNA);
if (tSA != null) nSA += fillOcean(map, tSA, southAtlantic, rSA);
Rectangle rN = new Rectangle(0,0, maxx,midy);
Rectangle rS = new Rectangle(0,midy, maxx,maxy-midy);
if (tNP != null) nNP += fillOcean(map, tNP, northPacific, rN);
if (tSP != null) nSP += fillOcean(map, tSP, southPacific, rS);
if (tNA != null) nNA += fillOcean(map, tNA, northAtlantic, rN);
if (tSA != null) nSA += fillOcean(map, tSA, southAtlantic, rS);
Rectangle rAll = new Rectangle(0,0, maxx,maxy);
if (tNP != null) nNP += fillOcean(map, tNP, northPacific, rAll);
if (tSP != null) nSP += fillOcean(map, tSP, southPacific, rAll);
if (tNA != null) nNA += fillOcean(map, tNA, northAtlantic, rAll);
if (tSA != null) nSA += fillOcean(map, tSA, southAtlantic, rAll);
lb.add(" filled ocean regions ",
nNP, " North Pacific, ",
nSP, " South Pacific, ",
nNA, " North Atlantic, ",
nSA, " South Atlantic.\n");
}
return result;
}
/**
* Flood fill ocean regions.
*
* @param map The {@code Map} to fill in.
* @param tile A valid starting {@code Tile}.
* @param region A {@code ServerRegion} to fill with.
* @param bounds A {@code Rectangle} that bounds the filling.
* @return The number of tiles filled.
*/
private static int fillOcean(Map map, Tile tile, ServerRegion region,
Rectangle bounds) {
Queue<Tile> q = new LinkedList<>();
int n = 0;
boolean[][] visited = new boolean[map.getWidth()][map.getHeight()];
visited[tile.getX()][tile.getY()] = true;
q.add(tile);
while ((tile = q.poll()) != null) {
region.addTile(tile);
n++;
for (Direction direction : Direction.values()) {
Tile t = map.getAdjacentTile(tile, direction);
if (t != null
&& !visited[t.getX()][t.getY()]
&& bounds.contains(t.getX(), t.getY())) {
visited[t.getX()][t.getY()] = true;
if ((t.getRegion() == null || t.getRegion() == region)
&& !t.isLand()) {
q.add(t);
}
}
}
}
return n;
}
// Override Object
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(32);
sb.append(super.toString());
sb.setLength(sb.length() - 1);
sb.append(' ').append(size).append(' ').append(bounds)
.append(']');
return sb.toString();
}
}