diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index abe0f0018c..fd329e6587 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -5,6 +5,7 @@ import { Game, GameMapType, GameMode, + GameType, Gold, Player, PlayerInfo, @@ -28,6 +29,7 @@ export enum GameEnv { export interface ServerConfig { turnIntervalMs(): number; + startDelay(gameType: GameType): number; gameCreationRate(): number; lobbyMaxPlayers( map: GameMapType, @@ -72,6 +74,7 @@ export interface NukeMagnitude { } export interface Config { + turnIntervalMs(): number; samHittingChance(): number; samWarheadHittingChance(): number; spawnImmunityDuration(): Tick; @@ -90,6 +93,12 @@ export interface Config { instantBuild(): boolean; isRandomSpawn(): boolean; numSpawnPhaseTurns(): number; + numGracePeriodTurns(): number; + isSpawnPhase( + ticks: number, + gameType: GameType, + firstHumanSpawnTick?: number, + ): boolean; userSettings(): UserSettings; playerTeams(): TeamCountConfig; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 4a215da49b..c6c830f30f 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -175,6 +175,10 @@ export abstract class DefaultServerConfig implements ServerConfig { turnIntervalMs(): number { return 100; } + // Start game immediately for single player games, with delay for multiplayer games + startDelay(gameType: GameType): number { + return gameType === GameType.Singleplayer ? 0 : 2000; + } gameCreationRate(): number { return 60 * 1000; } @@ -273,6 +277,10 @@ export class DefaultConfig implements Config { return this._serverConfig; } + turnIntervalMs(): number { + return this._serverConfig.turnIntervalMs(); + } + userSettings(): UserSettings { if (this._userSettings === null) { throw new Error("userSettings is null"); @@ -625,9 +633,31 @@ export class DefaultConfig implements Config { boatMaxNumber(): number { return 3; } + numSpawnPhaseTurns(): number { return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300; } + // Amount of ticks for player to change spawn placement in singleplayer + numGracePeriodTurns(): number { + return 15; + } + isSpawnPhase( + ticks: number, + gameType: GameType, + firstHumanSpawnTick?: number, + ): boolean { + if (ticks > this.numSpawnPhaseTurns()) { + return false; + } + if (gameType !== GameType.Singleplayer) { + return true; + } + if (!firstHumanSpawnTick) { + return true; + } + return ticks <= firstHumanSpawnTick + this.numGracePeriodTurns(); + } + numBots(): number { return this.bots(); } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 10805d4b17..67787f7b7b 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -149,7 +149,8 @@ export class FakeHumanExecution implements Execution { this.trackTradeShipsAndRetaliate(); } - if (ticks % this.attackRate !== this.attackTick) { + // Tries to spawn nations on the first tick of the match + if (ticks > 1 && ticks % this.attackRate !== this.attackTick) { return; } diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 57baff6eeb..03c699bc38 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -37,18 +37,21 @@ export class SpawnExecution implements Execution { player = this.mg.addPlayer(this.playerInfo); } - player.tiles().forEach((t) => player.relinquish(t)); - getSpawnTiles(this.mg, this.tile).forEach((t) => { - player.conquer(t); - }); - - if (!player.hasSpawned()) { - this.mg.addExecution(new PlayerExecution(player)); - if (player.type() === PlayerType.Bot) { - this.mg.addExecution(new BotExecution(player)); + if (player) { + player.tiles().forEach((t) => player.relinquish(t)); + getSpawnTiles(this.mg, this.tile).forEach((t) => { + player.conquer(t); + }); + + if (player && !player.hasSpawned()) { + this.mg.addExecution(new PlayerExecution(player)); + if (player.type() === PlayerType.Bot) { + this.mg.addExecution(new BotExecution(player)); + } + + player.setHasSpawned(true); } } - player.setHasSpawned(true); } isActive(): boolean { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 2c23ad8641..f3dc53e142 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -69,6 +69,8 @@ export class GameImpl implements Game { private _height: number; _terraNullius: TerraNulliusImpl; + private firstHumanSpawnTick: number; + allianceRequests: AllianceRequestImpl[] = []; alliances_: AllianceImpl[] = []; @@ -338,7 +340,17 @@ export class GameImpl implements Game { } inSpawnPhase(): boolean { - return this._ticks <= this.config().numSpawnPhaseTurns(); + if (!this.firstHumanSpawnTick) { + const humanSpawned = Array.from(this._players.values()).some( + (p) => p.type() === PlayerType.Human && p.hasSpawned(), + ); + if (humanSpawned) this.firstHumanSpawnTick = this._ticks; + } + return this._config.isSpawnPhase( + this.ticks(), + this.config().gameConfig().gameType, + this.firstHumanSpawnTick, + ); } ticks(): number { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index c525129d10..d3f77d8674 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -475,6 +475,8 @@ export class GameView implements GameMap { private _map: GameMap; + private firstHumanSpawnTick: number; + constructor( public worker: WorkerClient, private _config: Config, @@ -655,7 +657,17 @@ export class GameView implements GameMap { return this.lastUpdate.tick; } inSpawnPhase(): boolean { - return this.ticks() <= this._config.numSpawnPhaseTurns(); + if (!this.firstHumanSpawnTick) { + const humanSpawned = this.playerViews().some( + (p) => p.type() === PlayerType.Human && p.hasSpawned(), + ); + if (humanSpawned) this.firstHumanSpawnTick = this.ticks(); + } + return this.config().isSpawnPhase( + this.ticks(), + this.config().gameConfig().gameType, + this.firstHumanSpawnTick, + ); } config(): Config { return this._config; diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 0cd4420da7..2096fa3fa5 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -88,14 +88,15 @@ export class GameManager { if (!game.hasStarted()) { // Prestart tells clients to start loading the game. game.prestart(); - // Start game on delay to allow time for clients to connect. + // Start game immediately for single player games, with delay for multiplayer games + const startDelay = this.config.startDelay(game.gameConfig.gameType); setTimeout(() => { try { game.start(); } catch (error) { this.log.error(`error starting game ${id}: ${error}`); } - }, 2000); + }, startDelay); } } diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 963b134c13..1622df5412 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -59,7 +59,7 @@ export async function setup( gameMap: GameMapType.Asia, gameMapSize: GameMapSize.Normal, gameMode: GameMode.FFA, - gameType: GameType.Singleplayer, + gameType: GameType.Public, difficulty: Difficulty.Medium, disableNPCs: false, donateGold: false, diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index 6f20fa1cd6..d1a5f58748 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -1,6 +1,6 @@ import { JWK } from "jose"; import { GameEnv, ServerConfig } from "../../src/core/configuration/Config"; -import { GameMapType } from "../../src/core/game/Game"; +import { GameMapType, GameType } from "../../src/core/game/Game"; import { GameID } from "../../src/core/Schemas"; export class TestServerConfig implements ServerConfig { @@ -64,6 +64,9 @@ export class TestServerConfig implements ServerConfig { numWorkers(): number { throw new Error("Method not implemented."); } + startDelay(gameType: GameType): number { + throw new Error("Method not implemented."); + } workerIndex(gameID: GameID): number { throw new Error("Method not implemented."); }