mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:10:43 +00:00
42c944c9cc
## 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
586 lines
16 KiB
TypeScript
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);
|
|
});
|
|
});
|