Merge branch 'main' into patterned-territory

This commit is contained in:
Aotumuri
2025-05-29 06:22:10 +09:00
committed by GitHub
119 changed files with 4787 additions and 1589 deletions
+24 -2
View File
@@ -50,6 +50,28 @@ describe("PlayerInfo", () => {
expect(playerInfo.clan).toBe("ABCDE");
});
test("should extract clan from name when format is [xxxxx]Name", () => {
const playerInfo = new PlayerInfo(
"fr",
"[abcde]PlayerName",
PlayerType.Human,
null,
"player_id",
);
expect(playerInfo.clan).toBe("abcde");
});
test("should extract clan from name when format is [XxXxX]Name", () => {
const playerInfo = new PlayerInfo(
"fr",
"[AbCdE]PlayerName",
PlayerType.Human,
null,
"player_id",
);
expect(playerInfo.clan).toBe("AbCdE");
});
test("should return null when name doesn't start with [", () => {
const playerInfo = new PlayerInfo(
undefined,
@@ -86,11 +108,11 @@ describe("PlayerInfo", () => {
expect(playerInfo.clan).toBeNull();
});
test("should return null when clan tag contains non-uppercase letters", () => {
test("should return null when clan tag contains non alphanumeric characters", () => {
const playerInfo = new PlayerInfo(
undefined,
"fr",
"[Abc]PlayerName",
"[A1c]PlayerName",
PlayerType.Human,
null,
"player_id",
+4 -4
View File
@@ -65,10 +65,10 @@ describe("SAM", () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 1), {
detonationDst: game.ref(2, 1),
targetTile: game.ref(2, 1),
});
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
detonationDst: game.ref(1, 2),
targetTile: game.ref(1, 2),
});
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
@@ -82,7 +82,7 @@ describe("SAM", () => {
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
expect(sam.isInCooldown()).toBeFalsy();
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
detonationDst: game.ref(1, 2),
targetTile: game.ref(1, 2),
});
executeTicks(game, 3);
@@ -106,7 +106,7 @@ describe("SAM", () => {
const sam2 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 2), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 2), {
detonationDst: game.ref(2, 2),
targetTile: game.ref(2, 2),
});
executeTicks(game, 3);
+232
View File
@@ -0,0 +1,232 @@
import {
Game,
Player,
PlayerInfo,
PlayerType,
UnitType,
} from "../src/core/game/Game";
import { Stats } from "../src/core/game/Stats";
import { StatsImpl } from "../src/core/game/StatsImpl";
import { replacer } from "../src/core/Util";
import { setup } from "./util/Setup";
let stats: Stats;
let game: Game;
let player1: Player;
let player2: Player;
describe("Stats", () => {
beforeEach(async () => {
stats = new StatsImpl();
game = await setup("half_land_half_ocean", {}, [
new PlayerInfo(
"us",
"boat dude",
PlayerType.Human,
"client1",
"player_1_id",
),
new PlayerInfo(
"us",
"boat dude",
PlayerType.Human,
"client2",
"player_2_id",
),
]);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player1 = game.player("player_1_id");
player2 = game.player("player_2_id");
});
test("attack", () => {
stats.attack(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
attacks: [1n],
},
client2: {
attacks: [0n, 1n],
},
});
});
test("attackCancel", () => {
stats.attackCancel(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
attacks: [-1n, 0n, 1n],
},
client2: {
attacks: [0n, -1n],
},
});
});
test("betray", () => {
stats.betray(player1);
expect(stats.stats()).toStrictEqual({
client1: {
betrayals: 1n,
},
});
});
test("boatSendTrade", () => {
stats.boatSendTrade(player1, player2);
expect(stats.stats()).toStrictEqual({
client1: {
boats: {
trade: [1n],
},
},
});
});
test("boatArriveTrade", () => {
stats.boatArriveTrade(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
boats: { trade: [0n, 1n] },
gold: [0n, 0n, 1n],
},
client2: {
gold: [0n, 0n, 1n],
},
});
});
test("boatCapturedTrade", () => {
stats.boatCapturedTrade(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
boats: { trade: [0n, 0n, 1n] },
gold: [0n, 0n, 0n, 1n],
},
});
});
test("boatDestroyTrade", () => {
stats.boatDestroyTrade(player1, player2);
expect(stats.stats()).toStrictEqual({
client1: {
boats: { trade: [0n, 0n, 0n, 1n] },
},
});
});
test("boatSendTroops", () => {
stats.boatSendTroops(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
boats: {
trans: [1n],
},
},
});
});
test("boatArriveTroops", () => {
stats.boatArriveTroops(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
boats: { trans: [0n, 1n] },
},
});
});
test("boatDestroyTroops", () => {
stats.boatDestroyTroops(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
boats: { trans: [0n, 0n, 0n, 1n] },
},
});
});
test("bombLaunch", () => {
stats.bombLaunch(player1, player2, UnitType.AtomBomb);
expect(stats.stats()).toStrictEqual({
client1: { bombs: { abomb: [1n] } },
});
});
test("bombLand", () => {
stats.bombLand(player1, player2, UnitType.HydrogenBomb);
expect(stats.stats()).toStrictEqual({
client1: { bombs: { hbomb: [0n, 1n] } },
});
});
test("bombIntercept", () => {
stats.bombIntercept(player1, player2, UnitType.MIRVWarhead);
expect(stats.stats()).toStrictEqual({
client1: { bombs: { mirvw: [0n, 0n, 1n] } },
});
});
test("goldWar", () => {
stats.goldWar(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: { gold: [0n, 1n] },
});
});
test("goldWork", () => {
stats.goldWork(player1, 1);
expect(stats.stats()).toStrictEqual({
client1: { gold: [1n] },
});
});
test("unitBuild", () => {
stats.unitBuild(player1, UnitType.City);
expect(stats.stats()).toStrictEqual({
client1: { units: { city: [1n] } },
});
});
test("unitCapture", () => {
stats.unitCapture(player1, UnitType.DefensePost);
expect(stats.stats()).toStrictEqual({
client1: {
units: {
defp: [0n, 0n, 1n],
},
},
});
});
test("unitDestroy", () => {
stats.unitDestroy(player1, UnitType.MissileSilo);
expect(stats.stats()).toStrictEqual({
client1: {
units: {
silo: [0n, 1n],
},
},
});
});
test("unitLose", () => {
stats.unitLose(player1, UnitType.Port);
expect(stats.stats()).toStrictEqual({
client1: {
units: {
port: [0n, 0n, 0n, 1n],
},
},
});
});
test("stringify", () => {
stats.unitLose(player1, UnitType.Port);
expect(JSON.stringify(stats.stats(), replacer)).toBe(
'{"client1":{"units":{"port":["0","0","0","1"]}}}',
);
});
});
+180 -62
View File
@@ -1,4 +1,5 @@
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
import { MoveWarshipExecution } from "../src/core/execution/MoveWarshipExecution";
import { WarshipExecution } from "../src/core/execution/WarshipExecution";
import {
Game,
Player,
@@ -7,7 +8,7 @@ import {
UnitType,
} from "../src/core/game/Game";
import { setup } from "./util/Setup";
import { constructionExecution } from "./util/utils";
import { executeTicks } from "./util/utils";
const coastX = 7;
let game: Game;
@@ -16,46 +17,38 @@ let player2: Player;
describe("Warship", () => {
beforeEach(async () => {
game = await setup("half_land_half_ocean", {
infiniteGold: true,
instantBuild: true,
});
const player_1_info = new PlayerInfo(
undefined,
"us",
"boat dude",
PlayerType.Human,
null,
"player_1_id",
);
game.addPlayer(player_1_info);
const player_2_info = new PlayerInfo(
undefined,
"us",
"boat dude",
PlayerType.Human,
null,
"player_2_id",
);
game.addPlayer(player_2_info);
game.addExecution(
new SpawnExecution(
game.player(player_1_info.id).info(),
game.ref(coastX, 10),
),
new SpawnExecution(
game.player(player_2_info.id).info(),
game.ref(coastX, 15),
),
game = await setup(
"half_land_half_ocean",
{
infiniteGold: true,
instantBuild: true,
},
[
new PlayerInfo(
undefined,
"us",
"boat dude",
PlayerType.Human,
null,
"player_1_id",
),
new PlayerInfo(
undefined,
"us",
"boat dude",
PlayerType.Human,
null,
"player_2_id",
),
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player1 = game.player(player_1_info.id);
player2 = game.player(player_2_info.id);
player1 = game.player("player_1_id");
player2 = game.player("player_2_id");
});
test("Warship heals only if player has port", async () => {
@@ -69,8 +62,11 @@ describe("Warship", () => {
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 10),
{},
{
patrolTile: game.ref(coastX + 1, 10),
},
);
game.addExecution(new WarshipExecution(warship));
game.executeNextTick();
@@ -87,26 +83,21 @@ describe("Warship", () => {
});
test("Warship captures trade if player has port", async () => {
constructionExecution(game, player1.id(), coastX, 10, UnitType.Port);
constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship);
// Warship need one more tick (for warship exec to actually build warship)
game.executeNextTick();
expect(player1.units(UnitType.Warship)).toHaveLength(1);
expect(player1.units(UnitType.Port)).toHaveLength(1);
const dstPort = player2.buildUnit(
UnitType.Port,
game.ref(coastX + 2, 10),
{},
const portTile = game.ref(coastX, 10);
player1.buildUnit(UnitType.Port, portTile, {});
game.addExecution(
new WarshipExecution(
player1.buildUnit(UnitType.Warship, portTile, {
patrolTile: portTile,
}),
),
);
// Cannot buildExec with trade ship as it's not buildable (but
// we can obviously directly add it to the player)
const tradeShip = player2.buildUnit(
UnitType.TradeShip,
game.ref(coastX + 1, 7),
{
dstPort,
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
},
);
@@ -115,32 +106,159 @@ describe("Warship", () => {
for (let i = 0; i < 10; i++) {
game.executeNextTick();
}
expect(tradeShip.owner().id()).toBe(player1.id());
expect(tradeShip.owner()).toBe(player1);
});
test("Warship do not capture trade if player has no port", async () => {
constructionExecution(game, player1.id(), coastX, 10, UnitType.Port);
constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship);
expect(player1.units(UnitType.Warship)).toHaveLength(1);
game.addExecution(
new WarshipExecution(
player1.buildUnit(UnitType.Warship, game.ref(coastX + 1, 11), {
patrolTile: game.ref(coastX + 1, 11),
}),
),
);
const [dstPort] = player1.units(UnitType.Port);
player1.units(UnitType.Port)[0].delete();
// Cannot buildExec with trade ship as it's not buildable (but
// we can obviously directly add it to the player)
const tradeShip = player2.buildUnit(
UnitType.TradeShip,
game.ref(coastX + 1, 11),
{
dstPort,
targetUnit: player1.buildUnit(UnitType.Port, game.ref(coastX, 11), {}),
},
);
expect(tradeShip.owner().id()).toBe(player2.id());
// Let plenty of time for A* to execute
// Let plenty of time for warship to potentially capture trade ship
for (let i = 0; i < 10; i++) {
game.executeNextTick();
}
expect(tradeShip.owner().id()).toBe(player2.id());
});
test("Warship does not target trade ships that are safe from pirates", async () => {
// build port so warship can target trade ships
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 10),
{
patrolTile: game.ref(coastX + 1, 10),
},
);
game.addExecution(new WarshipExecution(warship));
const tradeShip = player2.buildUnit(
UnitType.TradeShip,
game.ref(coastX + 1, 10),
{
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
},
);
tradeShip.setSafeFromPirates();
executeTicks(game, 10);
expect(tradeShip.owner().id()).toBe(player2.id());
});
test("Warship moves to new patrol tile", async () => {
game.config().warshipTargettingRange = () => 1;
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 10),
{
patrolTile: game.ref(coastX + 1, 10),
},
);
game.addExecution(new WarshipExecution(warship));
game.addExecution(
new MoveWarshipExecution(player1, warship.id(), game.ref(coastX + 5, 15)),
);
executeTicks(game, 10);
expect(warship.patrolTile()).toBe(game.ref(coastX + 5, 15));
});
test("Warship does not not target trade ships outside of patrol range", async () => {
game.config().warshipTargettingRange = () => 3;
// build port so warship can target trade ships
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 10),
{
patrolTile: game.ref(coastX + 1, 10),
},
);
game.addExecution(new WarshipExecution(warship));
const tradeShip = player2.buildUnit(
UnitType.TradeShip,
game.ref(coastX + 1, 15),
{
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
},
);
executeTicks(game, 10);
// Trade ship should not be captured
expect(tradeShip.owner().id()).toBe(player2.id());
});
test("MoveWarshipExecution fails if player is not the owner", async () => {
const originalPatrolTile = game.ref(coastX + 1, 10);
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 5),
{
patrolTile: originalPatrolTile,
},
);
new MoveWarshipExecution(
player2,
warship.id(),
game.ref(coastX + 5, 15),
).init(game, 0);
expect(warship.patrolTile()).toBe(originalPatrolTile);
});
test("MoveWarshipExecution fails if warship is not active", async () => {
const originalPatrolTile = game.ref(coastX + 1, 10);
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 5),
{
patrolTile: originalPatrolTile,
},
);
warship.delete();
new MoveWarshipExecution(
player1,
warship.id(),
game.ref(coastX + 5, 15),
).init(game, 0);
expect(warship.patrolTile()).toBe(originalPatrolTile);
});
test("MoveWarshipExecution fails gracefully if warship not found", async () => {
const exec = new MoveWarshipExecution(
player1,
123,
game.ref(coastX + 5, 15),
);
// Verify that no error is thrown.
exec.init(game, 0);
expect(exec.isActive()).toBe(false);
});
});
-1
View File
@@ -55,7 +55,6 @@ export async function setup(
false,
);
// Create and return the game
return createGame(humans, [], gameMap, miniGameMap, config);
}