mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
add testing infrastructure and example test (#276)
This commit is contained in:
+16
-2
@@ -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",
|
||||
};
|
||||
|
||||
Generated
+6
-6
@@ -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": {
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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<GameMap> {
|
||||
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<GameMap> {
|
||||
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<GameMap> {
|
||||
|
||||
// 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++;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<Terrain[][]> {
|
||||
@@ -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<string>();
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<Tile> {
|
||||
// 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)
|
||||
// });
|
||||
// });
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,38 @@
|
||||
import { generateMap } from "../../src/scripts/TerrainMapGenerator";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { createGame } from "../../src/core/game/GameImpl";
|
||||
import { genTerrainFromBin } from "../../src/core/game/TerrainMapLoader";
|
||||
import { TestConfig } from "./TestConfig";
|
||||
import { TestServerConfig } from "./TestServerConfig";
|
||||
import { UserSettings } from "../../src/core/game/UserSettings";
|
||||
import { Difficulty, GameType } from "../../src/core/game/Game";
|
||||
|
||||
export async function setup(mapName: string) {
|
||||
// Load the specified map
|
||||
const mapPath = path.join(__dirname, "..", "testdata", `${mapName}.png`);
|
||||
const imageBuffer = await fs.readFile(mapPath);
|
||||
const { map, miniMap } = await generateMap(imageBuffer);
|
||||
const gameMap = await genTerrainFromBin(String.fromCharCode.apply(null, map));
|
||||
const miniGameMap = await genTerrainFromBin(
|
||||
String.fromCharCode.apply(null, miniMap),
|
||||
);
|
||||
const nationMap = { nations: [] };
|
||||
|
||||
// Configure the game
|
||||
const serverConfig = new TestServerConfig();
|
||||
const gameConfig = {
|
||||
gameMap: null,
|
||||
gameType: GameType.Singleplayer,
|
||||
difficulty: Difficulty.Medium,
|
||||
disableNPCs: false,
|
||||
bots: 0,
|
||||
infiniteGold: false,
|
||||
infiniteTroops: false,
|
||||
instantBuild: false,
|
||||
};
|
||||
const config = new TestConfig(serverConfig, gameConfig, new UserSettings());
|
||||
|
||||
// Create and return the game
|
||||
return createGame(gameMap, miniGameMap, nationMap, config);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DefaultConfig } from "../../src/core/configuration/DefaultConfig";
|
||||
|
||||
export class TestConfig extends DefaultConfig {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { GameEnv, ServerConfig } from "../../src/core/configuration/Config";
|
||||
import { GameMapType } from "../../src/core/game/Game";
|
||||
import { GameID } from "../../src/core/Schemas";
|
||||
|
||||
export class TestServerConfig implements ServerConfig {
|
||||
turnIntervalMs(): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
lobbyMaxPlayers(map: GameMapType): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
discordRedirectURI(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
numWorkers(): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
workerIndex(gameID: GameID): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
workerPath(gameID: GameID): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
workerPort(gameID: GameID): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
workerPortByIndex(workerID: number): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
env(): GameEnv {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
adminToken(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
adminHeader(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
gitCommit(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2Bucket(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2Endpoint(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2AccessKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2SecretKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["tests/**/*"]
|
||||
}
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"rootDir": "src",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
@@ -14,7 +14,7 @@
|
||||
"src/**/*",
|
||||
"resources/**/*",
|
||||
"generated/**/*",
|
||||
"test/core/GameImpl.test.ts",
|
||||
"tests/**/*",
|
||||
"src/scripts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user