diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 31a9a194a..7b63d5544 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -4,8 +4,7 @@ import { EventBus } from "../core/EventBus"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler" import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas"; -import { loadTerrainFromFile, loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader"; -import { and, bfs, dist, generateID, manhattanDist } from "../core/Util"; +import { loadTerrainFromFile, loadTerrainMap } from "../core/game/TerrainMapLoader"; import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport"; import { createCanvas } from "./Utils"; import { MessageType } from '../core/game/Game'; @@ -72,10 +71,10 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise { const config = getConfig(gameConfig) - const terrainMap = await loadTerrainMap(gameConfig.gameMap); + const gameMap = await loadTerrainMap(gameConfig.gameMap); const worker = new WorkerClient(lobbyConfig.gameID, gameConfig) await worker.initialize() - const gameView = new GameView(worker, config, terrainMap.terrain) + const gameView = new GameView(worker, config, gameMap.gameMap) consolex.log('going to init path finder') @@ -177,12 +176,12 @@ export class ClientGameRunner { return } const cell = this.renderer.transformHandler.screenToWorldCoordinates(event.x, event.y) - if (!this.gameView.isOnMap(cell)) { + if (!this.gameView.isValidCoord(cell.x, cell.y)) { return } consolex.log(`clicked cell ${cell}`) - const tile = this.gameView.tile(cell) - if (tile.terrain().isLand() && !tile.hasOwner() && this.gameView.inSpawnPhase()) { + const tile = this.gameView.ref(cell.x, cell.y) + if (this.gameView.isLand(tile) && !this.gameView.hasOwner(tile) && this.gameView.inSpawnPhase()) { this.eventBus.emit(new SendSpawnIntentEvent(cell)) return } @@ -200,7 +199,7 @@ export class ClientGameRunner { if (actions.canAttack) { this.eventBus.emit( new SendAttackIntentEvent( - tile.owner().id(), + this.gameView.owner(tile).id(), this.myPlayer.troops() * this.renderer.uiState.attackRatio ) ) diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 461f77d87..3bf4cca43 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,7 +1,7 @@ import { Config, ServerConfig } from "../core/configuration/Config" import { SendLogEvent } from "../core/Consolex" import { EventBus, GameEvent } from "../core/EventBus" -import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" +import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, UnitType } from "../core/game/Game" import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig, ClientLogMessageSchema } from "../core/Schemas" import { LobbyConfig } from "./ClientGameRunner" import { LocalServer } from "./LocalServer" @@ -298,10 +298,6 @@ export class Transport { attackerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, - sourceX: null, - sourceY: null, - targetX: null, - targetY: null, }) } diff --git a/src/client/graphics/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts index 73d1f8806..4e085486d 100644 --- a/src/client/graphics/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -1,5 +1,4 @@ -import { Game, Player, Tile, Cell, NameViewData } from '../../core/game/Game'; -import { GameView } from '../../core/GameView'; +import { Game, Player, Cell, NameViewData } from '../../core/game/Game'; import { calculateBoundingBox, within } from '../../core/Util'; export interface Point { @@ -17,12 +16,7 @@ export interface Rectangle { export function placeName(game: Game, player: Player): NameViewData { - return { - x: 0, - y: 0, - size: 0 - } - const boundingBox = calculateBoundingBox(player.borderTiles()); + const boundingBox = calculateBoundingBox(game, player.borderTiles()); const rawScalingFactor = (boundingBox.max.x - boundingBox.min.x) / 100 @@ -72,8 +66,8 @@ export function createGrid(game: Game, player: Player, boundingBox: { min: Point for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) { const cell = new Cell(x * scalingFactor, y * scalingFactor); if (game.isOnMap(cell)) { - const tile = game.tile(cell); - grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = tile.terrain().isLake() || tile.owner() === player; // TODO: okay if lake + const tile = game.ref(cell.x, cell.y); + grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = this.game.isLake(tile) || this.game.owner(tile) === player; // TODO: okay if lake } } } diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index ab6b98f20..d60017061 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -1,7 +1,7 @@ import { colord } from "colord"; import { EventBus } from "../../core/EventBus" import { Cell, Game, Player } from "../../core/game/Game"; -import { calculateBoundingBox, calculateBoundingBoxCenter, manhattanDist } from "../../core/Util"; +import { calculateBoundingBox, calculateBoundingBoxCenter } from "../../core/Util"; import { ZoomEvent, DragEvent } from "../InputHandler"; import { GoToPlayerEvent } from "./layers/Leaderboard"; import { placeName } from "./NameBoxCalculator"; @@ -131,7 +131,7 @@ export class TransformHandler { const { screenX, screenY } = this.screenCenter() const screenMapCenter = new Cell(screenX, screenY) - if (manhattanDist(screenMapCenter, this.target) < 2) { + if (this.game.manhattanDist(this.game.ref(screenX, screenY), this.game.ref(this.target.x, this.target.y)) < 2) { this.clearTarget() return } diff --git a/src/client/graphics/layers/Leaderboard.ts b/src/client/graphics/layers/Leaderboard.ts index cb319ebfe..cc02524ee 100644 --- a/src/client/graphics/layers/Leaderboard.ts +++ b/src/client/graphics/layers/Leaderboard.ts @@ -1,7 +1,6 @@ import { LitElement, html, css } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { Layer } from './Layer'; -import { Game, Player } from '../../../core/game/Game'; import { ClientID } from '../../../core/Schemas'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { EventBus, GameEvent } from '../../../core/EventBus'; @@ -59,7 +58,7 @@ export class Leaderboard extends LitElement implements Layer { .map((player, index) => ({ name: player.displayName(), position: index + 1, - score: formatPercentage(player.numTilesOwned() / this.game.terrainMap().numLandTiles()), + score: formatPercentage(player.numTilesOwned() / this.game.numLandTiles()), gold: renderNumber(player.gold()), isMyPlayer: player == myPlayer, player: player @@ -78,7 +77,7 @@ export class Leaderboard extends LitElement implements Layer { this.players.push({ name: myPlayer.displayName(), position: place, - score: formatPercentage(myPlayer.numTilesOwned() / this.game.terrainMap().numLandTiles()), + score: formatPercentage(myPlayer.numTilesOwned() / this.game.numLandTiles()), gold: renderNumber(myPlayer.gold()), isMyPlayer: true, player: myPlayer diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 25320adb0..36032a8b6 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -6,10 +6,25 @@ import { ClientID } from '../../../core/Schemas'; import { EventBus } from '../../../core/EventBus'; import { TransformHandler } from '../TransformHandler'; import { MouseMoveEvent } from '../../InputHandler'; -import { euclideanDist, distSortUnit } from '../../../core/Util'; -import { renderNumber, renderTroops } from '../../Utils'; -import { PauseGameEvent } from '../../Transport'; import { GameView, PlayerView } from '../../../core/GameView'; +import { TileRef } from '../../../core/game/GameMap'; +import { PauseGameEvent } from '../../Transport'; + +function euclideanDistWorld(coord: { x: number, y: number }, tileRef: TileRef, game: GameView): number { + const x = game.x(tileRef); + const y = game.y(tileRef); + const dx = coord.x - x; + const dy = coord.y - y; + return Math.sqrt(dx * dx + dy * dy); +} + +function distSortUnitWorld(coord: { x: number, y: number }, game: GameView) { + return (a: Unit, b: Unit) => { + const distA = euclideanDistWorld(coord, a.tile(), game); + const distB = euclideanDistWorld(coord, b.tile(), game); + return distA - distB; + }; +} @customElement('player-info-overlay') export class PlayerInfoOverlay extends LitElement implements Layer { @@ -29,13 +44,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer { private player: Player | null = null; @state() - private playerProfile: PlayerProfile | null = null + private playerProfile: PlayerProfile | null = null; @state() private unit: Unit | null = null; @state() - private showPauseButton: boolean = true + private showPauseButton: boolean = true; @state() private _isInfoVisible: boolean = false; @@ -43,39 +58,40 @@ export class PlayerInfoOverlay extends LitElement implements Layer { @state() private _isPaused: boolean = false; - private _isActive = false + private _isActive = false; init() { this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) => this.onMouseEvent(e)); - this._isActive = true - this.showPauseButton = this.game.config().gameConfig().gameType == GameType.Singleplayer + this._isActive = true; + this.showPauseButton = this.game.config().gameConfig().gameType == GameType.Singleplayer; } private onMouseEvent(event: MouseMoveEvent) { this.setVisible(false); this.unit = null; - const lastPlayer = this.player this.player = null; const worldCoord = this.transform.screenToWorldCoordinates(event.x, event.y); - if (!this.game.isOnMap(worldCoord)) { + if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) { return; } - const tile = this.game.tile(worldCoord); - const owner = tile.owner(); + const tile = this.game.ref(worldCoord.x, worldCoord.y); + if (!tile) return; - if (owner.isPlayer()) { + const owner = this.game.owner(tile); + + if (owner && owner.isPlayer()) { this.player = owner; (this.player as PlayerView).profile().then(p => { - console.log(`got profile ${JSON.stringify(p)}`) - this.playerProfile = p - }) + console.log(`got profile ${JSON.stringify(p)}`); + this.playerProfile = p; + }); this.setVisible(true); - } else if (!tile.terrain().isLand()) { + } else if (!this.game.isLand(tile)) { const units = this.game.units(UnitType.Destroyer, UnitType.Battleship, UnitType.TradeShip) - .filter(u => euclideanDist(worldCoord, u.tile().cell()) < 50) - .sort(distSortUnit(tile)); + .filter(u => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50) + .sort(distSortUnitWorld(worldCoord, this.game)); if (units.length > 0) { this.unit = units[0]; @@ -120,28 +136,28 @@ export class PlayerInfoOverlay extends LitElement implements Layer { private renderPlayerInfo(player: Player) { const myPlayer = this.myPlayer(); const isAlly = (myPlayer?.isAlliedWith(player) || player == this.myPlayer()) ?? false; - let relationHtml = null + let relationHtml = null; if (player.type() == PlayerType.FakeHuman && myPlayer != null) { - let classType = '' - let relationName = '' - const relation = this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral + let classType = ''; + let relationName = ''; + const relation = this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral; switch (relation) { case Relation.Hostile: - classType = 'hostile' - relationName = 'Hostile' - break + classType = 'hostile'; + relationName = 'Hostile'; + break; case Relation.Distrustful: - classType = 'distrustful' - relationName = 'Distrustful' - break + classType = 'distrustful'; + relationName = 'Distrustful'; + break; case Relation.Neutral: - classType = 'neutral' - relationName = 'Neutral' - break + classType = 'neutral'; + relationName = 'Neutral'; + break; case Relation.Friendly: - classType = 'friendly' - relationName = 'Friendly' - break + classType = 'friendly'; + relationName = 'Friendly'; + break; } relationHtml = html`
Attitude: ${relationName}
`; @@ -149,8 +165,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer { return html`
${player.name()}
-
Troops: ${renderTroops(player.troops())}
-
Gold: ${renderNumber(player.gold())}
+
Troops: ${player.troops()}
+
Gold: ${player.gold()}
${relationHtml == null ? '' : relationHtml}
`; @@ -173,7 +189,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { render() { if (!this._isActive) { - return html`` + return html``; } return html`
@@ -277,6 +293,19 @@ export class PlayerInfoOverlay extends LitElement implements Layer { margin-top: 4px; } + .hostile { + color: #ff4444; + } + .distrustful { + color: #ff8888; + } + .neutral { + color: #ffffff; + } + .friendly { + color: #4CAF50; + } + @media (max-width: 768px) { .container { top: 5px; @@ -302,19 +331,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer { .type-label { font-size: 12px; } - - } - .hostile { - color: #ff4444; - } - .distrustful { - color: #ff8888; - } - .neutral { - color: #ffffff; - } - .friendly { - color: #4CAF50; } `; } \ No newline at end of file diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 9a594d9ea..8b1165eac 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -1,7 +1,5 @@ import { colord, Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; -import { Unit, Cell, Game, Tile, UnitType } from "../../../core/game/Game"; -import { bfs, dist, euclDist } from "../../../core/Util"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; @@ -10,6 +8,8 @@ import missileSiloIcon from '../../../../resources/images/MissileSiloUnit.png'; import shieldIcon from '../../../../resources/images/ShieldIcon.png'; import cityIcon from '../../../../resources/images/CityIcon.png'; import { GameView } from "../../../core/GameView"; +import { Cell, Unit, UnitType } from "../../../core/game/Game"; +import { euclDistFN } from "../../../core/game/GameMap"; interface UnitRenderConfig { icon: string; @@ -17,14 +17,13 @@ interface UnitRenderConfig { territoryRadius: number; } - export class StructureLayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private unitImages: Map = new Map(); private theme: Theme = null; - private seenUnits = new Set() + private seenUnits = new Set(); // Configuration for supported unit types only private readonly unitConfigs: Partial> = { @@ -70,20 +69,20 @@ export class StructureLayer implements Layer { } tick() { - this.game.units().forEach(u => this.handleUnitRendering(u)) + this.game.units().forEach(u => this.handleUnitRendering(u)); } init() { - this.redraw() + this.redraw(); } redraw() { - console.log('structure layer redrawing') + console.log('structure layer redrawing'); this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext("2d", { alpha: true }); this.canvas.width = this.game.width(); this.canvas.height = this.game.height(); - this.game.units().forEach(u => this.handleUnitRendering(u)) + this.game.units().forEach(u => this.handleUnitRendering(u)); } renderLayer(context: CanvasRenderingContext2D) { @@ -106,11 +105,11 @@ export class StructureLayer implements Layer { if (unit.isActive() && this.seenUnits.has(unit)) { // Already rendered, so don't do anything. - return + return; } if (!unit.isActive() && !this.seenUnits.has(unit)) { // Has been deleted and render is cleared so don't do anything. - return + return; } const config = this.unitConfigs[unitType]; @@ -119,14 +118,15 @@ export class StructureLayer implements Layer { if (!config || !unitImage) return; // Clear previous rendering - bfs(unit.tile(), euclDist(unit.tile(), config.borderRadius)) - .forEach(t => this.clearCell(t.cell())); + for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.borderRadius))) { + this.clearCell(new Cell(this.game.x(tile), this.game.y(tile))); + } if (!unit.isActive()) { - this.seenUnits.delete(unit) + this.seenUnits.delete(unit); return; } - this.seenUnits.add(unit) + this.seenUnits.add(unit); // Create temporary canvas for icon processing const tempCanvas = document.createElement('canvas'); @@ -138,16 +138,25 @@ export class StructureLayer implements Layer { tempContext.drawImage(unitImage, 0, 0); const iconData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height); - const cell = unit.tile().cell(); - const startX = cell.x - Math.floor(tempCanvas.width / 2); - const startY = cell.y - Math.floor(tempCanvas.height / 2); + const startX = this.game.x(unit.tile()) - Math.floor(tempCanvas.width / 2); + const startY = this.game.y(unit.tile()) - Math.floor(tempCanvas.height / 2); // Draw border and territory - bfs(unit.tile(), euclDist(unit.tile(), config.borderRadius)) - .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(unit.owner().info()), 255)); + for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.borderRadius))) { + this.paintCell( + new Cell(this.game.x(tile), this.game.y(tile)), + this.theme.borderColor(unit.owner().info()), + 255 + ); + } - bfs(unit.tile(), euclDist(unit.tile(), config.territoryRadius)) - .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(unit.owner().info()), 130)); + for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.territoryRadius))) { + this.paintCell( + new Cell(this.game.x(tile), this.game.y(tile)), + this.theme.territoryColor(unit.owner().info()), + 130 + ); + } // Draw the icon this.renderIcon(iconData, startX, startY, tempCanvas.width, tempCanvas.height, unit); @@ -184,7 +193,7 @@ export class StructureLayer implements Layer { } paintCell(cell: Cell, color: Colord, alpha: number) { - this.clearCell(cell) + this.clearCell(cell); this.context.fillStyle = color.alpha(alpha / 255).toRgbString(); this.context.fillRect(cell.x, cell.y, 1, 1); } diff --git a/src/client/graphics/layers/TerrainLayer.ts b/src/client/graphics/layers/TerrainLayer.ts index 4f19d2b2c..aefec5d7f 100644 --- a/src/client/graphics/layers/TerrainLayer.ts +++ b/src/client/graphics/layers/TerrainLayer.ts @@ -1,8 +1,4 @@ -import { inherits } from "util" -import { Game } from "../../../core/game/Game"; -import { throws } from "assert"; import { Layer } from "./Layer"; -import { TransformHandler } from "../TransformHandler"; import { GameView } from "../../../core/GameView"; export class TerrainLayer implements Layer { @@ -37,8 +33,9 @@ export class TerrainLayer implements Layer { initImageData() { const theme = this.game.config().theme() this.game.forEachTile((tile) => { - let terrainColor = theme.terrainColor(tile) - const index = (tile.cell().y * this.game.width()) + tile.cell().x + let terrainColor = theme.terrainColor(this.game, tile) + // TODO: isn'te tileref and index the same? + const index = (this.game.y(tile) * this.game.width()) + this.game.x(tile) const offset = index * 4 this.imageData.data[offset] = terrainColor.rgba.r; this.imageData.data[offset + 1] = terrainColor.rgba.g; diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index f3c0fdc31..7f4b27ff1 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -1,90 +1,91 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; -import { Cell, Game, Player, PlayerType, Tile, Unit, UnitType, UnitUpdate } from "../../../core/game/Game"; +import { Cell, Game, Player, PlayerType, Unit, UnitType, UnitUpdate } from "../../../core/game/Game"; import { PseudoRandom } from "../../../core/PseudoRandom"; import { colord, Colord } from "colord"; -import { bfs, dist, euclDist, euclideanDist } from "../../../core/Util"; import { Theme } from "../../../core/configuration/Config"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; import { AlternateViewEvent, DragEvent, MouseDownEvent } from "../../InputHandler"; import { GameView, PlayerView } from "../../../core/GameView"; +import { euclDistFN, TileRef } from "../../../core/game/GameMap"; export class TerritoryLayer implements Layer { - private canvas: HTMLCanvasElement - private context: CanvasRenderingContext2D - private imageData: ImageData + private canvas: HTMLCanvasElement; + private context: CanvasRenderingContext2D; + private imageData: ImageData; - private tileToRenderQueue: PriorityQueue<{ tile: Tile, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate }) - private random = new PseudoRandom(123) - private theme: Theme = null + private tileToRenderQueue: PriorityQueue<{ tile: TileRef, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate }); + private random = new PseudoRandom(123); + private theme: Theme = null; // Used for spawn highlighting - private highlightCanvas: HTMLCanvasElement - private highlightContext: CanvasRenderingContext2D + private highlightCanvas: HTMLCanvasElement; + private highlightContext: CanvasRenderingContext2D; - private alternativeView = false - private lastDragTime = 0 - private nodrawDragDuration = 200 - - private refreshRate = 50 - private lastRefresh = 0 + private alternativeView = false; + private lastDragTime = 0; + private nodrawDragDuration = 200; + private refreshRate = 50; + private lastRefresh = 0; constructor(private game: GameView, private eventBus: EventBus) { - this.theme = game.config().theme() + this.theme = game.config().theme(); } shouldTransform(): boolean { - return true + return true; } tick() { this.game.recentlyUpdatedTiles() - .forEach(t => this.enqueueTile(t)) - + .forEach(t => this.enqueueTile(t)); if (!this.game.inSpawnPhase()) { - return + return; } if (this.game.ticks() % 5 == 0) { - return + return; } this.highlightContext.clearRect(0, 0, this.game.width(), this.game.height()); const humans = this.game.playerViews() - .filter(p => p.type() == PlayerType.Human) + .filter(p => p.type() == PlayerType.Human); for (const human of humans) { - const center = human.nameLocation() + const center = human.nameLocation(); if (!center) { - continue + continue; } - const centerTile = this.game.tile(new Cell(center.x, center.y)) + const centerTile = this.game.ref(center.x, center.y) if (!centerTile) { - continue + continue; } - for (const tile of bfs(centerTile, euclDist(centerTile, 9))) { - if (!tile.hasOwner()) { - this.paintHighlightCell(tile.cell(), this.theme.spawnHighlightColor(), 255) + for (const tile of this.game.bfs(centerTile, euclDistFN(centerTile, 9))) { + if (!this.game.hasOwner(tile)) { + this.paintHighlightCell( + new Cell(this.game.x(tile), this.game.y(tile)), + this.theme.spawnHighlightColor(), + 255 + ); } } } } init() { - this.eventBus.on(AlternateViewEvent, e => { this.alternativeView = e.alternateView }) - this.eventBus.on(DragEvent, e => { this.lastDragTime = Date.now() }) - this.redraw() + this.eventBus.on(AlternateViewEvent, e => { this.alternativeView = e.alternateView; }); + this.eventBus.on(DragEvent, e => { this.lastDragTime = Date.now(); }); + this.redraw(); } - redraw() { - console.log('redrew territory layer') + console.log('redrew territory layer'); this.canvas = document.createElement('canvas'); - this.context = this.canvas.getContext("2d") + this.context = this.canvas.getContext("2d"); - this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height()) - this.initImageData() + this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height()); + this.initImageData(); this.canvas.width = this.game.width(); this.canvas.height = this.game.height(); this.context.putImageData(this.imageData, 0, 0); @@ -96,26 +97,27 @@ export class TerritoryLayer implements Layer { this.highlightCanvas.height = this.game.height(); this.game.forEachTile(t => { - this.paintTerritory(t) - }) + this.paintTerritory(t); + }); } initImageData() { this.game.forEachTile((tile) => { - const index = (tile.cell().y * this.game.width()) + tile.cell().x - const offset = index * 4 - this.imageData.data[offset + 3] = 0 - }) + const cell = new Cell(this.game.x(tile), this.game.y(tile)); + const index = (cell.y * this.game.width()) + cell.x; + const offset = index * 4; + this.imageData.data[offset + 3] = 0; + }); } renderLayer(context: CanvasRenderingContext2D) { if (Date.now() > this.lastDragTime + this.nodrawDragDuration && Date.now() > this.lastRefresh + this.refreshRate) { - this.lastRefresh = Date.now() - this.renderTerritory() + this.lastRefresh = Date.now(); + this.renderTerritory(); this.context.putImageData(this.imageData, 0, 0); } if (this.alternativeView) { - return + return; } context.drawImage( @@ -124,7 +126,7 @@ export class TerritoryLayer implements Layer { -this.game.height() / 2, this.game.width(), this.game.height() - ) + ); if (this.game.inSpawnPhase()) { context.drawImage( this.highlightCanvas, @@ -137,62 +139,60 @@ export class TerritoryLayer implements Layer { } renderTerritory() { - let numToRender = Math.floor(this.tileToRenderQueue.size() / 5) + let numToRender = Math.floor(this.tileToRenderQueue.size() / 5); if (numToRender == 0 || this.game.inSpawnPhase()) { - numToRender = this.tileToRenderQueue.size() + numToRender = this.tileToRenderQueue.size(); } while (numToRender > 0) { - numToRender-- - const tile = this.tileToRenderQueue.pop().tile - this.paintTerritory(tile) - tile.neighbors().forEach(t => this.paintTerritory(t, true)) + numToRender--; + const tile = this.tileToRenderQueue.pop().tile; + this.paintTerritory(tile); + for (const neighbor of this.game.neighbors(tile)) { + this.paintTerritory(neighbor, true); + } } } - paintTerritory(tile: Tile, isBorder: boolean = false) { - if (isBorder && !tile.hasOwner()) { - return + paintTerritory(tile: TileRef, isBorder: boolean = false) { + if (isBorder && !this.game.hasOwner(tile)) { + return; } - if (!tile.hasOwner()) { - if (tile.hasFallout()) { - this.paintCell(tile.cell(), this.theme.falloutColor(), 150) - return + if (!this.game.hasOwner(tile)) { + if (this.game.hasFallout(tile)) { + this.paintCell( + new Cell(this.game.x(tile), this.game.y(tile)), + this.theme.falloutColor(), + 150 + ); + return; } - this.clearCell(tile.cell()) - return + this.clearCell(new Cell(this.game.x(tile), this.game.y(tile))); + return; } - const owner = tile.owner() as Player - if (tile.isBorder()) { - if (tile.hasDefenseBonus()) { - this.paintCell( - tile.cell(), - this.theme.defendedBorderColor(owner.info()), - 255 - ) - } else { - this.paintCell( - tile.cell(), - this.theme.borderColor(owner.info()), - 255 - ) - } + const owner = this.game.owner(tile) as Player; + if (this.game.isBorder(tile)) { + this.paintCell( + new Cell(this.game.x(tile), this.game.y(tile)), + this.theme.borderColor(owner.info()), + 255 + ); } else { this.paintCell( - tile.cell(), + new Cell(this.game.x(tile), this.game.y(tile)), this.theme.territoryColor(owner.info()), 150 - ) + ); } } paintCell(cell: Cell, color: Colord, alpha: number) { - const index = (cell.y * this.game.width()) + cell.x - const offset = index * 4 + const index = (cell.y * this.game.width()) + cell.x; + const offset = index * 4; this.imageData.data[offset] = color.rgba.r; this.imageData.data[offset + 1] = color.rgba.g; this.imageData.data[offset + 2] = color.rgba.b; - this.imageData.data[offset + 3] = alpha + this.imageData.data[offset + 3] = alpha; } clearCell(cell: Cell) { @@ -201,12 +201,15 @@ export class TerritoryLayer implements Layer { this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) } - enqueueTile(tile: Tile) { - this.tileToRenderQueue.push({ tile: tile, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) }) + enqueueTile(tile: TileRef) { + this.tileToRenderQueue.push({ + tile: tile, + lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) + }); } paintHighlightCell(cell: Cell, color: Colord, alpha: number) { - this.clearCell(cell) + this.clearCell(cell); this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString(); this.highlightContext.fillRect(cell.x, cell.y, 1, 1); } diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index cf97d1555..ac4285f23 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -1,12 +1,12 @@ import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; -import { Unit, Cell, Game, Tile, UnitType, Player, UnitUpdate } from "../../../core/game/Game"; -import { bfs, dist, euclDist } from "../../../core/Util"; +import { Unit, UnitType, Player, UnitUpdate } from "../../../core/game/Game"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; import { AlternateViewEvent } from "../../InputHandler"; import { ClientID } from "../../../core/Schemas"; import { GameView } from "../../../core/GameView"; +import { euclDistFN, manhattanDistFN, TileRef } from "../../../core/game/GameMap"; enum Relationship { Self, @@ -18,15 +18,15 @@ export class UnitLayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; - private boatToTrail = new Map>(); + private boatToTrail = new Map>(); private theme: Theme = null; - private alternateView = false + private alternateView = false; - private myPlayer: Player | null = null + private myPlayer: Player | null = null; - private oldShellTile = new Map() + private oldShellTile = new Map(); constructor(private game: GameView, private eventBus: EventBus, private clientID: ClientID) { this.theme = game.config().theme(); @@ -38,17 +38,17 @@ export class UnitLayer implements Layer { tick() { if (this.myPlayer == null) { - this.myPlayer = this.game.playerByClientID(this.clientID) + this.myPlayer = this.game.playerByClientID(this.clientID); } for (const unit of this.game.units()) { if (unit.wasUpdated()) - this.onUnitEvent(unit) + this.onUnitEvent(unit); } } init() { - this.eventBus.on(AlternateViewEvent, e => this.onAlternativeViewEvent(e)) - this.redraw() + this.eventBus.on(AlternateViewEvent, e => this.onAlternativeViewEvent(e)); + this.redraw(); } renderLayer(context: CanvasRenderingContext2D) { @@ -62,11 +62,10 @@ export class UnitLayer implements Layer { } onAlternativeViewEvent(event: AlternateViewEvent) { - this.alternateView = event.alternateView - this.redraw() + this.alternateView = event.alternateView; + this.redraw(); } - redraw() { this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext("2d"); @@ -80,15 +79,15 @@ export class UnitLayer implements Layer { private relationship(unit: Unit): Relationship { if (this.myPlayer == null) { - return Relationship.Enemy + return Relationship.Enemy; } if (this.myPlayer == unit.owner()) { - return Relationship.Self + return Relationship.Self; } if (this.myPlayer.isAlliedWith(unit.owner())) { - return Relationship.Ally + return Relationship.Ally; } - return Relationship.Enemy + return Relationship.Enemy; } onUnitEvent(unit: Unit) { @@ -103,137 +102,262 @@ export class UnitLayer implements Layer { this.handleBattleshipEvent(unit); break; case UnitType.Shell: - this.handleShellEvent(unit) + this.handleShellEvent(unit); break; case UnitType.TradeShip: - this.handleTradeShipEvent(unit) + this.handleTradeShipEvent(unit); break; case UnitType.AtomBomb: case UnitType.HydrogenBomb: - this.handleNuke(unit) - break + this.handleNuke(unit); + break; } } private handleDestroyerEvent(unit: Unit) { - const rel = this.relationship(unit) - bfs(unit.lastTile(), euclDist(unit.lastTile(), 4)).forEach(t => { - this.clearCell(t.cell()); - }); - if (!unit.isActive()) { - return + const rel = this.relationship(unit); + + // Clear previous area + for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 4))) { + this.clearCell(this.game.x(t), this.game.y(t)); + } + + if (!unit.isActive()) { + return; + } + + // Paint border + for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 4))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + } + + // Paint territory + for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 3))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner().info()), + 255 + ); } - bfs(unit.tile(), euclDist(unit.tile(), 4)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255)); - bfs(unit.tile(), dist(unit.tile(), 3)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255)); } private handleBattleshipEvent(unit: Unit) { - const rel = this.relationship(unit) - bfs(unit.lastTile(), euclDist(unit.lastTile(), 6)).forEach(t => { - this.clearCell(t.cell()); - }); - if (!unit.isActive()) { - return + const rel = this.relationship(unit); + + // Clear previous area + for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 6))) { + this.clearCell(this.game.x(t), this.game.y(t)); + } + + if (!unit.isActive()) { + return; + } + + // Paint outer territory + for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 5))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner().info()), + 255 + ); + } + + // Paint border + for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 4))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + } + + // Paint inner territory + for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 1))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner().info()), + 255 + ); } - bfs(unit.tile(), euclDist(unit.tile(), 5)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255)); - bfs(unit.tile(), dist(unit.tile(), 4)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255)); - bfs(unit.tile(), euclDist(unit.tile(), 1)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255)); } private handleShellEvent(unit: Unit) { - const rel = this.relationship(unit) + const rel = this.relationship(unit); - this.clearCell(unit.lastTile().cell()) + // Clear current and previous positions + this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile())); if (this.oldShellTile.has(unit)) { - this.clearCell(this.oldShellTile.get(unit).cell()) + const oldTile = this.oldShellTile.get(unit); + this.clearCell(this.game.x(oldTile), this.game.y(oldTile)); } - this.oldShellTile.set(unit, unit.lastTile()) + this.oldShellTile.set(unit, unit.lastTile()); if (!unit.isActive()) { - return + return; } - this.paintCell(unit.tile().cell(), rel, this.theme.borderColor(unit.owner().info()), 255) - this.paintCell(unit.lastTile().cell(), rel, this.theme.borderColor(unit.owner().info()), 255) + + // Paint current and previous positions + this.paintCell( + this.game.x(unit.tile()), + this.game.y(unit.tile()), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + this.paintCell( + this.game.x(unit.lastTile()), + this.game.y(unit.lastTile()), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); } - private handleNuke(unit: Unit) { - const rel = this.relationship(unit) - bfs(unit.lastTile(), euclDist(unit.lastTile(), 2)).forEach(t => { - this.clearCell(t.cell()); - }); - if (unit.isActive()) { - bfs(unit.tile(), euclDist(unit.tile(), 2)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255)); + const rel = this.relationship(unit); + + // Clear previous area + for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 2))) { + this.clearCell(this.game.x(t), this.game.y(t)); } + if (unit.isActive()) { + // Paint area + for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 2))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + } + } } private handleTradeShipEvent(unit: Unit) { - const rel = this.relationship(unit) - bfs(unit.lastTile(), euclDist(unit.lastTile(), 3)).forEach(t => { - this.clearCell(t.cell()); - }); - if (unit.isActive()) { - bfs(unit.tile(), dist(unit.tile(), 2)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255)); + const rel = this.relationship(unit); + + // Clear previous area + for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 3))) { + this.clearCell(this.game.x(t), this.game.y(t)); } + if (unit.isActive()) { - bfs(unit.tile(), dist(unit.tile(), 1)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255)); + // Paint territory + for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 2))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner().info()), + 255 + ); + } + + // Paint border + for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 1))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + } } } private handleBoatEvent(unit: Unit) { - const rel = this.relationship(unit) + const rel = this.relationship(unit); + if (!this.boatToTrail.has(unit)) { - this.boatToTrail.set(unit, new Set()); + this.boatToTrail.set(unit, new Set()); } const trail = this.boatToTrail.get(unit); trail.add(unit.lastTile()); - bfs(unit.lastTile(), dist(unit.lastTile(), 3)).forEach(t => { - this.clearCell(t.cell()); - }); + + // Clear previous area + for (const t of this.game.bfs(unit.lastTile(), manhattanDistFN(unit.lastTile(), 3))) { + this.clearCell(this.game.x(t), this.game.y(t)); + } + if (unit.isActive()) { + // Paint trail for (const t of trail) { - this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 150); + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner().info()), + 150 + ); + } + + // Paint border + for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 2))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.borderColor(unit.owner().info()), + 255 + ); + } + + // Paint territory + for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 1))) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner().info()), + 255 + ); } - bfs(unit.tile(), dist(unit.tile(), 2)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255)); - bfs(unit.tile(), dist(unit.tile(), 1)) - .forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255)); } else { - trail.forEach(t => this.clearCell(t.cell())); + for (const t of trail) { + this.clearCell(this.game.x(t), this.game.y(t)); + } this.boatToTrail.delete(unit); } } - paintCell(cell: Cell, relationship: Relationship, color: Colord, alpha: number) { - this.clearCell(cell) + paintCell(x: number, y: number, relationship: Relationship, color: Colord, alpha: number) { + this.clearCell(x, y); if (this.alternateView) { switch (relationship) { case Relationship.Self: - this.context.fillStyle = this.theme.selfColor().toRgbString() - break + this.context.fillStyle = this.theme.selfColor().toRgbString(); + break; case Relationship.Ally: - this.context.fillStyle = this.theme.allyColor().toRgbString() - break + this.context.fillStyle = this.theme.allyColor().toRgbString(); + break; case Relationship.Enemy: - this.context.fillStyle = this.theme.enemyColor().toRgbString() - break + this.context.fillStyle = this.theme.enemyColor().toRgbString(); + break; } } else { this.context.fillStyle = color.alpha(alpha / 255).toRgbString(); } - this.context.fillRect(cell.x, cell.y, 1, 1); + this.context.fillRect(x, y, 1, 1); } - clearCell(cell: Cell) { - this.context.clearRect(cell.x, cell.y, 1, 1); + clearCell(x: number, y: number) { + this.context.clearRect(x, y, 1, 1); } } \ No newline at end of file diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index f9e83107a..b2c3ddac7 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -230,7 +230,7 @@ export class BuildMenu extends LitElement { } showMenu(player: PlayerView, clickedCell: Cell) { - player.actions(this.game.tile(clickedCell)).then(actions => { + player.actions(this.game.ref(clickedCell.x, clickedCell.y)).then(actions => { console.log(`got actions: ${JSON.stringify(actions)}`) this.playerActions = actions this.myPlayer = player; diff --git a/src/client/graphics/layers/radial/RadialMenu.ts b/src/client/graphics/layers/radial/RadialMenu.ts index 96d4b1d08..9fe0b0f99 100644 --- a/src/client/graphics/layers/radial/RadialMenu.ts +++ b/src/client/graphics/layers/radial/RadialMenu.ts @@ -1,7 +1,6 @@ import { EventBus } from "../../../../core/EventBus"; -import { AllPlayers, Cell, Game, Player, PlayerActions, Tile, UnitType } from "../../../../core/game/Game"; +import { AllPlayers, Cell, Game, Player, PlayerActions, } from "../../../../core/game/Game"; import { ClientID } from "../../../../core/Schemas"; -import { and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../../../../core/Util"; import { ContextMenuEvent, MouseUpEvent, ShowBuildMenuEvent } from "../../../InputHandler"; import { SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent } from "../../../Transport"; import { TransformHandler } from "../../TransformHandler"; @@ -21,6 +20,7 @@ import { UIState } from "../../UIState"; import { BuildMenu } from "./BuildMenu"; import { consolex } from "../../../../core/Consolex"; import { GameView, PlayerView } from "../../../../core/GameView"; +import { TileRef } from "../../../../core/game/GameMap"; enum Slot { @@ -54,7 +54,7 @@ export class RadialMenu implements Layer { constructor( private eventBus: EventBus, - private game: GameView, + private g: GameView, private transformHandler: TransformHandler, private clientID: ClientID, private emojiTable: EmojiTable, @@ -70,10 +70,10 @@ export class RadialMenu implements Layer { if (clickedCell == null) { return } - if (!this.game.isOnMap(clickedCell)) { + if (!this.g.isValidCoord(clickedCell.x, clickedCell.y)) { return } - const p = this.game.playerByClientID(this.clientID) + const p = this.g.playerByClientID(this.clientID) if (p == null) { return } @@ -233,19 +233,19 @@ export class RadialMenu implements Layer { } this.clickedCell = this.transformHandler.screenToWorldCoordinates(event.x, event.y) - if (!this.game.isOnMap(this.clickedCell)) { + if (!this.g.isValidCoord(this.clickedCell.x, this.clickedCell.y)) { return } - const tile = this.game.tile(this.clickedCell) + const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y) - if (this.game.inSpawnPhase()) { - if (tile.terrain().isLand() && !tile.hasOwner()) { + if (this.g.inSpawnPhase()) { + if (this.g.isLand(tile) && !this.g.hasOwner(tile)) { this.enableCenterButton(true) } return } - const myPlayer = this.game.playerViews().find(p => p.clientID() == this.clientID) + const myPlayer = this.g.playerViews().find(p => p.clientID() == this.clientID) if (!myPlayer) { consolex.warn('my player not found') return @@ -255,13 +255,13 @@ export class RadialMenu implements Layer { }) } - private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: Tile) { + private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: TileRef) { this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => { this.buildMenu.showMenu(myPlayer, this.clickedCell) }) if (actions.interaction?.canSendEmoji) { this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { - const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player) + const target = this.g.owner(tile) == myPlayer ? AllPlayers : (this.g.owner(tile) as Player) this.emojiTable.onEmojiClicked = (emoji: string) => { this.emojiTable.hideTable() this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)) @@ -274,7 +274,7 @@ export class RadialMenu implements Layer { this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { this.eventBus.emit( new SendBoatAttackIntentEvent( - tile.owner().id(), + this.g.owner(tile).id(), this.clickedCell, this.uiState.attackRatio * myPlayer.troops() ) @@ -285,10 +285,10 @@ export class RadialMenu implements Layer { this.enableCenterButton(true) } - if (!tile.hasOwner()) { + if (!this.g.hasOwner(tile)) { return } - const other = tile.owner() as Player + const other = this.g.owner(tile) as Player if (actions?.interaction.canDonate) { @@ -351,13 +351,13 @@ export class RadialMenu implements Layer { return } consolex.log('Center button clicked'); - const clicked = this.game.tile(this.clickedCell) - if (this.game.inSpawnPhase()) { + const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y) + if (this.g.inSpawnPhase()) { this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell)) } else { - const myPlayer = this.game.players().find(p => p.clientID() == this.clientID) - if (myPlayer != null && clicked.owner() != myPlayer) { - this.eventBus.emit(new SendAttackIntentEvent(clicked.owner().id(), this.uiState.attackRatio * myPlayer.troops())) + const myPlayer = this.g.players().find(p => p.clientID() == this.clientID) + if (myPlayer != null && this.g.owner(clicked) != myPlayer) { + this.eventBus.emit(new SendAttackIntentEvent(this.g.owner(clicked).id(), this.uiState.attackRatio * myPlayer.troops())) } } this.hideRadialMenu(); diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index d18ae9aae..eb261c1f5 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -4,17 +4,18 @@ 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, MutableTile, NameViewData, Player, PlayerActions, PlayerID, PlayerProfile, Tile, TileUpdate, UnitType, UnitUpdate } from "./game/Game"; +import { Cell, DisplayMessageUpdate, Game, GameUpdateType, MessageType, MutableGame, NameViewData, Player, PlayerActions, PlayerID, PlayerProfile, UnitType } from "./game/Game"; import { createGame } from "./game/GameImpl"; -import { loadTerrainMap } from "./game/TerrainMapLoader"; +import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader"; import { GameConfig, Turn } from "./Schemas"; -import { and, bfs, dist, targetTransportTile } from "./Util"; -import { GameUpdateViewData, packTileData } from "./GameView"; +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 { const config = getConfig(gameConfig) - const terrainMap = await loadTerrainMap(gameConfig.gameMap); - const game = createGame(terrainMap.gameMap, terrainMap.miniGameMap, terrainMap.nationMap, config) + 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 @@ -69,12 +70,12 @@ export class GameRunner { } // Many tiles are updated to pack it into an array - const packedTileUpdates = updates[GameUpdateType.Tile].map(u => packTileData(u as TileUpdate)) + const packedTileUpdates = updates[GameUpdateType.Tile].map(u => u.update) updates[GameUpdateType.Tile] = [] this.callBack({ tick: this.game.ticks(), - packedTileUpdates: packedTileUpdates, + packedTileUpdates: new BigUint64Array(packedTileUpdates), updates: updates, playerNameViewData: this.playerViewData }) @@ -83,15 +84,15 @@ export class GameRunner { public playerActions(playerID: PlayerID, x: number, y: number): PlayerActions { const player = this.game.player(playerID) - const tile = this.game.tile(new Cell(x, y)) + 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 (tile.hasOwner()) { - const other = tile.owner() as Player + if (this.game.hasOwner(tile)) { + const other = this.game.owner(tile) as Player actions.interaction = { sharedBorder: player.sharesBorderWith(other), canSendEmoji: player.canSendEmoji(other), @@ -120,25 +121,25 @@ export class GameRunner { }; } - private canBoat(myPlayer: Player, tile: Tile): boolean { - const other = tile.owner() + 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 (bt.terrain().isOceanShore()) { + if (this.game.isOceanShore(bt)) { myPlayerBordersOcean = true break } } let otherPlayerBordersOcean = false - if (!tile.hasOwner()) { + if (!this.game.hasOwner(tile)) { otherPlayerBordersOcean = true } else { for (const bt of (other as Player).borderTiles()) { - if (bt.terrain().isOceanShore()) { + if (this.game.isOceanShore(bt)) { otherPlayerBordersOcean = true break } @@ -150,8 +151,8 @@ export class GameRunner { } let nearOcean = false - for (const t of bfs(tile, and(t => t.owner() == tile.owner() && t.terrain().isLand(), dist(tile, 25)))) { - if (t.terrain().isOceanShore()) { + 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 } @@ -161,7 +162,7 @@ export class GameRunner { } if (myPlayerBordersOcean && otherPlayerBordersOcean) { - const dst = targetTransportTile(this.game.width(), tile) + const dst = targetTransportTile(this.game, tile) if (dst != null) { if (myPlayer.canBuild(UnitType.TransportShip, dst)) { return true @@ -170,24 +171,24 @@ export class GameRunner { } } - private canAttack(myPlayer: Player, tile: Tile): boolean { - if (tile.owner() == myPlayer) { + private canAttack(myPlayer: Player, tile: TileRef): boolean { + if (this.game.owner(tile) == myPlayer) { return false } // TODO: fix event bus - if (tile.owner().isPlayer() && myPlayer.isAlliedWith(tile.owner() as Player)) { + 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 (!tile.terrain().isLand()) { + if (!this.game.isLand(tile)) { return false } - if (tile.hasOwner()) { - return myPlayer.sharesBorderWith(tile.owner()) + if (this.game.hasOwner(tile)) { + return myPlayer.sharesBorderWith(this.game.owner(tile)) } else { - for (const t of bfs(tile, and(t => !t.hasOwner() && t.terrain().isLand(), dist(tile, 200)))) { - for (const n of t.neighbors()) { - if (n.owner() == myPlayer) { + 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 } } diff --git a/src/core/GameView.ts b/src/core/GameView.ts index f0de9c088..cbe7d1671 100644 --- a/src/core/GameView.ts +++ b/src/core/GameView.ts @@ -1,70 +1,10 @@ -import { GameUpdates, GameUpdateType, MapPos, MessageType, NameViewData, Player, PlayerActions, PlayerProfile, PlayerUpdate, Tile, TileUpdate, Unit, UnitUpdate } from './game/Game'; +import { GameUpdates, GameUpdateType, MapPos, MessageType, NameViewData, Player, PlayerActions, PlayerProfile, PlayerUpdate, Unit, UnitUpdate } from './game/Game'; import { Config } from "./configuration/Config"; -import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage, Execution, ExecutionView, Game, Gold, MutableTile, Nation, PlayerID, PlayerInfo, PlayerType, Relation, TerrainMap, TerrainTile, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "./game/Game"; +import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage, Execution, ExecutionView, Game, Gold, Nation, PlayerID, PlayerInfo, PlayerType, Relation, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "./game/Game"; import { ClientID } from "./Schemas"; import { TerraNulliusImpl } from './game/TerraNulliusImpl'; import { WorkerClient } from './worker/WorkerClient'; -import { GameMapImpl, TileRef } from './game/GameMap'; - - -export class TileView { - - private _neighbors: TileView[] = [] - - constructor(private game: GameView, public data: TileUpdate, private _terrain: TerrainTile) { } - - ref(): TileRef { - if (!this.data) { return 0 } - - return this.data.pos.x * this.game.width() + this.data.pos.y - } - type(): TerrainType { - return this._terrain.type() - } - owner(): PlayerView | TerraNullius { - if (!this.hasOwner()) { - return new TerraNulliusImpl() - } - return this.game.playerBySmallID(this.data?.ownerID) - } - hasOwner(): boolean { - return this.data?.ownerID !== undefined && this.data.ownerID !== 0; - } - isBorder(): boolean { - for (const n of this.neighbors()) { - if (n.data?.ownerID != this.data?.ownerID) { - return true - } - } - return false - } - isBorderUpdated(): boolean { - return this.data.isBorder - } - cell(): Cell { - return this._terrain.cell() - } - hasFallout(): boolean { - return this.data?.hasFallout - } - terrain(): TerrainTile { - return this._terrain - } - - neighbors(): TileView[] { - if (this._neighbors.length == 0) { - this._neighbors = this._terrain.neighbors().map(t => this.game.tile(t.cell())) - } - return this._neighbors - } - - hasDefenseBonus(): boolean { - return this.data?.hasDefenseBonus ?? false - } - cost(): number { - return this._terrain.cost() - } -} +import { GameMap, GameMapImpl, TileRef, TileUpdate } from './game/GameMap'; export class UnitView implements Unit { public _wasUpdated = true @@ -78,15 +18,15 @@ export class UnitView implements Unit { return this._wasUpdated } - lastTiles(): Tile[] { - return this.lastPos.map(pos => this.gameView.tile(new Cell(pos.x, pos.y))) + lastTiles(): TileRef[] { + return this.lastPos.map(pos => this.gameView.ref(pos.x, pos.y)) } - lastTile(): Tile { + lastTile(): TileRef { if (this.lastPos.length == 0) { - return this.gameView.tile(new Cell(this.data.pos.x, this.data.pos.y)) + return this.gameView.ref(this.data.pos.x, this.data.pos.y) } - return this.gameView.tile(new Cell(this.lastPos[0].x, this.lastPos[0].y)) + return this.gameView.ref(this.lastPos[0].x, this.lastPos[0].y) } update(data: UnitUpdate) { @@ -105,8 +45,8 @@ export class UnitView implements Unit { troops(): number { return this.data.troops } - tile(): Tile { - return this.gameView.tile(new Cell(this.data.pos.x, this.data.pos.y)) + tile(): TileRef { + return this.gameView.ref(this.data.pos.x, this.data.pos.y) } owner(): PlayerView { return this.gameView.playerBySmallID(this.data.ownerID) @@ -125,12 +65,12 @@ export class UnitView implements Unit { export class PlayerView implements Player { constructor(private game: GameView, public data: PlayerUpdate, public nameData: NameViewData) { } - borderTiles(): ReadonlySet { + borderTiles(): ReadonlySet { throw new Error('Method not implemented.'); } - async actions(tile: Tile): Promise { - return this.game.worker.playerInteraction(this.id(), tile) + async actions(tile: TileRef): Promise { + return this.game.worker.playerInteraction(this.id(), this.game.x(tile), this.game.y(tile)) } nameLocation(): NameViewData { @@ -192,9 +132,6 @@ export class PlayerView implements Player { allianceWith(other: Player): Alliance | null { return null } - borderTileRefs(): ReadonlySet { - return new Set() - } units(...types: UnitType[]): Unit[] { return [] } @@ -246,7 +183,7 @@ export class PlayerView implements Player { canDonate(recipient: Player): boolean { return false } - canBuild(type: UnitType, targetTile: Tile): Tile | false { + canBuild(type: UnitType, targetTile: TileRef): TileRef | false { return false } info(): PlayerInfo { @@ -257,31 +194,21 @@ export class PlayerView implements Player { export interface GameUpdateViewData { tick: number updates: GameUpdates - packedTileUpdates: Uint16Array[] + packedTileUpdates: BigUint64Array playerNameViewData: Record } -export class GameView { +export class GameView implements GameMap { private lastUpdate: GameUpdateViewData - private tiles: TileView[][] = [] private smallIDToID = new Map() private _players = new Map() private _units = new Map() - private updatedTiles: TileView[] = [] + private updatedTiles: TileRef[] = [] - constructor(public worker: WorkerClient, private _config: Config, private _terrainMap: TerrainMap) { - // Initialize the 2D array - this.tiles = Array(_terrainMap.width()).fill(null).map(() => Array(_terrainMap.height()).fill(null)); - - // Fill the array with new TileView objects - for (let x = 0; x < _terrainMap.width(); x++) { - for (let y = 0; y < _terrainMap.height(); y++) { - this.tiles[x][y] = new TileView(this, null, _terrainMap.terrain(new Cell(x, y))); - } - } + constructor(public worker: WorkerClient, private _config: Config, private _map: GameMap) { this.lastUpdate = { tick: 0, - packedTileUpdates: [], + packedTileUpdates: new BigUint64Array([]), // TODO: make this empty map instead of null? updates: null, playerNameViewData: {}, @@ -295,12 +222,9 @@ export class GameView { public update(gu: GameUpdateViewData) { this.lastUpdate = gu - const updated = new Set() - this.lastUpdate.packedTileUpdates.map(tu => unpackTileData(tu)).forEach(tu => { - this.tiles[tu.pos.x][tu.pos.y].data = tu - updated.add(tu.pos) + this.lastUpdate.packedTileUpdates.forEach(tu => { + this.updatedTiles.push(this.updateTile(tu)) }) - this.updatedTiles = Array.from(updated).map(pos => this.tiles[pos.x][pos.y]) gu.updates[GameUpdateType.Player].forEach((pu) => { this.smallIDToID.set(pu.smallID, pu.id); @@ -324,7 +248,7 @@ export class GameView { }) } - recentlyUpdatedTiles(): TileView[] { + recentlyUpdatedTiles(): TileRef[] { return this.updatedTiles } @@ -359,26 +283,11 @@ export class GameView { players(): Player[] { return [] } - tile(cell: Cell): TileView { - return this.tiles[cell.x][cell.y] - } - isOnMap(cell: Cell): boolean { - return this._terrainMap.isOnMap(cell) - } - width(): number { - return this._terrainMap.width() - } - height(): number { - return this._terrainMap.height() + + owner(tile: TileRef): PlayerView { + return this.playerBySmallID(this.ownerID(tile)) } - forEachTile(fn: (tile: Tile) => void): void { - for (let x = 0; x < this._terrainMap.width(); x++) { - for (let y = 0; y < this._terrainMap.height(); y++) { - fn(this.tile(new Cell(x, y))) - } - } - } ticks(): Tick { return this.lastUpdate.tick } @@ -394,35 +303,37 @@ export class GameView { unitInfo(type: UnitType): UnitInfo { return this._config.unitInfo(type) } - terrainMap(): TerrainMap { - return this._terrainMap - } -} - -export function packTileData(tile: TileUpdate): Uint16Array { - const packed = new Uint16Array(4); - packed[0] = tile.pos.x; - packed[1] = tile.pos.y; - packed[2] = tile.ownerID; - - // Pack booleans into bits - packed[3] = (tile.hasFallout ? 1 : 0) | - (tile.hasDefenseBonus ? 2 : 0) | - (tile.isBorder ? 4 : 0) - - return packed; -} - -export function unpackTileData(packed: Uint16Array): TileUpdate { - return { - type: GameUpdateType.Tile, - pos: { - x: packed[0], - y: packed[1], - }, - ownerID: packed[2], - hasFallout: !!(packed[3] & 1), - hasDefenseBonus: !!(packed[3] & 2), - isBorder: !!(packed[3] & 4), - }; + + ref(x: number, y: number): TileRef { return this._map.ref(x, y) } + x(ref: TileRef): number { return this._map.x(ref) } + y(ref: TileRef): number { return this._map.y(ref) } + cell(ref: TileRef): Cell { return this._map.cell(ref) } + width(): number { return this._map.width() } + height(): number { return this._map.height() } + numLandTiles(): number { return this._map.numLandTiles() } + isValidCoord(x: number, y: number): boolean { return this._map.isValidCoord(x, y) } + isLand(ref: TileRef): boolean { return this._map.isLake(ref) } + isOceanShore(ref: TileRef): boolean { return this._map.isOceanShore(ref) } + isOcean(ref: TileRef): boolean { return this._map.isOcean(ref) } + isShoreline(ref: TileRef): boolean { return this._map.isShoreline(ref) } + magnitude(ref: TileRef): number { return this._map.magnitude(ref) } + ownerID(ref: TileRef): number { return this._map.ownerID(ref) } + hasOwner(ref: TileRef): boolean { return this._map.hasOwner(ref) } + setOwnerID(ref: TileRef, playerId: number): void { return this._map.setOwnerID(ref, playerId) } + hasFallout(ref: TileRef): boolean { return this._map.hasFallout(ref) } + setFallout(ref: TileRef, value: boolean): void { return this._map.setFallout(ref, value) } + isBorder(ref: TileRef): boolean { return this._map.isBorder(ref) } + setBorder(ref: TileRef, value: boolean): void { return this._map.setBorder(ref, value) } + neighbors(ref: TileRef): TileRef[] { return this._map.neighbors(ref) } + isWater(ref: TileRef): boolean { return this._map.isWater(ref) } + isLake(ref: TileRef): boolean { return this._map.isLake(ref) } + isShore(ref: TileRef): boolean { return this._map.isShore(ref) } + cost(ref: TileRef): number { return this._map.cost(ref) } + terrainType(ref: TileRef): TerrainType { return this._map.terrainType(ref) } + forEachTile(fn: (tile: TileRef) => void): void { return this._map.forEachTile(fn) } + manhattanDist(c1: TileRef, c2: TileRef): number { return this._map.manhattanDist(c1, c2) } + euclideanDist(c1: TileRef, c2: TileRef): number { return this._map.euclideanDist(c1, c2) } + bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set { return this._map.bfs(tile, filter) } + toTileUpdate(tile: TileRef): bigint { return this._map.toTileUpdate(tile) } + updateTile(tu: TileUpdate): TileRef { return this._map.updateTile(tu) } } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 35c22739e..8b8827e1f 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -100,10 +100,6 @@ export const AttackIntentSchema = BaseIntentSchema.extend({ attackerID: ID, targetID: ID.nullable(), troops: z.number().nullable(), - sourceX: z.number().nullable(), - sourceY: z.number().nullable(), - targetX: z.number().nullable(), - targetY: z.number().nullable() }); export const SpawnIntentSchema = BaseIntentSchema.extend({ diff --git a/src/core/Util.ts b/src/core/Util.ts index 4666aa2bc..2402ae095 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -3,22 +3,14 @@ import twemoji from 'twemoji'; import DOMPurify from 'dompurify'; -import { Cell, Game, Player, TerraNullius, Tile, Unit } from "./game/Game"; -import { number } from 'zod'; +import { Cell, Game, MutableGame, Player, Unit } from "./game/Game"; import { GameConfig, GameID, GameRecord, PlayerRecord, Turn } from './Schemas'; import { customAlphabet, nanoid } from 'nanoid'; -import { GameView } from './GameView'; -import { TileRef } from './game/GameMap'; +import { andFN, GameMap, manhattanDistFN, TileRef } from './game/GameMap'; -export function manhattanDist(c1: Cell, c2: Cell): number { - return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y); -} -export function euclideanDist(c1: Cell, c2: Cell): number { - return Math.sqrt(Math.pow(c1.x - c2.x, 2) + Math.pow(c1.y - c2.y, 2)); -} export function manhattanDistWrapped(c1: Cell, c2: Cell, width: number): number { // Calculate x distance @@ -37,95 +29,69 @@ export function within(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } -export function euclDist(root: Tile, dist: number): (tile: Tile) => boolean { - return (n: Tile) => euclideanDist(root.cell(), n.cell()) <= dist; -} -export function dist(root: Tile, dist: number): (tile: Tile) => boolean { - return (n: Tile) => manhattanDist(root.cell(), n.cell()) <= dist; -} - -export function distSort(target: Tile): (a: Tile, b: Tile) => number { - return (a: Tile, b: Tile) => { - return manhattanDist(a.cell(), target.cell()) - manhattanDist(b.cell(), target.cell()); +export function distSort(gm: GameMap, target: TileRef): (a: TileRef, b: TileRef) => number { + return (a: TileRef, b: TileRef) => { + return gm.manhattanDist(a, target) - gm.manhattanDist(b, target); } } -export function distSortUnit(target: Unit | Tile): (a: Unit, b: Unit) => number { - const targetCell = ('tile' in target) ? target.tile().cell() : target.cell(); +export function distSortUnit(gm: GameMap, target: Unit | TileRef): (a: Unit, b: Unit) => number { + const targetRef = (typeof target === 'number') ? target : target.tile() return (a: Unit, b: Unit) => { - return manhattanDist(a.tile().cell(), targetCell) - manhattanDist(b.tile().cell(), targetCell); + return gm.manhattanDist(a.tile(), targetRef) - gm.manhattanDist(b.tile(), targetRef); } } -export function and(x: (tile: Tile) => boolean, y: (tile: Tile) => boolean): (tile: Tile) => boolean { - return (tile: Tile) => x(tile) && y(tile) -} // TODO: refactor to new file -export function sourceDstOceanShore(game: GameView, src: Player, tile: Tile): [Tile | null, Tile | null] { - const dst = tile.owner() - let srcTile = closestOceanShoreFromPlayer(src, tile, game.width()) - let dstTile: Tile | null = null +export function sourceDstOceanShore(gm: MutableGame, src: Player, tile: TileRef): [TileRef | null, TileRef | null] { + const dst = gm.owner(tile) + let srcTile = closestOceanShoreFromPlayer(gm, src, tile) + let dstTile: TileRef | null = null if (dst.isPlayer()) { - dstTile = closestOceanShoreFromPlayer(dst as Player, tile, game.width()) + dstTile = closestOceanShoreFromPlayer(gm, dst as Player, tile) } else { - dstTile = closestOceanShoreTN(tile, 300) + dstTile = closestOceanShoreTN(gm, tile, 300) } return [srcTile, dstTile] } -export function targetTransportTile(gameWidth: number, tile: Tile): Tile | null { - const dst = tile.owner() - let dstTile: Tile | null = null +export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null { + const dst = gm.playerBySmallID(gm.ownerID(tile)) + let dstTile: TileRef | null = null if (dst.isPlayer()) { - dstTile = closestOceanShoreFromPlayer(dst as Player, tile, gameWidth) + dstTile = closestOceanShoreFromPlayer(gm, dst as Player, tile) } else { - dstTile = closestOceanShoreTN(tile, 300) + dstTile = closestOceanShoreTN(gm, tile, 300) } return dstTile } -export function closestOceanShoreFromPlayer(player: Player, target: Tile, width: number): Tile | null { - const shoreTiles = Array.from(player.borderTiles()).filter(t => t.terrain().isOceanShore()) +export function closestOceanShoreFromPlayer(gm: GameMap, player: Player, target: TileRef): TileRef | null { + const shoreTiles = Array.from(player.borderTiles()).filter(t => gm.isOceanShore(t)) if (shoreTiles.length == 0) { return null } return shoreTiles.reduce((closest, current) => { - const closestDistance = manhattanDistWrapped(target.cell(), closest.cell(), width); - const currentDistance = manhattanDistWrapped(target.cell(), current.cell(), width); + const closestDistance = manhattanDistWrapped(gm.cell(target), gm.cell(closest), gm.width()); + const currentDistance = manhattanDistWrapped(gm.cell(target), gm.cell(current), gm.width()); return currentDistance < closestDistance ? current : closest; }); } -function closestOceanShoreTN(tile: Tile, searchDist: number): Tile { - const tn = Array.from(bfs(tile, and(t => !t.hasOwner(), dist(tile, searchDist)))) - .filter(t => t.terrain().isOceanShore()) - .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) +function closestOceanShoreTN(gm: GameMap, tile: TileRef, searchDist: number): TileRef { + const tn = Array.from(gm.bfs(tile, andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)))) + .filter(t => gm.isOceanShore(t)) + .sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b)) if (tn.length == 0) { return null } return tn[0] } -export function bfs(tile: Tile, filter: (tile: Tile) => boolean): Set { - const seen = new Map() - const q: Tile[] = [] - q.push(tile) - while (q.length > 0) { - const curr = q.pop() - seen.set(curr.ref(), curr) - for (const n of curr.neighbors()) { - if (!seen.has(n.ref()) && filter(n)) { - q.push(n) - } - } - } - return new Set(seen.values()) -} - export function simpleHash(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { @@ -136,11 +102,11 @@ export function simpleHash(str: string): number { return Math.abs(hash); } -export function calculateBoundingBox(borderTiles: ReadonlySet): { min: Cell; max: Cell } { +export function calculateBoundingBox(gm: GameMap, borderTiles: ReadonlySet): { min: Cell; max: Cell } { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - borderTiles.forEach((tile: Tile) => { - const cell = tile.cell(); + borderTiles.forEach((tile: TileRef) => { + const cell = gm.cell(tile); minX = Math.min(minX, cell.x); minY = Math.min(minY, cell.y); maxX = Math.max(maxX, cell.x); @@ -150,8 +116,8 @@ export function calculateBoundingBox(borderTiles: ReadonlySet): { min: Cel return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) } } -export function calculateBoundingBoxCenter(borderTiles: ReadonlySet): Cell { - const { min, max } = calculateBoundingBox(borderTiles) +export function calculateBoundingBoxCenter(gm: GameMap, borderTiles: ReadonlySet): Cell { + const { min, max } = calculateBoundingBox(gm, borderTiles) return new Cell( min.x + Math.floor((max.x - min.x) / 2), min.y + Math.floor((max.y - min.y) / 2) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 7378e8a56..ac9ce08d4 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -1,4 +1,4 @@ -import { Difficulty, GameType, Gold, Player, PlayerID, PlayerInfo, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game"; +import { Difficulty, GameType, Gold, Player, PlayerID, PlayerInfo, TerraNullius, Tick, UnitInfo, UnitType } from "../game/Game"; import { Colord, colord } from "colord"; import { preprodConfig } from "./PreprodConfig"; import { prodConfig } from "./ProdConfig"; @@ -6,6 +6,7 @@ import { consolex } from "../Consolex"; import { GameConfig } from "../Schemas"; import { DefaultConfig } from "./DefaultConfig"; import { DevConfig, DevServerConfig } from "./DevConfig"; +import { GameMap, TileRef } from "../game/GameMap"; export enum GameEnv { Dev, @@ -61,7 +62,7 @@ export interface Config { goldAdditionRate(player: Player): number troopAdjustmentRate(player: Player): number attackTilesPerTick(attckTroops: number, attacker: Player, defender: Player | TerraNullius, numAdjacentTilesWithEnemy: number): number - attackLogic(attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: Tile): { + attackLogic(gm: GameMap, attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: TileRef): { attackerTroopLoss: number, defenderTroopLoss: number, tilesPerTickUsed: number @@ -81,7 +82,7 @@ export interface Config { donateCooldown(): Tick defaultDonationAmount(sender: Player): number unitInfo(type: UnitType): UnitInfo - tradeShipGold(src: Unit, dst: Unit): Gold + tradeShipGold(dist: number): Gold tradeShipSpawnRate(): number defensePostRange(): number defensePostDefenseBonus(): number @@ -94,7 +95,7 @@ export interface Theme { territoryColor(playerInfo: PlayerInfo): Colord; borderColor(playerInfo: PlayerInfo): Colord; defendedBorderColor(playerInfo: PlayerInfo): Colord; - terrainColor(tile: Tile): Colord; + terrainColor(gm: GameMap, tile: TileRef): Colord; backgroundColor(): Colord; falloutColor(): Colord font(): string; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 894c1434d..ff016623e 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -1,6 +1,7 @@ -import { Difficulty, GameType, Gold, MutableTile, Player, PlayerInfo, PlayerType, TerrainType, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game"; +import { Difficulty, GameType, Gold, Player, PlayerInfo, PlayerType, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "../game/Game"; +import { GameMap, TileRef } from "../game/GameMap"; import { GameConfig } from "../Schemas"; -import { assertNever, distSort, manhattanDist, simpleHash, within } from "../Util"; +import { assertNever, within } from "../Util"; import { Config, ServerConfig, Theme } from "./Config"; import { pastelTheme } from "./PastelTheme"; @@ -62,8 +63,7 @@ export class DefaultConfig implements Config { spawnNPCs(): boolean { return true } - tradeShipGold(src: Unit, dst: Unit): Gold { - const dist = manhattanDist(src.tile().cell(), dst.tile().cell()) + tradeShipGold(dist: number): Gold { return 10000 + 100 * Math.pow(dist, 1.1) } tradeShipSpawnRate(): number { @@ -186,10 +186,10 @@ export class DefaultConfig implements Config { } theme(): Theme { return pastelTheme; } - attackLogic(attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: MutableTile): { attackerTroopLoss: number; defenderTroopLoss: number; tilesPerTickUsed: number } { + attackLogic(gm: GameMap, attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: TileRef): { attackerTroopLoss: number; defenderTroopLoss: number; tilesPerTickUsed: number } { let mag = 0 let speed = 0 - const type = tileToConquer.terrain().type() + const type = gm.terrainType(tileToConquer) switch (type) { case TerrainType.Plains: mag = 80 @@ -209,7 +209,7 @@ export class DefaultConfig implements Config { // TODO // mag *= tileToConquer.defenseBonus(attacker) // speed *= tileToConquer.defenseBonus(attacker) - if (tileToConquer.hasFallout()) { + if (gm.hasFallout(tileToConquer)) { mag *= this.falloutDefenseModifier() speed *= this.falloutDefenseModifier() } diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 8854c8ea0..f7c43351f 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,9 +1,10 @@ import { Colord, colord, random } from "colord"; -import { PlayerID, PlayerInfo, TerrainType, Tile } from "../game/Game"; +import { Game, PlayerID, PlayerInfo, TerrainType } from "../game/Game"; import { Theme } from "./Config"; import { time } from "console"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; +import { GameMap, TileRef } from "../game/GameMap"; export const pastelTheme = new class implements Theme { @@ -154,19 +155,19 @@ export const pastelTheme = new class implements Theme { }) } - terrainColor(tile: Tile): Colord { - let mag = tile.terrain().magnitude() - if (tile.terrain().isShore()) { + terrainColor(gm: GameMap, tile: TileRef): Colord { + let mag = gm.magnitude(tile) + if (gm.isShore(tile)) { return this.shore } - switch (tile.terrain().type()) { + switch (gm.terrainType(tile)) { case TerrainType.Ocean: case TerrainType.Lake: const w = this.water.rgba - if (tile.terrain().isShorelineWater()) { + if (gm.isShoreline(tile) && gm.isWater(tile)) { return this.shorelineWater } - if (tile.terrain().magnitude() < 7) { + if (gm.magnitude(tile) < 7) { return colord({ r: Math.max(w.r - 7 + mag, 0), g: Math.max(w.g - 7 + mag, 0), diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 6535bbc3e..bbe8a8939 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -1,7 +1,6 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; -import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerType, TerrainType, TerraNullius, Tile } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerType, TerrainType, TerraNullius } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; -import { manhattanDist } from "../Util"; import { MessageType } from '../game/Game'; import { renderNumber } from "../../client/Utils"; import { TileRef } from "../game/GameMap"; @@ -32,8 +31,7 @@ export class AttackExecution implements Execution { private troops: number | null, private _ownerID: PlayerID, private _targetID: PlayerID | null, - private sourceCell: Cell | null, - private targetCell: Cell | null, + private sourceTile: TileRef | null, private removeTroops: boolean = true, ) { } @@ -51,8 +49,6 @@ export class AttackExecution implements Execution { } this.mg = mg - this.targetCell = null - this._owner = mg.player(this._ownerID) this.target = this._targetID == this.mg.terraNullius().id() ? mg.terraNullius() : mg.player(this._targetID) @@ -84,7 +80,7 @@ export class AttackExecution implements Execution { } } // Existing attack on same target, add troops - if (otherAttack._owner == this._owner && otherAttack._targetID == this._targetID && this.sourceCell == otherAttack.sourceCell) { + if (otherAttack._owner == this._owner && otherAttack._targetID == this._targetID && this.sourceTile == otherAttack.sourceTile) { otherAttack.troops += this.troops otherAttack.refreshToConquer() this.active = false @@ -95,8 +91,8 @@ export class AttackExecution implements Execution { if (this._owner.type() != PlayerType.Bot && this.target.isPlayer() && this.target.type() == PlayerType.Human) { mg.displayMessage(`You are being attacked by ${this._owner.displayName()}`, MessageType.ERROR, this._targetID) } - if (this.sourceCell != null) { - this.addNeighbors(mg.tile(this.sourceCell)) + if (this.sourceTile != null) { + this.addNeighbors(this.sourceTile) } else { this.refreshToConquer() } @@ -154,14 +150,14 @@ export class AttackExecution implements Execution { } const tileToConquer = this.toConquer.dequeue().tile - this.border.delete(tileToConquer.ref()) + this.border.delete(tileToConquer) - const onBorder = tileToConquer.neighbors().filter(t => t.owner() == this._owner).length > 0 - if (tileToConquer.owner() != this.target || !onBorder) { + const onBorder = this.mg.neighbors(tileToConquer).filter(t => this.mg.owner(t) == this._owner).length > 0 + if (this.mg.owner(tileToConquer) != this.target || !onBorder) { continue } this.addNeighbors(tileToConquer) - const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg.config().attackLogic(this.troops, this._owner, this.target, tileToConquer) + const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg.config().attackLogic(this.mg, this.troops, this._owner, this.target, tileToConquer) numTilesPerTick -= tilesPerTickUsed this.troops -= attackerTroopLoss if (this.target.isPlayer()) { @@ -172,25 +168,22 @@ export class AttackExecution implements Execution { } } - private addNeighbors(tile: Tile) { - for (const neighbor of tile.neighbors()) { - if (neighbor.terrain().isWater() || neighbor.owner() != this.target) { + private addNeighbors(tile: TileRef) { + for (const neighbor of this.mg.neighbors(tile)) { + if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) { continue } - this.border.add(neighbor.ref()) - let numOwnedByMe = neighbor.neighbors() - .filter(t => t.terrain().isLand()) - .filter(t => t.owner() == this._owner) + this.border.add(neighbor) + let numOwnedByMe = this.mg.neighbors(neighbor) + .filter(t => this.mg.isLake(t)) + .filter(t => this.mg.owner(t) == this._owner) .length let dist = 0 - if (this.targetCell != null) { - dist = manhattanDist(tile.cell(), this.targetCell) - } if (numOwnedByMe > 2) { numOwnedByMe = 10 } let mag = 0 - switch (tile.terrain().type()) { + switch (this.mg.terrainType(tile)) { case TerrainType.Plains: mag = 1 break @@ -218,11 +211,12 @@ export class AttackExecution implements Execution { for (let i = 0; i < 10; i++) { for (const tile of this.target.tiles()) { - if (tile.borders(this._owner)) { + const borders = this.mg.neighbors(tile).some(t => this.mg.owner(t) == this._owner) + if (borders) { this._owner.conquer(tile) } else { - for (const neighbor of tile.neighbors()) { - const no = neighbor.owner() + for (const neighbor of this.mg.neighbors(tile)) { + const no = this.mg.owner(neighbor) if (no.isPlayer() && no != this.target) { this.mg.player(no.id()).conquer(tile) break @@ -246,5 +240,5 @@ export class AttackExecution implements Execution { class TileContainer { - constructor(public readonly tile: Tile, public readonly priority: number, public readonly tick: number) { } + constructor(public readonly tile: TileRef, public readonly priority: number, public readonly tick: number) { } } \ No newline at end of file diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts index beef3abf1..77a9a3ac8 100644 --- a/src/core/execution/BattleshipExecution.ts +++ b/src/core/execution/BattleshipExecution.ts @@ -1,11 +1,11 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Tile, Unit, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Unit, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; -import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; -import { distSort, distSortUnit, manhattanDist } from "../Util"; +import { distSort, distSortUnit } from "../Util"; import { ShellExecution } from "./ShellExecution"; import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; export class BattleshipExecution implements Execution { private random: PseudoRandom @@ -17,8 +17,7 @@ export class BattleshipExecution implements Execution { private pathfinder: PathFinder - private patrolTile: Tile; - private patrolCenterTile: Tile + private patrolTile: TileRef; // TODO: put in config private searchRange = 100 @@ -29,7 +28,7 @@ export class BattleshipExecution implements Execution { constructor( private playerID: PlayerID, - private cell: Cell, + private patrolCenterTile: TileRef, ) { } @@ -37,7 +36,6 @@ export class BattleshipExecution implements Execution { this.pathfinder = PathFinder.Mini(mg, 5000, false) this._owner = mg.player(this.playerID) this.mg = mg - this.patrolCenterTile = mg.tile(this.cell) this.patrolTile = this.patrolCenterTile this.random = new PseudoRandom(mg.ticks()) } @@ -85,15 +83,15 @@ export class BattleshipExecution implements Execution { } let ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship) - .filter(u => manhattanDist(u.tile().cell(), this.battleship.tile().cell()) < 100) + .filter(u => this.mg.manhattanDist(u.tile(), this.battleship.tile()) < 100) .filter(u => u.owner() != this.battleship.owner()) .filter(u => u != this.battleship) .filter(u => !u.owner().isAlliedWith(this.battleship.owner())) .filter(u => !this.alreadyTargeted.has(u)) - .sort(distSortUnit(this.battleship)); + .sort(distSortUnit(this.mg, this.battleship)); const friendlyDestroyerNearby = this.battleship.owner().units(UnitType.Destroyer) - .filter(d => manhattanDist(d.tile().cell(), this.battleship.tile().cell()) < 120) + .filter(d => this.mg.manhattanDist(d.tile(), this.battleship.tile()) < 120) .length > 0 if (friendlyDestroyerNearby) { @@ -124,16 +122,15 @@ export class BattleshipExecution implements Execution { return false } - randomTile(): Tile { + randomTile(): TileRef { while (true) { - const x = this.patrolCenterTile.cell().x + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) - const y = this.patrolCenterTile.cell().y + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) - const cell = new Cell(x, y) - if (!this.mg.isOnMap(cell)) { + const x = this.mg.x(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) + const y = this.mg.y(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) + if (!this.mg.isValidCoord(x, y)) { continue } - const tile = this.mg.tile(cell) - if (!tile.terrain().isOcean()) { + const tile = this.mg.ref(x, y) + if (!this.mg.isOcean(tile)) { continue } return tile diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index c3f0b837d..7e17523c8 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -55,8 +55,8 @@ export class BotExecution implements Execution { if (this.neighborsTerraNullius) { for (const b of this.bot.borderTiles()) { - for (const n of b.neighbors()) { - if (n.owner() == this.mg.terraNullius() && n.terrain().isLand()) { + for (const n of this.mg.neighbors(b)) { + if (!this.mg.hasOwner(n) && this.mg.isLake(n)) { this.sendAttack(this.mg.terraNullius()) return } @@ -65,14 +65,16 @@ export class BotExecution implements Execution { this.neighborsTerraNullius = false } - const border = Array.from(this.bot.borderTiles()).flatMap(t => t.neighbors()).filter(t => t.hasOwner() && t.owner() != this.bot) + const border = Array.from(this.bot.borderTiles()) + .flatMap(t => this.mg.neighbors(t)) + .filter(t => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot) if (border.length == 0) { return } const toAttack = border[this.random.nextInt(0, border.length)] - const owner = toAttack.owner() + const owner = this.mg.owner(toAttack) if (owner.isPlayer()) { if (this.bot.isAlliedWith(owner)) { diff --git a/src/core/execution/BotSpawner.ts b/src/core/execution/BotSpawner.ts index d10ba59ba..472eb4983 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -1,9 +1,10 @@ import { consolex } from "../Consolex"; -import {Cell, Game, PlayerType, Tile} from "../game/Game"; -import {PseudoRandom} from "../PseudoRandom"; -import {GameID, SpawnIntent} from "../Schemas"; -import {bfs, dist as dist, manhattanDist, simpleHash} from "../Util"; -import {BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES} from "./utils/BotNames"; +import { Cell, Game, PlayerType } from "../game/Game"; +import { TileRef } from "../game/GameMap"; +import { PseudoRandom } from "../PseudoRandom"; +import { GameID, SpawnIntent } from "../Schemas"; +import { simpleHash } from "../Util"; +import { BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES } from "./utils/BotNames"; export class BotSpawner { @@ -34,11 +35,11 @@ export class BotSpawner { spawnBot(botName: string): SpawnIntent | null { const tile = this.randTile() - if (!tile.terrain().isLand()) { + if (!this.gs.isLand(tile)) { return null } for (const spawn of this.bots) { - if (manhattanDist(new Cell(spawn.x, spawn.y), tile.cell()) < 30) { + if (this.gs.manhattanDist(this.gs.ref(spawn.x, spawn.y), tile) < 30) { return null } } @@ -47,8 +48,8 @@ export class BotSpawner { playerID: this.random.nextID(), name: botName, playerType: PlayerType.Bot, - x: tile.cell().x, - y: tile.cell().y + x: this.gs.x(tile), + y: this.gs.y(tile) }; } @@ -58,10 +59,10 @@ export class BotSpawner { return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`; } - private randTile(): Tile { - return this.gs.tile(new Cell( + private randTile(): TileRef { + return this.gs.ref( this.random.nextInt(0, this.gs.width()), this.random.nextInt(0, this.gs.height()) - )) + ) } } diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts index b5b5a49d5..aa8526a3e 100644 --- a/src/core/execution/CityExecution.ts +++ b/src/core/execution/CityExecution.ts @@ -1,20 +1,18 @@ import { consolex } from "../Consolex"; -import { Cell, DefenseBonus, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { bfs, dist } from "../Util"; +import { Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, UnitType } from "../game/Game"; +import { TileRef } from "../game/GameMap"; export class CityExecution implements Execution { private player: MutablePlayer private mg: MutableGame private city: MutableUnit - private tile: Tile private active: boolean = true - constructor(private ownerId: PlayerID, private cell: Cell) { } + constructor(private ownerId: PlayerID, private tile: TileRef) { } init(mg: MutableGame, ticks: number): void { this.mg = mg - this.tile = mg.tile(this.cell) this.player = mg.player(this.ownerId) } diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index 1f443828b..e0426a082 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -1,22 +1,20 @@ import { consolex } from "../Consolex"; -import { Cell, DefenseBonus, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { bfs, dist } from "../Util"; +import { Cell, DefenseBonus, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, UnitType } from "../game/Game"; +import { manhattanDistFN, TileRef } from "../game/GameMap"; export class DefensePostExecution implements Execution { private player: MutablePlayer private mg: MutableGame private post: MutableUnit - private tile: Tile private active: boolean = true private defenseBonuses: DefenseBonus[] = [] - constructor(private ownerId: PlayerID, private cell: Cell) { } + constructor(private ownerId: PlayerID, private tile: TileRef) { } init(mg: MutableGame, ticks: number): void { this.mg = mg - this.tile = mg.tile(this.cell) this.player = mg.player(this.ownerId) } @@ -29,9 +27,11 @@ export class DefensePostExecution implements Execution { return } this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile) - bfs(spawnTile, dist(spawnTile, this.mg.config().defensePostRange())).forEach(t => { - if (t.terrain().isLand()) { - this.defenseBonuses.push(this.mg.addTileDefenseBonus(t, this.post, this.mg.config().defensePostDefenseBonus())) + this.mg.bfs(spawnTile, manhattanDistFN(spawnTile, this.mg.config().defensePostRange())).forEach(t => { + if (this.mg.isLake(t)) { + this.defenseBonuses.push( + this.mg.addTileDefenseBonus(t, this.post, this.mg.config().defensePostDefenseBonus()) + ) } }) } diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index cfeddd36d..7dea9da54 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,10 +1,10 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Tile, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; -import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; -import { distSort, distSortUnit, manhattanDist } from "../Util"; +import { distSort, distSortUnit } from "../Util"; import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; export class DestroyerExecution implements Execution { private random: PseudoRandom @@ -17,15 +17,14 @@ export class DestroyerExecution implements Execution { private target: MutableUnit = null private pathfinder: PathFinder - private patrolTile: Tile; - private patrolCenterTile: Tile + private patrolTile: TileRef; // TODO: put in config private searchRange = 100 constructor( private playerID: PlayerID, - private cell: Cell, + private patrolCenterTile: TileRef, ) { } @@ -33,7 +32,6 @@ export class DestroyerExecution implements Execution { this.pathfinder = PathFinder.Mini(mg, 5000, false) this._owner = mg.player(this.playerID) this.mg = mg - this.patrolCenterTile = mg.tile(this.cell) this.patrolTile = this.patrolCenterTile this.random = new PseudoRandom(mg.ticks()) } @@ -57,7 +55,7 @@ export class DestroyerExecution implements Execution { } if (this.target == null) { const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship) - .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell()) < 100) + .filter(u => this.mg.manhattanDist(u.tile(), this.destroyer.tile()) < 100) .filter(u => u.type() != UnitType.Destroyer || u.health() < this.destroyer.health()) // only attack Destroyers weaker than it. .filter(u => u.owner() != this.destroyer.owner()) .filter(u => u != this.destroyer) @@ -80,7 +78,7 @@ export class DestroyerExecution implements Execution { } return } - this.target = ships.sort(distSortUnit(this.destroyer))[0] + this.target = ships.sort(distSortUnit(this.mg, this.destroyer))[0] } if (!this.target.isActive() || this.target.owner() == this._owner) { // Incase another destroyer captured or destroyed target @@ -132,16 +130,15 @@ export class DestroyerExecution implements Execution { return false } - randomTile(): Tile { + randomTile(): TileRef { while (true) { - const x = this.patrolCenterTile.cell().x + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) - const y = this.patrolCenterTile.cell().y + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) - const cell = new Cell(x, y) - if (!this.mg.isOnMap(cell)) { + const x = this.mg.x(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) + const y = this.mg.y(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) + if (!this.mg.isValidCoord(x, y)) { continue } - const tile = this.mg.tile(cell) - if (!tile.terrain().isOcean()) { + const tile = this.mg.ref(x, y) + if (!this.mg.isOcean(tile)) { continue } return tile diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index d2058633b..43a51bbb6 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, PlayerType, Alliance, UnitType } from "../game/Game"; import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../Schemas"; import { AttackExecution } from "./AttackExecution"; import { SpawnExecution } from "./SpawnExecution"; @@ -21,6 +21,7 @@ import { MissileSiloExecution } from "./MissileSiloExecution"; import { BattleshipExecution } from "./BattleshipExecution"; import { DefensePostExecution } from "./DefensePostExecution"; import { CityExecution } from "./CityExecution"; +import { TileRef } from "../game/GameMap"; @@ -30,7 +31,7 @@ export class Executor { // private random = new PseudoRandom(999) private random: PseudoRandom = null - constructor(private gs: Game, private gameID: GameID) { + constructor(private mg: Game, private gameID: GameID) { // Add one to avoid id collisions with bots. this.random = new PseudoRandom(simpleHash(gameID) + 1) } @@ -42,30 +43,23 @@ export class Executor { createExec(intent: Intent): Execution { switch (intent.type) { case "attack": { - const source: Cell | null = intent.sourceX != null && intent.sourceY != null - ? new Cell(intent.sourceX, intent.sourceY) - : null; - const target: Cell | null = intent.targetX != null && intent.targetY != null - ? new Cell(intent.targetX, intent.targetY) - : null; return new AttackExecution( intent.troops, intent.attackerID, intent.targetID, - source, - target, + null ); } case "spawn": return new SpawnExecution( new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID), - new Cell(intent.x, intent.y) + this.mg.ref(intent.x, intent.y) ); case "boat": return new TransportShipExecution( intent.attackerID, intent.targetID, - new Cell(intent.x, intent.y), + this.mg.ref(intent.x, intent.y), intent.troops ); case "allianceRequest": @@ -86,19 +80,19 @@ export class Executor { switch (intent.unit) { case UnitType.AtomBomb: case UnitType.HydrogenBomb: - return new NukeExecution(intent.unit, intent.player, new Cell(intent.x, intent.y)) + return new NukeExecution(intent.unit, intent.player, this.mg.ref(intent.x, intent.y)) case UnitType.Destroyer: - return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y)) + return new DestroyerExecution(intent.player, this.mg.ref(intent.x, intent.y)) case UnitType.Battleship: - return new BattleshipExecution(intent.player, new Cell(intent.x, intent.y)) + return new BattleshipExecution(intent.player, this.mg.ref(intent.x, intent.y)) case UnitType.Port: - return new PortExecution(intent.player, new Cell(intent.x, intent.y)) + return new PortExecution(intent.player, this.mg.ref(intent.x, intent.y)) case UnitType.MissileSilo: - return new MissileSiloExecution(intent.player, new Cell(intent.x, intent.y)) + return new MissileSiloExecution(intent.player, this.mg.ref(intent.x, intent.y)) case UnitType.DefensePost: - return new DefensePostExecution(intent.player, new Cell(intent.x, intent.y)) + return new DefensePostExecution(intent.player, this.mg.ref(intent.x, intent.y)) case UnitType.City: - return new CityExecution(intent.player, new Cell(intent.x, intent.y)) + return new CityExecution(intent.player, this.mg.ref(intent.x, intent.y)) default: throw Error(`unit type ${intent.unit} not supported`) } @@ -108,12 +102,12 @@ export class Executor { } spawnBots(numBots: number): Execution[] { - return new BotSpawner(this.gs, this.gameID).spawnBots(numBots).map(i => this.createExec(i)) + return new BotSpawner(this.mg, this.gameID).spawnBots(numBots).map(i => this.createExec(i)) } fakeHumanExecutions(): Execution[] { const execs = [] - for (const nation of this.gs.nations()) { + for (const nation of this.mg.nations()) { execs.push(new FakeHumanExecution( this.gameID, new PlayerInfo( @@ -123,7 +117,7 @@ export class Executor { this.random.nextID() ), nation.cell, - nation.strength * this.gs.config().difficultyModifier(this.gs.config().gameConfig().difficulty) + nation.strength * this.mg.config().difficultyModifier(this.mg.config().gameConfig().difficulty) )) } return execs diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 4597e01e9..cddf3b520 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -1,6 +1,5 @@ -import { AllianceRequest, Cell, Difficulty, Execution, MutableGame, MutablePlayer, Player, PlayerInfo, PlayerType, Relation, TerrainType, TerraNullius, Tick, Tile, UnitType } from "../game/Game" +import { AllianceRequest, Cell, Difficulty, Execution, MutableGame, MutablePlayer, Player, PlayerInfo, PlayerType, Relation, TerrainType, TerraNullius, UnitType } from "../game/Game" import { PseudoRandom } from "../PseudoRandom" -import { and, bfs, calculateBoundingBox, dist, euclDist, manhattanDist, simpleHash } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { SpawnExecution } from "./SpawnExecution"; @@ -15,6 +14,8 @@ import { MissileSiloExecution } from "./MissileSiloExecution"; import { EmojiExecution } from "./EmojiExecution"; import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution"; import { closestTwoTiles } from "./Util"; +import { calculateBoundingBox, simpleHash } from "../Util"; +import { andFN, manhattanDistFN, TileRef } from "../game/GameMap"; export class FakeHumanExecution implements Execution { @@ -52,7 +53,7 @@ export class FakeHumanExecution implements Execution { } this.mg.addExecution(new SpawnExecution( this.playerInfo, - rl.cell() + rl )) } return @@ -89,7 +90,9 @@ export class FakeHumanExecution implements Execution { this.handleEnemies() this.handleUnits() - const enemyborder = Array.from(this.player.borderTiles()).flatMap(t => t.neighbors()).filter(t => t.terrain().isLand() && t.owner() != this.player) + const enemyborder = Array.from(this.player.borderTiles()) + .flatMap(t => this.mg.neighbors(t)) + .filter(t => this.mg.isLake(t) && this.mg.ownerID(t) != this.player.smallID()) if (enemyborder.length == 0) { if (this.random.chance(5)) { @@ -102,7 +105,7 @@ export class FakeHumanExecution implements Execution { return } - const enemiesWithTN = enemyborder.map(t => t.owner()) + const enemiesWithTN = enemyborder.map(t => this.mg.playerBySmallID(this.mg.ownerID(t))) if (enemiesWithTN.filter(o => !o.isPlayer()).length > 0) { this.sendAttack(this.mg.terraNullius()) return @@ -224,15 +227,15 @@ export class FakeHumanExecution implements Execution { if (tile == null) { return } - for (const t of bfs(tile, dist(tile, 15))) { + for (const t of this.mg.bfs(tile, manhattanDistFN(tile, 15))) { // Make sure we nuke at least 15 tiles in border - if (t.hasOwner() && t.owner() != other) { + if (this.mg.owner(t) != other) { continue outer } } if (this.player.canBuild(UnitType.AtomBomb, tile)) { this.mg.addExecution( - new NukeExecution(UnitType.AtomBomb, this.player.id(), tile.cell()) + new NukeExecution(UnitType.AtomBomb, this.player.id(), tile) ) return } @@ -241,17 +244,18 @@ export class FakeHumanExecution implements Execution { private maybeSendBoatAttack(other: Player) { const closest = closestTwoTiles( - Array.from(this.player.borderTiles()).filter(t => t.terrain().isOceanShore()), - Array.from(other.borderTiles()).filter(t => t.terrain().isOceanShore()) + this.mg, + Array.from(this.player.borderTiles()).filter(t => this.mg.isOceanShore(t)), + Array.from(other.borderTiles()).filter(t => this.mg.isOceanShore(t)) ) if (closest == null) { return } - if (manhattanDist(closest.x.cell(), closest.y.cell()) < this.mg.config().boatMaxDistance()) { + if (this.mg.manhattanDist(closest.x, closest.y) < this.mg.config().boatMaxDistance()) { this.mg.addExecution(new TransportShipExecution( this.player.id(), other.id(), - closest.y.cell(), + closest.y, this.player.troops() / 5 )) } @@ -260,24 +264,24 @@ export class FakeHumanExecution implements Execution { private handleUnits() { const ports = this.player.units(UnitType.Port) if (ports.length == 0 && this.player.gold() > this.cost(UnitType.Port)) { - const oceanTiles = Array.from(this.player.borderTiles()).filter(t => t.terrain().isOceanShore()) + const oceanTiles = Array.from(this.player.borderTiles()).filter(t => this.mg.isOceanShore(t)) if (oceanTiles.length > 0) { const buildTile = this.random.randElement(oceanTiles) - this.mg.addExecution(new PortExecution(this.player.id(), buildTile.cell())) + this.mg.addExecution(new PortExecution(this.player.id(), buildTile)) } return } - this.maybeSpawnStructure(UnitType.City, 2, t => new CityExecution(this.player.id(), t.cell())) + this.maybeSpawnStructure(UnitType.City, 2, t => new CityExecution(this.player.id(), t)) if (this.maybeSpawnWarship(UnitType.Destroyer)) { return } if (this.maybeSpawnWarship(UnitType.Battleship)) { return } - this.maybeSpawnStructure(UnitType.MissileSilo, 1, t => new MissileSiloExecution(this.player.id(), t.cell())) + this.maybeSpawnStructure(UnitType.MissileSilo, 1, t => new MissileSiloExecution(this.player.id(), t)) } - private maybeSpawnStructure(type: UnitType, maxNum: number, build: (tile: Tile) => Execution) { + private maybeSpawnStructure(type: UnitType, maxNum: number, build: (tile: TileRef) => Execution) { const units = this.player.units(type) if (units.length >= maxNum) { return @@ -315,10 +319,10 @@ export class FakeHumanExecution implements Execution { } switch (shipType) { case UnitType.Destroyer: - this.mg.addExecution(new DestroyerExecution(this.player.id(), targetTile.cell())) + this.mg.addExecution(new DestroyerExecution(this.player.id(), targetTile)) break case UnitType.Battleship: - this.mg.addExecution(new BattleshipExecution(this.player.id(), targetTile.cell())) + this.mg.addExecution(new BattleshipExecution(this.player.id(), targetTile)) break } return true @@ -326,8 +330,8 @@ export class FakeHumanExecution implements Execution { return false } - private randTerritoryTile(p: Player): Tile | null { - const boundingBox = calculateBoundingBox(p.borderTiles()) + private randTerritoryTile(p: Player): TileRef | null { + const boundingBox = calculateBoundingBox(this.mg, p.borderTiles()) for (let i = 0; i < 100; i++) { const randX = this.random.nextInt(boundingBox.min.x, boundingBox.max.x) const randY = this.random.nextInt(boundingBox.min.y, boundingBox.max.y) @@ -335,29 +339,28 @@ export class FakeHumanExecution implements Execution { // Sanity check should never happen continue } - const randTile = this.mg.tile(new Cell(randX, randY)) - if (randTile.owner() == p) { + const randTile = this.mg.ref(randX, randY) + if (this.mg.owner(randTile) == p) { return randTile } } return null } - private warshipSpawnTile(portTile: Tile): Tile | null { + private warshipSpawnTile(portTile: TileRef): TileRef | null { const radius = this.mg.config().boatMaxDistance() / 2 for (let attempts = 0; attempts < 50; attempts++) { - const randX = this.random.nextInt(portTile.cell().x - radius, portTile.cell().x + radius) - const randY = this.random.nextInt(portTile.cell().y - radius, portTile.cell().y + radius) - const cell = new Cell(randX, randY) - if (!this.mg.isOnMap(cell)) { + const randX = this.random.nextInt(this.mg.x(portTile) - radius, this.mg.x(portTile) + radius) + const randY = this.random.nextInt(this.mg.y(portTile) - radius, this.mg.y(portTile) + radius) + if (!this.mg.isValidCoord(randX, randY)) { continue } + const tile = this.mg.ref(randX, randY) // Sanity check - if (manhattanDist(cell, portTile.cell()) >= this.mg.config().boatMaxDistance()) { + if (this.mg.manhattanDist(tile, portTile) >= this.mg.config().boatMaxDistance()) { continue } - const tile = this.mg.tile(cell) - if (!tile.terrain().isOcean()) { + if (!this.mg.isOcean(tile)) { continue } return tile @@ -394,13 +397,13 @@ export class FakeHumanExecution implements Execution { ) } - sendBoat(tries: number = 0, oceanShore: Tile[] = null) { + sendBoat(tries: number = 0, oceanShore: TileRef[] = null) { if (tries > 10) { return } if (oceanShore == null) { - oceanShore = Array.from(this.player.borderTileRefs()).filter(t => this.mg.isOceanShore(t)).map(tr => this.mg.fromRef(tr)) + oceanShore = Array.from(this.player.borderTiles()).filter(t => this.mg.isOceanShore(t)) } if (oceanShore.length == 0) { return @@ -408,11 +411,11 @@ export class FakeHumanExecution implements Execution { const src = this.random.randElement(oceanShore) const otherShore = Array.from( - bfs( + this.mg.bfs( src, - and((t) => t.terrain().isOcean() || t.terrain().isOceanShore(), dist(src, 200)) + andFN((gm, t) => gm.isOcean(t) || gm.isOceanShore(t), manhattanDistFN(src, 200)) ) - ).filter(t => t.terrain().isOceanShore() && t.owner() != this.player) + ).filter(t => this.mg.isOceanShore(t) && this.mg.owner(t) != this.player) if (otherShore.length == 0) { return @@ -423,14 +426,14 @@ export class FakeHumanExecution implements Execution { if (this.isSmallIsland(dst)) { continue } - if (dst.owner().isPlayer() && this.player.isAlliedWith(dst.owner() as Player)) { + if (this.mg.owner(dst).isPlayer() && this.player.isAlliedWith(this.mg.owner(dst) as Player)) { continue } this.mg.addExecution(new TransportShipExecution( this.player.id(), - dst.hasOwner() ? dst.owner().id() : null, - dst.cell(), + this.mg.hasOwner(dst) ? this.mg.owner(dst).id() : null, + dst, this.player.troops() / 5, )) return @@ -438,21 +441,19 @@ export class FakeHumanExecution implements Execution { this.sendBoat(tries + 1, oceanShore) } - randomLand(): Tile | null { + randomLand(): TileRef | null { const delta = 25 let tries = 0 while (tries < 50) { tries++ - const cell = new Cell( - this.random.nextInt(this.cell.x - delta, this.cell.x + delta), - this.random.nextInt(this.cell.y - delta, this.cell.y + delta) - ) - if (!this.mg.isOnMap(cell)) { + const x = this.random.nextInt(this.cell.x - delta, this.cell.x + delta) + const y = this.random.nextInt(this.cell.y - delta, this.cell.y + delta) + if (!this.mg.isValidCoord(x, y)) { continue } - const tile = this.mg.tile(cell) - if (tile.terrain().isLand() && !tile.hasOwner()) { - if (tile.terrain().type() == TerrainType.Mountain && this.random.chance(2)) { + const tile = this.mg.ref(x, y) + if (this.mg.isLand(tile) && !this.mg.hasOwner(tile)) { + if (this.mg.terrainType(tile) == TerrainType.Mountain && this.random.chance(2)) { continue } return tile @@ -471,8 +472,8 @@ export class FakeHumanExecution implements Execution { )) } - isSmallIsland(tile: Tile): boolean { - return bfs(tile, and((t) => t.terrain().isLand(), dist(tile, 10))).size < 50 + isSmallIsland(tile: TileRef): boolean { + return this.mg.bfs(tile, andFN((gm, t) => gm.isLand(t), manhattanDistFN(tile, 10))).size < 50 } owner(): MutablePlayer { diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index 4a233b786..85449dc5a 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -1,5 +1,6 @@ import { consolex } from "../Consolex"; -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, UnitType } from "../game/Game"; +import { TileRef } from "../game/GameMap"; export class MissileSiloExecution implements Execution { @@ -10,7 +11,7 @@ export class MissileSiloExecution implements Execution { constructor( private _owner: PlayerID, - private cell: Cell + private tile: TileRef ) { } @@ -21,13 +22,12 @@ export class MissileSiloExecution implements Execution { tick(ticks: number): void { if (this.silo == null) { - const tile = this.mg.tile(this.cell) - if (!this.player.canBuild(UnitType.MissileSilo, tile)) { - consolex.warn(`player ${this.player} cannot build port at ${this.cell}`) + if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) { + consolex.warn(`player ${this.player} cannot build port at ${this.tile}`) this.active = false return } - this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, tile) + this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile) } } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index eb929127a..b1ae2f519 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,10 +1,10 @@ import { nextTick } from "process"; -import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile, MutableUnit, UnitType, Player, TerraNullius } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, MutableUnit, UnitType, Player, TerraNullius } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; -import { bfs, dist, distSortUnit, euclideanDist, manhattanDist } from "../Util"; import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; export class NukeExecution implements Execution { @@ -15,13 +15,12 @@ export class NukeExecution implements Execution { private mg: MutableGame private nuke: MutableUnit - private dst: Tile private pathFinder: PathFinder constructor( private type: UnitType.AtomBomb | UnitType.HydrogenBomb, private senderID: PlayerID, - private cell: Cell, + private dst: TileRef, ) { } @@ -29,11 +28,10 @@ export class NukeExecution implements Execution { this.mg = mg this.pathFinder = PathFinder.Mini(mg, 10_000, true) this.player = mg.player(this.senderID) - this.dst = this.mg.tile(this.cell) } public target(): Player | TerraNullius { - return this.dst.owner() + return this.mg.owner(this.dst) } tick(ticks: number): void { @@ -70,9 +68,8 @@ export class NukeExecution implements Execution { private detonate() { const magnitude = this.type == UnitType.AtomBomb ? { inner: 15, outer: 40 } : { inner: 140, outer: 160 } const rand = new PseudoRandom(this.mg.ticks()) - const tile = this.mg.tile(this.cell) - const toDestroy = bfs(tile, (n: Tile) => { - const d = euclideanDist(tile.cell(), n.cell()) + const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => { + const d = this.mg.euclideanDist(this.dst, n) return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer }) @@ -81,7 +78,7 @@ export class NukeExecution implements Execution { ) const attacked = new Map() for (const tile of toDestroy) { - const owner = tile.owner() + const owner = this.mg.owner(tile) if (owner.isPlayer()) { const mp = this.mg.player(owner.id()) mp.relinquish(tile) @@ -92,8 +89,8 @@ export class NukeExecution implements Execution { const prev = attacked.get(mp) attacked.set(mp, prev + 1) } - if (tile.terrain().isLand()) { - this.mg.addFallout(tile) + if (this.mg.isLand(tile)) { + this.mg.setFallout(tile, true) } } for (const [other, tilesDestroyed] of attacked) { @@ -110,7 +107,7 @@ export class NukeExecution implements Execution { for (const unit of this.mg.units()) { if (unit.type() != UnitType.AtomBomb && unit.type() != UnitType.HydrogenBomb) { - if (euclideanDist(this.cell, unit.tile().cell()) < magnitude.outer) { + if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) { unit.delete() } } diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 8207cd3ed..fec9ee955 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -1,9 +1,9 @@ import { Config } from "../configuration/Config" -import { Execution, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, UnitType } from "../game/Game" -import { bfs, calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util" +import { Execution, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, UnitType } from "../game/Game" +import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util" import { GameImpl } from "../game/GameImpl" import { consolex } from "../Consolex" -import { TileRef } from "../game/GameMap" +import { GameMap, TileRef } from "../game/GameMap" export class PlayerExecution implements Execution { @@ -37,7 +37,7 @@ export class PlayerExecution implements Execution { return } u.modifyHealth(1) - const tileOwner = u.tile().owner() + const tileOwner = this.mg.owner(u.tile()) if (u.info().territoryBound) { if (tileOwner.isPlayer()) { if (tileOwner != this.player) { @@ -80,8 +80,7 @@ export class PlayerExecution implements Execution { if (this.player.lastTileChange() > this.lastCalc) { this.lastCalc = ticks const start = performance.now() - // TODO - // this.removeClusters() + this.removeClusters() const end = performance.now() if (end - start > 1000) { consolex.log(`player ${this.player.name()}, took ${end - start}ms`) @@ -143,8 +142,8 @@ export class PlayerExecution implements Execution { if (enemyTiles.size == 0) { return false } - const enemyBox = calculateBoundingBox(new Set(Array.from(enemyTiles).map(tr => this.mg.fromRef(tr)))) - const clusterBox = calculateBoundingBox(new Set(Array.from(cluster).map(tr => this.mg.fromRef(tr)))) + const enemyBox = calculateBoundingBox(this.mg, enemyTiles) + const clusterBox = calculateBoundingBox(this.mg, cluster) return inscribed(enemyBox, clusterBox) } @@ -161,8 +160,8 @@ export class PlayerExecution implements Execution { return } const firstTile = arr[0] - const filter = (n: Tile): boolean => n.owner().smallID() == this.mg.ownerID(firstTile) - const tiles = bfs(this.mg.fromRef(firstTile), filter) + const filter = (_, t: TileRef): boolean => this.mg.ownerID(t) == this.mg.ownerID(firstTile) + const tiles = this.mg.bfs(firstTile, filter) const modePlayer = this.mg.playerBySmallID(mode) if (!modePlayer.isPlayer()) { @@ -175,7 +174,7 @@ export class PlayerExecution implements Execution { private calculateClusters(): Set[] { const seen = new Set() - const border = this.player.borderTileRefs() + const border = this.player.borderTiles() const clusters: Set[] = [] for (const tile of border) { if (seen.has(tile)) { @@ -191,12 +190,12 @@ export class PlayerExecution implements Execution { const curr = queue.shift() cluster.add(curr) - const neighbors = (this.mg as GameImpl).neighborsWithDiag(this.mg.fromRef(curr)) + const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr) for (const neighbor of neighbors) { - if (neighbor.isBorder() && border.has(neighbor.ref())) { - if (!seen.has(neighbor.ref())) { - queue.push(neighbor.ref()) - seen.add(neighbor.ref()) + if (this.mg.isBorder(neighbor) && border.has(neighbor)) { + if (!seen.has(neighbor)) { + queue.push(neighbor) + seen.add(neighbor) } } } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 5a61af92e..b98913b7d 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,13 +1,11 @@ -import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, TerrainType, Tile, Unit, UnitType } from "../game/Game"; +import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, TerrainType, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; -import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; -import { bfs, dist, manhattanDist } from "../Util"; import { TradeShipExecution } from "./TradeShipExecution"; import { consolex } from "../Consolex"; import { MiniAStar } from "../pathfinding/MiniAStar"; -import { TileRef } from "../game/GameMap"; +import { manhattanDistFN, TileRef } from "../game/GameMap"; export class PortExecution implements Execution { @@ -15,12 +13,12 @@ export class PortExecution implements Execution { private mg: MutableGame private port: MutableUnit private random: PseudoRandom - private portPaths = new Map() + private portPaths = new Map() private computingPaths = new Map() constructor( private _owner: PlayerID, - private cell: Cell, + private tile: TileRef, ) { } @@ -33,16 +31,16 @@ export class PortExecution implements Execution { if (this.port == null) { // TODO: use canBuild - const tile = this.mg.tile(this.cell) + const tile = this.tile const player = this.mg.player(this._owner) if (!player.canBuild(UnitType.Port, tile)) { - consolex.warn(`player ${player} cannot build port at ${this.cell}`) + consolex.warn(`player ${player} cannot build port at ${this.tile}`) this.active = false return } - const spawns = Array.from(bfs(tile, dist(tile, 20))) - .filter(t => t.terrain().isOceanShore() && t.owner() == player) - .sort((a, b) => manhattanDist(a.cell(), tile.cell()) - manhattanDist(b.cell(), tile.cell())) + const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20))) + .filter(t => this.mg.isOceanShore(t) && this.mg.owner(t) == player) + .sort((a, b) => this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile)) if (spawns.length == 0) { consolex.warn(`cannot find spawn for port`) @@ -73,7 +71,7 @@ export class PortExecution implements Execution { const aStar = this.computingPaths.get(port) switch (aStar.compute()) { case PathFindResultType.Completed: - this.portPaths.set(port, aStar.reconstructPath().map(cell => this.mg.tile(cell))) + this.portPaths.set(port, aStar.reconstructPath()) this.computingPaths.delete(port) break case PathFindResultType.Pending: @@ -88,8 +86,8 @@ export class PortExecution implements Execution { const pf = new MiniAStar( this.mg.map(), this.mg.miniMap(), - this.port.tile().ref(), - port.tile().ref(), + this.port.tile(), + port.tile(), (tr: TileRef) => this.mg.miniMap().isOcean(tr), 10_000, 25 diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 47cc88980..234b77d25 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -1,7 +1,8 @@ -import { Execution, MutableGame, MutablePlayer, MutableUnit, Tile, Unit, UnitType } from "../game/Game"; +import { Execution, MutableGame, MutablePlayer, MutableUnit, Unit, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; export class ShellExecution implements Execution { @@ -9,7 +10,7 @@ export class ShellExecution implements Execution { private pathFinder: PathFinder private shell: MutableUnit - constructor(private spawn: Tile, private _owner: MutablePlayer, private ownerUnit: Unit, private target: MutableUnit) { + constructor(private spawn: TileRef, private _owner: MutablePlayer, private ownerUnit: Unit, private target: MutableUnit) { } diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index f8d1e26c5..25eeb2b09 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -1,4 +1,5 @@ import { Cell, Execution, MutableGame, MutablePlayer, PlayerInfo, PlayerType } from "../game/Game" +import { TileRef } from "../game/GameMap" import { BotExecution } from "./BotExecution" import { PlayerExecution } from "./PlayerExecution" import { getSpawnTiles } from "./Util" @@ -10,7 +11,7 @@ export class SpawnExecution implements Execution { constructor( private playerInfo: PlayerInfo, - private cell: Cell + private tile: TileRef ) { } init(mg: MutableGame, ticks: number) { @@ -25,17 +26,16 @@ export class SpawnExecution implements Execution { } const existing = this.mg.players().find(p => p.id() == this.playerInfo.id) - const tile = this.mg.tile(this.cell) if (existing) { existing.tiles().forEach(t => existing.relinquish(t)) - getSpawnTiles(tile).forEach(t => { + getSpawnTiles(this.mg, this.tile).forEach(t => { existing.conquer(t) }) return } const player = this.mg.addPlayer(this.playerInfo, this.mg.config().startManpower(this.playerInfo)) - getSpawnTiles(tile).forEach(t => { + getSpawnTiles(this.mg, this.tile).forEach(t => { player.conquer(t) }) this.mg.addExecution(new PlayerExecution(player.id())) diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 57aa0a2a9..2cf543d12 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -1,12 +1,11 @@ import { MessageType } from '../game/Game'; import { renderNumber } from "../../client/Utils"; -import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; +import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; -import { SerialAStar } from "../pathfinding/SerialAStar"; -import { PseudoRandom } from "../PseudoRandom"; -import { bfs, dist, distSortUnit, manhattanDist } from "../Util"; +import { distSortUnit } from "../Util"; import { consolex } from "../Consolex"; +import { TileRef } from '../game/GameMap'; export class TradeShipExecution implements Execution { @@ -23,7 +22,7 @@ export class TradeShipExecution implements Execution { private dstPort: MutableUnit, private pathFinder: PathFinder, // don't modify - private path: Tile[] + private path: TileRef[] ) { } @@ -60,7 +59,7 @@ export class TradeShipExecution implements Execution { } if (this.wasCaptured) { - const ports = this.tradeShip.owner().units(UnitType.Port).sort(distSortUnit(this.tradeShip)) + const ports = this.tradeShip.owner().units(UnitType.Port).sort(distSortUnit(this.mg, this.tradeShip)) if (ports.length == 0) { this.tradeShip.delete(false) this.active = false @@ -70,7 +69,7 @@ export class TradeShipExecution implements Execution { const result = this.pathFinder.nextTile(this.tradeShip.tile(), dstPort.tile()) switch (result.type) { case PathFindResultType.Completed: - const gold = this.mg.config().tradeShipGold(this.srcPort, dstPort) + const gold = this.mg.config().tradeShipGold(this.mg.manhattanDist(this.srcPort.tile(), dstPort.tile())) this.tradeShip.owner().addGold(gold) this.mg.displayMessage( `Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`, @@ -97,7 +96,7 @@ export class TradeShipExecution implements Execution { if (this.index >= this.path.length) { this.active = false - const gold = this.mg.config().tradeShipGold(this.srcPort, this.dstPort) + const gold = this.mg.config().tradeShipGold(this.mg.manhattanDist(this.srcPort.tile(), this.dstPort.tile())) this.srcPort.owner().addGold(gold) this.dstPort.owner().addGold(gold) this.mg.displayMessage( diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index c5caa165d..f8d11e640 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -1,12 +1,11 @@ -import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, UnitType, TerrainType } from "../game/Game"; -import { and, bfs, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../Util"; +import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, UnitType, TerrainType } from "../game/Game"; import { AttackExecution } from "./AttackExecution"; import { MessageType } from '../game/Game'; -import { DisplayMessageUpdate } from '../game/Game'; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; -import { SerialAStar } from "../pathfinding/SerialAStar"; import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; +import { targetTransportTile } from "../Util"; export class TransportShipExecution implements Execution { @@ -22,9 +21,9 @@ export class TransportShipExecution implements Execution { private target: MutablePlayer | TerraNullius // TODO make private - public path: Tile[] - private src: Tile | null - private dst: Tile | null + public path: TileRef[] + private src: TileRef | null + private dst: TileRef | null private boat: MutableUnit @@ -34,7 +33,7 @@ export class TransportShipExecution implements Execution { constructor( private attackerID: PlayerID, private targetID: PlayerID | null, - private cell: Cell, + private ref: TileRef, private troops: number | null, ) { } @@ -68,7 +67,7 @@ export class TransportShipExecution implements Execution { this.troops = Math.min(this.troops, this.attacker.troops()) - this.dst = targetTransportTile(this.mg.width(), this.mg.tile(this.cell)) + this.dst = targetTransportTile(this.mg, this.ref) if (this.dst == null) { consolex.warn(`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`) this.active = false @@ -102,7 +101,7 @@ export class TransportShipExecution implements Execution { const result = this.pathFinder.nextTile(this.boat.tile(), this.dst) switch (result.type) { case PathFindResultType.Completed: - if (this.dst.owner() == this.attacker) { + if (this.mg.owner(this.dst) == this.attacker) { this.attacker.addTroops(this.troops) this.boat.delete(false) this.active = false @@ -113,7 +112,7 @@ export class TransportShipExecution implements Execution { } else { this.attacker.conquer(this.dst) this.mg.addExecution( - new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst.cell(), null, false) + new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst, false) ) } this.boat.delete(false) diff --git a/src/core/execution/Util.ts b/src/core/execution/Util.ts index 7c60e603d..aa44b65c9 100644 --- a/src/core/execution/Util.ts +++ b/src/core/execution/Util.ts @@ -1,15 +1,14 @@ -import { Game, Cell, Tile } from "../game/Game"; -import { and, bfs, euclDist } from "../Util"; +import { euclDistFN, GameMap, TileRef } from "../game/GameMap"; -export function getSpawnTiles(tile: Tile): Tile[] { - return Array.from(bfs(tile, euclDist(tile, 4))) - .filter(t => !t.hasOwner() && t.terrain().isLand()) +export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] { + return Array.from(gm.bfs(tile, euclDistFN(tile, 4))) + .filter(t => !gm.hasOwner(t) && gm.isLand(t)) } -export function closestTwoTiles(x: Iterable, y: Iterable): { x: Tile, y: Tile } { - const xSorted = Array.from(x).sort((a, b) => a.cell().x - b.cell().x); - const ySorted = Array.from(y).sort((a, b) => a.cell().x - b.cell().x); +export function closestTwoTiles(gm: GameMap, x: Iterable, y: Iterable): { x: TileRef, y: TileRef } { + const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b)); + const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b)); if (xSorted.length == 0 || ySorted.length == 0) { return null; @@ -25,8 +24,8 @@ export function closestTwoTiles(x: Iterable, y: Iterable): { x: Tile const currentY = ySorted[j]; const distance = - Math.abs(currentX.cell().x - currentY.cell().x) + - Math.abs(currentX.cell().y - currentY.cell().y); + Math.abs(gm.x(currentX) - gm.x(currentY)) + + Math.abs(gm.y(currentX) - gm.y(currentY)); if (distance < minDistance) { minDistance = distance; @@ -42,7 +41,7 @@ export function closestTwoTiles(x: Iterable, y: Iterable): { x: Tile i++; } // Otherwise, move whichever pointer has smaller x value - else if (currentX.cell().x < currentY.cell().x) { + else if (gm.x(currentX) < gm.x(currentY)) { i++; } else { j++; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 256d2fed5..5ca1ac95c 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -1,7 +1,7 @@ import { Config } from "../configuration/Config" import { GameEvent } from "../EventBus" import { ClientID, GameConfig, GameID } from "../Schemas" -import { GameMap, GameMapImpl, TileRef } from "./GameMap" +import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap" export type PlayerID = string export type Tick = number @@ -15,7 +15,7 @@ type UpdateTypeMap = Extract; // Then use it to create the record type export type GameUpdates = { - [K in GameUpdateType]: UpdateTypeMap[]; + [K in GameUpdateType]: UpdateTypeMap[] } export interface MapPos { @@ -176,76 +176,26 @@ export class PlayerInfo { ) { } } -export interface TerrainMap { - terrain(cell: Cell): TerrainTile - neighbors(terrainTile: TerrainTile): TerrainTile[] - width(): number - height(): number - isOnMap(cell: Cell): boolean - numLandTiles(): number -} - -export type TerrainTileKey = string - - - -export interface TerrainTile { - isLand(): boolean - isShore(): boolean - isOceanShore(): boolean - isWater(): boolean - isShorelineWater(): boolean - isOcean(): boolean - isLake(): boolean - type(): TerrainType - magnitude(): number - equals(other: TerrainTile): boolean - cell(): Cell - neighbors(): TerrainTile[] - cost(): number -} - export interface DefenseBonus { // Unit providing the defense bonus unit: Unit amount: number - tile: Tile -} - -export interface Tile { - owner(): Player | TerraNullius - hasOwner(): boolean - isBorder(): boolean - cell(): Cell - hasFallout(): boolean - terrain(): TerrainTile - neighbors(): Tile[] - hasDefenseBonus(): boolean - ref(): TileRef -} - -export interface MutableTile extends Tile { - // defense bonus against this player - defenseBonus(player: Player): number - borders(other: Player | TerraNullius): boolean - neighborsWrapped(): Tile[] - defenseBonuses(): DefenseBonus[] - toUpdate(isBorderOnly: boolean): TileUpdate + tile: TileRef } export interface Unit { type(): UnitType troops(): number - tile(): Tile + tile(): TileRef owner(): Player isActive(): boolean hasHealth(): boolean health(): number - lastTile(): Tile + lastTile(): TileRef } export interface MutableUnit extends Unit { - move(tile: Tile): void + move(tile: TileRef): void owner(): MutablePlayer setTroops(troops: number): void info(): UnitInfo @@ -255,7 +205,6 @@ export interface MutableUnit extends Unit { } export interface TerraNullius { - ownsTile(cell: Cell): boolean isPlayer(): false id(): PlayerID // always zero, maybe make it TerraNulliusID? clientID(): ClientID @@ -272,8 +221,7 @@ export interface Player { type(): PlayerType units(...types: UnitType[]): Unit[] isAlive(): boolean - borderTileRefs(): ReadonlySet - borderTiles(): ReadonlySet + borderTiles(): ReadonlySet isPlayer(): this is Player numTilesOwned(): number sharesBorderWith(other: Player | TerraNullius): boolean @@ -306,7 +254,7 @@ export interface Player { troops(): number // If can build returns the spawn tile, false otherwise - canBuild(type: UnitType, targetTile: Tile): Tile | false + canBuild(type: UnitType, targetTile: TileRef): TileRef | false lastTileChange(): Tick } @@ -315,11 +263,9 @@ export interface MutablePlayer extends Player { targets(): Player[] // Targets of player and all allies. neighbors(): (Player | TerraNullius)[] - tiles(): ReadonlySet - ownsTile(cell: Cell): boolean - tiles(): ReadonlySet - conquer(tile: Tile): void - relinquish(tile: Tile): void + tiles(): ReadonlySet + conquer(tile: TileRef): void + relinquish(tile: TileRef): void executions(): Execution[] neighbors(): (MutablePlayer | TerraNullius)[] units(...types: UnitType[]): MutableUnit[] @@ -348,7 +294,7 @@ export interface MutablePlayer extends Player { addTroops(troops: number): void removeTroops(troops: number): number - buildUnit(type: UnitType, troops: number, tile: Tile): MutableUnit + buildUnit(type: UnitType, troops: number, tile: TileRef): MutableUnit captureUnit(unit: MutableUnit): void toUpdate(): PlayerUpdate @@ -360,11 +306,10 @@ export interface Game extends GameMap { playerByClientID(id: ClientID): Player | null hasPlayer(id: PlayerID): boolean players(): Player[] - tile(cell: Cell): Tile isOnMap(cell: Cell): boolean width(): number height(): number - forEachTile(fn: (tile: Tile) => void): void + forEachTile(fn: (tile: TileRef) => void): void executions(): ExecutionView[] terraNullius(): TerraNullius executeNextTick(): GameUpdates @@ -377,13 +322,12 @@ export interface Game extends GameMap { units(...types: UnitType[]): Unit[] unitInfo(type: UnitType): UnitInfo playerBySmallID(id: number): Player | TerraNullius - fromRef(ref: TileRef): Tile - map(): GameMapImpl - miniMap(): GameMapImpl + map(): GameMap + miniMap(): GameMap + owner(ref: TileRef): Player | TerraNullius } export interface MutableGame extends Game { - tile(cell: Cell): MutableTile player(id: PlayerID): MutablePlayer playerByClientID(id: ClientID): MutablePlayer | null players(): MutablePlayer[] @@ -391,9 +335,9 @@ export interface MutableGame extends Game { addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer executions(): Execution[] units(...types: UnitType[]): MutableUnit[] - addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus + addTileDefenseBonus(tile: TileRef, unit: Unit, amount: number): DefenseBonus removeTileDefenseBonus(bonus: DefenseBonus): void - addFallout(tile: Tile): void + addFallout(tile: TileRef): void setWinner(winner: Player): void } @@ -438,7 +382,7 @@ export interface PlayerInteraction { canDonate: boolean } -export type GameUpdate = TileUpdate +export type GameUpdate = TileUpdateWrapper | UnitUpdate | PlayerUpdate | AllianceRequestUpdate @@ -450,13 +394,9 @@ export type GameUpdate = TileUpdate | EmojiUpdate | WinUpdate -export interface TileUpdate { - type: GameUpdateType.Tile - ownerID: number - pos: MapPos - isBorder: boolean - hasFallout: boolean - hasDefenseBonus: boolean +export interface TileUpdateWrapper { + type: GameUpdateType.Tile, + update: TileUpdate } export interface UnitUpdate { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index d27e15a9c..3eab4883b 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,18 +1,17 @@ import { Config } from "../configuration/Config"; -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerID, PlayerInfo, Player, TerraNullius, Tile, Unit, MutableAllianceRequest, Alliance, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus, MutableTile, GameUpdate, GameUpdateType, AllPlayers, GameUpdates } from "./Game"; -import { NationMap, TerrainMapImpl } from "./TerrainMapLoader"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerID, PlayerInfo, Player, TerraNullius, Unit, MutableAllianceRequest, Alliance, Nation, UnitType, UnitInfo, DefenseBonus, GameUpdate, GameUpdateType, AllPlayers, GameUpdates, TerrainType } from "./Game"; +import { NationMap } from "./TerrainMapLoader"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; -import { TileImpl } from "./TileImpl"; import { AllianceRequestImpl } from "./AllianceRequestImpl"; import { AllianceImpl } from "./AllianceImpl"; import { ClientID, GameConfig } from "../Schemas"; import { MessageType } from './Game'; import { UnitImpl } from "./UnitImpl"; import { consolex } from "../Consolex"; -import { GameMapImpl, TileRef } from "./GameMap"; +import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; -export function createGame(gameMap: GameMapImpl, miniGameMap: GameMapImpl, nationMap: NationMap, config: Config): Game { +export function createGame(gameMap: GameMap, miniGameMap: GameMap, nationMap: NationMap, config: Config): Game { return new GameImpl(gameMap, miniGameMap, nationMap, config) } @@ -43,8 +42,8 @@ export class GameImpl implements MutableGame { private updates: GameUpdates = createGameUpdatesMap() constructor( - private _map: GameMapImpl, - private miniGameMap: GameMapImpl, + private _map: GameMap, + private miniGameMap: GameMap, nationMap: NationMap, private _config: Config, ) { @@ -58,16 +57,19 @@ export class GameImpl implements MutableGame { n.strength )) } + owner(ref: TileRef): Player | TerraNullius { + return this.playerBySmallID(this.ownerID(ref)) + } playerBySmallID(id: number): Player | TerraNullius { if (id == 0) { return this.terraNullius() } return this._playersBySmallID[id - 1] } - map(): GameMapImpl { + map(): GameMap { return this._map } - miniMap(): GameMapImpl { + miniMap(): GameMap { return this.miniGameMap } @@ -82,16 +84,18 @@ export class GameImpl implements MutableGame { return old } - addFallout(tile: Tile) { - const ti = tile as TileImpl - if (tile.hasOwner()) { + addFallout(tile: TileRef) { + if (this.hasOwner(tile)) { throw Error(`cannot set fallout, tile ${tile} has owner`) } - this._map.setFallout(tile.ref(), true) - this.addUpdate(ti.toUpdate()) + this._map.setFallout(tile, true) + this.addUpdate({ + type: GameUpdateType.Tile, + update: this.toTileUpdate(tile) + }) } - addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus { + addTileDefenseBonus(tile: TileRef, unit: Unit, amount: number): DefenseBonus { // TODO!! const df = { unit: unit, tile: tile, amount: amount }; // (tile as TileImpl)._defenseBonuses.push(df) @@ -257,13 +261,6 @@ export class GameImpl implements MutableGame { this.unInitExecs = this.unInitExecs.filter(execution => execution !== exec) } - forEachTile(fn: (tile: Tile) => void): void { - for (let x = 0; x < this._width; x++) { - for (let y = 0; y < this._height; y++) { - fn(this.tile(new Cell(x, y))) - } - } - } playerView(id: PlayerID): MutablePlayer { return this.player(id) @@ -294,11 +291,6 @@ export class GameImpl implements MutableGame { } - tile(cell: Cell): MutableTile { - this.assertIsOnMap(cell) - return new TileImpl(this, this._map.ref(cell.x, cell.y)) - } - isOnMap(cell: Cell): boolean { return cell.x >= 0 && cell.x < this._width @@ -306,22 +298,17 @@ export class GameImpl implements MutableGame { && cell.y < this._height } - fromRef(ref: TileRef): Tile { - return new TileImpl(this, ref) - } - - - neighborsWithDiag(tile: Tile): Tile[] { - const x = tile.cell().x - const y = tile.cell().y - const ns: Tile[] = [] + neighborsWithDiag(tile: TileRef): TileRef[] { + const x = this.x(tile) + const y = this.y(tile) + const ns: TileRef[] = [] for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { if (dx === 0 && dy === 0) continue // Skip the center tile const newX = x + dx const newY = y + dy if (newX >= 0 && newX < this._width && newY >= 0 && newY < this._height) { - ns.push(this.fromRef(this._map.ref(newX, newY))) + ns.push(this._map.ref(newX, newY)) } } } @@ -334,75 +321,78 @@ export class GameImpl implements MutableGame { } } - conquer(owner: PlayerImpl, tile: Tile): void { - if (!tile.terrain().isLand()) { + conquer(owner: PlayerImpl, tile: TileRef): void { + if (!this.isLand(tile)) { throw Error(`cannot conquer water`) } - const tileImpl = tile as TileImpl - let previousOwner = tileImpl.owner() as TerraNullius | PlayerImpl + let previousOwner = this.owner(tile) as TerraNullius | PlayerImpl if (previousOwner.isPlayer()) { previousOwner._lastTileChange = this._ticks - previousOwner._tiles.delete(tile.cell().toString()) - previousOwner._borderTiles.delete(tileImpl.ref()) - this._map.setBorder(tileImpl.ref(), false) + previousOwner._tiles.delete(tile) + previousOwner._borderTiles.delete(tile) + this._map.setBorder(tile, false) } - this._map.setOwnerID(tileImpl.ref(), owner.smallID()) - owner._tiles.set(tile.cell().toString(), tile) + this._map.setOwnerID(tile, owner.smallID()) + owner._tiles.add(tile) owner._lastTileChange = this._ticks this.updateBorders(tile) - this._map.setFallout(tileImpl.ref(), false) - this.addUpdate((tile as TileImpl).toUpdate()) + this._map.setFallout(tile, false) + this.addUpdate({ + type: GameUpdateType.Tile, + update: this.toTileUpdate(tile) + }) } - relinquish(tile: Tile) { - if (!tile.hasOwner()) { - throw new Error(`Cannot relinquish tile because it is unowned: cell ${tile.cell().toString()}`) + relinquish(tile: TileRef) { + if (!this.hasOwner(tile)) { + throw new Error(`Cannot relinquish tile because it is unowned`) } - if (tile.terrain().isWater()) { + if (this.isWater(tile)) { throw new Error("Cannot relinquish water") } - const tileImpl = tile as TileImpl - let previousOwner = tileImpl.owner() as PlayerImpl + let previousOwner = this.owner(tile) as PlayerImpl previousOwner._lastTileChange = this._ticks - previousOwner._tiles.delete(tile.cell().toString()) - previousOwner._borderTiles.delete(tileImpl.ref()) - this._map.setBorder(tileImpl.ref(), false) + previousOwner._tiles.delete(tile) + previousOwner._borderTiles.delete(tile) + this._map.setBorder(tile, false) - this._map.setOwnerID(tileImpl.ref(), 0) + this._map.setOwnerID(tile, 0) this.updateBorders(tile) - this.addUpdate( - (tile as TileImpl).toUpdate() + this.addUpdate({ + type: GameUpdateType.Tile, + update: this.toTileUpdate(tile) + } ) } - private updateBorders(tile: Tile) { - const tiles: TileImpl[] = [] - tiles.push(tile as TileImpl) - tile.neighbors().forEach(t => tiles.push(t as TileImpl)) + private updateBorders(tile: TileRef) { + const tiles: TileRef[] = [] + tiles.push(tile) + this.neighbors(tile).forEach(t => tiles.push(t)) for (const t of tiles) { - if (!t.hasOwner()) { - this._map.setBorder(t.ref(), false) + if (!this.hasOwner(t)) { + this._map.setBorder(t, false) continue } if (this.calcIsBorder(t)) { - (t.owner() as PlayerImpl)._borderTiles.add(t.ref()); - this._map.setBorder(t.ref(), true) + (this.owner(t) as PlayerImpl)._borderTiles.add(t); + this._map.setBorder(t, true) } else { - (t.owner() as PlayerImpl)._borderTiles.delete(t.ref()); - this._map.setBorder(t.ref(), false) + (this.owner(t) as PlayerImpl)._borderTiles.delete(t); + this._map.setBorder(t, false) } // this.updates.push(t.toUpdate()) } } - private calcIsBorder(tile: Tile): boolean { - if (!tile.hasOwner()) { + private calcIsBorder(tile: TileRef): boolean { + if (!this.hasOwner(tile)) { return false } - for (const neighbor of (tile as MutableTile).neighbors()) { - let bordersEnemy = tile.owner() != neighbor.owner() + for (const neighbor of this.neighbors(tile)) { + let bordersEnemy = this.owner(tile) != this.owner(neighbor) if (bordersEnemy) { return true } @@ -517,6 +507,17 @@ export class GameImpl implements MutableGame { isBorder(ref: TileRef): boolean { return this._map.isBorder(ref) } setBorder(ref: TileRef, value: boolean): void { return this._map.setBorder(ref, value) } neighbors(ref: TileRef): TileRef[] { return this._map.neighbors(ref) } + isWater(ref: TileRef): boolean { return this._map.isWater(ref) } + isLake(ref: TileRef): boolean { return this._map.isLake(ref) } + isShore(ref: TileRef): boolean { return this._map.isShore(ref) } + cost(ref: TileRef): number { return this._map.cost(ref) } + terrainType(ref: TileRef): TerrainType { return this._map.terrainType(ref) } + forEachTile(fn: (tile: TileRef) => void): void { return this._map.forEachTile(fn) } + manhattanDist(c1: TileRef, c2: TileRef): number { return this._map.manhattanDist(c1, c2) } + euclideanDist(c1: TileRef, c2: TileRef): number { return this._map.euclideanDist(c1, c2) } + bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set { return this._map.bfs(tile, filter) } + toTileUpdate(tile: TileRef): bigint { return this._map.toTileUpdate(tile) } + updateTile(tu: TileUpdate): TileRef { return this._map.updateTile(tu) } } // Or a more dynamic approach that will catch new enum values: diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index aaabf482a..41cbf6b52 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -1,6 +1,7 @@ import { Cell, TerrainType } from "./Game"; export type TileRef = number; +export type TileUpdate = bigint export interface GameMap { ref(x: number, y: number): TileRef @@ -29,6 +30,20 @@ export interface GameMap { isBorder(ref: TileRef): boolean setBorder(ref: TileRef, value: boolean): void neighbors(ref: TileRef): TileRef[] + isWater(ref: TileRef): boolean + isLake(ref: TileRef): boolean + isShore(ref: TileRef): boolean + cost(ref: TileRef): number + terrainType(ref: TileRef): TerrainType + forEachTile(fn: (tile: TileRef) => void): void + + + manhattanDist(c1: TileRef, c2: TileRef): number + euclideanDist(c1: TileRef, c2: TileRef): number + bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set + + toTileUpdate(tile: TileRef): bigint + updateTile(tu: TileUpdate): TileRef } export class GameMapImpl implements GameMap { @@ -180,7 +195,7 @@ export class GameMapImpl implements GameMap { return this.magnitude(ref) < 10 ? 2 : 1; } - getTerrainType(ref: TileRef): TerrainType { + terrainType(ref: TileRef): TerrainType { if (this.isLand(ref)) { const magnitude = this.magnitude(ref); if (magnitude < 10) return TerrainType.Plains; @@ -205,4 +220,64 @@ export class GameMapImpl implements GameMap { return neighbors; } + + forEachTile(fn: (tile: TileRef) => void): void { + for (let x = 0; x < this.width_; x++) { + for (let y = 0; y < this.height_; y++) { + fn(this.ref(x, y)) + } + } + } + + manhattanDist(c1: TileRef, c2: TileRef): number { + return Math.abs(this.x(c1) - this.x(c2)) + Math.abs(this.y(c1) - this.y(c2)); + } + euclideanDist(c1: TileRef, c2: TileRef): number { + return Math.sqrt(Math.pow(this.x(c1) - this.x(c2), 2) + Math.pow(this.y(c1) - this.y(c2), 2)); + } + bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set { + const seen = new Set() + const q: TileRef[] = [] + q.push(tile) + while (q.length > 0) { + const curr = q.pop() + seen.add(curr) + for (const n of this.neighbors(curr)) { + if (!seen.has(n) && filter(this, n)) { + q.push(n) + } + } + } + return seen + } + + toTileUpdate(tile: TileRef): bigint { + // Pack the tile reference and state into a bigint + // Format: [32 bits for tile reference][16 bits for state] + return (BigInt(tile) << 16n) | BigInt(this.state[tile]); + } + + updateTile(tu: TileUpdate): TileRef { + // Extract tile reference and state from the TileUpdate + // Last 16 bits are state, rest is tile reference + const tileRef = Number(tu >> 16n); + const state = Number(tu & 0xFFFFn); + + // Update the state for this tile + this.state[tileRef] = state; + + return tileRef; + } +} + +export function euclDistFN(root: TileRef, dist: number): (gm: GameMap, tile: TileRef) => boolean { + return (gm: GameMap, n: TileRef) => gm.euclideanDist(root, n) <= dist; +} + +export function manhattanDistFN(root: TileRef, dist: number): (gm: GameMap, tile: TileRef) => boolean { + return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist; +} + +export function andFN(x: (gm: GameMap, tile: TileRef) => boolean, y: (gm: GameMap, tile: TileRef) => boolean): (gm: GameMap, tile: TileRef) => boolean { + return (gm: GameMap, tile: TileRef) => x(gm, tile) && y(gm, tile) } \ No newline at end of file diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index bfc4f790c..8c38fd072 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -1,13 +1,12 @@ -import { MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, EmojiMessage, AllPlayers, Gold, UnitType, Unit, MutableUnit, Relation, MutableTile, PlayerUpdate, GameUpdateType } from "./Game"; +import { MutablePlayer, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, EmojiMessage, AllPlayers, Gold, UnitType, Unit, MutableUnit, Relation, PlayerUpdate, GameUpdateType } from "./Game"; import { ClientID } from "../Schemas"; -import { assertNever, bfs, closestOceanShoreFromPlayer, dist, distSortUnit, manhattanDist, manhattanDistWrapped, processName, simpleHash, sourceDstOceanShore, within } from "../Util"; +import { assertNever, closestOceanShoreFromPlayer, distSortUnit, simpleHash, sourceDstOceanShore, within } from "../Util"; import { CellString, GameImpl } from "./GameImpl"; import { UnitImpl } from "./UnitImpl"; -import { TileImpl } from "./TileImpl"; import { MessageType } from './Game'; import { renderTroops } from "../../client/Utils"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; -import { TileRef } from "./GameMap"; +import { manhattanDistFN, TileRef } from "./GameMap"; interface Target { tick: Tick @@ -32,7 +31,7 @@ export class PlayerImpl implements MutablePlayer { public _borderTiles: Set = new Set(); public _units: UnitImpl[] = []; - public _tiles: Map = new Map(); + public _tiles: Set private _name: string; private _displayName: string; @@ -48,7 +47,7 @@ export class PlayerImpl implements MutablePlayer { private relations = new Map() - constructor(private gs: GameImpl, private _smallID: number, private readonly playerInfo: PlayerInfo, startPopulation: number) { + constructor(private mg: GameImpl, private _smallID: number, private readonly playerInfo: PlayerInfo, startPopulation: number) { this._name = playerInfo.name; this._targetTroopRatio = 1 this._troops = startPopulation * this._targetTroopRatio; @@ -112,8 +111,8 @@ export class PlayerImpl implements MutablePlayer { sharesBorderWith(other: Player | TerraNullius): boolean { for (const border of this._borderTiles) { - for (const neighbor of this.gs.map().neighbors(border)) { - if (this.gs.map().ownerID(neighbor) == other.smallID()) { + for (const neighbor of this.mg.map().neighbors(border)) { + if (this.mg.map().ownerID(neighbor) == other.smallID()) { return true; } } @@ -124,26 +123,22 @@ export class PlayerImpl implements MutablePlayer { return this._tiles.size; } - tiles(): ReadonlySet { - return new Set(this._tiles.values()) as Set; + tiles(): ReadonlySet { + return new Set(this._tiles.values()) as Set; } - borderTileRefs(): ReadonlySet { + borderTiles(): ReadonlySet { return this._borderTiles; } - borderTiles(): ReadonlySet { - return new Set(Array.from(this._borderTiles).map(tr => this.gs.fromRef(tr))) - } - neighbors(): (MutablePlayer | TerraNullius)[] { const ns: Set<(MutablePlayer | TerraNullius)> = new Set(); - for (const border of this.borderTileRefs()) { - for (const neighbor of this.gs.map().neighbors(border)) { - if (this.gs.map().isLake(neighbor)) { - const owner = this.gs.map().ownerID(neighbor) + for (const border of this.borderTiles()) { + for (const neighbor of this.mg.map().neighbors(border)) { + if (this.mg.map().isLake(neighbor)) { + const owner = this.mg.map().ownerID(neighbor) if (owner != this.smallID()) { - ns.add(this.gs.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl); + ns.add(this.mg.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl); } } } @@ -152,31 +147,30 @@ export class PlayerImpl implements MutablePlayer { } isPlayer(): this is MutablePlayer { return true as const; } - ownsTile(cell: Cell): boolean { return this._tiles.has(cell.toString()); } setTroops(troops: number) { this._troops = Math.floor(troops); } - conquer(tile: Tile) { this.gs.conquer(this, tile); } - relinquish(tile: Tile) { - if (tile.owner() != this) { + conquer(tile: TileRef) { this.mg.conquer(this, tile); } + relinquish(tile: TileRef) { + if (this.mg.owner(tile) != this) { throw new Error(`Cannot relinquish tile not owned by this player`); } - this.gs.relinquish(tile); + this.mg.relinquish(tile); } info(): PlayerInfo { return this.playerInfo; } isAlive(): boolean { return this._tiles.size > 0; } executions(): Execution[] { - return this.gs.executions().filter(exec => exec.owner().id() == this.id()); + return this.mg.executions().filter(exec => exec.owner().id() == this.id()); } incomingAllianceRequests(): MutableAllianceRequest[] { - return this.gs.allianceRequests.filter(ar => ar.recipient() == this) + return this.mg.allianceRequests.filter(ar => ar.recipient() == this) } outgoingAllianceRequests(): MutableAllianceRequest[] { - return this.gs.allianceRequests.filter(ar => ar.requestor() == this) + return this.mg.allianceRequests.filter(ar => ar.requestor() == this) } alliances(): MutableAlliance[] { - return this.gs.alliances_.filter(a => a.requestor() == this || a.recipient() == this) + return this.mg.alliances_.filter(a => a.requestor() == this || a.recipient() == this) } allies(): MutablePlayer[] { @@ -212,13 +206,13 @@ export class PlayerImpl implements MutablePlayer { return false } - const delta = this.gs.ticks() - recent[0].createdAt() + const delta = this.mg.ticks() - recent[0].createdAt() - return delta < this.gs.config().allianceRequestCooldown() + return delta < this.mg.config().allianceRequestCooldown() } breakAlliance(alliance: Alliance): void { - this.gs.breakAlliance(this, alliance) + this.mg.breakAlliance(this, alliance) } @@ -230,7 +224,7 @@ export class PlayerImpl implements MutablePlayer { if (this.isAlliedWith(recipient)) { throw new Error(`cannot create alliance request, already allies`) } - return this.gs.createAllianceRequest(this, recipient as MutablePlayer) + return this.mg.createAllianceRequest(this, recipient as MutablePlayer) } relation(other: Player): Relation { @@ -291,7 +285,7 @@ export class PlayerImpl implements MutablePlayer { return false } for (const t of this.targets_) { - if (this.gs.ticks() - t.tick < this.gs.config().targetCooldown()) { + if (this.mg.ticks() - t.tick < this.mg.config().targetCooldown()) { return false } } @@ -299,13 +293,13 @@ export class PlayerImpl implements MutablePlayer { } target(other: Player): void { - this.targets_.push({ tick: this.gs.ticks(), target: other }) - this.gs.target(this, other) + this.targets_.push({ tick: this.mg.ticks(), target: other }) + this.mg.target(this, other) } targets(): PlayerImpl[] { return this.targets_ - .filter(t => this.gs.ticks() - t.tick < this.gs.config().targetDuration()) + .filter(t => this.mg.ticks() - t.tick < this.mg.config().targetDuration()) .map(t => t.target as PlayerImpl) } @@ -319,21 +313,21 @@ export class PlayerImpl implements MutablePlayer { if (recipient == this) { throw Error(`Cannot send emoji to oneself: ${this}`) } - const msg = new EmojiMessage(this, recipient, emoji, this.gs.ticks()) + const msg = new EmojiMessage(this, recipient, emoji, this.mg.ticks()) this.outgoingEmojis_.push(msg) - this.gs.sendEmojiUpdate(this, recipient, emoji) + this.mg.sendEmojiUpdate(this, recipient, emoji) } outgoingEmojis(): EmojiMessage[] { return this.outgoingEmojis_ - .filter(e => this.gs.ticks() - e.createdAt < this.gs.config().emojiMessageDuration()) + .filter(e => this.mg.ticks() - e.createdAt < this.mg.config().emojiMessageDuration()) .sort((a, b) => b.createdAt - a.createdAt) } canSendEmoji(recipient: Player | typeof AllPlayers): boolean { const prevMsgs = this.outgoingEmojis_.filter(msg => msg.recipient == recipient) for (const msg of prevMsgs) { - if (this.gs.ticks() - msg.createdAt < this.gs.config().emojiMessageCooldown()) { + if (this.mg.ticks() - msg.createdAt < this.mg.config().emojiMessageCooldown()) { return false } } @@ -346,7 +340,7 @@ export class PlayerImpl implements MutablePlayer { } for (const donation of this.sentDonations) { if (donation.recipient == recipient) { - if (this.gs.ticks() - donation.tick < this.gs.config().donateCooldown()) { + if (this.mg.ticks() - donation.tick < this.mg.config().donateCooldown()) { return false } } @@ -355,10 +349,10 @@ export class PlayerImpl implements MutablePlayer { } donate(recipient: MutablePlayer, troops: number): void { - this.sentDonations.push(new Donation(recipient, this.gs.ticks())) + this.sentDonations.push(new Donation(recipient, this.mg.ticks())) recipient.addTroops(this.removeTroops(troops)) - this.gs.displayMessage(`Sent ${renderTroops(troops)} troops to ${recipient.name()}`, MessageType.INFO, this.id()) - this.gs.displayMessage(`Recieved ${renderTroops(troops)} troops from ${this.name()}`, MessageType.SUCCESS, recipient.id()) + this.mg.displayMessage(`Sent ${renderTroops(troops)} troops to ${recipient.name()}`, MessageType.INFO, this.id()) + this.mg.displayMessage(`Recieved ${renderTroops(troops)} troops from ${this.name()}`, MessageType.SUCCESS, recipient.id()) } gold(): Gold { @@ -426,24 +420,24 @@ export class PlayerImpl implements MutablePlayer { (prev as PlayerImpl)._units = (prev as PlayerImpl)._units.filter(u => u != unit); (unit as UnitImpl)._owner = this this._units.push(unit as UnitImpl) - this.gs.fireUnitUpdateEvent(unit) - this.gs.displayMessage(`${unit.type()} captured by ${this.displayName()}`, MessageType.ERROR, prev.id()) - this.gs.displayMessage(`Captured ${unit.type()} from ${prev.displayName()}`, MessageType.SUCCESS, this.id()) + this.mg.fireUnitUpdateEvent(unit) + this.mg.displayMessage(`${unit.type()} captured by ${this.displayName()}`, MessageType.ERROR, prev.id()) + this.mg.displayMessage(`Captured ${unit.type()} from ${prev.displayName()}`, MessageType.SUCCESS, this.id()) } - buildUnit(type: UnitType, troops: number, spawnTile: Tile): UnitImpl { - const cost = this.gs.unitInfo(type).cost(this) - const b = new UnitImpl(type, this.gs, spawnTile, troops, this.gs.nextUnitID(), this); + buildUnit(type: UnitType, troops: number, spawnTile: TileRef): UnitImpl { + const cost = this.mg.unitInfo(type).cost(this) + const b = new UnitImpl(type, this.mg, spawnTile, troops, this.mg.nextUnitID(), this); this._units.push(b); this.removeGold(cost) this.removeTroops(troops) - this.gs.fireUnitUpdateEvent(b); + this.mg.fireUnitUpdateEvent(b); return b; } - canBuild(unitType: UnitType, targetTile: Tile): Tile | false { - const cost = this.gs.unitInfo(unitType).cost(this) + canBuild(unitType: UnitType, targetTile: TileRef): TileRef | false { + const cost = this.mg.unitInfo(unitType).cost(this) if (!this.isAlive() || this.gold() < cost) { return false } @@ -473,56 +467,56 @@ export class PlayerImpl implements MutablePlayer { } } - nukeSpawn(tile: Tile): Tile | false { - const spawns = this.units(UnitType.MissileSilo).map(u => u as Unit).sort(distSortUnit(tile)) + nukeSpawn(tile: TileRef): TileRef | false { + const spawns = this.units(UnitType.MissileSilo).map(u => u as Unit).sort(distSortUnit(this.mg, tile)) if (spawns.length == 0) { return false } return spawns[0].tile() } - portSpawn(tile: Tile): Tile | false { - const spawns = Array.from(bfs(tile, dist(tile, 20))) - .filter(t => t.owner() == this && t.terrain().isOceanShore()) - .sort((a, b) => manhattanDist(a.cell(), tile.cell()) - manhattanDist(b.cell(), tile.cell())) + portSpawn(tile: TileRef): TileRef | false { + const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20))) + .filter(t => this.mg.owner(t) == this && this.mg.isOceanShore(t)) + .sort((a, b) => this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile)) if (spawns.length == 0) { return false } return spawns[0] } - warshipSpawn(tile: Tile): Tile | false { - if (!tile.terrain().isOcean()) { + warshipSpawn(tile: TileRef): TileRef | false { + if (!this.mg.isOcean(tile)) { return false } const spawns = this.units(UnitType.Port) - .filter(u => manhattanDist(u.tile().cell(), tile.cell()) < this.gs.config().boatMaxDistance()) - .sort((a, b) => manhattanDist(a.tile().cell(), tile.cell()) - manhattanDist(b.tile().cell(), tile.cell())) + .filter(u => this.mg.manhattanDist(u.tile(), tile) < this.mg.config().boatMaxDistance()) + .sort((a, b) => this.mg.manhattanDist(a.tile(), tile) - this.mg.manhattanDist(b.tile(), tile)) if (spawns.length == 0) { return false } return spawns[0].tile() } - landBasedStructureSpawn(tile: Tile): Tile | false { - if (tile.owner() != this) { + landBasedStructureSpawn(tile: TileRef): TileRef | false { + if (this.mg.owner(tile) != this) { return false } return tile } - transportShipSpawn(targetTile: Tile): Tile | false { - if (!targetTile.terrain().isOceanShore()) { + transportShipSpawn(targetTile: TileRef): TileRef | false { + if (!this.mg.isOceanShore(targetTile)) { return false } - const spawn = closestOceanShoreFromPlayer(this, targetTile, this.gs.width()) + const spawn = closestOceanShoreFromPlayer(this.mg, this, targetTile) if (spawn == null) { return false } return spawn } - tradeShipSpawn(targetTile: Tile): Tile | false { + tradeShipSpawn(targetTile: TileRef): TileRef | false { const spawns = this.units(UnitType.Port).filter(u => u.tile() == targetTile) if (spawns.length == 0) { return false diff --git a/src/core/game/TerraNulliusImpl.ts b/src/core/game/TerraNulliusImpl.ts index f40a341b5..10537fb90 100644 --- a/src/core/game/TerraNulliusImpl.ts +++ b/src/core/game/TerraNulliusImpl.ts @@ -1,10 +1,10 @@ import { ClientID } from "../Schemas"; -import { TerraNullius, Cell, Tile, PlayerID } from "./Game"; +import { TerraNullius, Cell, PlayerID } from "./Game"; import { GameImpl } from "./GameImpl"; +import { TileRef } from "./GameMap"; export class TerraNulliusImpl implements TerraNullius { - public tiles: Map = new Map(); constructor() { @@ -20,8 +20,5 @@ export class TerraNulliusImpl implements TerraNullius { return null } - ownsTile(cell: Cell): boolean { - return this.tiles.has(cell); - } isPlayer(): false { return false as const; } } diff --git a/src/core/game/TerrainMapFileLoader.ts b/src/core/game/TerrainMapFileLoader.ts index e0cbc751c..57c5084c5 100644 --- a/src/core/game/TerrainMapFileLoader.ts +++ b/src/core/game/TerrainMapFileLoader.ts @@ -1,4 +1,4 @@ -import { Cell, GameMapType, TerrainMap, TerrainTile, TerrainType } from './Game'; +import { Cell, GameMapType, TerrainType } from './Game'; import { consolex } from '../Consolex'; import { NationMap } from './TerrainMapLoader'; diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 7c32b5956..6d9160b96 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -1,9 +1,9 @@ import { consolex } from '../Consolex'; -import { Cell, GameMapType, TerrainMap, TerrainTile, TerrainType } from './Game'; -import { GameMapImpl } from './GameMap'; +import { Cell, GameMapType, TerrainType } from './Game'; +import { GameMap, GameMapImpl } from './GameMap'; import { terrainMapFileLoader } from './TerrainMapFileLoader'; -const loadedMaps = new Map() +const loadedMaps = new Map() export interface NationMap { name: string; @@ -18,138 +18,7 @@ export interface Nation { strength: number; } - -export class TerrainTileImpl implements TerrainTile { - public shoreline: boolean = false - public _magnitude: number = 0 - public ocean = false - public land = false - - constructor(private map: TerrainMap, public _type: TerrainType, private _cell: Cell) { } - - key(): string { - throw new Error('Method not implemented.'); - } - - equals(other: TerrainTile): boolean { - return this._cell.x == other.cell().x && this._cell.y == other.cell().y - } - type(): TerrainType { - return this._type - } - isLake(): boolean { - return !this.isLand() && !this.isOcean(); - } - isOcean(): boolean { - return this.ocean; - } - magnitude(): number { - return this._magnitude; - } - isShore(): boolean { - return this.isLand() && this.shoreline; - } - isOceanShore(): boolean { - return this.isShore() && this.neighbors().filter(n => n.isOcean()).length > -1; - } - isShorelineWater(): boolean { - return this.isWater() && this.shoreline; - } - isLand(): boolean { - return this.land; - } - isWater(): boolean { - return !this.land; - } - cost(): number { - return this._magnitude < 10 ? 2 : 1 - } - - cell(): Cell { - return this._cell - } - - neighbors(): TerrainTile[] { - const positions = [ - { x: this._cell.x - 1, y: this._cell.y }, // Left - { x: this._cell.x + 1, y: this._cell.y }, // Right - { x: this._cell.x, y: this._cell.y - 1 }, // Up - { x: this._cell.x, y: this._cell.y + 1 } // Down - ]; - - return positions - .filter(pos => pos.x >= 0 && pos.x < this.map.width() && - pos.y >= 0 && pos.y < this.map.height()) - .map(pos => this.map.terrain(new Cell(pos.x, pos.y))); - } -} - -export class TerrainMapImpl implements TerrainMap { - public rawData: Uint8Array; - public width_: number; - public height_: number; - public _numLandTiles: number; - public nationMap: NationMap; - - - constructor() { } - - terrain(cell: Cell): TerrainTileImpl { - - const idx = cell.y * this.width_ + cell.x; - const packedByte = this.rawData[idx]; - - const isLand: boolean = (packedByte & 0b10000000) !== 0; - const shoreline = !!(packedByte & 0b01000000); - const ocean = !!(packedByte & 0b00100000); - const magnitude = packedByte & 0b00011111; - - let type: TerrainType; - if (isLand) { - if (magnitude < 10) { - type = TerrainType.Plains; - } else if (magnitude < 20) { - type = TerrainType.Highland; - } else { - type = TerrainType.Mountain; - } - } else { - type = ocean ? TerrainType.Ocean : TerrainType.Lake; - } - - const tile = new TerrainTileImpl(this, type, cell); - tile.shoreline = shoreline; - tile._magnitude = magnitude; - tile.ocean = ocean; - tile.land = isLand; - - return tile; - } - - isOnMap(cell: Cell): boolean { - return cell.x >= 0 && cell.x < this.width_ && - cell.y >= 0 && cell.y < this.height_; - } - - width(): number { - return this.width_; - } - - height(): number { - return this.height_; - } - - numLandTiles(): number { - return this._numLandTiles; - } - - neighbors(terrainTile: TerrainTile): TerrainTile[] { - return (terrainTile as TerrainTileImpl).neighbors(); - } -} - - -export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: NationMap, gameMap: GameMapImpl, miniGameMap: GameMapImpl, terrain: TerrainMap }> { +export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: NationMap, gameMap: GameMap, miniGameMap: GameMap }> { if (loadedMaps.has(map)) { return loadedMaps.get(map) } @@ -157,12 +26,12 @@ export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: Nat const gameMap = await loadTerrainFromFile(mapFiles.mapBin) const miniGameMap = await loadTerrainFromFile(mapFiles.miniMapBin) - const result = { nationMap: mapFiles.nationMap, gameMap: gameMap.map, miniGameMap: miniGameMap.map, terrain: gameMap.terrain } + const result = { nationMap: mapFiles.nationMap, gameMap: gameMap, miniGameMap: miniGameMap } loadedMaps.set(map, result) return result } -export async function loadTerrainFromFile(fileData: string): Promise<{ map: GameMapImpl, terrain: TerrainMap }> { +export async function loadTerrainFromFile(fileData: string): Promise { const width = (fileData.charCodeAt(1) << 8) | fileData.charCodeAt(0); const height = (fileData.charCodeAt(3) << 8) | fileData.charCodeAt(2); @@ -170,24 +39,19 @@ export async function loadTerrainFromFile(fileData: string): Promise<{ map: Game throw new Error(`Invalid data: buffer size ${fileData.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`); } - const m = new TerrainMapImpl(); - m.width_ = width; - m.height_ = height; // Store raw data in Uint8Array - m.rawData = new Uint8Array(width * height); + const rawData = new Uint8Array(width * height); let numLand = 0; // Copy data starting after the header for (let i = 0; i < width * height; i++) { const packedByte = fileData.charCodeAt(i + 4); - m.rawData[i] = packedByte; + rawData[i] = packedByte; if (packedByte & 0b10000000) numLand++; } - const gm = new GameMapImpl(width, height, m.rawData, numLand) + return new GameMapImpl(width, height, rawData, numLand) - m._numLandTiles = numLand; - return { map: gm, terrain: m } } diff --git a/src/core/game/TerrainSearchMap.ts b/src/core/game/TerrainSearchMap.ts index dcb48c4e6..fd968d31b 100644 --- a/src/core/game/TerrainSearchMap.ts +++ b/src/core/game/TerrainSearchMap.ts @@ -1,4 +1,3 @@ -import { TerrainMapImpl } from "./TerrainMapLoader"; export enum SearchMapTileType { Land, Shore, diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts deleted file mode 100644 index ea2fe23f4..000000000 --- a/src/core/game/TileImpl.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer, TerrainTile, DefenseBonus, MutableTile, TileUpdate, GameUpdateType, TerrainTileKey } from "./Game"; -import { TerrainMapImpl, TerrainTileImpl } from "./TerrainMapLoader"; -import { GameImpl } from "./GameImpl"; -import { PlayerImpl } from "./PlayerImpl"; -import { TerraNulliusImpl } from "./TerraNulliusImpl"; -import { GameMapImpl, TileRef } from "./GameMap"; - - -export class TileImpl implements MutableTile { - - - constructor( - private readonly gs: GameImpl, - private ref_: TileRef - ) { } - terrain(): TerrainTile { - return new TerrainRef(this.gs.map(), this.ref_) - } - - neighborsWrapped(): Tile[] { - // TODO: implement! - return this.neighbors() - } - - ref(): TileRef { - return this.ref_ - } - - toUpdate(): TileUpdate { - return { - type: GameUpdateType.Tile, - pos: { - x: this.x(), - y: this.y() - }, - ownerID: this.owner().smallID(), - hasFallout: this.hasFallout(), - hasDefenseBonus: this.hasDefenseBonus(), - isBorder: this.isBorder(), - } - } - - hasFallout(): boolean { - return this.gs.map().hasFallout(this.ref_) - } - - type(): TerrainType { - return this.gs.map().getTerrainType(this.ref_) - } - - hasDefenseBonus(): boolean { - return this.defenseBonuses.length > 0 - } - - defenseBonus(player: Player): number { - // TODO! - return 0 - // if (this.owner() == player) { - // throw Error(`cannot get defense bonus of tile already owned by player, ${player}`) - // } - // let bonusAmount = 0 - // for (const bonus of this._defenseBonuses) { - // if (bonus.unit.owner() != player) { - // bonusAmount += bonus.amount - // } - // } - // return Math.max(bonusAmount, 1) - } - - defenseBonuses(): DefenseBonus[] { - // TODO! - return [] - } - - borders(other: Player | TerraNullius): boolean { - for (const n of this.neighbors()) { - if (n.owner() == other) { - return true; - } - } - return false; - } - - hasOwner(): boolean { return this.owner().smallID() != 0 } - - owner(): MutablePlayer | TerraNullius { - const ownerID = this.gs.map().ownerID(this.ref_) - if (ownerID == 0) { - return this.gs.terraNullius() - } - return this.gs.playerBySmallID(ownerID) as MutablePlayer - } - isBorder(): boolean { return this.gs.map().isBorder(this.ref_); } - - cell(): Cell { return new Cell(this.x(), this.y()); } - - x(): number { - return this.gs.map().x(this.ref_) - } - y(): number { - return this.gs.map().y(this.ref_) - } - - neighbors(): Tile[] { - return this.gs.neighbors(this.ref()).map(n => this.gs.fromRef(n)) - } - -} - -export class TerrainRef implements TerrainTile { - - constructor(private map: GameMapImpl, private ref: TileRef) { } - - isLand(): boolean { - return this.map.isLand(this.ref) - } - isShore(): boolean { - return this.map.isShore(this.ref) - } - - isOceanShore(): boolean { - return this.isShore() && this.neighbors().filter(n => n.isOcean()).length > 0; - } - - isWater(): boolean { - return !this.map.isLand(this.ref) - } - isShorelineWater(): boolean { - return this.isWater() && this.isShore() - } - isOcean(): boolean { - return this.map.isOcean(this.ref) - } - isLake(): boolean { - return this.isWater() && !this.isOcean() - } - type(): TerrainType { - return this.map.getTerrainType(this.ref) - } - magnitude(): number { - return this.map.magnitude(this.ref) - } - equals(other: TerrainTile): boolean { - return this.ref == (other as TerrainRef).ref - } - cell(): Cell { - return this.map.cell(this.ref) - } - neighbors(): TerrainTile[] { - return this.map.neighbors(this.ref).map(tr => new TerrainRef(this.map, tr)) - } - cost(): number { - return this.map.cost(this.ref) - } - -} \ No newline at end of file diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 1aa0281ff..2a2c1e617 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,25 +1,26 @@ import { GameUpdateType, MessageType, UnitUpdate } from './Game'; import { simpleHash, within } from "../Util"; -import { MutableUnit, Tile, TerraNullius, UnitType, Player, UnitInfo } from "./Game"; +import { MutableUnit, TerraNullius, UnitType, Player, UnitInfo } from "./Game"; import { GameImpl } from "./GameImpl"; import { PlayerImpl } from "./PlayerImpl"; +import { TileRef } from './GameMap'; export class UnitImpl implements MutableUnit { private _active = true; private _health: number - private _lastTile: Tile = null + private _lastTile: TileRef = null constructor( private _type: UnitType, - private g: GameImpl, - private _tile: Tile, + private mg: GameImpl, + private _tile: TileRef, private _troops: number, private _id: number, public _owner: PlayerImpl, ) { // default to half health (or 1 is no health specified) - this._health = (this.g.unitInfo(_type).maxHealth ?? 2) / 2 + this._health = (this.mg.unitInfo(_type).maxHealth ?? 2) / 2 this._lastTile = _tile } @@ -32,8 +33,9 @@ export class UnitImpl implements MutableUnit { troops: this._troops, ownerID: this._owner.smallID(), isActive: this._active, - pos: this._tile.cell().pos(), - lastPos: this._lastTile.cell().pos() + pos: { x: this.mg.x(this._tile), y: this.mg.y(this._tile) }, + lastPos: { x: this.mg.x(this._lastTile), y: this.mg.y(this._lastTile) } + } } @@ -41,17 +43,17 @@ export class UnitImpl implements MutableUnit { return this._type } - lastTile(): Tile { + lastTile(): TileRef { return this._lastTile } - move(tile: Tile): void { + move(tile: TileRef): void { if (tile == null) { throw new Error("tile cannot be null") } this._lastTile = this._tile this._tile = tile; - this.g.fireUnitUpdateEvent(this); + this.mg.fireUnitUpdateEvent(this); } setTroops(troops: number): void { this._troops = troops; @@ -65,23 +67,23 @@ export class UnitImpl implements MutableUnit { hasHealth(): boolean { return this.info().maxHealth != undefined } - tile(): Tile { - return this._tile; + tile(): TileRef { + return this._tile } owner(): PlayerImpl { return this._owner; } info(): UnitInfo { - return this.g.unitInfo(this._type) + return this.mg.unitInfo(this._type) } setOwner(newOwner: Player): void { const oldOwner = this._owner oldOwner._units = oldOwner._units.filter(u => u != this) this._owner = newOwner as PlayerImpl - this.g.fireUnitUpdateEvent(this) - this.g.displayMessage( + this.mg.fireUnitUpdateEvent(this) + this.mg.displayMessage( `Your ${this.type()} was captured by ${newOwner.displayName()}`, MessageType.ERROR, oldOwner.id() @@ -103,9 +105,9 @@ export class UnitImpl implements MutableUnit { } this._owner._units = this._owner._units.filter(b => b != this); this._active = false; - this.g.fireUnitUpdateEvent(this); + this.mg.fireUnitUpdateEvent(this); if (displayMessage) { - this.g.displayMessage(`Your ${this.type()} was destroyed`, MessageType.ERROR, this.owner().id()) + this.mg.displayMessage(`Your ${this.type()} was destroyed`, MessageType.ERROR, this.owner().id()) } } isActive(): boolean { @@ -113,7 +115,7 @@ export class UnitImpl implements MutableUnit { } hash(): number { - return this.tile().cell().x + this.tile().cell().y + simpleHash(this.type()) + return this.tile() + simpleHash(this.type()) } toString(): string { diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts index 9e4f8cb97..637ee89df 100644 --- a/src/core/pathfinding/AStar.ts +++ b/src/core/pathfinding/AStar.ts @@ -1,8 +1,8 @@ -import { Cell, TerrainType, Tile } from "../game/Game"; +import { TileRef } from "../game/GameMap"; export interface AStar { compute(): PathFindResultType - reconstructPath(): Cell[] + reconstructPath(): TileRef[] } export enum PathFindResultType { @@ -12,12 +12,12 @@ export enum PathFindResultType { PathNotFound } export type TileResult = { type: PathFindResultType.NextTile; - tile: Tile; + tile: TileRef; } | { type: PathFindResultType.Pending; } | { type: PathFindResultType.Completed; - tile: Tile; + tile: TileRef; } | { type: PathFindResultType.PathNotFound; }; diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts index 8beed47f7..d7b03fddb 100644 --- a/src/core/pathfinding/MiniAStar.ts +++ b/src/core/pathfinding/MiniAStar.ts @@ -1,6 +1,5 @@ -import { GameManager } from "../../server/GameManager"; -import { Cell, Game, TerrainMap, TerrainType } from "../game/Game"; -import { GameMapImpl, TileRef } from "../game/GameMap"; +import { Cell, } from "../game/Game"; +import { GameMap, GameMapImpl, TileRef } from "../game/GameMap"; import { AStar, PathFindResultType, } from "./AStar"; import { SerialAStar } from "./SerialAStar"; @@ -10,8 +9,8 @@ export class MiniAStar implements AStar { private aStar: SerialAStar constructor( - private gameMap: GameMapImpl, - private miniMap: GameMapImpl, + private gameMap: GameMap, + private miniMap: GameMap, private src: TileRef, private dst: TileRef, private canMove: (t: TileRef) => boolean, @@ -40,10 +39,10 @@ export class MiniAStar implements AStar { return this.aStar.compute() } - reconstructPath(): Cell[] { - const upscaled = upscalePath(this.aStar.reconstructPath()) + reconstructPath(): TileRef[] { + const upscaled = upscalePath(this.aStar.reconstructPath().map(tr => new Cell(this.gameMap.x(tr), this.gameMap.y(tr)))) upscaled.push(new Cell(this.gameMap.x(this.dst), this.gameMap.y(this.dst))) - return upscaled + return upscaled.map(c => this.gameMap.ref(c.x, c.y)) } } diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index c5337a522..7e2d5fc15 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -1,5 +1,4 @@ -import { Cell, Game, TerrainTile, TerrainType, Tile } from "../game/Game"; -import { manhattanDist } from "../Util"; +import { Cell, Game } from "../game/Game"; import { AStar, PathFindResultType, TileResult } from "./AStar"; import { SerialAStar } from "./SerialAStar"; import { MiniAStar } from "./MiniAStar"; @@ -8,29 +7,27 @@ import { TileRef } from "../game/GameMap"; export class PathFinder { - private curr: Tile = null - private dst: Tile = null - private path: Cell[] + private curr: TileRef = null + private dst: TileRef = null + private path: TileRef[] private aStar: AStar private computeFinished = true private constructor( private game: Game, - private newAStar: (curr: Tile, dst: Tile) => AStar + private newAStar: (curr: TileRef, dst: TileRef) => AStar ) { } public static Mini(game: Game, iterations: number, canMoveOnLand: boolean, maxTries: number = 20) { return new PathFinder( game, - (curr: Tile, dst: Tile) => { - const currRef = game.map().ref(curr.cell().x, curr.cell().y) - const dstRef = game.map().ref(dst.cell().x, dst.cell().y) + (curr: TileRef, dst: TileRef) => { return new MiniAStar( game.map(), game.miniMap(), - currRef, - dstRef, + curr, + dst, (tr: TileRef): boolean => { if (canMoveOnLand) { return true @@ -44,7 +41,7 @@ export class PathFinder { ) } - nextTile(curr: Tile, dst: Tile, dist: number = 1): TileResult { + nextTile(curr: TileRef, dst: TileRef, dist: number = 1): TileResult { if (curr == null) { consolex.error('curr is null') } @@ -52,7 +49,7 @@ export class PathFinder { consolex.error('dst is null') } - if (manhattanDist(curr.cell(), dst.cell()) < dist) { + if (this.game.manhattanDist(curr, dst) < dist) { return { type: PathFindResultType.Completed, tile: curr } } @@ -65,7 +62,7 @@ export class PathFinder { this.computeFinished = false return this.nextTile(curr, dst) } else { - return { type: PathFindResultType.NextTile, tile: this.game.tile(this.path.shift()) } + return { type: PathFindResultType.NextTile, tile: this.path.shift() } } } @@ -83,11 +80,11 @@ export class PathFinder { } } - private shouldRecompute(curr: Tile, dst: Tile) { + private shouldRecompute(curr: TileRef, dst: TileRef) { if (this.path == null || this.curr == null || this.dst == null) { return true } - const dist = manhattanDist(curr.cell(), dst.cell()) + const dist = this.game.manhattanDist(curr, dst) let tolerance = 10 if (dist > 50) { tolerance = 10 @@ -98,7 +95,7 @@ export class PathFinder { } else { tolerance = 0 } - if (manhattanDist(this.dst.cell(), dst.cell()) > tolerance) { + if (this.game.manhattanDist(this.dst, dst) > tolerance) { return true } return false diff --git a/src/core/pathfinding/SerialAStar.ts b/src/core/pathfinding/SerialAStar.ts index 1eaffbd30..713aba3bd 100644 --- a/src/core/pathfinding/SerialAStar.ts +++ b/src/core/pathfinding/SerialAStar.ts @@ -1,9 +1,8 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { AStar } from "./AStar"; import { PathFindResultType } from "./AStar"; -import { Cell } from "../game/Game"; import { consolex } from "../Consolex"; -import { GameMapImpl, TileRef } from "../game/GameMap"; +import { GameMap, GameMapImpl, TileRef } from "../game/GameMap"; export class SerialAStar implements AStar { @@ -22,7 +21,7 @@ export class SerialAStar implements AStar { private canMove: (t: TileRef) => boolean, private iterations: number, private maxTries: number, - private gameMap: GameMapImpl + private gameMap: GameMap ) { this.fwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number; }>( (a, b) => a.fScore - b.fScore @@ -118,7 +117,7 @@ export class SerialAStar implements AStar { } } - public reconstructPath(): Cell[] { + public reconstructPath(): TileRef[] { if (!this.meetingPoint) return []; // Reconstruct path from start to meeting point @@ -136,6 +135,6 @@ export class SerialAStar implements AStar { fwdPath.push(current); } - return fwdPath.map(sn => new Cell(this.gameMap.x(sn), this.gameMap.y(sn))); + return fwdPath } } diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index bad808697..42f2f3fe9 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -1,4 +1,4 @@ -import { PlayerActions, PlayerID, PlayerInfo, PlayerProfile, Tile } from "../game/Game"; +import { PlayerActions, PlayerID, PlayerInfo, PlayerProfile } from "../game/Game"; import { GameUpdateViewData } from "../GameView"; import { GameConfig, GameID, Turn } from "../Schemas"; import { generateID } from "../Util"; @@ -115,7 +115,7 @@ export class WorkerClient { }) } - playerInteraction(playerID: PlayerID, tile: Tile): Promise { + playerInteraction(playerID: PlayerID, x: number, y: number): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error('Worker not initialized')); @@ -134,8 +134,8 @@ export class WorkerClient { type: 'player_actions', id: messageId, playerID: playerID, - x: tile.cell().x, - y: tile.cell().y + x: x, + y: y }); }); } diff --git a/src/scripts/TerrainMapGenerator.ts b/src/scripts/TerrainMapGenerator.ts index a9cbf9098..fcd7f1876 100644 --- a/src/scripts/TerrainMapGenerator.ts +++ b/src/scripts/TerrainMapGenerator.ts @@ -2,7 +2,6 @@ import { decodePNGFromStream } from 'pureimage'; import path from 'path'; import fs from 'fs/promises'; import { createReadStream } from 'fs'; import { fileURLToPath } from 'url'; -import { TerrainTile } from '../core/game/Game'; const __filename = fileURLToPath(import.meta.url);