From cd1f8b95866b54b406dd52208e2d9b2fffe13f12 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 17 Mar 2025 12:20:23 -0700 Subject: [PATCH] add testing infrastructure and example test (#276) --- jest.config.ts | 18 ++- package-lock.json | 12 +- package.json | 4 +- src/core/game/TerrainMapLoader.ts | 19 ++- src/scripts/TerrainMapGenerator.ts | 85 +++---------- src/scripts/generateTerrainMaps.ts | 61 +++++++++ tests/GameImpl.test.ts | 182 --------------------------- tests/NameBoxCalculator.test.ts | 193 ----------------------------- tests/TerritoryCapture.test.ts | 25 ++++ tests/testdata/Plains.png | Bin 0 -> 7266 bytes tests/util/Setup.ts | 38 ++++++ tests/util/TestConfig.ts | 3 + tests/util/TestServerConfig.ts | 57 +++++++++ tsconfig.jest.json | 9 ++ tsconfig.json | 4 +- 15 files changed, 246 insertions(+), 464 deletions(-) create mode 100644 src/scripts/generateTerrainMaps.ts delete mode 100644 tests/GameImpl.test.ts delete mode 100644 tests/NameBoxCalculator.test.ts create mode 100644 tests/TerritoryCapture.test.ts create mode 100644 tests/testdata/Plains.png create mode 100644 tests/util/Setup.ts create mode 100644 tests/util/TestConfig.ts create mode 100644 tests/util/TestServerConfig.ts create mode 100644 tsconfig.jest.json diff --git a/jest.config.ts b/jest.config.ts index dca45a805..02f05ddd8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,6 +1,20 @@ -module.exports = { - transform: { "^.+\\.ts?$": "ts-jest" }, +export default { testEnvironment: "node", testRegex: "/tests/.*\\.(test|spec)?\\.(ts|tsx)$", moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + tsconfig: "tsconfig.jest.json", + }, + ], + }, + transformIgnorePatterns: ["node_modules/(?!(node:)/)"], + preset: "ts-jest/presets/default-esm", }; diff --git a/package-lock.json b/package-lock.json index 3932ee363..d71bfb067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "jimp": "^0.22.12", "lit": "^3.2.1", "msgpack5": "^6.0.2", - "nanoid": "^5.0.9", + "nanoid": "^3.3.6", "node-addon-api": "^8.1.0", "node-gyp": "^10.2.0", "obscenity": "^0.4.3", @@ -14398,9 +14398,9 @@ } }, "node_modules/nanoid": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", - "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "funding": [ { "type": "github", @@ -14409,10 +14409,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^18 || >=20" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { diff --git a/package.json b/package.json index 8007c2483..80dfc2883 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openfront-client", "scripts": { - "build-map": "node --loader ts-node/esm --experimental-specifier-resolution=node src/scripts/TerrainMapGenerator.ts", + "build-map": "node --loader ts-node/esm --experimental-specifier-resolution=node src/scripts/generateTerrainMaps.ts", "build-dev": "webpack --config webpack.config.js --mode development", "build-prod": "webpack --config webpack.config.js --mode production", "start:client": "webpack serve --open --node-env development", @@ -103,7 +103,7 @@ "jimp": "^0.22.12", "lit": "^3.2.1", "msgpack5": "^6.0.2", - "nanoid": "^5.0.9", + "nanoid": "^3.3.6", "node-addon-api": "^8.1.0", "node-gyp": "^10.2.0", "obscenity": "^0.4.3", diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 0a98b67ae..a017b8851 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -9,9 +9,6 @@ const loadedMaps = new Map< >(); export interface NationMap { - name: string; - width: number; - height: number; nations: Nation[]; } @@ -30,8 +27,8 @@ export async function loadTerrainMap( } const mapFiles = await terrainMapFileLoader.getMapData(map); - const gameMap = await loadTerrainFromFile(mapFiles.mapBin); - const miniGameMap = await loadTerrainFromFile(mapFiles.miniMapBin); + const gameMap = await genTerrainFromBin(mapFiles.mapBin); + const miniGameMap = await genTerrainFromBin(mapFiles.miniMapBin); const result = { nationMap: mapFiles.nationMap, gameMap: gameMap, @@ -41,13 +38,13 @@ export async function loadTerrainMap( return result; } -export async function loadTerrainFromFile(fileData: string): Promise { - const width = (fileData.charCodeAt(1) << 8) | fileData.charCodeAt(0); - const height = (fileData.charCodeAt(3) << 8) | fileData.charCodeAt(2); +export async function genTerrainFromBin(data: string): Promise { + const width = (data.charCodeAt(1) << 8) | data.charCodeAt(0); + const height = (data.charCodeAt(3) << 8) | data.charCodeAt(2); - if (fileData.length != width * height + 4) { + if (data.length != width * height + 4) { throw new Error( - `Invalid data: buffer size ${fileData.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`, + `Invalid data: buffer size ${data.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`, ); } @@ -57,7 +54,7 @@ export async function loadTerrainFromFile(fileData: string): Promise { // Copy data starting after the header for (let i = 0; i < width * height; i++) { - const packedByte = fileData.charCodeAt(i + 4); + const packedByte = data.charCodeAt(i + 4); rawData[i] = packedByte; if (packedByte & 0b10000000) numLand++; } diff --git a/src/scripts/TerrainMapGenerator.ts b/src/scripts/TerrainMapGenerator.ts index d9aa7d94f..2827d96dc 100644 --- a/src/scripts/TerrainMapGenerator.ts +++ b/src/scripts/TerrainMapGenerator.ts @@ -2,23 +2,8 @@ import { decodePNGFromStream } from "pureimage"; import path from "path"; import fs from "fs/promises"; import { createReadStream } from "fs"; -import { fileURLToPath } from "url"; +import { Readable } from "stream"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const maps = [ - "Africa", - "Asia", - "WorldMap", - "BlackSea", - "Europe", - "Mars", - "Mena", - "Oceania", - "NorthAmerica", - "SouthAmerica", -]; const min_island_size = 30; interface Coord { @@ -38,21 +23,14 @@ class Terrain { constructor(public type: TerrainType) {} } -async function loadTerrainMap(mapName: string): Promise { - const imagePath = path.resolve( - __dirname, - "..", - "..", - "resources", - "maps", - mapName + ".png", - ); +export async function generateMap( + imageBuffer: Buffer, +): Promise<{ map: Uint8Array; miniMap: Uint8Array }> { + const stream = Readable.from(imageBuffer); + const img = await decodePNGFromStream(stream); - const readStream = createReadStream(imagePath); - const img = await decodePNGFromStream(readStream); - - console.log(`${mapName}: Image loaded successfully`); - console.log(`${mapName}: `, "Image dimensions:", img.width, "x", img.height); + console.log("Image loaded successfully"); + console.log("Image dimensions:", img.width, "x", img.height); const terrain: Terrain[][] = Array(img.width) .fill(null) @@ -79,34 +57,17 @@ async function loadTerrainMap(mapName: string): Promise { } removeSmallIslands(terrain); - removeSmallLakes(mapName, terrain); + removeSmallLakes(terrain); const shorelineWaters = processShore(terrain); processDistToLand(shorelineWaters, terrain); processOcean(terrain); - const outputPath = path.join( - __dirname, - "..", - "..", - "resources", - "maps", - mapName + ".bin", - ); - fs.writeFile(outputPath, packTerrain(mapName, terrain)); const miniTerrain = await createMiniMap(terrain); - const miniOutputPath = path.join( - __dirname, - "..", - "..", - "resources", - "maps", - mapName + "Mini.bin", - ); - fs.writeFile(miniOutputPath, packTerrain(mapName, miniTerrain)); -} -export async function loadTerrainMaps() { - await Promise.all(maps.map((map) => loadTerrainMap(map))); + return { + map: packTerrain(terrain), + miniMap: packTerrain(miniTerrain), + }; } export async function createMiniMap(tm: Terrain[][]): Promise { @@ -225,7 +186,7 @@ function processOcean(map: Terrain[][]) { } } -function packTerrain(mapName: string, map: Terrain[][]): Uint8Array { +function packTerrain(map: Terrain[][]): Uint8Array { const width = map.length; const height = map[0].length; const packedData = new Uint8Array(4 + width * height); @@ -262,7 +223,7 @@ function packTerrain(mapName: string, map: Terrain[][]): Uint8Array { packedData[4 + y * width + x] = packedByte; } } - logBinaryAsBits(mapName, packedData); + logBinaryAsBits(packedData); return packedData; } @@ -315,13 +276,11 @@ function removeSmallIslands(map: Terrain[][]) { } } -function removeSmallLakes(mapName: string, map: Terrain[][]) { +function removeSmallLakes(map: Terrain[][]) { const visited = new Set(); const min_lake_size = 200; - console.log( - `${mapName}: removing small lakes ${map.length}, ${map[0].length}`, - ); + console.log(`removing small lakes ${map.length}, ${map[0].length}`); for (let x = 0; x < map.length; x++) { for (let y = 0; y < map[0].length; y++) { @@ -341,15 +300,11 @@ function removeSmallLakes(mapName: string, map: Terrain[][]) { } } -function logBinaryAsBits( - mapName: string, - data: Uint8Array, - length: number = 8, -) { +function logBinaryAsBits(data: Uint8Array, length: number = 8) { const bits = Array.from(data.slice(0, length)) .map((b) => b.toString(2).padStart(8, "0")) .join(" "); - console.log(`${mapName}: Binary data (bits):`, bits); + console.log(`Binary data (bits):`, bits); } function getNeighborCoords(x: number, y: number, map: Terrain[][]): Coord[] { @@ -368,5 +323,3 @@ function getNeighborCoords(x: number, y: number, map: Terrain[][]): Coord[] { } return coords; } - -await loadTerrainMaps(); diff --git a/src/scripts/generateTerrainMaps.ts b/src/scripts/generateTerrainMaps.ts new file mode 100644 index 000000000..4e606476c --- /dev/null +++ b/src/scripts/generateTerrainMaps.ts @@ -0,0 +1,61 @@ +import { generateMap } from "./TerrainMapGenerator.js"; +import path from "path"; +import fs from "fs/promises"; + +const maps = [ + "Africa", + "Asia", + "WorldMap", + "BlackSea", + "Europe", + "Mars", + "Mena", + "Oceania", + "NorthAmerica", + "SouthAmerica", +]; + +async function loadTerrainMaps() { + await Promise.all( + maps.map(async (map) => { + const mapPath = path.resolve( + process.cwd(), + "resources", + "maps", + map + ".png", + ); + const imageBuffer = await fs.readFile(mapPath); + const { map: mainMap, miniMap } = await generateMap(imageBuffer); + + const outputPath = path.join( + process.cwd(), + "resources", + "maps", + map + ".bin", + ); + const miniOutputPath = path.join( + process.cwd(), + "resources", + "maps", + map + "Mini.bin", + ); + + await Promise.all([ + fs.writeFile(outputPath, mainMap), + fs.writeFile(miniOutputPath, miniMap), + ]); + }), + ); +} + +async function main() { + try { + await loadTerrainMaps(); + console.log("Terrain maps generated successfully"); + } catch (error) { + console.error("Error generating terrain maps:", error); + process.exit(1); + } +} + +main(); diff --git a/tests/GameImpl.test.ts b/tests/GameImpl.test.ts deleted file mode 100644 index d0edc1617..000000000 --- a/tests/GameImpl.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { GameImpl, PlayerImpl } from "../src/core/GameImpl"; -import { EventBus } from "../src/core/EventBus"; -import { - Game, - Cell, - MutablePlayer, - PlayerInfo, - TerrainMap, - TerrainTypes, - Tile, -} from "../src/core/Game"; - -describe("borderTilesWith", () => { - let game: GameImpl; - let player1: PlayerImpl; - let player2: PlayerImpl; - let terrainMap: TerrainMap; - - beforeEach(() => { - // Create a 5x5 terrain map - terrainMap = { - terrain: jest.fn().mockReturnValue(TerrainTypes.Land), - width: jest.fn().mockReturnValue(5), - height: jest.fn().mockReturnValue(5), - }; - const eventBus = new EventBus(); - game = new GameImpl(terrainMap, eventBus); - player1 = game.addPlayer(new PlayerInfo("Player 1", false)) as PlayerImpl; - player2 = game.addPlayer(new PlayerInfo("Player 2", false)) as PlayerImpl; - }); - - test("should return an empty set when players have no bordering tiles", () => { - const borderTiles = player1.borderTilesWith(player2); - expect(borderTiles.size).toBe(0); - }); - - test("should return correct border tiles when players are adjacent", () => { - game.conquer(player1, new Cell(0, 0)); - game.conquer(player2, new Cell(1, 0)); - - const borderTilesP1 = player1.borderTilesWith(player2); - const borderTilesP2 = player2.borderTilesWith(player1); - - expect(borderTilesP1.size).toBe(1); - expect(borderTilesP2.size).toBe(1); - - const p1BorderTile = Array.from(borderTilesP1)[0]; - const p2BorderTile = Array.from(borderTilesP2)[0]; - - expect(p1BorderTile.cell()).toEqual(new Cell(0, 0)); - expect(p2BorderTile.cell()).toEqual(new Cell(1, 0)); - }); - - test("should update border tiles when a new tile is conquered", () => { - game.conquer(player1, new Cell(0, 0)); - game.conquer(player2, new Cell(2, 0)); - - expect(player1.borderTilesWith(player2).size).toBe(0); - - game.conquer(player2, new Cell(1, 0)); - - const borderTiles = player1.borderTilesWith(player2); - expect(borderTiles.size).toBe(1); - expect(Array.from(borderTiles)[0].cell()).toEqual(new Cell(0, 0)); - }); - - test("should handle multiple border tiles correctly", () => { - game.conquer(player1, new Cell(0, 0)); - game.conquer(player1, new Cell(0, 1)); - game.conquer(player2, new Cell(1, 0)); - game.conquer(player2, new Cell(1, 1)); - - const borderTiles = player1.borderTilesWith(player2); - expect(borderTiles.size).toBe(2); - - const borderCells = Array.from(borderTiles).map((tile) => tile.cell()); - expect(borderCells).toEqual( - expect.arrayContaining([new Cell(0, 0), new Cell(0, 1)]), - ); - }); - - test("should update border tiles when a tile changes ownership", () => { - game.conquer(player1, new Cell(0, 0)); - game.conquer(player1, new Cell(1, 0)); - game.conquer(player2, new Cell(2, 0)); - - expect(player1.borderTilesWith(player2).size).toBe(1); - - game.conquer(player2, new Cell(1, 0)); - - const borderTilesP1 = player1.borderTilesWith(player2); - const borderTilesP2 = player2.borderTilesWith(player1); - - expect(borderTilesP1.size).toBe(1); - expect(borderTilesP2.size).toBe(1); - - expect(Array.from(borderTilesP1)[0].cell()).toEqual(new Cell(0, 0)); - expect(Array.from(borderTilesP2).map((t) => t.cell())).toEqual( - expect.arrayContaining([new Cell(1, 0)]), - ); - }); - - test("should handle border tiles with TerraNullius", () => { - game.conquer(player1, new Cell(0, 0)); - - const borderWithTerraNullius = player1.borderTilesWith(game.terraNullius()); - expect(borderWithTerraNullius.size).toBe(1); - - const borderCells = Array.from(borderWithTerraNullius).map((tile) => - tile.cell(), - ); - expect(borderCells).toEqual(expect.arrayContaining([new Cell(0, 0)])); - }); - - test("should not include diagonal tiles as borders", () => { - game.conquer(player1, new Cell(0, 0)); - game.conquer(player2, new Cell(1, 1)); - - expect(player1.borderTilesWith(player2).size).toBe(0); - expect(player2.borderTilesWith(player1).size).toBe(0); - }); - - // test('should handle complex border scenarios', () => { - // // Create a more complex border scenario - // // 0 1 2 3 4 - // // 0 1 1 2 2 2 - // // 1 1 1 2 2 2 - // // 2 1 1 1 2 2 - // // 3 1 1 1 1 2 - // // 4 1 1 1 1 1 - - // for (let y = 0; y < 5; y++) { - // for (let x = 0; x < 5; x++) { - // if (x + y < 6) { - // game.conquer(player1, new Cell(x, y)); - // } else { - // game.conquer(player2, new Cell(x, y)); - // } - // } - // } - - // const borderTilesP1 = player1.borderTilesWith(player2); - // const borderTilesP2 = player2.borderTilesWith(player1); - - // expect(borderTilesP1.size).toBe(5); - // expect(borderTilesP2.size).toBe(5); - - // const expectedBorderP1 = [ - // new Cell(2, 0), - // new Cell(2, 1), - // new Cell(3, 2), - // new Cell(3, 3), - // new Cell(4, 3) - // ]; - - // const expectedBorderP2 = [ - // new Cell(2, 2), - // new Cell(3, 1), - // new Cell(3, 2), - // new Cell(4, 1), - // new Cell(4, 2) - // ]; - - // const actualBorderP1 = Array.from(borderTilesP1).map(t => t.cell()); - // const actualBorderP2 = Array.from(borderTilesP2).map(t => t.cell()); - - // expect(actualBorderP1).toEqual(expect.arrayContaining(expectedBorderP1)); - // expect(actualBorderP2).toEqual(expect.arrayContaining(expectedBorderP2)); - // }); - - test("should handle border updates when a player loses all tiles", () => { - game.conquer(player1, new Cell(0, 0)); - game.conquer(player2, new Cell(1, 0)); - - expect(player1.borderTilesWith(player2).size).toBe(1); - - game.conquer(player1, new Cell(1, 0)); // Player 1 takes Player 2's only tile - - expect(player1.borderTilesWith(player2).size).toBe(0); - expect(player2.borderTilesWith(player1).size).toBe(0); - }); -}); diff --git a/tests/NameBoxCalculator.test.ts b/tests/NameBoxCalculator.test.ts deleted file mode 100644 index dbdbc3c18..000000000 --- a/tests/NameBoxCalculator.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -// import {Game, Player, Tile, Cell, TerraNullius, PlayerInfo} from '../src/core/GameApi'; -// import {placeName, calculateBoundingBox, createGrid, findLargestInscribedRectangle, largestRectangleInHistogram, calculateFontSize} from '../src/client/NameBoxCalculator'; - -// class MockPlayer implements Player { -// constructor(private playerTiles: [number, number][]) { } - -// info(): PlayerInfo { -// return new PlayerInfo("TestPlayer", false); -// } - -// id(): PlayerID { -// return 1; -// } - -// troops(): number { -// return 0; -// } - -// ownsTile(cell: Cell): boolean { -// return this.playerTiles.some(([x, y]) => x === cell.x && y === cell.y); -// } - -// isAlive(): boolean { -// return true; -// } - -// gameState(): Game { -// return {} as Game; // This should be properly implemented -// } - -// executions(): ExecutionView[] { -// return []; -// } - -// borderTilesWith(other: Player | TerraNullius): ReadonlySet { -// return new Set(); -// } - -// isPlayer(): this is Player { -// return true; -// } - -// neighbors(): (Player | TerraNullius)[] { -// return []; -// } -// } - -// class MockGame implements Game { -// private tiles: Tile[][] = []; -// private mockPlayer: Player; - -// constructor(width: number, height: number, playerTiles: [number, number][]) { -// this.tiles = Array(height).fill(null).map(() => Array(width).fill(null)); -// this.mockPlayer = new MockPlayer(playerTiles); - -// for (let y = 0; y < height; y++) { -// for (let x = 0; x < width; x++) { -// this.tiles[y][x] = { -// owner: () => playerTiles.some(([px, py]) => px === x && py === y) ? this.mockPlayer : this.terraNullius(), -// hasOwner: () => playerTiles.some(([px, py]) => px === x && py === y), -// isBorder: () => false, -// isInterior: () => false, -// cell: () => new Cell(x, y), -// terrain: () => ({expansionCost: 1, expansionTime: 1}), -// game: () => this, -// neighbors: () => [] -// }; -// } -// } -// } - -// player(id: PlayerID): Player {return this.mockPlayer;} -// tile(cell: Cell): Tile {return this.tiles[cell.y][cell.x];} -// isOnMap(cell: Cell): boolean {return cell.x >= 0 && cell.x < this.width() && cell.y >= 0 && cell.y < this.height();} -// neighbors(cell: Cell): Cell[] {return [];} -// width(): number {return this.tiles[0].length;} -// height(): number {return this.tiles.length;} -// forEachTile(fn: (tile: Tile) => void): void {this.tiles.flat().forEach(fn);} -// executions(): ExecutionView[] {return [];} -// terraNullius(): TerraNullius {return {ownsTile: () => false, isPlayer: () => false};} -// tick() { } -// addExecution(...exec: Execution[]) { } -// } - -// // Mock implementations -// class MockGame implements Game { -// private tiles: Tile[][] = []; -// private mockPlayer: Player; - -// constructor(width: number, height: number, playerTiles: [number, number][]) { -// this.tiles = Array(height).fill(null).map(() => Array(width).fill(null)); -// this.mockPlayer = { -// info: () => new PlayerInfo("TestPlayer", false), -// id: () => 1, -// troops: () => 0, -// ownsTile: (cell: Cell) => playerTiles.some(([x, y]) => x === cell.x && y === cell.y), -// isAlive: () => true, -// gameState: () => this, -// executions: () => [], -// borderTilesWith: () => new Set(), -// isPlayer: function (this: Player): this is Player {return true}, -// neighbors: () => [] -// }; - -// for (let y = 0; y < height; y++) { -// for (let x = 0; x < width; x++) { -// this.tiles[y][x] = { -// owner: () => playerTiles.some(([px, py]) => px === x && py === y) ? this.mockPlayer : this.terraNullius(), -// hasOwner: () => playerTiles.some(([px, py]) => px === x && py === y), -// isBorder: () => false, -// isInterior: () => false, -// cell: () => new Cell(x, y), -// terrain: () => ({expansionCost: 1, expansionTime: 1}), -// game: () => this, -// neighbors: () => [] -// }; -// } -// } -// } - -// player(id: number): Player {return this.mockPlayer;} -// tile(cell: Cell): Tile {return this.tiles[cell.y][cell.x];} -// isOnMap(cell: Cell): boolean {return cell.x >= 0 && cell.x < this.width() && cell.y >= 0 && cell.y < this.height();} -// neighbors(cell: Cell): Cell[] {return [];} -// width(): number {return this.tiles[0].length;} -// height(): number {return this.tiles.length;} -// forEachTile(fn: (tile: Tile) => void): void {this.tiles.flat().forEach(fn);} -// executions(): any[] {return [];} -// terraNullius(): TerraNullius {return {ownsTile: () => false, isPlayer: () => false};} -// tick() { } -// addExecution(...exec: any[]) { } -// } - -// describe('Territory Name Placement', () => { -// test('placeName should return a position and font size', () => { -// const game = new MockGame(5, 5, [[1, 1], [2, 1], [3, 1], [2, 2], [2, 3]]); -// const player = game.player(1); -// const result = placeName(game, player); - -// expect(result).toHaveProperty('position'); -// expect(result).toHaveProperty('fontSize'); -// expect(result.position).toHaveProperty('x'); -// expect(result.position).toHaveProperty('y'); -// expect(typeof result.fontSize).toBe('number'); -// }); - -// test('calculateBoundingBox should return correct bounding box', () => { -// const game = new MockGame(5, 5, [[1, 1], [3, 3]]); -// const player = game.player(1); -// const boundingBox = calculateBoundingBox(game, player); - -// expect(boundingBox).toEqual({min: {x: 1, y: 1}, max: {x: 3, y: 3}}); -// }); - -// test('createGrid should create correct boolean grid', () => { -// const game = new MockGame(3, 3, [[0, 0], [1, 1], [2, 2]]); -// const player = game.player(1); -// const boundingBox = {min: {x: 0, y: 0}, max: {x: 2, y: 2}}; -// const grid = createGrid(game, player, boundingBox); - -// expect(grid).toEqual([ -// [true, false, false], -// [false, true, false], -// [false, false, true] -// ]); -// }); - -// test('findLargestInscribedRectangle should find correct rectangle', () => { -// const grid = [ -// [true, true, true], -// [true, true, false], -// [true, true, false] -// ]; -// const result = findLargestInscribedRectangle(grid); - -// expect(result).toEqual({x: 0, y: 0, width: 2, height: 3}); -// }); - -// test('largestRectangleInHistogram should find correct rectangle', () => { -// const heights = [2, 1, 5, 6, 2, 3]; -// const result = largestRectangleInHistogram(heights); - -// expect(result).toEqual({x: 2, y: 0, width: 2, height: 5}); -// }); - -// test('calculateFontSize should return correct font size', () => { -// const rectangle = {x: 0, y: 0, width: 100, height: 50}; -// const name = "TestPlayer"; -// const fontSize = calculateFontSize(rectangle, name); - -// expect(fontSize).toBe(25); // 50 / 2 = 25 (height constrained) -// }); -// }); diff --git a/tests/TerritoryCapture.test.ts b/tests/TerritoryCapture.test.ts new file mode 100644 index 000000000..58d16475c --- /dev/null +++ b/tests/TerritoryCapture.test.ts @@ -0,0 +1,25 @@ +import { Player, PlayerInfo, PlayerType } from "../src/core/game/Game"; +import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { setup } from "./util/Setup"; + +describe("Territory management", () => { + test("player owns the tile it spawns on", async () => { + const game = await setup("Plains"); + game.addPlayer( + new PlayerInfo("us", "test_player", PlayerType.Human, null, "test_id"), + 1000, + ); + const spawnTile = game.map().ref(50, 50); + game.addExecution( + new SpawnExecution(game.player("test_id").info(), spawnTile), + ); + // Init the execution + game.executeNextTick(); + // Execute the execution. + game.executeNextTick(); + + const owner = game.owner(spawnTile); + expect(owner.isPlayer()).toBe(true); + expect((owner as Player).name()).toBe("test_player"); + }); +}); diff --git a/tests/testdata/Plains.png b/tests/testdata/Plains.png new file mode 100644 index 0000000000000000000000000000000000000000..2e73bdbb259f44dbd94ec413d4edccae888b6033 GIT binary patch literal 7266 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