Merge branch 'main' into give-territory

This commit is contained in:
Théodore Léon
2025-06-07 13:31:54 +02:00
committed by GitHub
170 changed files with 4343 additions and 2968 deletions
+9 -7
View File
@@ -21,7 +21,7 @@ let attackerSpawn: TileRef;
function sendBoat(target: TileRef, source: TileRef, troops: number) {
game.addExecution(
new TransportShipExecution(defender.id(), null, target, troops, source),
new TransportShipExecution(defender, null, target, troops, source),
);
}
@@ -64,7 +64,9 @@ describe("Attack", () => {
attacker = game.player(attackerInfo.id);
defender = game.player(defenderInfo.id);
game.addExecution(new AttackExecution(100, defender.id(), null));
game.addExecution(
new AttackExecution(100, defender, game.terraNullius().id()),
);
game.executeNextTick();
while (defender.outgoingAttacks().length > 0) {
game.executeNextTick();
@@ -76,10 +78,10 @@ describe("Attack", () => {
test("Nuke reduce attacking troop counts", async () => {
// Not building exactly spawn to it's better protected from attacks (but still
// on defender territory)
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
game.addExecution(new AttackExecution(100, attacker.id(), defender.id()));
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
game.addExecution(new AttackExecution(100, attacker, defender.id()));
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
const nuke = defender.units(UnitType.AtomBomb)[0];
expect(nuke.isActive()).toBe(true);
@@ -94,12 +96,12 @@ describe("Attack", () => {
});
test("Nuke reduce attacking boat troop count", async () => {
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
sendBoat(game.ref(15, 8), game.ref(10, 5), 100);
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
const nuke = defender.units(UnitType.AtomBomb)[0];
expect(nuke.isActive()).toBe(true);
+150
View File
@@ -0,0 +1,150 @@
import { BotBehavior } from "../src/core/execution/utils/BotBehavior";
import {
AllianceRequest,
Game,
Player,
PlayerInfo,
PlayerType,
Tick,
} from "../src/core/game/Game";
import { PseudoRandom } from "../src/core/PseudoRandom";
import { setup } from "./util/Setup";
let game: Game;
let player: Player;
let requestor: Player;
let botBehavior: BotBehavior;
describe("BotBehavior.handleAllianceRequests", () => {
beforeEach(async () => {
game = await setup("BigPlains", { infiniteGold: true, instantBuild: true });
const playerInfo = new PlayerInfo(
"us",
"player_id",
PlayerType.Bot,
null,
"player_id",
);
const requestorInfo = new PlayerInfo(
"fr",
"requestor_id",
PlayerType.Human,
null,
"requestor_id",
);
game.addPlayer(playerInfo);
game.addPlayer(requestorInfo);
player = game.player("player_id");
requestor = game.player("requestor_id");
const random = new PseudoRandom(42);
botBehavior = new BotBehavior(random, game, player, 0.5, 0.5);
});
function setupAllianceRequest({
isTraitor = false,
relationDelta = 2,
numTilesPlayer = 10,
numTilesRequestor = 10,
alliancesCount = 0,
} = {}) {
if (isTraitor) requestor.markTraitor();
player.updateRelation(requestor, relationDelta);
requestor.updateRelation(player, relationDelta);
game.map().forEachTile((tile) => {
if (game.map().isLand(tile)) {
if (numTilesPlayer > 0) {
player.conquer(tile);
numTilesPlayer--;
} else if (numTilesRequestor > 0) {
requestor.conquer(tile);
numTilesRequestor--;
}
}
});
jest
.spyOn(requestor, "alliances")
.mockReturnValue(new Array(alliancesCount));
const mockRequest = {
requestor: () => requestor,
recipient: () => player,
createdAt: () => 0 as unknown as Tick,
accept: jest.fn(),
reject: jest.fn(),
} as unknown as AllianceRequest;
jest
.spyOn(player, "incomingAllianceRequests")
.mockReturnValue([mockRequest]);
return mockRequest;
}
test("should accept alliance when all conditions are met", () => {
const request = setupAllianceRequest({});
botBehavior.handleAllianceRequests();
expect(request.accept).toHaveBeenCalled();
expect(request.reject).not.toHaveBeenCalled();
});
test("should reject alliance if requestor is a traitor", () => {
const request = setupAllianceRequest({ isTraitor: true });
botBehavior.handleAllianceRequests();
expect(request.accept).not.toHaveBeenCalled();
expect(request.reject).toHaveBeenCalled();
});
test("should reject alliance if relation is malicious", () => {
const request = setupAllianceRequest({ relationDelta: -2 });
botBehavior.handleAllianceRequests();
expect(request.accept).not.toHaveBeenCalled();
expect(request.reject).toHaveBeenCalled();
});
test("should accept alliance if requestor is much larger (> 3 times size of recipient) and has too many alliances (>= 3)", () => {
const request = setupAllianceRequest({
numTilesRequestor: 40,
alliancesCount: 4,
});
botBehavior.handleAllianceRequests();
expect(request.accept).toHaveBeenCalled();
expect(request.reject).not.toHaveBeenCalled();
});
test("should accept alliance if requestor is much larger (> 3 times size of recipient) and does not have too many alliances (< 3)", () => {
const request = setupAllianceRequest({
numTilesRequestor: 40,
alliancesCount: 2,
});
botBehavior.handleAllianceRequests();
expect(request.accept).toHaveBeenCalled();
expect(request.reject).not.toHaveBeenCalled();
});
test("should reject alliance if requestor is acceptably small (<= 3 times size of recipient) and has too many alliances (>= 3)", () => {
const request = setupAllianceRequest({ alliancesCount: 3 });
botBehavior.handleAllianceRequests();
expect(request.accept).not.toHaveBeenCalled();
expect(request.reject).toHaveBeenCalled();
});
});
+1 -1
View File
@@ -159,7 +159,7 @@ describe("Disconnected", () => {
executeTicks(game, 1);
expect(player1.isDisconnected()).toBe(true);
});
test("Breaking alliance with disconnected player doesn't make you a traitor", () => {
player1.createAllianceRequest(player2);
player2
+2 -2
View File
@@ -20,7 +20,7 @@ function attackerBuildsNuke(
initialize = true,
) {
game.addExecution(
new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source),
new NukeExecution(UnitType.AtomBomb, attacker, target, source),
);
if (initialize) {
game.executeNextTick();
@@ -50,7 +50,7 @@ describe("MissileSilo", () => {
attacker = game.player("attacker_id");
constructionExecution(game, attacker.id(), 1, 1, UnitType.MissileSilo);
constructionExecution(game, attacker, 1, 1, UnitType.MissileSilo);
});
test("missilesilo should launch nuke", async () => {
+58 -9
View File
@@ -1,3 +1,4 @@
import { NukeExecution } from "../src/core/execution/NukeExecution";
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
import {
@@ -13,10 +14,11 @@ import { constructionExecution, executeTicks } from "./util/utils";
let game: Game;
let attacker: Player;
let defender: Player;
let far_defender: Player;
describe("SAM", () => {
beforeEach(async () => {
game = await setup("Plains", { infiniteGold: true, instantBuild: true });
game = await setup("BigPlains", { infiniteGold: true, instantBuild: true });
const defender_info = new PlayerInfo(
"us",
"defender_id",
@@ -24,6 +26,13 @@ describe("SAM", () => {
null,
"defender_id",
);
const far_defender_info = new PlayerInfo(
"us",
"far_defender_id",
PlayerType.Human,
null,
"far_defender_id",
);
const attacker_info = new PlayerInfo(
"fr",
"attacker_id",
@@ -32,10 +41,15 @@ describe("SAM", () => {
"attacker_id",
);
game.addPlayer(defender_info);
game.addPlayer(far_defender_info);
game.addPlayer(attacker_info);
game.addExecution(
new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)),
new SpawnExecution(
game.player(far_defender_info.id).info(),
game.ref(199, 1),
),
new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)),
);
@@ -43,16 +57,19 @@ describe("SAM", () => {
game.executeNextTick();
}
defender = game.player("defender_id");
attacker = game.player("attacker_id");
defender = game.player("defender_id");
far_defender = game.player("far_defender_id");
constructionExecution(game, attacker.id(), 7, 7, UnitType.MissileSilo);
constructionExecution(game, attacker, 7, 7, UnitType.MissileSilo);
});
test("one sam should take down one nuke", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender, null, sam));
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {
targetTile: game.ref(2, 1),
});
executeTicks(game, 3);
@@ -61,7 +78,7 @@ describe("SAM", () => {
test("sam should only get one nuke at a time", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
game.addExecution(new SAMLauncherExecution(defender, null, sam));
attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 1), {
targetTile: game.ref(2, 1),
});
@@ -77,7 +94,7 @@ describe("SAM", () => {
test("sam should cooldown as long as configured", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
game.addExecution(new SAMLauncherExecution(defender, null, sam));
expect(sam.isInCooldown()).toBeFalsy();
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
targetTile: game.ref(1, 2),
@@ -100,9 +117,9 @@ describe("SAM", () => {
const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {
cooldownDuration: 10,
});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1));
game.addExecution(new SAMLauncherExecution(defender, null, sam1));
const sam2 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 2), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
game.addExecution(new SAMLauncherExecution(defender, null, sam2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 2), {
targetTile: game.ref(2, 2),
});
@@ -112,4 +129,36 @@ describe("SAM", () => {
expect(nuke.isActive()).toBeFalsy();
expect([sam1, sam2].filter((s) => s.isInCooldown())).toHaveLength(1);
});
test("SAMs should target only nukes aimed at nearby targets", async () => {
const targetDistance = 199;
// Close SAM: should not intercept anything
const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender, null, sam1));
// Far SAM: Should intercept the nuke. Use the far_defender so the SAM can be built
const sam2 = far_defender.buildUnit(
UnitType.SAMLauncher,
game.ref(targetDistance, 1),
{},
);
game.addExecution(new SAMLauncherExecution(far_defender, null, sam2));
const nukeExecution = new NukeExecution(
UnitType.AtomBomb,
attacker,
game.ref(targetDistance, 1),
null,
);
game.addExecution(nukeExecution);
// Long distance nuke: compute the proper number of ticks
const ticksToExecute = Math.ceil(
targetDistance / game.config().defaultNukeSpeed(),
);
executeTicks(game, ticksToExecute);
expect(nukeExecution.isActive()).toBeFalsy();
expect(sam1.isInCooldown()).toBeFalsy();
expect(sam2.isInCooldown()).toBeTruthy();
});
});
+15
View File
@@ -4,6 +4,21 @@ import { GameMapType } from "../../src/core/game/Game";
import { GameID } from "../../src/core/Schemas";
export class TestServerConfig implements ServerConfig {
cloudflareConfigDir(): string {
throw new Error("Method not implemented.");
}
domain(): string {
throw new Error("Method not implemented.");
}
subdomain(): string {
throw new Error("Method not implemented.");
}
cloudflareAccountId(): string {
throw new Error("Method not implemented.");
}
cloudflareApiToken(): string {
throw new Error("Method not implemented.");
}
jwtAudience(): string {
throw new Error("Method not implemented.");
}
+3 -3
View File
@@ -4,18 +4,18 @@
// If you also need execution use function below. Does not work with things not
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
import { Game, PlayerID, UnitType } from "../../src/core/game/Game";
import { Game, Player, UnitType } from "../../src/core/game/Game";
// built via UI (e.g.: trade ships)
export function constructionExecution(
game: Game,
playerID: PlayerID,
_owner: Player,
x: number,
y: number,
unit: UnitType,
ticks = 4,
) {
game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit));
game.addExecution(new ConstructionExecution(_owner, game.ref(x, y), unit));
// 4 ticks by default as it usually goes like this
// Init of construction execution