Files
OpenFrontIO/tests/core/executions/WinCheckExecution.test.ts
Evan 42c944c9cc Create ranked type enum, last person not afk wins in 1v1 (#2892)
## Description:

* Add RankedType enum, for now it's just 1v1
* Add new method to MapPlaylist to create 1v1 game config
* Update WinCheck so the last player is declared a winner on 1v1.

## 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:

evan
2026-01-13 19:48:14 -08:00

586 lines
16 KiB
TypeScript

import { WinCheckExecution } from "../../../src/core/execution/WinCheckExecution";
import {
ColoredTeams,
GameMode,
PlayerInfo,
PlayerType,
RankedType,
} from "../../../src/core/game/Game";
import { playerInfo, 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 = vi.fn();
winCheck = new WinCheckExecution();
winCheck.init(mg, 0);
});
it("should call checkWinnerFFA in FFA mode", () => {
const spy = vi.spyOn(winCheck as any, "checkWinnerFFA");
winCheck.tick(10);
expect(spy).toHaveBeenCalled();
});
it("should call checkWinnerTeam in non-FFA mode", () => {
mg.config = vi.fn(() => ({
gameConfig: vi.fn(() => ({
maxTimerValue: 5,
gameMode: GameMode.Team,
})),
percentageTilesOwnedToWin: vi.fn(() => 50),
}));
winCheck.init(mg, 0);
const spy = vi.spyOn(winCheck as any, "checkWinnerTeam");
winCheck.tick(10);
expect(spy).toHaveBeenCalled();
});
it("should set winner in FFA if percentage is reached", () => {
const player = {
numTilesOwned: vi.fn(() => 81),
name: vi.fn(() => "P1"),
};
mg.players = vi.fn(() => [player]);
mg.numLandTiles = vi.fn(() => 100);
mg.numTilesWithFallout = vi.fn(() => 0);
winCheck.checkWinnerFFA();
expect(mg.setWinner).toHaveBeenCalledWith(player, expect.anything());
});
it("should set winner in FFA if timer is 0", () => {
const player = {
numTilesOwned: vi.fn(() => 10),
name: vi.fn(() => "P1"),
};
mg.players = vi.fn(() => [player]);
mg.numLandTiles = vi.fn(() => 100);
mg.numTilesWithFallout = vi.fn(() => 0);
mg.stats = vi.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 = vi.fn(() => []);
winCheck.checkWinnerFFA();
expect(mg.setWinner).not.toHaveBeenCalled();
});
it("should return false for activeDuringSpawnPhase", () => {
expect(winCheck.activeDuringSpawnPhase()).toBe(false);
});
});
describe("WinCheckExecution - Nation Winners", () => {
test("should set Nation as winner when reaching 80% territory", async () => {
// Setup game
const game = await setup("big_plains", {
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
});
// Create Nation player
const nationInfo = new PlayerInfo(
"TestNation",
PlayerType.Nation,
null,
"nation_id",
);
game.addPlayer(nationInfo);
const nation = game.player("nation_id");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign 81% of land to Nation
const totalLand = game.numLandTiles();
const targetTiles = Math.ceil(totalLand * 0.81);
let assigned = 0;
game.map().forEachTile((tile) => {
if (assigned >= targetTiles) return;
if (!game.map().isLand(tile)) return;
nation.conquer(tile);
assigned++;
});
// Verify territory ownership
expect(nation.numTilesOwned()).toBeGreaterThanOrEqual(targetTiles);
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerFFA();
// Verify Nation declared winner
expect(setWinnerSpy).toHaveBeenCalledWith(nation, expect.anything());
expect(winCheck.isActive()).toBe(false);
});
test("should set Nation as winner when timer expires with most territory", async () => {
// Setup game with timer
const game = await setup("big_plains", {
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
maxTimerValue: 5,
});
// Create human player
const humanInfo = new PlayerInfo(
"HumanPlayer",
PlayerType.Human,
null,
"human_id",
);
game.addPlayer(humanInfo);
const human = game.player("human_id");
// Create Nation player
const nationInfo = new PlayerInfo(
"TestNation",
PlayerType.Nation,
null,
"nation_id",
);
game.addPlayer(nationInfo);
const nation = game.player("nation_id");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Give Nation 60% territory (below 80% threshold)
// Give human 30% territory
const totalLand = game.numLandTiles();
const nationTiles = Math.ceil(totalLand * 0.6);
const humanTiles = Math.ceil(totalLand * 0.3);
let nationAssigned = 0;
let humanAssigned = 0;
game.map().forEachTile((tile) => {
if (!game.map().isLand(tile)) return;
if (nationAssigned < nationTiles) {
nation.conquer(tile);
nationAssigned++;
} else if (humanAssigned < humanTiles) {
human.conquer(tile);
humanAssigned++;
}
});
// Verify territory distribution
expect(nation.numTilesOwned()).toBeGreaterThan(human.numTilesOwned());
// Fast-forward game ticks past timer expiration
const threshold =
game.config().numSpawnPhaseTurns() +
(game.config().gameConfig().maxTimerValue ?? 0) * 600;
while (game.ticks() < threshold) {
game.executeNextTick();
}
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, game.ticks());
winCheck.checkWinnerFFA();
// Verify Nation declared winner (has most territory when timer expires)
expect(setWinnerSpy).toHaveBeenCalledWith(nation, expect.anything());
expect(winCheck.isActive()).toBe(false);
});
test("should set correct Nation as winner among multiple Nations", async () => {
// Setup game
const game = await setup("big_plains", {
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
});
// Create 3 Nation players
const nation1Info = new PlayerInfo(
"Nation1",
PlayerType.Nation,
null,
"nation1_id",
);
game.addPlayer(nation1Info);
const nation1 = game.player("nation1_id");
const nation2Info = new PlayerInfo(
"Nation2",
PlayerType.Nation,
null,
"nation2_id",
);
game.addPlayer(nation2Info);
const nation2 = game.player("nation2_id");
const nation3Info = new PlayerInfo(
"Nation3",
PlayerType.Nation,
null,
"nation3_id",
);
game.addPlayer(nation3Info);
const nation3 = game.player("nation3_id");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign territories: Nation1 (85%), Nation2 (10%), Nation3 (5%)
const totalLand = game.numLandTiles();
const nation1Tiles = Math.ceil(totalLand * 0.85);
const nation2Tiles = Math.ceil(totalLand * 0.1);
let nation1Assigned = 0;
let nation2Assigned = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let nation3Assigned = 0;
game.map().forEachTile((tile) => {
if (!game.map().isLand(tile)) return;
if (nation1Assigned < nation1Tiles) {
nation1.conquer(tile);
nation1Assigned++;
} else if (nation2Assigned < nation2Tiles) {
nation2.conquer(tile);
nation2Assigned++;
} else {
nation3.conquer(tile);
nation3Assigned++;
}
});
// Verify territory distribution
expect(nation1.numTilesOwned()).toBeGreaterThan(nation2.numTilesOwned());
expect(nation2.numTilesOwned()).toBeGreaterThan(nation3.numTilesOwned());
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerFFA();
// Verify Nation1 (highest territory) declared winner
expect(setWinnerSpy).toHaveBeenCalledWith(nation1, expect.anything());
expect(winCheck.isActive()).toBe(false);
});
test("should not set winner for bot team in Team mode", async () => {
// Setup Team mode game
const game = await setup("big_plains", {
infiniteGold: true,
gameMode: GameMode.Team,
instantBuild: true,
playerTeams: 2,
});
// Create 2 bot players (auto-assigned to Bot team)
const bot1Info = new PlayerInfo("Bot1", PlayerType.Bot, null, "bot1_id");
game.addPlayer(bot1Info);
const bot1 = game.player("bot1_id");
const bot2Info = new PlayerInfo("Bot2", PlayerType.Bot, null, "bot2_id");
game.addPlayer(bot2Info);
const bot2 = game.player("bot2_id");
// Verify bots are on Bot team
expect(bot1.team()).toBe(ColoredTeams.Bot);
expect(bot2.team()).toBe(ColoredTeams.Bot);
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign 96% of land to bot team (above 95% Team mode threshold)
const totalLand = game.numLandTiles();
const botTeamTiles = Math.ceil(totalLand * 0.96);
let bot1Assigned = 0;
let bot2Assigned = 0;
game.map().forEachTile((tile) => {
if (!game.map().isLand(tile)) return;
const totalAssigned = bot1Assigned + bot2Assigned;
if (totalAssigned >= botTeamTiles) return;
// Alternate between bots
if (bot1Assigned <= bot2Assigned) {
bot1.conquer(tile);
bot1Assigned++;
} else {
bot2.conquer(tile);
bot2Assigned++;
}
});
// Verify territory ownership (bot team has > 95%)
const botTeamTotal = bot1.numTilesOwned() + bot2.numTilesOwned();
expect(botTeamTotal / totalLand).toBeGreaterThan(0.95);
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerTeam();
// Verify no winner declared (bot teams excluded)
expect(setWinnerSpy).not.toHaveBeenCalled();
expect(winCheck.isActive()).toBe(true);
});
});
describe("WinCheckExecution - 1v1 Ranked Mode", () => {
test("should set winner when only one human remains connected", async () => {
// Setup game with 1v1 ranked mode and two human players
const game = await setup(
"big_plains",
{
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
rankedType: RankedType.OneVOne,
},
[
playerInfo("Player1", PlayerType.Human),
playerInfo("Player2", PlayerType.Human),
],
);
const human1 = game.player("Player1");
const human2 = game.player("Player2");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign some territory to both players
let human1Count = 0;
let human2Count = 0;
game.map().forEachTile((tile) => {
if (!game.map().isLand(tile)) return;
if (human1Count < 10) {
human1.conquer(tile);
human1Count++;
} else if (human2Count < 10) {
human2.conquer(tile);
human2Count++;
}
});
// Mark player 2 as disconnected
human2.markDisconnected(true);
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerFFA();
// Verify the remaining connected human is declared winner
expect(setWinnerSpy).toHaveBeenCalledWith(human1, expect.anything());
expect(winCheck.isActive()).toBe(false);
});
test("should not set winner when multiple humans are still connected", async () => {
// Setup game with 1v1 ranked mode and two human players
const game = await setup(
"big_plains",
{
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
rankedType: RankedType.OneVOne,
},
[
playerInfo("Player1", PlayerType.Human),
playerInfo("Player2", PlayerType.Human),
],
);
const human1 = game.player("Player1");
const human2 = game.player("Player2");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign territory to both players
let human1Count = 0;
let human2Count = 0;
game.map().forEachTile((tile) => {
if (!game.map().isLand(tile)) return;
if (human1Count < 10) {
human1.conquer(tile);
human1Count++;
} else if (human2Count < 10) {
human2.conquer(tile);
human2Count++;
}
});
// Both players remain connected
expect(human1.isDisconnected()).toBe(false);
expect(human2.isDisconnected()).toBe(false);
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerFFA();
// Verify no winner declared yet (both players still connected)
expect(setWinnerSpy).not.toHaveBeenCalled();
expect(winCheck.isActive()).toBe(true);
});
test("should not set winner when no humans remain connected", async () => {
// Setup game with 1v1 ranked mode and two human players
const game = await setup(
"big_plains",
{
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
rankedType: RankedType.OneVOne,
},
[
playerInfo("Player1", PlayerType.Human),
playerInfo("Player2", PlayerType.Human),
],
);
const human1 = game.player("Player1");
const human2 = game.player("Player2");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Both players disconnect
human1.markDisconnected(true);
human2.markDisconnected(true);
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerFFA();
// Verify no winner declared (no connected humans)
expect(setWinnerSpy).not.toHaveBeenCalled();
expect(winCheck.isActive()).toBe(true);
});
test("should ignore bots and nations in 1v1 ranked mode", async () => {
// Setup game with 1v1 ranked mode, one human, one bot, and one nation
const game = await setup(
"big_plains",
{
infiniteGold: true,
gameMode: GameMode.FFA,
instantBuild: true,
rankedType: RankedType.OneVOne,
},
[
playerInfo("HumanPlayer", PlayerType.Human),
playerInfo("BotPlayer", PlayerType.Bot),
playerInfo("NationPlayer", PlayerType.Nation),
],
);
const human = game.player("HumanPlayer");
const bot = game.player("BotPlayer");
const nation = game.player("NationPlayer");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign territory to all players
let humanCount = 0;
let botCount = 0;
let nationCount = 0;
game.map().forEachTile((tile) => {
if (!game.map().isLand(tile)) return;
if (humanCount < 10) {
human.conquer(tile);
humanCount++;
} else if (botCount < 10) {
bot.conquer(tile);
botCount++;
} else if (nationCount < 10) {
nation.conquer(tile);
nationCount++;
}
});
// Mock setWinner to capture calls
const setWinnerSpy = vi.fn();
game.setWinner = setWinnerSpy;
// Initialize and run win check
const winCheck = new WinCheckExecution();
winCheck.init(game, 0);
winCheck.checkWinnerFFA();
// Verify human is declared winner (only one human player)
expect(setWinnerSpy).toHaveBeenCalledWith(human, expect.anything());
expect(winCheck.isActive()).toBe(false);
});
});