mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:40:43 +00:00
thread_split: convert all tile to tileref
This commit is contained in:
@@ -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,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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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())
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import { TerrainMapImpl } from "./TerrainMapLoader";
|
||||
export enum SearchMapTileType {
|
||||
Land,
|
||||
Shore,
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user