Files
OpenFrontIO/src/core/GameRunner.ts
T
2025-02-01 12:05:11 -08:00

206 lines
7.3 KiB
TypeScript

import { utcDay } from "d3";
import { placeName } from "../client/graphics/NameBoxCalculator";
import { getConfig } from "./configuration/Config";
import { EventBus } from "./EventBus";
import { Executor } from "./execution/ExecutionManager";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import { Cell, DisplayMessageUpdate, Game, GameUpdateType, MessageType, MutableGame, NameViewData, Player, PlayerActions, PlayerID, PlayerProfile, PlayerType, UnitType } from "./game/Game";
import { createGame } from "./game/GameImpl";
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
import { GameConfig, Turn } from "./Schemas";
import { GameUpdateViewData } from "./GameView";
import { andFN, manhattanDistFN, TileRef } from "./game/GameMap";
import { targetTransportTile } from "./Util";
export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise<GameRunner> {
const config = getConfig(gameConfig)
const gameMap = await loadGameMap(gameConfig.gameMap);
const game = createGame(gameMap.gameMap, gameMap.miniGameMap, gameMap.nationMap, config)
const gr = new GameRunner(game as MutableGame, new Executor(game, gameID), callBack)
gr.init()
return gr
}
export class GameRunner {
private tickInterval = null
private turns: Turn[] = []
private currTurn = 0
private isExecuting = false
private playerViewData: Record<PlayerID, NameViewData> = {}
constructor(
public game: MutableGame,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData) => void
) {
}
init() {
this.game.addExecution(...this.execManager.spawnBots(this.game.config().numBots()))
if (this.game.config().spawnNPCs()) {
this.game.addExecution(...this.execManager.fakeHumanExecutions())
}
this.game.addExecution(new WinCheckExecution())
this.tickInterval = setInterval(() => this.executeNextTick(), 10)
}
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++
const updates = this.game.executeNextTick()
if (this.game.inSpawnPhase() && this.game.ticks() % 2 == 0) {
this.game.players()
.filter(p => p.type() == PlayerType.Human || p.type() == PlayerType.FakeHuman)
.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 packedTileUpdates = updates[GameUpdateType.Tile].map(u => u.update)
updates[GameUpdateType.Tile] = []
this.callBack({
tick: this.game.ticks(),
packedTileUpdates: new BigUint64Array(packedTileUpdates),
updates: updates,
playerNameViewData: this.playerViewData
})
this.isExecuting = false
}
public playerActions(playerID: PlayerID, x: number, y: number): PlayerActions {
const player = this.game.player(playerID)
const tile = this.game.ref(x, y)
const actions = {
canBoat: this.canBoat(player, tile),
canAttack: this.canAttack(player, tile),
buildableUnits: Object.values(UnitType).filter(ut => player.canBuild(ut, tile) != false)
} as PlayerActions
if (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.recentOrPendingAllianceRequestWith(other),
canBreakAlliance: player.isAlliedWith(other),
canDonate: player.canDonate(other)
}
}
return actions
}
public playerProfile(playerID: number): PlayerProfile {
const player = this.game.players().filter(p => p.smallID() == playerID)[0];
if (!player) {
throw new Error(`player with id ${playerID} not found`);
}
return {
relations: Object.fromEntries(
player.allRelationsSorted()
.map(({ player, relation }) => [player.smallID(), relation])
)
};
}
private canBoat(myPlayer: Player, tile: TileRef): boolean {
const other = this.game.owner(tile)
if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) {
return false
}
let myPlayerBordersOcean = false
for (const bt of myPlayer.borderTiles()) {
if (this.game.isOceanShore(bt)) {
myPlayerBordersOcean = true
break
}
}
let otherPlayerBordersOcean = false
if (!this.game.hasOwner(tile)) {
otherPlayerBordersOcean = true
} else {
for (const bt of (other as Player).borderTiles()) {
if (this.game.isOceanShore(bt)) {
otherPlayerBordersOcean = true
break
}
}
}
if (other.isPlayer() && myPlayer.allianceWith(other)) {
return false
}
let nearOcean = false
for (const t of this.game.bfs(tile, andFN((gm, t) => gm.ownerID(t) == gm.ownerID(tile) && gm.isLand(t), manhattanDistFN(tile, 25)))) {
if (this.game.isOceanShore(t)) {
nearOcean = true
break
}
}
if (!nearOcean) {
return false
}
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
const dst = targetTransportTile(this.game, tile)
if (dst != null) {
if (myPlayer.canBuild(UnitType.TransportShip, dst)) {
return true
}
}
}
}
private canAttack(myPlayer: Player, tile: TileRef): boolean {
if (this.game.owner(tile) == myPlayer) {
return false
}
// TODO: fix event bus
if (this.game.hasOwner(tile) && myPlayer.isAlliedWith(this.game.owner(tile) as Player)) {
// this.eventBus.emit(new DisplayMessageEvent("Cannot attack ally", MessageType.WARN))
return false
}
if (!this.game.isLand(tile)) {
return false
}
if (this.game.hasOwner(tile)) {
return myPlayer.sharesBorderWith(this.game.owner(tile))
} else {
for (const t of this.game.bfs(tile, andFN((gm, t) => !gm.hasOwner(t) && gm.isLand(t), manhattanDistFN(tile, 200)))) {
for (const n of this.game.neighbors(t)) {
if (this.game.owner(n) == myPlayer) {
return true
}
}
}
return false
}
}
}