update gamemap, rename gamemap enum => gamemaptype

This commit is contained in:
evanpelle
2025-01-14 08:15:50 -08:00
committed by Evan
parent 3eef0b772e
commit 0d764eb885
11 changed files with 120 additions and 167 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { Executor } from "../core/execution/ExecutionManager";
import { Cell, MutableGame, PlayerID, GameMap, Difficulty, GameType } from "../core/game/Game";
import { Cell, MutableGame, PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game";
import { createGame } from "../core/game/GameImpl";
import { EventBus } from "../core/EventBus";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
@@ -23,7 +23,7 @@ export interface LobbyConfig {
persistentID: string,
gameType: GameType
gameID: GameID,
map: GameMap | null
map: GameMapType | null
difficulty: Difficulty | null
}
+5 -5
View File
@@ -1,13 +1,13 @@
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Difficulty, GameMap, GameType } from '../core/game/Game';
import { Difficulty, GameMapType, GameType } from '../core/game/Game';
import { Lobby } from '../core/Schemas';
import { consolex } from '../core/Consolex';
@customElement('host-lobby-modal')
export class HostLobbyModal extends LitElement {
@state() private isModalOpen = false;
@state() private selectedMap: GameMap = GameMap.World;
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDiffculty: Difficulty = Difficulty.Medium;
@state() private lobbyId = '';
@state() private copySuccess = false;
@@ -116,7 +116,7 @@ export class HostLobbyModal extends LitElement {
<div>
<label for="map-select">Map: </label>
<select id="map-select" @change=${this.handleMapChange}>
${Object.entries(GameMap)
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(([key, value]) => html`
<option value=${value} ?selected=${this.selectedMap === value}>
@@ -179,7 +179,7 @@ export class HostLobbyModal extends LitElement {
}
private async handleMapChange(e: Event) {
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMap;
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMapType;
consolex.log(`updating map to ${this.selectedMap}`)
this.putGameConfig()
}
@@ -201,7 +201,7 @@ export class HostLobbyModal extends LitElement {
}
private async startGame() {
consolex.log(`Starting private game with map: ${GameMap[this.selectedMap]}`);
consolex.log(`Starting private game with map: ${GameMapType[this.selectedMap]}`);
this.close();
const response = await fetch(`/start_private_lobby/${this.lobbyId}`, {
method: 'POST',
+2 -2
View File
@@ -1,6 +1,6 @@
import { LitElement, html, css } from 'lit';
import { customElement, property, state, query } from 'lit/decorators.js';
import { GameMap, GameType } from '../core/game/Game';
import { GameMapType, GameType } from '../core/game/Game';
import { consolex } from '../core/Consolex';
@customElement('join-private-lobby-modal')
@@ -174,7 +174,7 @@ export class JoinPrivateLobbyModal extends LitElement {
detail: {
lobby: { id: lobbyId },
gameType: GameType.Private,
map: GameMap.World,
map: GameMapType.World,
},
bubbles: true,
composed: true
+2 -2
View File
@@ -1,7 +1,7 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { Lobby } from "../core/Schemas";
import { Difficulty, GameMap, GameType } from '../core/game/Game';
import { Difficulty, GameMapType, GameType } from '../core/game/Game';
import { consolex } from '../core/Consolex';
@customElement('public-lobby')
@@ -113,7 +113,7 @@ export class PublicLobby extends LitElement {
detail: {
lobby: lobby,
gameType: GameType.Public,
map: GameMap.World,
map: GameMapType.World,
difficulty: Difficulty.Medium,
},
bubbles: true,
+5 -5
View File
@@ -1,13 +1,13 @@
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Difficulty, GameMap, GameType } from '../core/game/Game';
import { Difficulty, GameMapType, GameType } from '../core/game/Game';
import { generateID as generateID } from '../core/Util';
import { consolex } from '../core/Consolex';
@customElement('single-player-modal')
export class SinglePlayerModal extends LitElement {
@state() private isModalOpen = false;
@state() private selectedMap: GameMap = GameMap.World;
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
static styles = css`
@@ -81,7 +81,7 @@ export class SinglePlayerModal extends LitElement {
<div>
<label for="map-select">Map: </label>
<select id="map-select" @change=${this.handleMapChange}>
${Object.entries(GameMap)
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(([key, value]) => html`
<option value=${value} ?selected=${this.selectedMap === value}>
@@ -118,13 +118,13 @@ export class SinglePlayerModal extends LitElement {
}
private handleMapChange(e: Event) {
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMap;
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMapType;
}
private handleDifficultyChange(e: Event) {
this.selectedDifficulty = String((e.target as HTMLSelectElement).value) as Difficulty;
}
private startGame() {
consolex.log(`Starting single player game with map: ${GameMap[this.selectedMap]}`);
consolex.log(`Starting single player game with map: ${GameMapType[this.selectedMap]}`);
this.dispatchEvent(new CustomEvent('join-lobby', {
detail: {
gameType: GameType.Singleplayer,
+2 -2
View File
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { AllPlayers, Difficulty, GameMap, GameType, PlayerType, UnitType } from './game/Game';
import { AllPlayers, Difficulty, GameMapType, GameType, PlayerType, UnitType } from './game/Game';
export type GameID = string
export type ClientID = string
@@ -65,7 +65,7 @@ export interface Lobby {
}
const GameConfigSchema = z.object({
gameMap: z.nativeEnum(GameMap),
gameMap: z.nativeEnum(GameMapType),
difficulty: z.nativeEnum(Difficulty),
gameType: z.nativeEnum(GameType)
})
+1 -1
View File
@@ -29,7 +29,7 @@ export enum Difficulty {
Impossible = "Impossible",
}
export enum GameMap {
export enum GameMapType {
World = "World",
Europe = "Europe",
Mena = "Mena",
+81 -128
View File
@@ -3,42 +3,36 @@ import { TerrainType } from "./Game";
export type TileRef = number;
export class GameMap {
private tiles: bigint[] = [];
private width_: number;
private height_: number;
private numLandTiles_: number = 0;
private readonly terrain: Uint8Array; // Immutable terrain data
private readonly state: Uint16Array; // Mutable game state
private readonly width_: number;
private readonly height_: number;
// Bit positions for tile data
private static readonly FALLOUT_BIT = 0n;
private static readonly OCEAN_BIT = 1n;
private static readonly SHORELINE_BIT = 2n;
private static readonly IS_LAND_BIT = 3n;
private static readonly IS_BORDER_BIT = 4n;
private static readonly HAS_DEFENSE_BONUS_BIT = 5n;
// Terrain bits (Uint8Array)
private static readonly IS_LAND_BIT = 0;
private static readonly SHORELINE_BIT = 1;
private static readonly OCEAN_BIT = 2;
private static readonly MAGNITUDE_OFFSET = 3; // Uses bits 3-7 (5 bits)
private static readonly MAGNITUDE_MASK = 0x1F; // 11111 in binary
private static readonly MAGNITUDE_OFFSET = 8n;
private static readonly MAGNITUDE_BITS = 5n;
private static readonly MAGNITUDE_MASK = (1n << GameMap.MAGNITUDE_BITS) - 1n;
// State bits (Uint16Array)
private static readonly PLAYER_ID_OFFSET = 0; // Uses bits 0-11 (12 bits)
private static readonly PLAYER_ID_MASK = 0xFFF;
private static readonly FALLOUT_BIT = 12;
private static readonly BORDER_BIT = 13;
private static readonly DEFENSE_BONUS_BIT = 14;
// Bit 15 still reserved
private static readonly PLAYER_ID_OFFSET = 13n;
private static readonly PLAYER_ID_BITS = 12n;
private static readonly PLAYER_ID_MASK = (1n << GameMap.PLAYER_ID_BITS) - 1n;
private static readonly X_COORD_OFFSET = 25n;
private static readonly X_COORD_BITS = 12n;
private static readonly X_COORD_MASK = (1n << GameMap.X_COORD_BITS) - 1n;
private static readonly Y_COORD_OFFSET = 37n;
private static readonly Y_COORD_BITS = 12n;
private static readonly Y_COORD_MASK = (1n << GameMap.Y_COORD_BITS) - 1n;
constructor(width: number, height: number) {
constructor(width: number, height: number, terrainData: Uint8Array, private numLandTiles_: number) {
if (terrainData.length !== width * height) {
throw new Error(`Terrain data length ${terrainData.length} doesn't match dimensions ${width}x${height}`);
}
this.width_ = width;
this.height_ = height;
this.tiles = new Array(width * height).fill(0n);
this.terrain = terrainData;
this.state = new Uint16Array(width * height);
}
// Get reference from coordinates
ref(x: number, y: number): TileRef {
if (!this.isValidCoord(x, y)) {
throw new Error(`Invalid coordinates: ${x},${y}`);
@@ -46,79 +40,85 @@ export class GameMap {
return y * this.width_ + x;
}
// Basic properties
x(ref: TileRef): number {
return ref % this.width_;
}
y(ref: TileRef): number {
return Math.floor(ref / this.width_);
}
width(): number { return this.width_; }
height(): number { return this.height_; }
numLandTiles(): number { return this.numLandTiles_; }
// Coordinate validation
isValidCoord(x: number, y: number): boolean {
return x >= 0 && x < this.width_ && y >= 0 && y < this.height_;
}
// Get coordinates from reference
x(ref: TileRef): number {
return Number((this.tiles[ref] >> GameMap.X_COORD_OFFSET) & GameMap.X_COORD_MASK);
}
y(ref: TileRef): number {
return Number((this.tiles[ref] >> GameMap.Y_COORD_OFFSET) & GameMap.Y_COORD_MASK);
}
// Tile property getters
playerId(ref: TileRef): number {
return Number((this.tiles[ref] >> GameMap.PLAYER_ID_OFFSET) & GameMap.PLAYER_ID_MASK);
}
magnitude(ref: TileRef): number {
return Number((this.tiles[ref] >> GameMap.MAGNITUDE_OFFSET) & GameMap.MAGNITUDE_MASK);
}
// Terrain getters (immutable)
isLand(ref: TileRef): boolean {
return Boolean(this.tiles[ref] & (1n << GameMap.IS_LAND_BIT));
return Boolean(this.terrain[ref] & (1 << GameMap.IS_LAND_BIT));
}
isOcean(ref: TileRef): boolean {
return Boolean(this.tiles[ref] & (1n << GameMap.OCEAN_BIT));
return Boolean(this.terrain[ref] & (1 << GameMap.OCEAN_BIT));
}
isShoreline(ref: TileRef): boolean {
return Boolean(this.tiles[ref] & (1n << GameMap.SHORELINE_BIT));
return Boolean(this.terrain[ref] & (1 << GameMap.SHORELINE_BIT));
}
magnitude(ref: TileRef): number {
return (this.terrain[ref] >> GameMap.MAGNITUDE_OFFSET) & GameMap.MAGNITUDE_MASK;
}
// State getters and setters (mutable)
playerId(ref: TileRef): number {
return this.state[ref] & GameMap.PLAYER_ID_MASK;
}
setPlayerId(ref: TileRef, playerId: number): void {
if (playerId > GameMap.PLAYER_ID_MASK) {
throw new Error(`Player ID ${playerId} exceeds maximum value ${GameMap.PLAYER_ID_MASK}`);
}
this.state[ref] = (this.state[ref] & ~GameMap.PLAYER_ID_MASK) | playerId;
}
hasFallout(ref: TileRef): boolean {
return Boolean(this.tiles[ref] & (1n << GameMap.FALLOUT_BIT));
return Boolean(this.state[ref] & (1 << GameMap.FALLOUT_BIT));
}
setFallout(ref: TileRef, value: boolean): void {
if (value) {
this.state[ref] |= 1 << GameMap.FALLOUT_BIT;
} else {
this.state[ref] &= ~(1 << GameMap.FALLOUT_BIT);
}
}
isBorder(ref: TileRef): boolean {
return Boolean(this.tiles[ref] & (1n << GameMap.IS_BORDER_BIT));
}
hasDefenseBonus(ref: TileRef): boolean {
return Boolean(this.tiles[ref] & (1n << GameMap.HAS_DEFENSE_BONUS_BIT));
}
// Tile property setters
setFallout(ref: TileRef, value: boolean): void {
if (value) {
this.tiles[ref] |= 1n << GameMap.FALLOUT_BIT;
} else {
this.tiles[ref] &= ~(1n << GameMap.FALLOUT_BIT);
}
return Boolean(this.state[ref] & (1 << GameMap.BORDER_BIT));
}
setBorder(ref: TileRef, value: boolean): void {
if (value) {
this.tiles[ref] |= 1n << GameMap.IS_BORDER_BIT;
this.state[ref] |= 1 << GameMap.BORDER_BIT;
} else {
this.tiles[ref] &= ~(1n << GameMap.IS_BORDER_BIT);
this.state[ref] &= ~(1 << GameMap.BORDER_BIT);
}
}
setPlayerId(ref: TileRef, playerId: number): void {
const mask = GameMap.PLAYER_ID_MASK << GameMap.PLAYER_ID_OFFSET;
this.tiles[ref] = (this.tiles[ref] & ~mask) |
((BigInt(playerId) & GameMap.PLAYER_ID_MASK) << GameMap.PLAYER_ID_OFFSET);
hasDefenseBonus(ref: TileRef): boolean {
return Boolean(this.state[ref] & (1 << GameMap.DEFENSE_BONUS_BIT));
}
setDefenseBonus(ref: TileRef, value: boolean): void {
if (value) {
this.state[ref] |= 1 << GameMap.DEFENSE_BONUS_BIT;
} else {
this.state[ref] &= ~(1 << GameMap.DEFENSE_BONUS_BIT);
}
}
// Helper methods
@@ -148,62 +148,15 @@ export class GameMap {
return this.isOcean(ref) ? TerrainType.Ocean : TerrainType.Lake;
}
// Get neighboring tiles
neighbors(ref: TileRef): TileRef[] {
const x = this.x(ref);
const y = this.y(ref);
const neighbors: TileRef[] = [];
const w = this.width_;
return [
{ x: x - 1, y },
{ x: x + 1, y },
{ x, y: y - 1 },
{ x, y: y + 1 }
]
.filter(pos => this.isValidCoord(pos.x, pos.y))
.map(pos => this.ref(pos.x, pos.y));
if (ref >= w) neighbors.push(ref - w);
if (ref < (this.height_ - 1) * w) neighbors.push(ref + w);
if (ref % w !== 0) neighbors.push(ref - 1);
if (ref % w !== w - 1) neighbors.push(ref + 1);
return neighbors;
}
// Method to set initial tile data
setTile(
ref: TileRef,
x: number,
y: number,
playerId: number,
magnitude: number,
isLand: boolean = false,
isOcean: boolean = false,
hasFallout: boolean = false,
isShoreline: boolean = false,
isBorder: boolean = false,
hasDefenseBonus: boolean = false
): void {
let tile = 0n;
// Set coordinates
tile |= (BigInt(x) & GameMap.X_COORD_MASK) << GameMap.X_COORD_OFFSET;
tile |= (BigInt(y) & GameMap.Y_COORD_MASK) << GameMap.Y_COORD_OFFSET;
// Set player ID
tile |= (BigInt(playerId) & GameMap.PLAYER_ID_MASK) << GameMap.PLAYER_ID_OFFSET;
// Set magnitude
tile |= (BigInt(magnitude) & GameMap.MAGNITUDE_MASK) << GameMap.MAGNITUDE_OFFSET;
// Set boolean flags
if (hasFallout) tile |= 1n << GameMap.FALLOUT_BIT;
if (isOcean) tile |= 1n << GameMap.OCEAN_BIT;
if (isShoreline) tile |= 1n << GameMap.SHORELINE_BIT;
if (isLand) {
tile |= 1n << GameMap.IS_LAND_BIT;
this.numLandTiles_++;
}
if (isBorder) tile |= 1n << GameMap.IS_BORDER_BIT;
if (hasDefenseBonus) tile |= 1n << GameMap.HAS_DEFENSE_BONUS_BIT;
this.tiles[ref] = tile;
}
}
export function createGameMap(width: number, height: number): GameMap {
return new GameMap(width, height);
}
+16 -16
View File
@@ -1,4 +1,4 @@
import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from './Game';
import { Cell, GameMapType, TerrainMap, TerrainTile, TerrainType } from './Game';
import { consolex } from '../Consolex';
import { NationMap } from './TerrainMapLoader';
@@ -23,25 +23,25 @@ interface NationMapModule {
}
// Mapping from GameMap enum values to file names
const MAP_FILE_NAMES: Record<GameMap, string> = {
[GameMap.World]: 'WorldMap',
[GameMap.Europe]: 'Europe',
[GameMap.Mena]: 'Mena',
[GameMap.NorthAmerica]: 'NorthAmerica',
[GameMap.Oceania]: 'Oceania',
[GameMap.BlackSea]: 'BlackSea',
const MAP_FILE_NAMES: Record<GameMapType, string> = {
[GameMapType.World]: 'WorldMap',
[GameMapType.Europe]: 'Europe',
[GameMapType.Mena]: 'Mena',
[GameMapType.NorthAmerica]: 'NorthAmerica',
[GameMapType.Oceania]: 'Oceania',
[GameMapType.BlackSea]: 'BlackSea',
};
class GameMapLoader {
private maps: Map<GameMap, MapCache>;
private loadingPromises: Map<GameMap, Promise<MapData>>;
private maps: Map<GameMapType, MapCache>;
private loadingPromises: Map<GameMapType, Promise<MapData>>;
constructor() {
this.maps = new Map<GameMap, MapCache>();
this.loadingPromises = new Map<GameMap, Promise<MapData>>();
this.maps = new Map<GameMapType, MapCache>();
this.loadingPromises = new Map<GameMapType, Promise<MapData>>();
}
public async getMapData(map: GameMap): Promise<MapData> {
public async getMapData(map: GameMapType): Promise<MapData> {
const cachedMap = this.maps.get(map);
if (cachedMap?.bin && cachedMap?.nationMap) {
return cachedMap as MapData;
@@ -56,7 +56,7 @@ class GameMapLoader {
return data;
}
private async loadMapData(map: GameMap): Promise<MapData> {
private async loadMapData(map: GameMapType): Promise<MapData> {
const fileName = MAP_FILE_NAMES[map];
if (!fileName) {
throw new Error(`No file name mapping found for map: ${map}`);
@@ -75,12 +75,12 @@ class GameMapLoader {
};
}
public isMapLoaded(map: GameMap): boolean {
public isMapLoaded(map: GameMapType): boolean {
const mapData = this.maps.get(map);
return !!mapData?.bin && !!mapData?.nationMap;
}
public getLoadedMaps(): GameMap[] {
public getLoadedMaps(): GameMapType[] {
return Array.from(this.maps.keys()).filter(map => this.isMapLoaded(map));
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export class TerrainTileImpl implements TerrainTile {
constructor(private map: TerrainMap, public _type: TerrainType, private _cell: Cell) { }
key(): string {
return this._cell.toString()
throw new Error('Method not implemented.');
}
equals(other: TerrainTile): boolean {
+3 -3
View File
@@ -3,7 +3,7 @@ import { ClientID, GameConfig, GameID } from "../core/Schemas";
import { v4 as uuidv4 } from 'uuid';
import { Client } from "./Client";
import { GamePhase, GameServer } from "./GameServer";
import { Difficulty, GameMap, GameType } from "../core/game/Game";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { generateID } from "../core/Util";
@@ -50,7 +50,7 @@ export class GameManager {
false,
this.config,
{
gameMap: GameMap.World,
gameMap: GameMapType.World,
gameType: GameType.Private,
difficulty: Difficulty.Medium
}
@@ -88,7 +88,7 @@ export class GameManager {
true,
this.config,
{
gameMap: GameMap.World,
gameMap: GameMapType.World,
gameType: GameType.Public,
difficulty: Difficulty.Medium
}