feat: remove spawn timer on singleplayer (#3199)

Resolves #1041 

## Description:

Remove the singleplayer spawn countdown so the game starts when the
player spawns, spawn nations immediately after player spawn, and align
game timer/max-timer timing with the new start point.

Added a singleplayer regression test for spawn-immunity timing
(GameImpl.test.ts) and updated spawn-phase loop tests to use gameType:
GameType.Public where singleplayer behavior is not under test (e.g.
MIRV/AI/Spawn/WinCheck-related suites), eliminating inSpawnPhase()
timeout hangs after the new singleplayer start logic.


https://github.com/user-attachments/assets/c07a585f-1153-490e-88ca-a91fc7ae5756

## 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:
aotumuri
This commit is contained in:
Aotumuri
2026-05-11 12:44:44 -07:00
committed by GitHub
parent 2b04c568fc
commit f1d162825e
46 changed files with 333 additions and 429 deletions
+1 -8
View File
@@ -18,20 +18,13 @@ describe("MIRVExecution", () => {
beforeEach(async () => {
game = await setup(
"big_plains",
{
infiniteGold: true,
instantBuild: true,
},
{ infiniteGold: true, instantBuild: true },
[
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player = game.player("player_id");
otherPlayer = game.player("other_id");
@@ -17,20 +17,13 @@ describe("PlayerExecution Annexation Bug", () => {
beforeEach(async () => {
game = await setup(
"big_plains",
{
infiniteGold: true,
instantBuild: true,
},
{ infiniteGold: true, instantBuild: true },
[
new PlayerInfo("large", PlayerType.Human, "client1", "large_id"),
new PlayerInfo("small", PlayerType.Human, "client2", "small_id"),
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
largePlayer = game.player("large_id");
smallPlayer = game.player("small_id");
+1 -8
View File
@@ -18,10 +18,7 @@ describe("NukeExecution", () => {
beforeEach(async () => {
game = await setup(
"big_plains",
{
infiniteGold: true,
instantBuild: true,
},
{ infiniteGold: true, instantBuild: true },
[
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
@@ -34,10 +31,6 @@ describe("NukeExecution", () => {
}));
(game.config() as TestConfig).nukeAllianceBreakThreshold = vi.fn(() => 5);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player = game.player("player_id");
otherPlayer = game.player("other_id");
@@ -17,20 +17,13 @@ describe("PlayerExecution", () => {
beforeEach(async () => {
game = await setup(
"big_plains",
{
infiniteGold: true,
instantBuild: true,
},
{ infiniteGold: true, instantBuild: true },
[
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player = game.player("player_id");
otherPlayer = game.player("other_id");
@@ -78,10 +78,6 @@ describe("SAM", () => {
),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
attacker = game.player("attacker_id");
defender = game.player("defender_id");
middle_defender = game.player("middle_defender_id");
@@ -65,12 +65,8 @@ describe("WinCheckExecution", () => {
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;
mg.endSpawnPhase();
const threshold = (mg.config().gameConfig().maxTimerValue ?? 0) * 600;
while (mg.ticks() < threshold) {
mg.executeNextTick();
}
@@ -109,9 +105,6 @@ describe("WinCheckExecution - Nation Winners", () => {
const nation = game.player("nation_id");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign 81% of land to Nation
const totalLand = game.numLandTiles();
@@ -171,10 +164,7 @@ describe("WinCheckExecution - Nation Winners", () => {
game.addPlayer(nationInfo);
const nation = game.player("nation_id");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.endSpawnPhase();
// Give Nation 60% territory (below 80% threshold)
// Give human 30% territory
@@ -200,9 +190,7 @@ describe("WinCheckExecution - Nation Winners", () => {
expect(nation.numTilesOwned()).toBeGreaterThan(human.numTilesOwned());
// Fast-forward game ticks past timer expiration
const threshold =
game.config().numSpawnPhaseTurns() +
(game.config().gameConfig().maxTimerValue ?? 0) * 600;
const threshold = (game.config().gameConfig().maxTimerValue ?? 0) * 600;
while (game.ticks() < threshold) {
game.executeNextTick();
}
@@ -258,9 +246,6 @@ describe("WinCheckExecution - Nation Winners", () => {
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();
@@ -327,9 +312,6 @@ describe("WinCheckExecution - Nation Winners", () => {
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();
@@ -392,9 +374,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
const human2 = game.player("Player2");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign some territory to both players
let human1Count = 0;
@@ -447,9 +426,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
const human2 = game.player("Player2");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign territory to both players
let human1Count = 0;
@@ -503,9 +479,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
const human2 = game.player("Player2");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Both players disconnect
human1.markDisconnected(true);
@@ -547,9 +520,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
const nation = game.player("NationPlayer");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Assign territory to all players
let humanCount = 0;