thread_split: convert all tile to tileref

This commit is contained in:
Evan
2025-01-17 20:13:26 -08:00
parent c42cc2a9b4
commit f0f5bae79f
53 changed files with 1104 additions and 1405 deletions
+7 -8
View File
@@ -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<ClientGameRunner> {
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
)
)
+1 -5
View File
@@ -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,
})
}
+4 -10
View File
@@ -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
}
}
}
+2 -2
View File
@@ -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
}
+2 -3
View File
@@ -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
+67 -51
View File
@@ -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`<div class="type-label">Attitude: <span class="${classType}">${relationName}</span></div>`;
@@ -149,8 +165,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<div class="info-content">
<div class="player-name ${isAlly ? 'ally' : ''}">${player.name()}</div>
<div class="type-label">Troops: ${renderTroops(player.troops())}</div>
<div class="type-label">Gold: ${renderNumber(player.gold())}</div>
<div class="type-label">Troops: ${player.troops()}</div>
<div class="type-label">Gold: ${player.gold()}</div>
${relationHtml == null ? '' : relationHtml}
</div>
`;
@@ -173,7 +189,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
render() {
if (!this._isActive) {
return html``
return html``;
}
return html`
<div class="container">
@@ -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;
}
`;
}
+31 -22
View File
@@ -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<string, HTMLImageElement> = new Map();
private theme: Theme = null;
private seenUnits = new Set<Unit>()
private seenUnits = new Set<Unit>();
// Configuration for supported unit types only
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
@@ -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);
}
+3 -6
View File
@@ -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;
+89 -86
View File
@@ -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);
}
+213 -89
View File
@@ -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<Unit, Set<Tile>>();
private boatToTrail = new Map<Unit, Set<TileRef>>();
private theme: Theme = null;
private alternateView = false
private alternateView = false;
private myPlayer: Player | null = null
private myPlayer: Player | null = null;
private oldShellTile = new Map<Unit, Tile>()
private oldShellTile = new Map<Unit, TileRef>();
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<Tile>());
this.boatToTrail.set(unit, new Set<TileRef>());
}
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);
}
}
@@ -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;
+20 -20
View File
@@ -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();
+29 -28
View File
@@ -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<GameRunner> {
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
}
}
+58 -147
View File
@@ -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<Tile> {
borderTiles(): ReadonlySet<TileRef> {
throw new Error('Method not implemented.');
}
async actions(tile: Tile): Promise<PlayerActions> {
return this.game.worker.playerInteraction(this.id(), tile)
async actions(tile: TileRef): Promise<PlayerActions> {
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<TileRef> {
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<number, NameViewData>
}
export class GameView {
export class GameView implements GameMap {
private lastUpdate: GameUpdateViewData
private tiles: TileView[][] = []
private smallIDToID = new Map<number, PlayerID>()
private _players = new Map<PlayerID, PlayerView>()
private _units = new Map<number, UnitView>()
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<MapPos>()
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<TileRef> { return this._map.bfs(tile, filter) }
toTileUpdate(tile: TileRef): bigint { return this._map.toTileUpdate(tile) }
updateTile(tu: TileUpdate): TileRef { return this._map.updateTile(tu) }
}
-4
View File
@@ -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({
+32 -66
View File
@@ -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<Tile> {
const seen = new Map<TileRef, Tile>()
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<Tile>): { min: Cell; max: Cell } {
export function calculateBoundingBox(gm: GameMap, borderTiles: ReadonlySet<TileRef>): { 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<Tile>): { min: Cel
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) }
}
export function calculateBoundingBoxCenter(borderTiles: ReadonlySet<Tile>): Cell {
const { min, max } = calculateBoundingBox(borderTiles)
export function calculateBoundingBoxCenter(gm: GameMap, borderTiles: ReadonlySet<TileRef>): 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)
+5 -4
View File
@@ -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;
+7 -7
View File
@@ -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()
}
+8 -7
View File
@@ -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),
+22 -28
View File
@@ -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) { }
}
+14 -17
View File
@@ -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
+6 -4
View File
@@ -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)) {
+13 -12
View File
@@ -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())
))
)
}
}
+3 -5
View File
@@ -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)
}
+8 -8
View File
@@ -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())
)
}
})
}
+13 -16
View File
@@ -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
+16 -22
View File
@@ -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
+51 -50
View File
@@ -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 {
+6 -6
View File
@@ -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)
}
}
+10 -13
View File
@@ -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<MutablePlayer, number>()
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()
}
}
+15 -16
View File
@@ -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<TileRef>[] {
const seen = new Set<TileRef>()
const border = this.player.borderTileRefs()
const border = this.player.borderTiles()
const clusters: Set<TileRef>[] = []
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)
}
}
}
+12 -14
View File
@@ -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<MutableUnit, Tile[]>()
private portPaths = new Map<MutableUnit, TileRef[]>()
private computingPaths = new Map<MutableUnit, MiniAStar>()
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
+3 -2
View File
@@ -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) {
}
+4 -4
View File
@@ -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()))
+7 -8
View File
@@ -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(
+10 -11
View File
@@ -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)
+10 -11
View File
@@ -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<Tile>, y: Iterable<Tile>): { 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<TileRef>, y: Iterable<TileRef>): { 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<Tile>, y: Iterable<Tile>): { 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<Tile>, y: Iterable<Tile>): { 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++;
+22 -82
View File
@@ -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<T extends GameUpdateType> = Extract<GameUpdate, { type: T }>;
// Then use it to create the record type
export type GameUpdates = {
[K in GameUpdateType]: UpdateTypeMap<K>[];
[K in GameUpdateType]: UpdateTypeMap<K>[]
}
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<TileRef>
borderTiles(): ReadonlySet<Tile>
borderTiles(): ReadonlySet<TileRef>
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<MutableTile>
ownsTile(cell: Cell): boolean
tiles(): ReadonlySet<Tile>
conquer(tile: Tile): void
relinquish(tile: Tile): void
tiles(): ReadonlySet<TileRef>
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 {
+75 -74
View File
@@ -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<TileRef> { 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:
+76 -1
View File
@@ -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<TileRef>
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<TileRef> {
const seen = new Set<TileRef>()
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)
}
+64 -70
View File
@@ -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<TileRef> = new Set();
public _units: UnitImpl[] = [];
public _tiles: Map<CellString, Tile> = new Map<CellString, TileImpl>();
public _tiles: Set<TileRef>
private _name: string;
private _displayName: string;
@@ -48,7 +47,7 @@ export class PlayerImpl implements MutablePlayer {
private relations = new Map<Player, number>()
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<MutableTile> {
return new Set(this._tiles.values()) as Set<MutableTile>;
tiles(): ReadonlySet<TileRef> {
return new Set(this._tiles.values()) as Set<TileRef>;
}
borderTileRefs(): ReadonlySet<TileRef> {
borderTiles(): ReadonlySet<TileRef> {
return this._borderTiles;
}
borderTiles(): ReadonlySet<Tile> {
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
+2 -5
View File
@@ -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<Cell, Tile> = new Map<Cell, Tile>();
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; }
}
+1 -1
View File
@@ -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';
+9 -145
View File
@@ -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<GameMapType, { nationMap: NationMap, gameMap: GameMapImpl, miniGameMap: GameMapImpl, terrain: TerrainMap }>()
const loadedMaps = new Map<GameMapType, { nationMap: NationMap, gameMap: GameMap, miniGameMap: GameMap }>()
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<GameMap> {
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 }
}
-1
View File
@@ -1,4 +1,3 @@
import { TerrainMapImpl } from "./TerrainMapLoader";
export enum SearchMapTileType {
Land,
Shore,
-156
View File
@@ -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)
}
}
+20 -18
View File
@@ -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 {
+4 -4
View File
@@ -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;
};
+7 -8
View File
@@ -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))
}
}
+14 -17
View File
@@ -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
+4 -5
View File
@@ -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
}
}
+4 -4
View File
@@ -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<PlayerActions> {
playerInteraction(playerID: PlayerID, x: number, y: number): Promise<PlayerActions> {
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
});
});
}
-1
View File
@@ -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);