mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-21 10:15:33 +00:00
Merge branch 'main' into test-reload-mobile
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { AllianceRequestExecution } from "../src/core/execution/alliance/AllianceRequestExecution";
|
||||
import { AllianceRequestReplyExecution } from "../src/core/execution/alliance/AllianceRequestReplyExecution";
|
||||
import { DonateGoldExecution } from "../src/core/execution/DonateGoldExecution";
|
||||
import { Game, Player, PlayerType } from "../src/core/game/Game";
|
||||
import { playerInfo, setup } from "./util/Setup";
|
||||
|
||||
let game: Game;
|
||||
let player1: Player;
|
||||
let player2: Player;
|
||||
|
||||
describe("Alliance Donation", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: false,
|
||||
instantBuild: true,
|
||||
infiniteTroops: false,
|
||||
donateGold: true,
|
||||
donateTroops: true,
|
||||
},
|
||||
[
|
||||
playerInfo("player1", PlayerType.Human),
|
||||
playerInfo("player2", PlayerType.Human),
|
||||
],
|
||||
);
|
||||
|
||||
player1 = game.player("player1");
|
||||
player1.conquer(game.ref(0, 0));
|
||||
player1.addGold(1000n);
|
||||
player1.addTroops(1000);
|
||||
|
||||
player2 = game.player("player2");
|
||||
player2.conquer(game.ref(0, 1));
|
||||
player2.addGold(100n);
|
||||
player2.addTroops(100);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Can donate gold after alliance formed by reply", () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player2, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
expect(player1.isFriendly(player2)).toBeTruthy();
|
||||
expect(player2.isFriendly(player1)).toBeTruthy();
|
||||
|
||||
expect(player1.canDonateGold(player2)).toBeTruthy();
|
||||
const goldBefore = player2.gold();
|
||||
const success = player1.donateGold(player2, 100n);
|
||||
expect(success).toBeTruthy();
|
||||
expect(player2.gold()).toBe(goldBefore + 100n);
|
||||
});
|
||||
|
||||
test("Can donate troops after alliance formed by reply", () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player2, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
|
||||
expect(player1.canDonateTroops(player2)).toBeTruthy();
|
||||
const troopsBefore = player2.troops();
|
||||
const success = player1.donateTroops(player2, 100);
|
||||
expect(success).toBeTruthy();
|
||||
expect(player2.troops()).toBe(troopsBefore + 100);
|
||||
});
|
||||
|
||||
test("Can donate gold after alliance formed by mutual request", () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
game.addExecution(new AllianceRequestExecution(player2, player1.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
expect(player1.isFriendly(player2)).toBeTruthy();
|
||||
expect(player2.isFriendly(player1)).toBeTruthy();
|
||||
|
||||
expect(player1.canDonateGold(player2)).toBeTruthy();
|
||||
const goldBefore = player2.gold();
|
||||
const success = player1.donateGold(player2, 100n);
|
||||
expect(success).toBeTruthy();
|
||||
expect(player2.gold()).toBe(goldBefore + 100n);
|
||||
});
|
||||
|
||||
test("Can donate troops after alliance formed by mutual request", () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
game.addExecution(new AllianceRequestExecution(player2, player1.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
|
||||
expect(player1.canDonateTroops(player2)).toBeTruthy();
|
||||
const troopsBefore = player2.troops();
|
||||
const success = player1.donateTroops(player2, 100);
|
||||
expect(success).toBeTruthy();
|
||||
expect(player2.troops()).toBe(troopsBefore + 100);
|
||||
});
|
||||
|
||||
test("Can donate immediately after accepting alliance (race condition)", () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
const goldBefore = player2.gold();
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player2, true),
|
||||
);
|
||||
game.addExecution(new DonateGoldExecution(player1, player2.id(), 100));
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
// Donation should have succeeded
|
||||
expect(player2.gold()).toBe(goldBefore + 100n);
|
||||
});
|
||||
});
|
||||
@@ -111,6 +111,26 @@ describe("Attack", () => {
|
||||
expect(nuke.isActive()).toBe(false);
|
||||
expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90);
|
||||
});
|
||||
|
||||
test("Boat penalty on retreat Transport Ship arrival", async () => {
|
||||
const player_start_troops = defender.troops();
|
||||
const boat_troops = player_start_troops * 0.5;
|
||||
|
||||
sendBoat(game.ref(15, 8), game.ref(10, 5), boat_troops);
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
const ship = defender.units(UnitType.TransportShip)[0];
|
||||
expect(ship.troops()).toBe(boat_troops);
|
||||
expect(ship.isActive()).toBe(true);
|
||||
|
||||
ship.orderBoatRetreat();
|
||||
game.executeNextTick();
|
||||
|
||||
expect(ship.isActive()).toBe(false);
|
||||
expect(boat_troops).toBeLessThan(defender.troops());
|
||||
expect(defender.troops()).toBeLessThan(player_start_troops);
|
||||
});
|
||||
});
|
||||
|
||||
let playerA: Player;
|
||||
|
||||
+321
-1
@@ -1,12 +1,25 @@
|
||||
import { AttackExecution } from "../src/core/execution/AttackExecution";
|
||||
import { MarkDisconnectedExecution } from "../src/core/execution/MarkDisconnectedExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game";
|
||||
import { TransportShipExecution } from "../src/core/execution/TransportShipExecution";
|
||||
import { WarshipExecution } from "../src/core/execution/WarshipExecution";
|
||||
import {
|
||||
Game,
|
||||
GameMode,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../src/core/game/Game";
|
||||
import { toInt } from "../src/core/Util";
|
||||
import { setup } from "./util/Setup";
|
||||
import { UseRealAttackLogic } from "./util/TestConfig";
|
||||
import { executeTicks } from "./util/utils";
|
||||
|
||||
let game: Game;
|
||||
let player1: Player;
|
||||
let player2: Player;
|
||||
let enemy: Player;
|
||||
|
||||
describe("Disconnected", () => {
|
||||
beforeEach(async () => {
|
||||
@@ -158,4 +171,311 @@ describe("Disconnected", () => {
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disconnected team member interactions", () => {
|
||||
const coastX = 7;
|
||||
|
||||
beforeEach(async () => {
|
||||
const player1Info = new PlayerInfo(
|
||||
"[CLAN]Player1",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player_1_id",
|
||||
);
|
||||
const player2Info = new PlayerInfo(
|
||||
"[CLAN]Player2",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player_2_id",
|
||||
);
|
||||
|
||||
game = await setup(
|
||||
"half_land_half_ocean",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
gameMode: GameMode.Team,
|
||||
playerTeams: 2, // ignore player2 "kicked" console warn
|
||||
},
|
||||
[player1Info, player2Info],
|
||||
undefined,
|
||||
UseRealAttackLogic, // don't use TestConfig's mock attackLogic
|
||||
);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(player1Info, game.map().ref(coastX - 2, 1)),
|
||||
new SpawnExecution(player2Info, game.map().ref(coastX - 2, 4)),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player(player1Info.id);
|
||||
player2 = game.player(player2Info.id);
|
||||
player2.markDisconnected(false);
|
||||
|
||||
expect(player1.team()).not.toBeNull();
|
||||
expect(player2.team()).not.toBeNull();
|
||||
expect(player1.isOnSameTeam(player2)).toBe(true);
|
||||
});
|
||||
|
||||
test("Team Warships should not attack disconnected team mate ships", () => {
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.map().ref(coastX + 1, 10),
|
||||
{
|
||||
patrolTile: game.map().ref(coastX + 1, 10),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
const transportShip = player2.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
game.map().ref(coastX + 1, 11),
|
||||
{
|
||||
troops: 100,
|
||||
},
|
||||
);
|
||||
|
||||
player2.markDisconnected(true);
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(warship.targetUnit()).toBe(undefined);
|
||||
expect(transportShip.isActive()).toBe(true);
|
||||
expect(transportShip.owner()).toBe(player2);
|
||||
});
|
||||
|
||||
test("Disconnected player Warship should not attack team members' ships", () => {
|
||||
const warship = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.map().ref(coastX + 1, 5),
|
||||
{
|
||||
patrolTile: game.map().ref(coastX + 1, 10),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
const transportShip = player1.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
game.map().ref(coastX + 1, 6),
|
||||
{
|
||||
troops: 100,
|
||||
},
|
||||
);
|
||||
|
||||
player2.markDisconnected(true);
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(warship.targetUnit()).toBe(undefined);
|
||||
expect(transportShip.isActive()).toBe(true);
|
||||
expect(transportShip.owner()).toBe(player1);
|
||||
});
|
||||
|
||||
test("Player can attack disconnected team mate without troop loss", () => {
|
||||
player2.conquer(game.map().ref(coastX - 2, 2));
|
||||
player2.conquer(game.map().ref(coastX - 2, 3));
|
||||
player2.markDisconnected(true);
|
||||
|
||||
const troopsBeforeAttack = player1.troops();
|
||||
const startTroops = troopsBeforeAttack * 0.25;
|
||||
|
||||
game.addExecution(
|
||||
new AttackExecution(startTroops, player1, player2.id(), null),
|
||||
);
|
||||
|
||||
let expectedTotalGrowth = 0n;
|
||||
let afterTickZero = false;
|
||||
|
||||
while (player2.isAlive()) {
|
||||
if (afterTickZero) {
|
||||
// No growth on tick 0, troop additions start from tick 1
|
||||
const troopIncThisTick = game.config().troopIncreaseRate(player1);
|
||||
expectedTotalGrowth += toInt(troopIncThisTick);
|
||||
}
|
||||
|
||||
game.executeNextTick();
|
||||
afterTickZero = true;
|
||||
}
|
||||
|
||||
// Tick for retreat() in AttackExecution to add back startTtoops to owner troops
|
||||
const troopIncThisTick1 = game.config().troopIncreaseRate(player1);
|
||||
expectedTotalGrowth += toInt(troopIncThisTick1);
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
const expectedFinalTroops = Number(
|
||||
toInt(troopsBeforeAttack) + expectedTotalGrowth,
|
||||
);
|
||||
|
||||
// Verify no troop loss
|
||||
expect(player1.troops()).toBe(expectedFinalTroops);
|
||||
});
|
||||
|
||||
test("Conqueror gets conquered disconnected team member's transport- and warships", () => {
|
||||
const warship = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.map().ref(coastX + 1, 1),
|
||||
{
|
||||
patrolTile: game.map().ref(coastX + 1, 1),
|
||||
},
|
||||
);
|
||||
const transportShip = player2.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
game.map().ref(coastX + 1, 3),
|
||||
{
|
||||
troops: 100,
|
||||
},
|
||||
);
|
||||
|
||||
player2.conquer(game.map().ref(coastX - 2, 1));
|
||||
player2.markDisconnected(true);
|
||||
|
||||
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
|
||||
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(player2.isAlive()).toBe(false);
|
||||
expect(warship.owner()).toBe(player1);
|
||||
expect(transportShip.owner()).toBe(player1);
|
||||
});
|
||||
|
||||
test("Captured transport ship landing attack should be in name of new owner", () => {
|
||||
player2.conquer(game.map().ref(coastX, 1));
|
||||
player2.conquer(game.map().ref(coastX - 1, 1));
|
||||
player2.conquer(game.map().ref(coastX, 2));
|
||||
|
||||
const enemyShoreTile = game.map().ref(coastX, 15);
|
||||
|
||||
game.addExecution(
|
||||
new TransportShipExecution(
|
||||
player2,
|
||||
null,
|
||||
enemyShoreTile,
|
||||
100,
|
||||
game.map().ref(coastX, 1),
|
||||
),
|
||||
);
|
||||
|
||||
executeTicks(game, 1);
|
||||
|
||||
expect(player2.isAlive()).toBe(true);
|
||||
const transportShip = player2.units(UnitType.TransportShip)[0];
|
||||
expect(player2.units(UnitType.TransportShip).length).toBe(1);
|
||||
|
||||
player2.markDisconnected(true);
|
||||
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
|
||||
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(player2.isAlive()).toBe(false);
|
||||
expect(transportShip.owner()).toBe(player1);
|
||||
|
||||
executeTicks(game, 30);
|
||||
|
||||
// Verify ship landed and tile ownership transferred to new ship owner
|
||||
expect(game.owner(enemyShoreTile)).toBe(player1);
|
||||
});
|
||||
|
||||
test("Captured transport ship should retreat to owner's shore tile", () => {
|
||||
player1.conquer(game.map().ref(coastX, 4));
|
||||
player2.conquer(game.map().ref(coastX, 1));
|
||||
|
||||
const enemyShoreTile = game.map().ref(coastX, 8);
|
||||
|
||||
game.addExecution(
|
||||
new TransportShipExecution(
|
||||
player2,
|
||||
null,
|
||||
enemyShoreTile,
|
||||
100,
|
||||
game.map().ref(coastX, 1),
|
||||
),
|
||||
);
|
||||
executeTicks(game, 1);
|
||||
|
||||
const transportShip = player2.units(UnitType.TransportShip)[0];
|
||||
expect(player2.units(UnitType.TransportShip).length).toBe(1);
|
||||
|
||||
expect(transportShip.targetTile()).toBe(enemyShoreTile);
|
||||
|
||||
player2.markDisconnected(true);
|
||||
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(player2.isAlive()).toBe(false);
|
||||
expect(transportShip.owner()).toBe(player1);
|
||||
|
||||
transportShip.orderBoatRetreat();
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(transportShip.targetTile()).not.toBe(enemyShoreTile);
|
||||
expect(game.owner(transportShip.targetTile()!)).toBe(player1);
|
||||
});
|
||||
|
||||
test("Retreating transport ship is deleted if new owner has no shore tiles", () => {
|
||||
player2.conquer(game.map().ref(coastX, 1));
|
||||
player2.conquer(game.map().ref(coastX - 6, 2));
|
||||
player1.conquer(game.map().ref(coastX - 6, 3));
|
||||
|
||||
const enemyShoreTile = game.map().ref(coastX, 15);
|
||||
|
||||
const boatTroops = 100;
|
||||
game.addExecution(
|
||||
new TransportShipExecution(
|
||||
player2,
|
||||
null,
|
||||
enemyShoreTile,
|
||||
boatTroops,
|
||||
game.map().ref(coastX, 1),
|
||||
),
|
||||
);
|
||||
executeTicks(game, 1);
|
||||
|
||||
const transportShip = player2.units(UnitType.TransportShip)[0];
|
||||
expect(player2.units(UnitType.TransportShip).length).toBe(1);
|
||||
|
||||
player2.markDisconnected(true);
|
||||
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(player2.isAlive()).toBe(false);
|
||||
expect(transportShip.owner()).toBe(player1);
|
||||
|
||||
// Make sure player1 has no shore tiles for the ship to retreat to anymore
|
||||
const enemyInfo = new PlayerInfo(
|
||||
"Enemy",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"enemy_id",
|
||||
);
|
||||
enemy = game.addPlayer(enemyInfo);
|
||||
|
||||
const shoreTiles = Array.from(player1.borderTiles()).filter((t) =>
|
||||
game.isShore(t),
|
||||
);
|
||||
shoreTiles.forEach((tile) => {
|
||||
enemy.conquer(tile);
|
||||
});
|
||||
|
||||
expect(
|
||||
Array.from(player1.borderTiles()).filter((t) => game.isShore(t)).length,
|
||||
).toBe(0);
|
||||
|
||||
executeTicks(game, 1);
|
||||
|
||||
const troopIncPerTick = game.config().troopIncreaseRate(player1);
|
||||
const expectedTroopGrowth = toInt(troopIncPerTick * 1);
|
||||
const expectedFinalTroops = Number(
|
||||
toInt(player1.troops()) + expectedTroopGrowth,
|
||||
);
|
||||
|
||||
transportShip.orderBoatRetreat();
|
||||
executeTicks(game, 1);
|
||||
|
||||
expect(transportShip.isActive()).toBe(false);
|
||||
// Also test if boat troops were returned to player1 as new ship owner
|
||||
expect(player1.troops()).toBe(expectedFinalTroops + boatTroops);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("FakeHuman MIRV Retaliation", () => {
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable retaliation logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.info());
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
|
||||
|
||||
// Try different game IDs to account for hesitation odds
|
||||
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
|
||||
@@ -247,7 +247,7 @@ describe("FakeHuman MIRV Retaliation", () => {
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable victory denial logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.info());
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
|
||||
|
||||
// Try different game IDs to account for hesitation odds
|
||||
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
|
||||
@@ -400,7 +400,7 @@ describe("FakeHuman MIRV Retaliation", () => {
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable steamroll stop logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.info());
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
|
||||
|
||||
// Try different game IDs to account for hesitation odds
|
||||
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
|
||||
@@ -551,7 +551,7 @@ describe("FakeHuman MIRV Retaliation", () => {
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable steamroll stop logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.info());
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
|
||||
|
||||
// Try different game IDs to account for hesitation odds
|
||||
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
|
||||
@@ -685,7 +685,7 @@ describe("FakeHuman MIRV Retaliation", () => {
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable team victory denial logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.info());
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
|
||||
|
||||
// Try different game IDs to account for hesitation odds
|
||||
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { computeAllianceClipPath } from "../src/client/graphics/PlayerIcons";
|
||||
|
||||
describe("PlayerIcons", () => {
|
||||
describe("computeAllianceClipPath", () => {
|
||||
test("returns full visibility (20% top cut) when alliance time is at 100%", () => {
|
||||
const result = computeAllianceClipPath(1.0);
|
||||
// topCut = 20 + (1 - 1.0) * 80 * 0.78 = 20 + 0 = 20.00
|
||||
expect(result).toBe("inset(20.00% -2px 0 -2px)");
|
||||
});
|
||||
|
||||
test("returns maximum cut (82.40% top cut) when alliance time is at 0%", () => {
|
||||
const result = computeAllianceClipPath(0.0);
|
||||
// topCut = 20 + (1 - 0.0) * 80 * 0.78 = 20 + 62.4 = 82.40
|
||||
expect(result).toBe("inset(82.40% -2px 0 -2px)");
|
||||
});
|
||||
|
||||
test("returns 51.20% top cut when alliance time is at 50%", () => {
|
||||
const result = computeAllianceClipPath(0.5);
|
||||
// topCut = 20 + (1 - 0.5) * 80 * 0.78 = 20 + 31.2 = 51.20
|
||||
expect(result).toBe("inset(51.20% -2px 0 -2px)");
|
||||
});
|
||||
|
||||
test("returns 27.80% top cut when alliance time is at 87.5%", () => {
|
||||
const result = computeAllianceClipPath(0.875);
|
||||
// topCut = 20 + (1 - 0.875) * 80 * 0.78 = 20 + 7.8 = 27.80
|
||||
expect(result).toBe("inset(27.80% -2px 0 -2px)");
|
||||
});
|
||||
|
||||
test("returns 74.60% top cut when alliance time is at 12.5%", () => {
|
||||
const result = computeAllianceClipPath(0.125);
|
||||
// topCut = 20 + (1 - 0.125) * 80 * 0.78 = 20 + 54.6 = 74.60
|
||||
expect(result).toBe("inset(74.60% -2px 0 -2px)");
|
||||
});
|
||||
|
||||
test("includes -2px horizontal overscan to prevent subpixel gaps", () => {
|
||||
const result = computeAllianceClipPath(0.5);
|
||||
expect(result).toContain("-2px");
|
||||
expect(result.match(/-2px/g)).toHaveLength(2); // Should appear twice (left and right)
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,12 @@ describe("PortExecution", () => {
|
||||
game.config().tradeShipShortRangeDebuff = () => 0;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const execution = new PortExecution(player, game.ref(7, 10));
|
||||
const spawn = player.canBuild(UnitType.Port, game.ref(7, 10));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build port for test");
|
||||
}
|
||||
const port = player.buildUnit(UnitType.Port, spawn, {});
|
||||
const execution = new PortExecution(port);
|
||||
execution.init(game, 0);
|
||||
execution.tick(0);
|
||||
|
||||
@@ -60,7 +65,12 @@ describe("PortExecution", () => {
|
||||
game.config().tradeShipShortRangeDebuff = () => 0;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const execution = new PortExecution(player, game.ref(7, 10));
|
||||
const spawn = player.canBuild(UnitType.Port, game.ref(7, 10));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build port for test");
|
||||
}
|
||||
const port = player.buildUnit(UnitType.Port, spawn, {});
|
||||
const execution = new PortExecution(port);
|
||||
execution.init(game, 0);
|
||||
execution.tick(0);
|
||||
|
||||
@@ -78,7 +88,12 @@ describe("PortExecution", () => {
|
||||
game.config().tradeShipShortRangeDebuff = () => 100;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const execution = new PortExecution(player, game.ref(7, 10));
|
||||
const spawn = player.canBuild(UnitType.Port, game.ref(7, 10));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build port for test");
|
||||
}
|
||||
const port = player.buildUnit(UnitType.Port, spawn, {});
|
||||
const execution = new PortExecution(port);
|
||||
execution.init(game, 0);
|
||||
execution.tick(0);
|
||||
|
||||
|
||||
@@ -145,7 +145,13 @@ describe("Shell Random Damage", () => {
|
||||
});
|
||||
|
||||
test("Defense post shell attacks have random damage", () => {
|
||||
const defensePost = new DefensePostExecution(player1, game.ref(coastX, 5));
|
||||
player1.conquer(game.ref(coastX, 5));
|
||||
const spawn = player1.canBuild(UnitType.DefensePost, game.ref(coastX, 5));
|
||||
if (spawn === false) {
|
||||
throw new Error("Unable to build defense post for test");
|
||||
}
|
||||
const defensePostUnit = player1.buildUnit(UnitType.DefensePost, spawn, {});
|
||||
const defensePost = new DefensePostExecution(defensePostUnit);
|
||||
|
||||
const target = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
|
||||
@@ -11,7 +11,6 @@ describe("assignTeams", () => {
|
||||
PlayerType.Human,
|
||||
null, // clientID (null for testing)
|
||||
id,
|
||||
null, // nation (null for testing)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ describe("UILayer", () => {
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
id: () => 2,
|
||||
type: () => "Construction",
|
||||
constructionType: () => "City",
|
||||
type: () => "City",
|
||||
isUnderConstruction: () => true,
|
||||
owner: () => ({ id: () => 1 }),
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
@@ -141,17 +141,20 @@ describe("UILayer", () => {
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
id: () => 2,
|
||||
type: () => "Construction",
|
||||
constructionType: () => "City",
|
||||
type: () => "City",
|
||||
isUnderConstruction: () => true,
|
||||
owner: () => ({ id: () => 1 }),
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
createdAt: () => 1,
|
||||
markedForDeletion: () => false,
|
||||
} as unknown as UnitView;
|
||||
ui.onUnitEvent(unit);
|
||||
expect(ui["allProgressBars"].has(2)).toBe(true);
|
||||
|
||||
game.ticks = () => 6; // simulate enough ticks for completion
|
||||
// simulate construction finished
|
||||
(unit as any).isUnderConstruction = () => false;
|
||||
ui.tick();
|
||||
expect(ui["allProgressBars"].has(2)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -40,6 +40,8 @@ describe("NukeExecution", () => {
|
||||
|
||||
player = game.player("player_id");
|
||||
otherPlayer = game.player("other_id");
|
||||
|
||||
player.conquer(game.ref(1, 1));
|
||||
});
|
||||
|
||||
test("nuke should destroy buildings and redraw out of range buildings", async () => {
|
||||
@@ -76,6 +78,7 @@ describe("NukeExecution", () => {
|
||||
});
|
||||
|
||||
test("nuke should only be targetable near src and dst", async () => {
|
||||
player.buildUnit(UnitType.MissileSilo, game.ref(1, 1), {});
|
||||
const nukeExec = new NukeExecution(
|
||||
UnitType.AtomBomb,
|
||||
player,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
|
||||
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../../src/core/game/Game";
|
||||
import { setup } from "../util/Setup";
|
||||
|
||||
describe("Construction economy", () => {
|
||||
let game: Game;
|
||||
let player: Player;
|
||||
|
||||
beforeEach(async () => {
|
||||
game = await setup("ocean_and_land", {
|
||||
infiniteGold: false,
|
||||
instantBuild: false,
|
||||
infiniteTroops: true,
|
||||
});
|
||||
const info = new PlayerInfo(
|
||||
"builder",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"builder_id",
|
||||
);
|
||||
game.addPlayer(info);
|
||||
const spawn = game.ref(0, 10);
|
||||
game.addExecution(new SpawnExecution(info, spawn));
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
player = game.player(info.id);
|
||||
});
|
||||
|
||||
test("City charges gold once and no refund thereafter (allow passive income)", () => {
|
||||
const target = game.ref(0, 10);
|
||||
const cost = game.unitInfo(UnitType.City).cost(player);
|
||||
player.addGold(cost);
|
||||
expect(player.gold()).toBe(cost);
|
||||
|
||||
const startTick = game.ticks();
|
||||
game.addExecution(new ConstructionExecution(player, UnitType.City, target));
|
||||
|
||||
// First tick usually initializes the execution, second tick performs build and deduction
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
const afterBuild = player.gold();
|
||||
const ticksAfterBuild = BigInt(game.ticks() - startTick);
|
||||
const passivePerTick = 100n; // DefaultConfig goldAdditionRate for humans
|
||||
expect(afterBuild < cost).toBe(true); // cost was deducted
|
||||
expect(afterBuild <= ticksAfterBuild * passivePerTick).toBe(true); // only passive income allowed
|
||||
|
||||
// Advance through construction duration
|
||||
const duration = game.unitInfo(UnitType.City).constructionDuration ?? 0;
|
||||
for (let i = 0; i <= duration + 2; i++) game.executeNextTick();
|
||||
|
||||
const finalGold = player.gold();
|
||||
const ticksElapsed = BigInt(game.ticks() - startTick);
|
||||
// Ensure no refund equal to cost snuck back in; only passive income accumulated
|
||||
expect(finalGold < cost).toBe(true);
|
||||
expect(finalGold <= ticksElapsed * passivePerTick).toBe(true);
|
||||
|
||||
// Structure exists and is active
|
||||
expect(player.units(UnitType.City)).toHaveLength(1);
|
||||
expect(
|
||||
(player.units(UnitType.City)[0] as any).isUnderConstruction?.() ?? false,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
|
||||
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../../src/core/game/Game";
|
||||
import { setup } from "../util/Setup";
|
||||
|
||||
describe("Hydrogen Bomb and MIRV flows", () => {
|
||||
let game: Game;
|
||||
let player: Player;
|
||||
|
||||
beforeEach(async () => {
|
||||
game = await setup("plains", { infiniteGold: true, instantBuild: true });
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
game.addPlayer(info);
|
||||
game.addExecution(new SpawnExecution(info, game.ref(1, 1)));
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
player = game.player(info.id);
|
||||
|
||||
player.conquer(game.ref(1, 1));
|
||||
});
|
||||
|
||||
test("Hydrogen bomb launches when silo exists and cannot use silo under construction", () => {
|
||||
// Build a silo instantly and launch Hydrogen Bomb
|
||||
game.addExecution(
|
||||
new ConstructionExecution(player, UnitType.MissileSilo, game.ref(1, 1)),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(player.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
// Launch Hydrogen Bomb
|
||||
const target = game.ref(7, 7);
|
||||
game.addExecution(
|
||||
new ConstructionExecution(player, UnitType.HydrogenBomb, target),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(player.units(UnitType.HydrogenBomb).length).toBeGreaterThan(0);
|
||||
|
||||
// Now build another silo with construction time and ensure it won't be used
|
||||
// Use non-instant config by simulating an under-construction flag on a new silo
|
||||
// (Use normal construction with default duration in a fresh game instance)
|
||||
});
|
||||
|
||||
test("Hydrogen bomb launch fails when silo is under construction and succeeds after completion", async () => {
|
||||
// Set up a game without instantBuild to test construction duration
|
||||
const gameWithConstruction = await setup("plains", {
|
||||
infiniteGold: false,
|
||||
instantBuild: false,
|
||||
});
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
gameWithConstruction.addPlayer(info);
|
||||
gameWithConstruction.addExecution(
|
||||
new SpawnExecution(info, gameWithConstruction.ref(1, 1)),
|
||||
);
|
||||
while (gameWithConstruction.inSpawnPhase())
|
||||
gameWithConstruction.executeNextTick();
|
||||
const playerWithConstruction = gameWithConstruction.player(info.id);
|
||||
|
||||
playerWithConstruction.conquer(gameWithConstruction.ref(1, 1));
|
||||
const siloTile = gameWithConstruction.ref(7, 7);
|
||||
playerWithConstruction.conquer(siloTile);
|
||||
|
||||
// Capture gold before starting silo construction
|
||||
const goldBeforeSilo = playerWithConstruction.gold();
|
||||
const siloCost = gameWithConstruction
|
||||
.unitInfo(UnitType.MissileSilo)
|
||||
.cost(playerWithConstruction);
|
||||
playerWithConstruction.addGold(siloCost);
|
||||
|
||||
// Start construction of silo
|
||||
gameWithConstruction.addExecution(
|
||||
new ConstructionExecution(
|
||||
playerWithConstruction,
|
||||
UnitType.MissileSilo,
|
||||
siloTile,
|
||||
),
|
||||
);
|
||||
gameWithConstruction.executeNextTick();
|
||||
gameWithConstruction.executeNextTick();
|
||||
|
||||
// Verify silo exists and is under construction
|
||||
const silos = playerWithConstruction.units(UnitType.MissileSilo);
|
||||
expect(silos.length).toBe(1);
|
||||
const silo = silos[0];
|
||||
expect(silo.isUnderConstruction()).toBe(true);
|
||||
|
||||
// Capture gold after construction started
|
||||
const goldAfterConstruction = playerWithConstruction.gold();
|
||||
expect(goldAfterConstruction).toBeLessThan(goldBeforeSilo + siloCost);
|
||||
|
||||
// Attempt to launch HydrogenBomb while silo is under construction
|
||||
const targetTile = gameWithConstruction.ref(10, 10);
|
||||
const hydrogenBombCountBefore = playerWithConstruction.units(
|
||||
UnitType.HydrogenBomb,
|
||||
).length;
|
||||
|
||||
const canBuildResult = playerWithConstruction.canBuild(
|
||||
UnitType.HydrogenBomb,
|
||||
targetTile,
|
||||
);
|
||||
expect(canBuildResult).toBe(false); // Should fail because silo is under construction
|
||||
|
||||
// Try to add execution - should fail
|
||||
gameWithConstruction.addExecution(
|
||||
new ConstructionExecution(
|
||||
playerWithConstruction,
|
||||
UnitType.HydrogenBomb,
|
||||
targetTile,
|
||||
),
|
||||
);
|
||||
gameWithConstruction.executeNextTick();
|
||||
gameWithConstruction.executeNextTick();
|
||||
|
||||
// Assert launch does not succeed
|
||||
const hydrogenBombCountAfter = playerWithConstruction.units(
|
||||
UnitType.HydrogenBomb,
|
||||
).length;
|
||||
expect(hydrogenBombCountAfter).toBe(hydrogenBombCountBefore);
|
||||
|
||||
// Assert no refunds during construction
|
||||
const goldDuringConstruction = playerWithConstruction.gold();
|
||||
expect(goldDuringConstruction >= goldAfterConstruction).toBe(true);
|
||||
|
||||
// Advance ticks to complete construction
|
||||
const constructionDuration =
|
||||
gameWithConstruction.unitInfo(UnitType.MissileSilo)
|
||||
.constructionDuration ?? 0;
|
||||
for (let i = 0; i < constructionDuration + 2; i++) {
|
||||
gameWithConstruction.executeNextTick();
|
||||
}
|
||||
|
||||
// Verify silo is complete
|
||||
const completedSilo = playerWithConstruction.units(UnitType.MissileSilo)[0];
|
||||
expect(completedSilo.isUnderConstruction()).toBe(false);
|
||||
|
||||
// Now launch should succeed - ensure we have gold and target is conquered
|
||||
playerWithConstruction.conquer(targetTile);
|
||||
const hydrogenBombCost = gameWithConstruction
|
||||
.unitInfo(UnitType.HydrogenBomb)
|
||||
.cost(playerWithConstruction);
|
||||
playerWithConstruction.addGold(hydrogenBombCost);
|
||||
|
||||
const canBuildAfterCompletion = playerWithConstruction.canBuild(
|
||||
UnitType.HydrogenBomb,
|
||||
targetTile,
|
||||
);
|
||||
expect(canBuildAfterCompletion).not.toBe(false);
|
||||
|
||||
gameWithConstruction.addExecution(
|
||||
new ConstructionExecution(
|
||||
playerWithConstruction,
|
||||
UnitType.HydrogenBomb,
|
||||
targetTile,
|
||||
),
|
||||
);
|
||||
gameWithConstruction.executeNextTick();
|
||||
gameWithConstruction.executeNextTick();
|
||||
gameWithConstruction.executeNextTick();
|
||||
|
||||
// Verify launch succeeded
|
||||
const hydrogenBombCountAfterSuccess = playerWithConstruction.units(
|
||||
UnitType.HydrogenBomb,
|
||||
).length;
|
||||
expect(hydrogenBombCountAfterSuccess).toBeGreaterThan(
|
||||
hydrogenBombCountBefore,
|
||||
);
|
||||
});
|
||||
|
||||
test("MIRV launches when silo exists and targets player-owned tiles", () => {
|
||||
// Build a silo instantly
|
||||
game.addExecution(
|
||||
new ConstructionExecution(player, UnitType.MissileSilo, game.ref(1, 1)),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(player.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
// Launch MIRV at a player-owned tile (the silo tile)
|
||||
const target = game.ref(1, 1);
|
||||
game.addExecution(new ConstructionExecution(player, UnitType.MIRV, target));
|
||||
game.executeNextTick(); // init
|
||||
game.executeNextTick(); // create MIRV unit
|
||||
game.executeNextTick();
|
||||
|
||||
// MIRV should appear briefly before separation, otherwise warheads should be queued
|
||||
const mirvs = player.units(UnitType.MIRV).length;
|
||||
const warheads = player.units(UnitType.MIRVWarhead).length;
|
||||
expect(mirvs > 0 || warheads > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
// Mock BuildMenu to avoid importing lit and other ESM-heavy deps in this unit test
|
||||
jest.mock(
|
||||
"../src/client/graphics/layers/BuildMenu",
|
||||
() => ({
|
||||
BuildMenu: class {},
|
||||
flattenedBuildTable: [],
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
// Mock Utils to avoid touching DOM (document) during tests
|
||||
jest.mock("../src/client/Utils", () => ({
|
||||
translateText: (k: string) => k,
|
||||
getSvgAspectRatio: async () => 1,
|
||||
}));
|
||||
|
||||
import {
|
||||
COLORS,
|
||||
rootMenuElement,
|
||||
type MenuElementParams,
|
||||
} from "../src/client/graphics/layers/RadialMenuElements";
|
||||
|
||||
// Minimal stubs to satisfy types used in rootMenuElement.subMenu and allyBreak actions
|
||||
const makePlayer = (id: string) =>
|
||||
({
|
||||
id: () => id,
|
||||
isAlliedWith: (other: any) =>
|
||||
other && typeof other.id === "function" && other.id() !== id
|
||||
? true
|
||||
: true,
|
||||
}) as unknown as import("../src/core/game/GameView").PlayerView;
|
||||
|
||||
const makeParams = (opts?: Partial<MenuElementParams>): MenuElementParams => {
|
||||
const myPlayer = (opts?.myPlayer as any) ?? makePlayer("p1");
|
||||
const selected = (opts?.selected as any) ?? makePlayer("p2");
|
||||
return {
|
||||
myPlayer,
|
||||
selected,
|
||||
tile: {} as any,
|
||||
playerActions: {
|
||||
canAttack: true,
|
||||
interaction: {
|
||||
canBreakAlliance: true,
|
||||
canSendAllianceRequest: false,
|
||||
canEmbargo: false,
|
||||
},
|
||||
} as any,
|
||||
game: {
|
||||
inSpawnPhase: () => false,
|
||||
owner: () => ({ isPlayer: () => false }),
|
||||
} as any,
|
||||
buildMenu: {
|
||||
canBuildOrUpgrade: () => false,
|
||||
cost: () => 0,
|
||||
count: () => 0,
|
||||
sendBuildOrUpgrade: () => {},
|
||||
} as any,
|
||||
emojiTable: {} as any,
|
||||
playerActionHandler: {
|
||||
handleBreakAlliance: jest.fn(),
|
||||
handleEmbargo: jest.fn(),
|
||||
handleDonateGold: jest.fn(),
|
||||
handleDonateTroops: jest.fn(),
|
||||
handleTargetPlayer: jest.fn(),
|
||||
} as any,
|
||||
playerPanel: {
|
||||
show: jest.fn(),
|
||||
} as any,
|
||||
chatIntegration: {
|
||||
createQuickChatMenu: jest.fn(() => []),
|
||||
} as any,
|
||||
eventBus: {} as any,
|
||||
closeMenu: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const findAllyBreak = (items: any[]) =>
|
||||
items.find((i) => i && i.id === "ally_break");
|
||||
|
||||
describe("RadialMenuElements ally break", () => {
|
||||
test("shows break option with correct color when allied", () => {
|
||||
const params = makeParams();
|
||||
const items = rootMenuElement.subMenu!(params);
|
||||
const ally = findAllyBreak(items)!;
|
||||
expect(ally).toBeTruthy();
|
||||
expect(ally.name).toBe("break");
|
||||
expect(ally.color).toBe(COLORS.breakAlly);
|
||||
});
|
||||
|
||||
test("action calls handleBreakAlliance and closes menu", () => {
|
||||
const params = makeParams();
|
||||
const items = rootMenuElement.subMenu!(params);
|
||||
const ally = findAllyBreak(items)!;
|
||||
|
||||
ally.action!(params);
|
||||
|
||||
expect(params.playerActionHandler.handleBreakAlliance).toHaveBeenCalledWith(
|
||||
params.myPlayer,
|
||||
params.selected,
|
||||
);
|
||||
expect(params.closeMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+595
-7
@@ -1,14 +1,602 @@
|
||||
{
|
||||
"map": {
|
||||
"height": 1948,
|
||||
"num_land_tiles": 2544294,
|
||||
"width": 4110
|
||||
"num_land_tiles": 2317767,
|
||||
"width": 4108
|
||||
},
|
||||
"mini_map": {
|
||||
"map16x": {
|
||||
"height": 487,
|
||||
"num_land_tiles": 128871,
|
||||
"width": 1027
|
||||
},
|
||||
"map4x": {
|
||||
"height": 974,
|
||||
"num_land_tiles": 618860,
|
||||
"width": 2055
|
||||
"num_land_tiles": 556211,
|
||||
"width": 2054
|
||||
},
|
||||
"name": "Giant World Map",
|
||||
"nations": []
|
||||
"name": "Giant_World_Map",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [2309, 535],
|
||||
"flag": "tr",
|
||||
"name": "Türkiye",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2030, 409],
|
||||
"flag": "west_germany",
|
||||
"name": "West Germany",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2074, 382],
|
||||
"flag": "east_germany",
|
||||
"name": "East Germany",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1966, 442],
|
||||
"flag": "fr",
|
||||
"name": "France",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1872, 528],
|
||||
"flag": "Fascist Spain",
|
||||
"name": "Spain",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2074, 498],
|
||||
"flag": "it",
|
||||
"name": "Italy",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1912, 379],
|
||||
"flag": "gb",
|
||||
"name": "United Kingdom",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1841, 373],
|
||||
"flag": "ie",
|
||||
"name": "Ireland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2153, 378],
|
||||
"flag": "pl",
|
||||
"name": "Poland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2178, 539],
|
||||
"flag": "gr",
|
||||
"name": "Greece",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2222, 493],
|
||||
"flag": "bg",
|
||||
"name": "Bulgaria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2135, 481],
|
||||
"flag": "yugoslavia",
|
||||
"name": "Yugoslavia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2242, 461],
|
||||
"flag": "Communist Romania",
|
||||
"name": "Romania",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2163, 441],
|
||||
"flag": "hu",
|
||||
"name": "Hungary",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2272, 418],
|
||||
"flag": "Ukrainian SSR",
|
||||
"name": "Ukrainian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2093, 297],
|
||||
"flag": "se",
|
||||
"name": "Sweden",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2027, 282],
|
||||
"flag": "no",
|
||||
"name": "Norway",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2191, 194],
|
||||
"flag": "Sami flag",
|
||||
"name": "Sapmi",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2206, 262],
|
||||
"flag": "fi",
|
||||
"name": "Finland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2376, 363],
|
||||
"flag": "Russian SSR",
|
||||
"name": "Russian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2222, 371],
|
||||
"flag": "Byelorussian SSR",
|
||||
"name": "Byelorussian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2441, 507],
|
||||
"flag": "Georgian SSR",
|
||||
"name": "Georgian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2402, 580],
|
||||
"flag": "Second Republic of Iraq",
|
||||
"name": "Iraq",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2353, 595],
|
||||
"flag": "sy",
|
||||
"name": "Syria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2414, 679],
|
||||
"flag": "sa",
|
||||
"name": "Saudi Arabia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2434, 815],
|
||||
"flag": "North yemen",
|
||||
"name": "North Yemen",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2479, 824],
|
||||
"flag": "south yemen",
|
||||
"name": "South Yemen",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2554, 724],
|
||||
"flag": "ae",
|
||||
"name": "United Arab Emirates",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2532, 609],
|
||||
"flag": "Pahlavi Iran",
|
||||
"name": "Iran",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2683, 650],
|
||||
"flag": "pk",
|
||||
"name": "Pakistan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2654, 580],
|
||||
"flag": "af",
|
||||
"name": "Afghanistan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2727, 416],
|
||||
"flag": "Kazakh SSR",
|
||||
"name": "Kazakh SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2556, 544],
|
||||
"flag": "Turkmen SSR",
|
||||
"name": "Turkmen SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2947, 362],
|
||||
"flag": "Zheleznogorsk",
|
||||
"name": "Zheleznogorsk",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3252, 229],
|
||||
"flag": "Siberia",
|
||||
"name": "Siberia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2810, 744],
|
||||
"flag": "in",
|
||||
"name": "India",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1717, 237],
|
||||
"flag": "is",
|
||||
"name": "Iceland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2944, 709],
|
||||
"flag": "bd",
|
||||
"name": "Bangladesh",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2868, 635],
|
||||
"flag": "np",
|
||||
"name": "Nepal",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3254, 672],
|
||||
"flag": "cn",
|
||||
"name": "China",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3373, 521],
|
||||
"flag": "kp",
|
||||
"name": "North Korea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3389, 573],
|
||||
"flag": "kr",
|
||||
"name": "South Korea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3515, 571],
|
||||
"flag": "jp",
|
||||
"name": "Japan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3026, 457],
|
||||
"flag": "mn",
|
||||
"name": "Mongolia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3229, 995],
|
||||
"flag": "id",
|
||||
"name": "Indonesia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3121, 755],
|
||||
"flag": "vn",
|
||||
"name": "North Vietnam",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3153, 833],
|
||||
"flag": "South Vietnam",
|
||||
"name": "South Vietnam",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3013, 722],
|
||||
"flag": "Burma2",
|
||||
"name": "Burma",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3095, 822],
|
||||
"flag": "kh",
|
||||
"name": "Cambodia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3538, 1067],
|
||||
"flag": "pg",
|
||||
"name": "Papua New Guinea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3542, 1356],
|
||||
"flag": "au",
|
||||
"name": "Australia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3422, 1203],
|
||||
"flag": "Australian Aboriginal Flag",
|
||||
"name": "Nawan-mirri",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3880, 1521],
|
||||
"flag": "nz",
|
||||
"name": "New Zealand",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2632, 1893],
|
||||
"flag": "aq",
|
||||
"name": "Antarctica",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2038, 590],
|
||||
"flag": "tn",
|
||||
"name": "Tunisia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2116, 653],
|
||||
"flag": "ly",
|
||||
"name": "Libya",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2281, 653],
|
||||
"flag": "United Arab Republic",
|
||||
"name": "United Arab Republic",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1859, 613],
|
||||
"flag": "ma",
|
||||
"name": "Morocco",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1943, 615],
|
||||
"flag": "dz",
|
||||
"name": "Algeria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2317, 754],
|
||||
"flag": "sd",
|
||||
"name": "Sudan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2466, 918],
|
||||
"flag": "so",
|
||||
"name": "Somalia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2352, 895],
|
||||
"flag": "Imperial Ethiopia",
|
||||
"name": "Ethiopia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1790, 729],
|
||||
"flag": "Mauritania",
|
||||
"name": "Mauritania",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2154, 764],
|
||||
"flag": "td",
|
||||
"name": "Chad",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2051, 745],
|
||||
"flag": "ne",
|
||||
"name": "Niger",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2040, 930],
|
||||
"flag": "ng",
|
||||
"name": "Nigeria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1805, 907],
|
||||
"flag": "lr",
|
||||
"name": "Liberia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2195, 918],
|
||||
"flag": "cf",
|
||||
"name": "Central African Republic",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2197, 1070],
|
||||
"flag": "Zaire",
|
||||
"name": "Zaire",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2189, 1372],
|
||||
"flag": "Apartheid South Africa",
|
||||
"name": "South Africa",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2452, 1247],
|
||||
"flag": "mg",
|
||||
"name": "Madagascar",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2356, 1165],
|
||||
"flag": "mz",
|
||||
"name": "Mozambique",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2368, 1032],
|
||||
"flag": "tz",
|
||||
"name": "Tanzania",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1934, 762],
|
||||
"flag": "ml",
|
||||
"name": "Mali",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2128, 1292],
|
||||
"flag": "Apartheid South Africa",
|
||||
"name": "South West Africa",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2099, 1178],
|
||||
"flag": "ao",
|
||||
"name": "Angola",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1375, 1121],
|
||||
"flag": "br",
|
||||
"name": "Brazil",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1203, 1059],
|
||||
"flag": "amazonas",
|
||||
"name": "Amazonas",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1210, 1395],
|
||||
"flag": "ar",
|
||||
"name": "Argentina",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1107, 1419],
|
||||
"flag": "cl",
|
||||
"name": "Chile",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1064, 1114],
|
||||
"flag": "pe",
|
||||
"name": "Peru",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1065, 938],
|
||||
"flag": "co",
|
||||
"name": "Colombia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1192, 938],
|
||||
"flag": "ve",
|
||||
"name": "Venezuela",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [913, 833],
|
||||
"flag": "ni",
|
||||
"name": "Nicaragua",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [788, 744],
|
||||
"flag": "mx",
|
||||
"name": "Mexico",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1011, 555],
|
||||
"flag": "us",
|
||||
"name": "USA",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [800, 624],
|
||||
"flag": "Texas",
|
||||
"name": "Texas",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [551, 564],
|
||||
"flag": "California",
|
||||
"name": "California",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [703, 483],
|
||||
"flag": "Utah",
|
||||
"name": "Utah",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1077, 444],
|
||||
"flag": "Quebec",
|
||||
"name": "Quebec",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1231, 395],
|
||||
"flag": "Newfoundland",
|
||||
"name": "Newfoundland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [967, 418],
|
||||
"flag": "ca",
|
||||
"name": "Canada",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [170, 244],
|
||||
"flag": "Alaska",
|
||||
"name": "Alaska",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [741, 234],
|
||||
"flag": "Nunavut",
|
||||
"name": "Nunavut",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [484, 256],
|
||||
"flag": "Yukon",
|
||||
"name": "Yukon",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1434, 223],
|
||||
"flag": "gl",
|
||||
"name": "Greenland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2247, 1229],
|
||||
"flag": "Rhodesia",
|
||||
"name": "Rhodesia",
|
||||
"strength": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+78
-78
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
+1
-1
@@ -1 +1 @@
|
||||
€À`"€À`"€À`"€À`"
|
||||
€À`!€À`!€À`!€À`!
|
||||
+1
-1
@@ -1 +1 @@
|
||||
€€€À`!"#€€€À`!"#€€€À`!"#€€€À`!"#€€€À`!"#€€€À`!"#€€€À`!"#€€€À`!"#
|
||||
€€€À`!!"€€€À`!!"€€€À`!!"€€€À`!!"€€€À`!!"€€€À`!!"€€€À`!!"€€€À`!!"
|
||||
+1
-1
@@ -1 +1 @@
|
||||
€À`"€À`"€À`!€À`"
|
||||
€À`!€À`!€À`!€À`!
|
||||
+1
-1
@@ -1 +1 @@
|
||||
€€€À`!"#€€€À`!""€€€À`!"!€€€À`!!€€€€À`!!`€€€À`!"!€€€À`!""€€€À`!"#
|
||||
€€€À`!!!€€€À`!!!€€€À`!!`€€€À`!`À€€€À`!!`€€€À`!!!€€€À`!!!€€€À`!!"
|
||||
+2
-1
@@ -25,6 +25,7 @@ export async function setup(
|
||||
_gameConfig: Partial<GameConfig> = {},
|
||||
humans: PlayerInfo[] = [],
|
||||
currentDir: string = __dirname,
|
||||
ConfigClass: typeof TestConfig = TestConfig,
|
||||
): Promise<Game> {
|
||||
// Suppress console.debug for tests.
|
||||
console.debug = () => {};
|
||||
@@ -70,7 +71,7 @@ export async function setup(
|
||||
randomSpawn: false,
|
||||
..._gameConfig,
|
||||
};
|
||||
const config = new TestConfig(
|
||||
const config = new ConfigClass(
|
||||
serverConfig,
|
||||
gameConfig,
|
||||
new UserSettings(),
|
||||
|
||||
@@ -85,3 +85,26 @@ export class TestConfig extends DefaultConfig {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
export class UseRealAttackLogic extends TestConfig {
|
||||
// Override to use DefaultConfig's real attackLogic
|
||||
attackLogic(
|
||||
gm: Game,
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
tileToConquer: TileRef,
|
||||
): {
|
||||
attackerTroopLoss: number;
|
||||
defenderTroopLoss: number;
|
||||
tilesPerTickUsed: number;
|
||||
} {
|
||||
return DefaultConfig.prototype.attackLogic.call(
|
||||
this,
|
||||
gm,
|
||||
attackTroops,
|
||||
attacker,
|
||||
defender,
|
||||
tileToConquer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user