Skip to content

Commit 7903f9a

Browse files
committed
fix
1 parent 5955129 commit 7903f9a

22 files changed

Lines changed: 224 additions & 30 deletions

src/core/execution/SpawnExecution.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,30 @@ export class SpawnExecution implements Execution {
3737
tick(ticks: number) {
3838
this.active = false;
3939

40-
const isSingleplayerNationSpawn =
41-
this.playerInfo.playerType === PlayerType.Nation &&
42-
this.mg.config().gameConfig().gameType === GameType.Singleplayer;
43-
44-
if (!this.mg.inSpawnPhase() && !isSingleplayerNationSpawn) {
45-
this.active = false;
46-
return;
47-
}
48-
4940
let player: Player | null = null;
5041
if (this.mg.hasPlayer(this.playerInfo.id)) {
5142
player = this.mg.player(this.playerInfo.id);
5243
} else {
5344
player = this.mg.addPlayer(this.playerInfo);
5445
}
5546

47+
const isSingleplayer =
48+
this.mg.config().gameConfig().gameType === GameType.Singleplayer;
49+
const isSingleplayerNationSpawn =
50+
this.playerInfo.playerType === PlayerType.Nation && isSingleplayer;
51+
const isSingleplayerInitialHumanSpawn =
52+
isSingleplayer &&
53+
this.playerInfo.playerType === PlayerType.Human &&
54+
!player.hasSpawned();
55+
56+
if (
57+
!this.mg.inSpawnPhase() &&
58+
!isSingleplayerNationSpawn &&
59+
!isSingleplayerInitialHumanSpawn
60+
) {
61+
return;
62+
}
63+
5664
// Security: If random spawn is enabled, prevent players from re-rolling their spawn location
5765
if (this.mg.config().isRandomSpawn() && player.hasSpawned()) {
5866
return;

src/core/execution/TransportShipExecution.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,14 @@ export class TransportShipExecution implements Execution {
156156
}
157157

158158
if (this.boat.retreating()) {
159-
// Ensure retreat source is still valid for (new) owner
160-
if (this.mg.owner(this.src!) !== this.attacker) {
161-
// Use bestTransportShipSpawn, not canBuild because of its max boats check etc
162-
const newSrc = this.attacker.bestTransportShipSpawn(this.dst);
163-
if (newSrc === false) {
164-
this.src = null;
165-
} else {
166-
this.src = newSrc;
167-
}
159+
// Recompute retreat source for current owner each tick. This guarantees
160+
// we don't retreat to a stale source when ownership/shore availability changes.
161+
// Use bestTransportShipSpawn, not canBuild because of its max boats check etc.
162+
const newSrc = this.attacker.bestTransportShipSpawn(this.dst);
163+
if (newSrc === false) {
164+
this.src = null;
165+
} else {
166+
this.src = newSrc;
168167
}
169168

170169
if (this.src === null) {

src/core/game/GameImpl.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export type CellString = string;
6666

6767
export class GameImpl implements Game {
6868
private _ticks = 0;
69+
private singleplayerStartTick: number | null = null;
70+
private singleplayerSpawnPhaseCompleted = false;
71+
private spawnPhaseLockedValue: boolean | null = null;
6972

7073
private unInitExecs: Execution[] = [];
7174

@@ -376,19 +379,63 @@ export class GameImpl implements Game {
376379
this.addUpdate({ type: GameUpdateType.GamePaused, paused });
377380
}
378381

379-
inSpawnPhase(): boolean {
382+
private computeInSpawnPhase(): boolean {
380383
if (this.config().gameConfig().gameType === GameType.Singleplayer) {
381-
// Singleplayer has no fixed spawn countdown; game starts once the human spawns.
382-
return !this._humans.every((human) => this.player(human.id).hasSpawned());
384+
if (this.singleplayerSpawnPhaseCompleted) {
385+
return false;
386+
}
387+
// Singleplayer has no fixed spawn countdown; game starts once all current human
388+
// players in the game have spawned. This supports tests that add players after
389+
// game construction.
390+
const humanPlayers = this.allPlayers().filter(
391+
(player) => player.type() === PlayerType.Human,
392+
);
393+
if (humanPlayers.length > 0) {
394+
return humanPlayers.some((player) => !player.hasSpawned());
395+
}
383396
}
384397
return this._ticks <= this.config().numSpawnPhaseTurns();
385398
}
386399

400+
inSpawnPhase(): boolean {
401+
if (this.spawnPhaseLockedValue !== null) {
402+
return this.spawnPhaseLockedValue;
403+
}
404+
return this.computeInSpawnPhase();
405+
}
406+
407+
private updateSingleplayerStartTick(): void {
408+
if (this.config().gameConfig().gameType !== GameType.Singleplayer) {
409+
return;
410+
}
411+
412+
const humanPlayers = this.allPlayers().filter(
413+
(player) => player.type() === PlayerType.Human,
414+
);
415+
const hasSpawnedHuman = humanPlayers.some((player) => player.hasSpawned());
416+
417+
if (this.inSpawnPhase() || !hasSpawnedHuman) {
418+
this.singleplayerStartTick = null;
419+
return;
420+
}
421+
422+
this.singleplayerStartTick ??= this.ticks();
423+
}
424+
387425
ticks(): number {
388426
return this._ticks;
389427
}
390428

391429
executeNextTick(): GameUpdates {
430+
this.spawnPhaseLockedValue = this.computeInSpawnPhase();
431+
if (
432+
this.config().gameConfig().gameType === GameType.Singleplayer &&
433+
!this.spawnPhaseLockedValue
434+
) {
435+
this.singleplayerSpawnPhaseCompleted = true;
436+
}
437+
this.updateSingleplayerStartTick();
438+
392439
this.updates = createGameUpdatesMap();
393440
this.execs.forEach((e) => {
394441
if (
@@ -424,6 +471,10 @@ export class GameImpl implements Game {
424471
hash: this.hash(),
425472
});
426473
}
474+
475+
this.spawnPhaseLockedValue = null;
476+
this.updateSingleplayerStartTick();
477+
427478
this._ticks++;
428479
return this.updates;
429480
}
@@ -731,6 +782,15 @@ export class GameImpl implements Game {
731782
}
732783

733784
public isSpawnImmunityActive(): boolean {
785+
if (this.config().gameConfig().gameType === GameType.Singleplayer) {
786+
this.updateSingleplayerStartTick();
787+
if (this.inSpawnPhase()) {
788+
return true;
789+
}
790+
const startTick = this.singleplayerStartTick ?? this.ticks();
791+
return startTick + this.config().spawnImmunityDuration() > this.ticks();
792+
}
793+
734794
return (
735795
this.config().numSpawnPhaseTurns() +
736796
this.config().spawnImmunityDuration() >

src/core/game/GameView.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ export class PlayerView {
584584

585585
export class GameView implements GameMap {
586586
private lastUpdate: GameUpdateViewData | null;
587+
private singleplayerStartTick: Tick | null = null;
587588
private smallIDToID = new Map<number, PlayerID>();
588589
private _players = new Map<PlayerID, PlayerView>();
589590
private _units = new Map<number, UnitView>();
@@ -818,7 +819,35 @@ export class GameView implements GameMap {
818819
}
819820
return this.ticks() <= this._config.numSpawnPhaseTurns();
820821
}
822+
823+
private updateSingleplayerStartTick(): void {
824+
if (this._config.gameConfig().gameType !== GameType.Singleplayer) {
825+
return;
826+
}
827+
828+
const humanPlayers = this.players().filter(
829+
(player) => player.type() === PlayerType.Human,
830+
);
831+
const hasSpawnedHuman = humanPlayers.some((player) => player.hasSpawned());
832+
833+
if (this.inSpawnPhase() || !hasSpawnedHuman) {
834+
this.singleplayerStartTick = null;
835+
return;
836+
}
837+
838+
this.singleplayerStartTick ??= this.ticks();
839+
}
840+
821841
isSpawnImmunityActive(): boolean {
842+
if (this._config.gameConfig().gameType === GameType.Singleplayer) {
843+
this.updateSingleplayerStartTick();
844+
if (this.inSpawnPhase()) {
845+
return true;
846+
}
847+
const startTick = this.singleplayerStartTick ?? this.ticks();
848+
return startTick + this._config.spawnImmunityDuration() > this.ticks();
849+
}
850+
822851
return (
823852
this._config.numSpawnPhaseTurns() + this._config.spawnImmunityDuration() >
824853
this.ticks()

tests/AiAttackBehavior.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { AiAttackBehavior } from "../src/core/execution/utils/AiAttackBehavior";
2-
import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game";
2+
import {
3+
Game,
4+
GameType,
5+
Player,
6+
PlayerInfo,
7+
PlayerType,
8+
} from "../src/core/game/Game";
39
import { PseudoRandom } from "../src/core/PseudoRandom";
410
import { setup } from "./util/Setup";
511

@@ -12,6 +18,7 @@ describe("Ai Attack Behavior", () => {
1218
// Helper function for basic test setup
1319
async function setupTestEnvironment() {
1420
const testGame = await setup("big_plains", {
21+
gameType: GameType.Public,
1522
infiniteGold: true,
1623
instantBuild: true,
1724
infiniteTroops: true,

tests/AllianceAcceptNukes.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { GameUpdateType } from "src/core/game/GameUpdates";
33
import { NukeExecution } from "../src/core/execution/NukeExecution";
44
import {
55
Game,
6+
GameType,
67
Player,
78
PlayerInfo,
89
PlayerType,
@@ -21,6 +22,7 @@ describe("Alliance acceptance immediately destroys in-flight nukes", () => {
2122
game = await setup(
2223
"plains",
2324
{
25+
gameType: GameType.Public,
2426
infiniteGold: true,
2527
instantBuild: true,
2628
infiniteTroops: true,

tests/AllianceDonation.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AllianceRequestExecution } from "../src/core/execution/alliance/AllianceRequestExecution";
22
import { AllianceRequestReplyExecution } from "../src/core/execution/alliance/AllianceRequestReplyExecution";
33
import { DonateGoldExecution } from "../src/core/execution/DonateGoldExecution";
4-
import { Game, Player, PlayerType } from "../src/core/game/Game";
4+
import { Game, GameType, Player, PlayerType } from "../src/core/game/Game";
55
import { playerInfo, setup } from "./util/Setup";
66

77
let game: Game;
@@ -13,6 +13,7 @@ describe("Alliance Donation", () => {
1313
game = await setup(
1414
"plains",
1515
{
16+
gameType: GameType.Public,
1617
infiniteGold: false,
1718
instantBuild: true,
1819
infiniteTroops: false,

tests/AllianceExtensionExecution.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { AllianceExtensionExecution } from "../src/core/execution/alliance/AllianceExtensionExecution";
22
import { AllianceRequestExecution } from "../src/core/execution/alliance/AllianceRequestExecution";
33
import { AllianceRequestReplyExecution } from "../src/core/execution/alliance/AllianceRequestReplyExecution";
4-
import { Game, MessageType, Player, PlayerType } from "../src/core/game/Game";
4+
import {
5+
Game,
6+
GameType,
7+
MessageType,
8+
Player,
9+
PlayerType,
10+
} from "../src/core/game/Game";
511
import { playerInfo, setup } from "./util/Setup";
612

713
let game: Game;
@@ -14,6 +20,7 @@ describe("AllianceExtensionExecution", () => {
1420
game = await setup(
1521
"ocean_and_land",
1622
{
23+
gameType: GameType.Public,
1724
infiniteGold: true,
1825
instantBuild: true,
1926
infiniteTroops: true,

tests/AllianceRequestExecution.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { AllianceRequestExecution } from "../src/core/execution/alliance/AllianceRequestExecution";
22
import { AllianceRequestReplyExecution } from "../src/core/execution/alliance/AllianceRequestReplyExecution";
33
import { NukeExecution } from "../src/core/execution/NukeExecution";
4-
import { Game, Player, PlayerType, UnitType } from "../src/core/game/Game";
4+
import {
5+
Game,
6+
GameType,
7+
Player,
8+
PlayerType,
9+
UnitType,
10+
} from "../src/core/game/Game";
511
import { playerInfo, setup } from "./util/Setup";
612
import { constructionExecution } from "./util/utils";
713

@@ -14,6 +20,7 @@ describe("AllianceRequestExecution", () => {
1420
game = await setup(
1521
"plains",
1622
{
23+
gameType: GameType.Public,
1724
infiniteGold: true,
1825
instantBuild: true,
1926
infiniteTroops: true,

tests/NationCounterWarshipInfestation.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Cell,
44
Difficulty,
55
GameMode,
6+
GameType,
67
Nation,
78
PlayerInfo,
89
PlayerType,
@@ -18,6 +19,7 @@ import { setup } from "./util/Setup";
1819
describe("Counter Warship Infestation", () => {
1920
test("rich nation sends counter-warship in FFA when enemy has too many warships", async () => {
2021
const game = await setup("half_land_half_ocean", {
22+
gameType: GameType.Public,
2123
infiniteGold: true,
2224
instantBuild: true,
2325
difficulty: Difficulty.Hard, // Required for counter-warship logic
@@ -168,6 +170,7 @@ describe("Counter Warship Infestation", () => {
168170
const game = await setup(
169171
"half_land_half_ocean",
170172
{
173+
gameType: GameType.Public,
171174
infiniteGold: true,
172175
instantBuild: true,
173176
difficulty: Difficulty.Hard, // Required for counter-warship logic

0 commit comments

Comments
 (0)