Files
OpenFrontIO/tests/core/pathfinding/WaterComponents.test.ts
T
Evan Pelle a7f992e9b0 refactor: convert to npm-workspaces monorepo (engine/core-public/shared/client/server)
Restructure the single src/ tree into an npm-workspaces monorepo under
packages/, rename core -> engine, extract a types-only core-public layer,
and break the pre-existing engine -> client dependency cycle.

Structure (packages/):
  core-public  public API/wire schemas + shared enums (clean leaf)
  shared       framework-agnostic helpers (clean leaf)
  engine       deterministic simulation (was src/core)
  client       rendering/UI (was src/client)
  server       coordination (was src/server)

Dependency DAG: engine -> {core-public, shared}; client -> {core-public,
shared, engine}; server -> {core-public, engine}.

- npm workspaces: root package.json workspaces + per-package package.json;
  tsconfig.base.json holds shared options + path aliases
  (core-public/* shared/* engine/* client/* server/*) resolved uniformly by
  tsc, Vite (resolve.tsconfigPaths), Vitest, and tsx. Lockfile regenerated.
- core-public: moved Schemas/ApiSchemas/CosmeticSchemas/StatsSchemas/
  ClanApiSchemas/WorkerSchemas/Base64/PatternDecoder; extracted the enums
  (GameTypes), GameEvent type, emoji table, and GraphicsOverrides schema.
  Engine re-exports the moved enums/types so existing imports keep working.
- Broke engine -> client cycle:
  - renderNumber/renderTroops -> shared/format
  - NameBoxCalculator moved into engine
  - username validation returns translation key + params; client translates
  - applyStateUpdate moved to client (operates on the render-only PlayerState)
  - Config/UnitGrid/execution-Util/GameImpl now use structural read
    interfaces (engine/game/ReadViews: PlayerLike/UnitLike/GameLike) instead
    of importing client view classes; client imports view classes from a new
    client/view barrel; deleted the engine/game/GameView re-export shim.
- Build/deploy updated: vite.config, index.html, eslint, Dockerfile
  (copies packages/ + tsconfig.base.json before npm ci), .vscode, tests.

Verified: tsc --noEmit clean; 1364 + 65 tests pass; production vite build
succeeds; engine has zero client/server imports; core-public and shared are
dependency leaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:51:03 +00:00

146 lines
4.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
ConnectedComponents,
LAND_MARKER,
} from "engine/pathfinding/algorithms/ConnectedComponents";
import { createGameMap, createIslandMap, L, W } from "./_fixtures";
// prettier-ignore
const twoComponentsMapData = {
width: 7, height: 5, grid: [
W, W, L, L, L, W, W,
W, W, L, L, L, W, W,
W, W, L, L, L, W, W,
W, W, L, L, L, W, W,
W, W, L, L, L, W, W,
],
};
describe("ConnectedComponents", () => {
describe("getComponentId", () => {
it("returns 0 before initialization", () => {
const map = createGameMap(createIslandMap());
const wc = new ConnectedComponents(map);
// Water tile at (0,0) - should return 0 (not initialized)
const waterTile = map.ref(0, 0);
expect(wc.getComponentId(waterTile)).toBe(0);
});
it("returns same component ID for all water tiles in single connected area", () => {
const map = createGameMap(createIslandMap());
const wc = new ConnectedComponents(map);
wc.initialize();
const water1 = map.ref(0, 0);
const water2 = map.ref(4, 0);
const water3 = map.ref(0, 4);
const water4 = map.ref(4, 4);
expect(map.isWater(water1)).toBe(true);
expect(map.isWater(water2)).toBe(true);
expect(map.isWater(water3)).toBe(true);
expect(map.isWater(water4)).toBe(true);
const id1 = wc.getComponentId(water1);
const id2 = wc.getComponentId(water2);
const id3 = wc.getComponentId(water3);
const id4 = wc.getComponentId(water4);
expect(id1).toBe(1);
expect(id2).toBe(id1);
expect(id3).toBe(id1);
expect(id4).toBe(id1);
});
it("returns different component IDs for disconnected water areas", () => {
const map = createGameMap(twoComponentsMapData);
const wc = new ConnectedComponents(map);
wc.initialize();
const leftWater1 = map.ref(0, 0);
const leftWater2 = map.ref(1, 2);
const rightWater1 = map.ref(5, 0);
const rightWater2 = map.ref(6, 4);
expect(map.isWater(leftWater1)).toBe(true);
expect(map.isWater(leftWater2)).toBe(true);
expect(map.isWater(rightWater1)).toBe(true);
expect(map.isWater(rightWater2)).toBe(true);
const leftId1 = wc.getComponentId(leftWater1);
const leftId2 = wc.getComponentId(leftWater2);
const rightId1 = wc.getComponentId(rightWater1);
const rightId2 = wc.getComponentId(rightWater2);
expect(leftId1).not.toBe(rightId1);
expect(leftId1).toBe(leftId2);
expect(leftId1).toBeGreaterThan(0);
expect(leftId1).not.toBe(LAND_MARKER);
expect(rightId1).toBe(rightId2);
expect(rightId1).toBeGreaterThan(0);
expect(rightId1).not.toBe(LAND_MARKER);
});
it("returns LAND_MARKER for land tiles", () => {
const map = createGameMap(twoComponentsMapData);
const wc = new ConnectedComponents(map);
wc.initialize();
const landTile1 = map.ref(2, 0);
const landTile2 = map.ref(3, 2);
const landTile3 = map.ref(4, 4);
expect(map.isLand(landTile1)).toBe(true);
expect(map.isLand(landTile2)).toBe(true);
expect(map.isLand(landTile3)).toBe(true);
expect(wc.getComponentId(landTile1)).toBe(LAND_MARKER);
expect(wc.getComponentId(landTile2)).toBe(LAND_MARKER);
expect(wc.getComponentId(landTile3)).toBe(LAND_MARKER);
});
});
describe("determinism", () => {
it("produces same component IDs on repeated initialization", () => {
const map = createGameMap(twoComponentsMapData);
const wc1 = new ConnectedComponents(map);
const wc2 = new ConnectedComponents(map);
wc1.initialize();
wc2.initialize();
// Check all tiles have same component ID
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 7; x++) {
const tile = map.ref(x, y);
expect(wc1.getComponentId(tile)).toBe(wc2.getComponentId(tile));
}
}
});
});
describe("direct terrain access optimization", () => {
it("produces same results with accessTerrainDirectly=false", () => {
const map = createGameMap(twoComponentsMapData);
const wcDirect = new ConnectedComponents(map, true);
const wcIndirect = new ConnectedComponents(map, false);
wcDirect.initialize();
wcIndirect.initialize();
// Check all tiles have same component ID
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 7; x++) {
const tile = map.ref(x, y);
expect(wcDirect.getComponentId(tile)).toBe(
wcIndirect.getComponentId(tile),
);
}
}
});
});
});