mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 13:52:45 +00:00
206 lines
7.3 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|