Files
Battle-for-Tismo/shared/game_state/territory.js
T

204 lines
7.2 KiB
JavaScript

// Territory: which civilisation owns each land tile, how ownership spreads
// from cities, and how it is redrawn when a city changes hands.
import { key, parseKey } from "../hex.js";
import { NEWS_CAPTURE } from "../data/relations.js";
import { serializeCells } from "./helpers.js";
export const territoryMethods = {
civAt(coords) {
const k = key(coords.x, coords.y);
return this.territory.has(k) ? this.territory.get(k) : -1;
},
terrainTypeAt(coords) {
const tile = this.tiles[key(coords.x, coords.y)];
return tile ? tile.terrainType : "Unknown";
},
getTilePopulation(coords) {
return Math.round(this.tilePopulation.get(key(coords.x, coords.y)) || 0);
},
getTileInfo(coords) {
const city = this.cityAt(coords);
return {
coords,
owner: this.civAt(coords),
terrainType: this.terrainTypeAt(coords),
population: this.getTilePopulation(coords),
improvements: city ? city.improvements : [],
};
},
_recomputeTerritory() {
this.territory = new Map();
this._gdpPerCapitaCache = new Map();
const ownerByCell = new Map();
let frontier = [];
for (const city of this.cities) {
const k = key(city.coords.x, city.coords.y);
ownerByCell.set(k, city.civ);
this.territory.set(k, city.civ);
frontier.push(city.coords);
}
while (frontier.length > 0) {
const next = [];
for (const coords of frontier) {
const owner = ownerByCell.get(key(coords.x, coords.y));
for (const neighbour of this._neighbours(coords)) {
const nk = key(neighbour.x, neighbour.y);
if (ownerByCell.has(nk) || !this._isLand(neighbour)) continue;
ownerByCell.set(nk, owner);
this.territory.set(nk, owner);
next.push(neighbour);
}
}
frontier = next;
}
this._rebuildRegionIndex();
this._indexTerritory();
},
// Subdivides each nation's territory into the regions of its own cities: a
// multi-source BFS grown from every city, but never crossing into another
// nation's land. Ties between equally close cities are settled by city order,
// so the result is deterministic and identical on both sides of the wire.
_rebuildRegionIndex() {
this._regionVersion += 1;
this.tileRegion = new Map();
this._regionByCity = new Map();
const cityByCell = new Map();
let frontier = [];
for (const city of this.cities) {
const k = key(city.coords.x, city.coords.y);
cityByCell.set(k, city);
this.tileRegion.set(k, city.id);
frontier.push(city.coords);
}
while (frontier.length > 0) {
const next = [];
for (const coords of frontier) {
const city = cityByCell.get(key(coords.x, coords.y));
for (const neighbour of this._neighbours(coords)) {
const nk = key(neighbour.x, neighbour.y);
if (cityByCell.has(nk) || !this._isLand(neighbour)) continue;
if (this.territory.get(nk) !== city.civ) continue;
cityByCell.set(nk, city);
this.tileRegion.set(nk, city.id);
next.push(neighbour);
}
}
frontier = next;
}
for (const [k, cityId] of this.tileRegion) {
if (!this._regionByCity.has(cityId)) this._regionByCity.set(cityId, []);
this._regionByCity.get(cityId).push(parseKey(k));
}
},
// The city whose region the tile belongs to, or null on unclaimed land.
regionCityAt(coords) {
const id = this.tileRegion.get(key(coords.x, coords.y));
return id !== undefined ? this._cityById.get(id) || null : null;
},
// The land tiles a city's region covers that are still owned by the city's
// civilisation. A freshly captured city only taxes the land that came with it.
regionTiles(city) {
const tiles = this._regionByCity.get(city.id) || [];
return tiles.filter((coords) => this.civAt(coords) === city.civ);
},
_indexTerritory() {
this._territoryVersion += 1;
this._territoryByCiv = new Map();
for (const [k, owner] of this.territory) {
if (!this._territoryByCiv.has(owner)) this._territoryByCiv.set(owner, []);
this._territoryByCiv.get(owner).push(parseKey(k));
}
this._serializedTerritory = serializeCells(this.territory);
this._gdpPerCapitaCache = new Map();
// Which tiles a nation holds changes whose people count toward approval,
// so a territory redraw invalidates the memoised approval figures.
this._touchPolitics();
},
// Rebuilds the owner index, the regions, the serialised territory and the
// GDP-per-capita cache after `this.territory` changes outside a full
// recompute.
_reindexTerritory() {
this._rebuildRegionIndex();
this._indexTerritory();
},
// Transfers `city` to `newCiv` and, with it, every tile of the old owner's
// territory that is strictly closer (through land) to the captured city than
// to any of their remaining cities. Ties stay with the old owner.
_captureCity(city, newCiv) {
const oldCiv = city.civ;
if (oldCiv === newCiv) return false;
const region = (this._territoryByCiv.get(oldCiv) || []).slice();
const claim = this._nearestCityOwner(city, oldCiv, newCiv);
city.civ = newCiv;
for (const coords of region) {
const k = key(coords.x, coords.y);
if (claim.get(k) === newCiv) this.territory.set(k, newCiv);
}
// The captured city's own tile always changes hands, ties or not.
this.territory.set(key(city.coords.x, city.coords.y), newCiv);
this.training.delete(city.id);
this._reindexTerritory();
this._rebuildCityIndex();
this._visibilityDirty = true;
this._addNews({ type: NEWS_CAPTURE, city: city.name, from: oldCiv, to: newCiv });
this._emitChanged();
return true;
},
// Multi-source BFS over land from the captured city and every remaining city
// of the old owner. Each tile records the civ of the nearest city; a tile
// reached at the same distance from both sides is credited to the old owner.
_nearestCityOwner(captured, oldCiv, newCiv) {
const claim = new Map();
const visited = new Set();
let frontier = [];
const sources = [{ coords: captured.coords, civ: newCiv }];
for (const city of this.cities) {
if (city !== captured && city.civ === oldCiv) {
sources.push({ coords: city.coords, civ: oldCiv });
}
}
for (const source of sources) {
const k = key(source.coords.x, source.coords.y);
if (visited.has(k)) continue;
visited.add(k);
claim.set(k, source.civ);
frontier.push(source.coords);
}
while (frontier.length > 0) {
const nextFrontier = [];
const nextClaim = new Map();
for (const coords of frontier) {
const civ = claim.get(key(coords.x, coords.y));
for (const neighbour of this._neighbours(coords)) {
const nk = key(neighbour.x, neighbour.y);
if (visited.has(nk) || !this._isLand(neighbour)) continue;
if (nextClaim.has(nk)) {
if (nextClaim.get(nk) === newCiv && civ === oldCiv) nextClaim.set(nk, oldCiv);
continue;
}
nextClaim.set(nk, civ);
nextFrontier.push(neighbour);
}
}
for (const [k, civ] of nextClaim) {
claim.set(k, civ);
visited.add(k);
}
frontier = nextFrontier;
}
return claim;
},
};