${this.secondsToHms(this.timer)}
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index b13f8ac17..4110bc2c1 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -164,6 +164,7 @@ export const GameConfigSchema = z.object({
infiniteTroops: z.boolean(),
instantBuild: z.boolean(),
maxPlayers: z.number().optional(),
+ maxTimerValue: z.number().int().min(1).max(120).optional(),
disabledUnits: z.enum(UnitType).array().optional(),
playerTeams: TeamCountConfigSchema.optional(),
});
diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts
index 8c2159a71..be9793cb8 100644
--- a/src/core/execution/WinCheckExecution.ts
+++ b/src/core/execution/WinCheckExecution.ts
@@ -28,6 +28,7 @@ export class WinCheckExecution implements Execution {
return;
}
if (this.mg === null) throw new Error("Not initialized");
+
if (this.mg.config().gameConfig().gameMode === GameMode.FFA) {
this.checkWinnerFFA();
} else {
@@ -44,11 +45,15 @@ export class WinCheckExecution implements Execution {
return;
}
const max = sorted[0];
+ const timeElapsed =
+ (this.mg.ticks() - this.mg.config().numSpawnPhaseTurns()) / 10;
const numTilesWithoutFallout =
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
if (
(max.numTilesOwned() / numTilesWithoutFallout) * 100 >
- this.mg.config().percentageTilesOwnedToWin()
+ this.mg.config().percentageTilesOwnedToWin() ||
+ (this.mg.config().gameConfig().maxTimerValue !== undefined &&
+ timeElapsed - this.mg.config().gameConfig().maxTimerValue! * 60 >= 0)
) {
this.mg.setWinner(max, this.mg.stats().stats());
console.log(`${max.name()} has won the game`);
@@ -75,10 +80,16 @@ export class WinCheckExecution implements Execution {
return;
}
const max = sorted[0];
+ const timeElapsed =
+ (this.mg.ticks() - this.mg.config().numSpawnPhaseTurns()) / 10;
const numTilesWithoutFallout =
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
const percentage = (max[1] / numTilesWithoutFallout) * 100;
- if (percentage > this.mg.config().percentageTilesOwnedToWin()) {
+ if (
+ percentage > this.mg.config().percentageTilesOwnedToWin() ||
+ (this.mg.config().gameConfig().maxTimerValue !== undefined &&
+ timeElapsed - this.mg.config().gameConfig().maxTimerValue! * 60 >= 0)
+ ) {
if (max[0] === ColoredTeams.Bot) return;
this.mg.setWinner(max[0], this.mg.stats().stats());
console.log(`${max[0]} has won the game`);
diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts
index 1d4d8f80c..f366a63ec 100644
--- a/src/server/GameManager.ts
+++ b/src/server/GameManager.ts
@@ -54,6 +54,7 @@ export class GameManager {
disableNPCs: false,
infiniteGold: false,
infiniteTroops: false,
+ maxTimerValue: undefined,
instantBuild: false,
gameMode: GameMode.FFA,
bots: 400,
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index eabef3f11..799c25c88 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -109,6 +109,9 @@ export class GameServer {
if (gameConfig.donateTroops !== undefined) {
this.gameConfig.donateTroops = gameConfig.donateTroops;
}
+ if (gameConfig.maxTimerValue !== undefined) {
+ this.gameConfig.maxTimerValue = gameConfig.maxTimerValue;
+ }
if (gameConfig.instantBuild !== undefined) {
this.gameConfig.instantBuild = gameConfig.instantBuild;
}
diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts
index acb14cce0..11e7b6ecb 100644
--- a/src/server/MapPlaylist.ts
+++ b/src/server/MapPlaylist.ts
@@ -88,6 +88,7 @@ export class MapPlaylist {
difficulty: Difficulty.Medium,
infiniteGold: false,
infiniteTroops: false,
+ maxTimerValue: undefined,
instantBuild: false,
disableNPCs: mode === GameMode.Team,
gameMode: mode,
diff --git a/tests/core/executions/WinCheckExecution.test.ts b/tests/core/executions/WinCheckExecution.test.ts
new file mode 100644
index 000000000..922a5b547
--- /dev/null
+++ b/tests/core/executions/WinCheckExecution.test.ts
@@ -0,0 +1,84 @@
+import { WinCheckExecution } from "../../../src/core/execution/WinCheckExecution";
+import { GameMode } from "../../../src/core/game/Game";
+import { setup } from "../../util/Setup";
+
+describe("WinCheckExecution", () => {
+ let mg: any;
+ let winCheck: WinCheckExecution;
+
+ beforeEach(async () => {
+ mg = await setup("big_plains", {
+ infiniteGold: true,
+ gameMode: GameMode.FFA,
+ maxTimerValue: 5,
+ instantBuild: true,
+ });
+ mg.setWinner = jest.fn();
+ winCheck = new WinCheckExecution();
+ winCheck.init(mg, 0);
+ });
+
+ it("should call checkWinnerFFA in FFA mode", () => {
+ const spy = jest.spyOn(winCheck as any, "checkWinnerFFA");
+ winCheck.tick(10);
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it("should call checkWinnerTeam in non-FFA mode", () => {
+ mg.config = jest.fn(() => ({
+ gameConfig: jest.fn(() => ({
+ maxTimerValue: 5,
+ gameMode: GameMode.Team,
+ })),
+ percentageTilesOwnedToWin: jest.fn(() => 50),
+ }));
+ winCheck.init(mg, 0);
+ const spy = jest.spyOn(winCheck as any, "checkWinnerTeam");
+ winCheck.tick(10);
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it("should set winner in FFA if percentage is reached", () => {
+ const player = {
+ numTilesOwned: jest.fn(() => 81),
+ name: jest.fn(() => "P1"),
+ };
+ mg.players = jest.fn(() => [player]);
+ mg.numLandTiles = jest.fn(() => 100);
+ mg.numTilesWithFallout = jest.fn(() => 0);
+ winCheck.checkWinnerFFA();
+ expect(mg.setWinner).toHaveBeenCalledWith(player, expect.anything());
+ });
+
+ it("should set winner in FFA if timer is 0", () => {
+ const player = {
+ numTilesOwned: jest.fn(() => 10),
+ name: jest.fn(() => "P1"),
+ };
+ mg.players = jest.fn(() => [player]);
+ mg.numLandTiles = jest.fn(() => 100);
+ mg.numTilesWithFallout = jest.fn(() => 0);
+ mg.stats = jest.fn(() => ({ stats: () => ({ mocked: true }) }));
+ // Advance ticks until timeElapsed (in seconds) >= maxTimerValue * 60
+ // timeElapsed = (ticks - numSpawnPhaseTurns) / 10 =>
+ // ticks >= numSpawnPhaseTurns + maxTimerValue * 600
+ const threshold =
+ mg.config().numSpawnPhaseTurns() +
+ (mg.config().gameConfig().maxTimerValue ?? 0) * 600;
+ while (mg.ticks() < threshold) {
+ mg.executeNextTick();
+ }
+ winCheck.checkWinnerFFA();
+ expect(mg.setWinner).toHaveBeenCalledWith(player, expect.any(Object));
+ });
+
+ it("should not set winner if no players", () => {
+ mg.players = jest.fn(() => []);
+ winCheck.checkWinnerFFA();
+ expect(mg.setWinner).not.toHaveBeenCalled();
+ });
+
+ it("should return false for activeDuringSpawnPhase", () => {
+ expect(winCheck.activeDuringSpawnPhase()).toBe(false);
+ });
+});