mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 01:47:43 +00:00
91f1748d70
## Description: Remove some lines of code that do nothing in surroundedBySamePlayer, gain a small bit of performance back. The code says "if it is an ocean shore tile but not an ocean shore tile" which can never be true. (Why where the lines there and why can they be removed: Before you were able to boat on a lake, you had no escape from your land if it bordered a lake and no ocean. So it was logical, if you bordered a lake with your cluster, to still treat it as fully surrounded annexable land. This is why this code was added in [this commit](https://github.com/openfrontio/OpenFrontIO/commit/ee56d687484131c092599434247b3a036e0e9668) back then: `const isOceanShore = this.mg.isOceanShore(tile); if (this.mg.isShore(tile) && !isOceanShore) { continue; }` But [this newer commit](https://github.com/openfrontio/OpenFrontIO/commit/c1383d76f1f76c5e5f9be3a6f30b8f3010258540#diff-fb1101a2b50dd7c353d082ff7a3351cff5469b8249b3ebca91c10573a3dfaaf1) made it so you could from then on boat on lakes. So you have an escape from your cluster since then. And just like being on the edge of the map or when bordering the ocean, this means your cluster won't get annexed. However, while the updated code for PlayerExecution in the last commit does its work as intended (it does not exclude lake shore tiles anymore)... The code contradicts itself: `const isOceanShore = this.mg.isOceanShore(tile); if (this.mg.isOceanShore(tile) && !isOceanShore) { continue;}` Conclusion: the code on lines 136-138 can be deleted because it literally says "if it is an ocean shore tile but not an ocean shore tile" which can never be true. Also remove the const and check isOceanShore directly in the if statement.) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33
302 lines
8.3 KiB
TypeScript
302 lines
8.3 KiB
TypeScript
import { Config } from "../configuration/Config";
|
|
import { Execution, Game, Player, UnitType } from "../game/Game";
|
|
import { GameImpl } from "../game/GameImpl";
|
|
import { GameMap, TileRef } from "../game/GameMap";
|
|
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
|
|
|
export class PlayerExecution implements Execution {
|
|
private readonly ticksPerClusterCalc = 20;
|
|
|
|
private config: Config;
|
|
private lastCalc = 0;
|
|
private mg: Game;
|
|
private active = true;
|
|
|
|
constructor(private player: Player) {}
|
|
|
|
activeDuringSpawnPhase(): boolean {
|
|
return false;
|
|
}
|
|
|
|
init(mg: Game, ticks: number) {
|
|
this.mg = mg;
|
|
this.config = mg.config();
|
|
this.lastCalc =
|
|
ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc);
|
|
}
|
|
|
|
tick(ticks: number) {
|
|
this.player.decayRelations();
|
|
for (const u of this.player.units()) {
|
|
if (!u.info().territoryBound) {
|
|
continue;
|
|
}
|
|
|
|
const owner = this.mg!.owner(u.tile());
|
|
if (!owner?.isPlayer()) {
|
|
u.delete();
|
|
continue;
|
|
}
|
|
if (owner === this.player) {
|
|
continue;
|
|
}
|
|
|
|
const captor = this.mg!.player(owner.id());
|
|
if (u.type() === UnitType.DefensePost) {
|
|
u.decreaseLevel(captor);
|
|
if (u.isActive()) {
|
|
captor.captureUnit(u);
|
|
}
|
|
} else {
|
|
captor.captureUnit(u);
|
|
}
|
|
}
|
|
|
|
if (!this.player.isAlive()) {
|
|
// Player has no tiles, delete any remaining units and gold
|
|
const gold = this.player.gold();
|
|
this.player.removeGold(gold);
|
|
this.player.units().forEach((u) => {
|
|
if (
|
|
u.type() !== UnitType.AtomBomb &&
|
|
u.type() !== UnitType.HydrogenBomb &&
|
|
u.type() !== UnitType.MIRVWarhead &&
|
|
u.type() !== UnitType.MIRV
|
|
) {
|
|
u.delete();
|
|
}
|
|
});
|
|
this.active = false;
|
|
this.mg.stats().playerKilled(this.player, ticks);
|
|
return;
|
|
}
|
|
|
|
const troopInc = this.config.troopIncreaseRate(this.player);
|
|
this.player.addTroops(troopInc);
|
|
const goldFromWorkers = this.config.goldAdditionRate(this.player);
|
|
this.player.addGold(goldFromWorkers);
|
|
|
|
// Record stats
|
|
this.mg.stats().goldWork(this.player, goldFromWorkers);
|
|
|
|
const alliances = Array.from(this.player.alliances());
|
|
for (const alliance of alliances) {
|
|
if (alliance.expiresAt() <= this.mg.ticks()) {
|
|
alliance.expire();
|
|
}
|
|
}
|
|
|
|
const embargoes = this.player.getEmbargoes();
|
|
for (const embargo of embargoes) {
|
|
if (
|
|
embargo.isTemporary &&
|
|
this.mg.ticks() - embargo.createdAt >
|
|
this.mg.config().temporaryEmbargoDuration()
|
|
) {
|
|
this.player.stopEmbargo(embargo.target);
|
|
}
|
|
}
|
|
|
|
if (ticks - this.lastCalc > this.ticksPerClusterCalc) {
|
|
if (this.player.lastTileChange() > this.lastCalc) {
|
|
this.lastCalc = ticks;
|
|
const start = performance.now();
|
|
this.removeClusters();
|
|
const end = performance.now();
|
|
if (end - start > 1000) {
|
|
console.log(`player ${this.player.name()}, took ${end - start}ms`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private removeClusters() {
|
|
const clusters = this.calculateClusters();
|
|
clusters.sort((a, b) => b.size - a.size);
|
|
|
|
const main = clusters.shift();
|
|
if (main === undefined) throw new Error("No clusters");
|
|
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
|
|
const surroundedBy = this.surroundedBySamePlayer(main);
|
|
if (surroundedBy && !surroundedBy.isFriendly(this.player)) {
|
|
this.removeCluster(main);
|
|
}
|
|
|
|
for (const cluster of clusters) {
|
|
if (this.isSurrounded(cluster)) {
|
|
this.removeCluster(cluster);
|
|
}
|
|
}
|
|
}
|
|
|
|
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
|
|
const enemies = new Set<number>();
|
|
for (const tile of cluster) {
|
|
if (
|
|
this.mg.isOceanShore(tile) ||
|
|
this.mg.isOnEdgeOfMap(tile) ||
|
|
this.mg.neighbors(tile).some((n) => !this.mg?.hasOwner(n))
|
|
) {
|
|
return false;
|
|
}
|
|
this.mg
|
|
.neighbors(tile)
|
|
.filter((n) => this.mg?.ownerID(n) !== this.player?.smallID())
|
|
.forEach((p) => this.mg && enemies.add(this.mg.ownerID(p)));
|
|
if (enemies.size !== 1) {
|
|
return false;
|
|
}
|
|
}
|
|
if (enemies.size !== 1) {
|
|
return false;
|
|
}
|
|
const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
|
|
const enemyBox = calculateBoundingBox(this.mg, enemy.borderTiles());
|
|
const clusterBox = calculateBoundingBox(this.mg, cluster);
|
|
if (inscribed(enemyBox, clusterBox)) {
|
|
return enemy;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private isSurrounded(cluster: Set<TileRef>): boolean {
|
|
const enemyTiles = new Set<TileRef>();
|
|
for (const tr of cluster) {
|
|
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
|
|
return false;
|
|
}
|
|
this.mg
|
|
.neighbors(tr)
|
|
.filter(
|
|
(n) =>
|
|
this.mg?.owner(n).isPlayer() &&
|
|
this.mg?.ownerID(n) !== this.player?.smallID(),
|
|
)
|
|
.forEach((n) => enemyTiles.add(n));
|
|
}
|
|
if (enemyTiles.size === 0) {
|
|
return false;
|
|
}
|
|
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
|
|
const clusterBox = calculateBoundingBox(this.mg, cluster);
|
|
return inscribed(enemyBox, clusterBox);
|
|
}
|
|
|
|
private removeCluster(cluster: Set<TileRef>) {
|
|
if (
|
|
Array.from(cluster).some(
|
|
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
|
|
)
|
|
) {
|
|
// Other removeCluster operations could change tile owners,
|
|
// so double check.
|
|
return;
|
|
}
|
|
|
|
const capturing = this.getCapturingPlayer(cluster);
|
|
if (capturing === null) {
|
|
return;
|
|
}
|
|
|
|
const firstTile = cluster.values().next().value;
|
|
if (!firstTile) {
|
|
return;
|
|
}
|
|
|
|
const filter = (_: GameMap, t: TileRef): boolean =>
|
|
this.mg?.ownerID(t) === this.player?.smallID();
|
|
const tiles = this.mg.bfs(firstTile, filter);
|
|
|
|
if (this.player.numTilesOwned() === tiles.size) {
|
|
this.mg.conquerPlayer(capturing, this.player);
|
|
}
|
|
|
|
for (const tile of tiles) {
|
|
capturing.conquer(tile);
|
|
}
|
|
}
|
|
|
|
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
|
|
const neighbors = new Map<Player, number>();
|
|
for (const t of cluster) {
|
|
for (const neighbor of this.mg.neighbors(t)) {
|
|
const owner = this.mg.owner(neighbor);
|
|
if (
|
|
owner.isPlayer() &&
|
|
owner !== this.player &&
|
|
!owner.isFriendly(this.player)
|
|
) {
|
|
neighbors.set(owner, (neighbors.get(owner) ?? 0) + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there are no enemies, return null
|
|
if (neighbors.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Get the largest attack from the neighbors
|
|
let largestNeighborAttack: Player | null = null;
|
|
let largestTroopCount = 0;
|
|
for (const [neighbor] of neighbors) {
|
|
for (const attack of neighbor.outgoingAttacks()) {
|
|
if (attack.target() === this.player) {
|
|
if (attack.troops() > largestTroopCount) {
|
|
largestTroopCount = attack.troops();
|
|
largestNeighborAttack = neighbor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (largestNeighborAttack !== null) {
|
|
return largestNeighborAttack;
|
|
}
|
|
|
|
// There are no ongoing attacks, so find the enemy with the largest border.
|
|
return getMode(neighbors);
|
|
}
|
|
|
|
private calculateClusters(): Set<TileRef>[] {
|
|
const seen = new Set<TileRef>();
|
|
const border = this.player.borderTiles();
|
|
const clusters: Set<TileRef>[] = [];
|
|
for (const tile of border) {
|
|
if (seen.has(tile)) {
|
|
continue;
|
|
}
|
|
|
|
const cluster = new Set<TileRef>();
|
|
const queue: TileRef[] = [tile];
|
|
seen.add(tile);
|
|
while (queue.length > 0) {
|
|
const curr = queue.shift();
|
|
if (curr === undefined) throw new Error("curr is undefined");
|
|
cluster.add(curr);
|
|
|
|
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
|
|
for (const neighbor of neighbors) {
|
|
if (border.has(neighbor) && !seen.has(neighbor)) {
|
|
queue.push(neighbor);
|
|
seen.add(neighbor);
|
|
}
|
|
}
|
|
}
|
|
clusters.push(cluster);
|
|
}
|
|
return clusters;
|
|
}
|
|
|
|
owner(): Player {
|
|
if (this.player === null) {
|
|
throw new Error("Not initialized");
|
|
}
|
|
return this.player;
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return this.active;
|
|
}
|
|
}
|