import { placeName } from "../client/graphics/NameBoxCalculator"; import { getConfig } from "./configuration/ConfigLoader"; import { Executor } from "./execution/ExecutionManager"; import { WinCheckExecution } from "./execution/WinCheckExecution"; import { AllPlayers, Attack, Cell, Game, GameUpdates, NameViewData, Player, PlayerActions, PlayerBorderTiles, PlayerID, PlayerInfo, PlayerProfile, PlayerType, } from "./game/Game"; import { createGame } from "./game/GameImpl"; import { TileRef } from "./game/GameMap"; import { GameMapLoader } from "./game/GameMapLoader"; import { ErrorUpdate, GameUpdateType, GameUpdateViewData, } from "./game/GameUpdates"; import { createNationsForGame } from "./game/NationCreation"; import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader"; import { PseudoRandom } from "./PseudoRandom"; import { ClientID, GameStartInfo, Turn } from "./Schemas"; import { simpleHash } from "./Util"; import { censorNameWithClanTag } from "./validations/username"; export async function createGameRunner( gameStart: GameStartInfo, clientID: ClientID, mapLoader: GameMapLoader, callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, ): Promise { const config = await getConfig(gameStart.config, null); const gameMap = await loadGameMap( gameStart.config.gameMap, gameStart.config.gameMapSize, mapLoader, ); const random = new PseudoRandom(simpleHash(gameStart.gameID)); const humans = gameStart.players.map((p) => { return new PlayerInfo( p.clientID === clientID ? p.username : censorNameWithClanTag(p.username), PlayerType.Human, p.clientID, random.nextID(), p.isLobbyCreator ?? false, ); }); const nations = createNationsForGame( gameStart, gameMap.nations, humans.length, random, ); const game: Game = createGame( humans, nations, gameMap.gameMap, gameMap.miniGameMap, config, ); const gr = new GameRunner( game, new Executor(game, gameStart.gameID, clientID), callBack, ); gr.init(); return gr; } export class GameRunner { private turns: Turn[] = []; private currTurn = 0; private isExecuting = false; private playerViewData: Record = {}; public tileUpdateSink?: (tile: TileRef) => void; constructor( public game: Game, private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, ) {} init() { if (this.game.config().isRandomSpawn()) { this.game.addExecution(...this.execManager.spawnPlayers()); } if (this.game.config().bots() > 0) { this.game.addExecution( ...this.execManager.spawnBots(this.game.config().numBots()), ); } if (this.game.config().spawnNations()) { this.game.addExecution(...this.execManager.nationExecutions()); } this.game.addExecution(new WinCheckExecution()); } public addTurn(turn: Turn): void { this.turns.push(turn); } public executeNextTick() { if (this.isExecuting) { return; } if (this.currTurn >= this.turns.length) { return; } this.isExecuting = true; this.game.addExecution( ...this.execManager.createExecs(this.turns[this.currTurn]), ); this.currTurn++; let updates: GameUpdates; let tickExecutionDuration: number = 0; try { const startTime = performance.now(); updates = this.game.executeNextTick(); const endTime = performance.now(); tickExecutionDuration = endTime - startTime; } catch (error: unknown) { if (error instanceof Error) { console.error("Game tick error:", error.message); this.callBack({ errMsg: error.message, stack: error.stack, } as ErrorUpdate); } else { console.error("Game tick error:", error); } return; } if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) { this.game .players() .filter( (p) => p.type() === PlayerType.Human || p.type() === PlayerType.Nation, ) .forEach( (p) => (this.playerViewData[p.id()] = placeName(this.game, p)), ); } if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) { this.game.players().forEach((p) => { this.playerViewData[p.id()] = placeName(this.game, p); }); } // Many tiles are updated to pack it into an array const tileUpdates = updates[GameUpdateType.Tile]; let packedTileUpdates: BigUint64Array; if (this.tileUpdateSink) { for (const u of tileUpdates) { // packed tile updates encode [tileRef << 16 | state] as bigint. const tileRef = Number(u.update >> 16n) as TileRef; this.tileUpdateSink(tileRef); } packedTileUpdates = new BigUint64Array(0); } else { packedTileUpdates = new BigUint64Array(tileUpdates.map((u) => u.update)); } updates[GameUpdateType.Tile] = []; this.callBack({ tick: this.game.ticks(), packedTileUpdates, updates: updates, playerNameViewData: this.playerViewData, tickExecutionDuration: tickExecutionDuration, }); this.isExecuting = false; } public playerActions( playerID: PlayerID, x?: number, y?: number, ): PlayerActions { const player = this.game.player(playerID); const tile = x !== undefined && y !== undefined ? this.game.ref(x, y) : null; const actions = { canAttack: tile !== null && player.canAttack(tile), buildableUnits: player.buildableUnits(tile), canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers), canEmbargoAll: player.canEmbargoAll(), } as PlayerActions; if (tile !== null && this.game.hasOwner(tile)) { const other = this.game.owner(tile) as Player; actions.interaction = { sharedBorder: player.sharesBorderWith(other), canSendEmoji: player.canSendEmoji(other), canTarget: player.canTarget(other), canSendAllianceRequest: player.canSendAllianceRequest(other), canBreakAlliance: player.isAlliedWith(other), canDonateGold: player.canDonateGold(other), canDonateTroops: player.canDonateTroops(other), canEmbargo: !player.hasEmbargoAgainst(other), }; const alliance = player.allianceWith(other as Player); if (alliance) { actions.interaction.allianceExpiresAt = alliance.expiresAt(); } } return actions; } public playerProfile(playerID: number): PlayerProfile { const player = this.game.playerBySmallID(playerID); if (!player.isPlayer()) { throw new Error(`player with id ${playerID} not found`); } return player.playerProfile(); } public playerBorderTiles(playerID: PlayerID): PlayerBorderTiles { const player = this.game.player(playerID); if (!player.isPlayer()) { throw new Error(`player with id ${playerID} not found`); } return { borderTiles: player.borderTiles(), } as PlayerBorderTiles; } public attackAveragePosition( playerID: number, attackID: string, ): Cell | null { const player = this.game.playerBySmallID(playerID); if (!player.isPlayer()) { throw new Error(`player with id ${playerID} not found`); } const condition = (a: Attack) => a.id() === attackID; const attack = player.outgoingAttacks().find(condition) ?? player.incomingAttacks().find(condition); if (attack === undefined) { return null; } return attack.averagePosition(); } public bestTransportShipSpawn( playerID: PlayerID, targetTile: TileRef, ): TileRef | false { const player = this.game.player(playerID); if (!player.isPlayer()) { throw new Error(`player with id ${playerID} not found`); } return player.bestTransportShipSpawn(targetTile); } }