- core/: Pure TS hex engine (axial coords, hex grid, terrain types, edge connectivity with constraint solver, HexMap state model) - src/map/: Leaflet L.CRS.Simple map init, Canvas-based hex overlay layer (L.GridLayer), click/edge interaction detection - src/ui/: Sidebar with toolbar (Select/Paint/Feature modes), terrain picker, hex inspector, map settings (hex size, grid, opacity) - pipeline/: Tile pyramid generator (sharp, from source image) - tests/: 32 passing tests for coords, hex-grid, edge-connectivity - Uses Kiepenkerl tiles (symlinked) for development Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
edgeMask,
|
|
hasEdge,
|
|
toggleEdge,
|
|
setEdge,
|
|
clearEdge,
|
|
edgeCount,
|
|
connectedEdges,
|
|
rotateMask,
|
|
enforceEdgeConstraints,
|
|
} from '@core/edge-connectivity';
|
|
import { HexEdge } from '@core/types';
|
|
import { HexMap } from '@core/hex-map';
|
|
|
|
describe('edgeMask operations', () => {
|
|
it('creates mask from edges', () => {
|
|
const mask = edgeMask(HexEdge.NE, HexEdge.SW);
|
|
expect(hasEdge(mask, HexEdge.NE)).toBe(true);
|
|
expect(hasEdge(mask, HexEdge.SW)).toBe(true);
|
|
expect(hasEdge(mask, HexEdge.E)).toBe(false);
|
|
expect(edgeCount(mask)).toBe(2);
|
|
});
|
|
|
|
it('toggles edges', () => {
|
|
let mask = edgeMask(HexEdge.E);
|
|
mask = toggleEdge(mask, HexEdge.E);
|
|
expect(hasEdge(mask, HexEdge.E)).toBe(false);
|
|
mask = toggleEdge(mask, HexEdge.E);
|
|
expect(hasEdge(mask, HexEdge.E)).toBe(true);
|
|
});
|
|
|
|
it('sets and clears edges', () => {
|
|
let mask = 0;
|
|
mask = setEdge(mask, HexEdge.NW);
|
|
expect(hasEdge(mask, HexEdge.NW)).toBe(true);
|
|
mask = clearEdge(mask, HexEdge.NW);
|
|
expect(hasEdge(mask, HexEdge.NW)).toBe(false);
|
|
});
|
|
|
|
it('connectedEdges returns correct edges', () => {
|
|
const mask = edgeMask(HexEdge.NE, HexEdge.SE, HexEdge.W);
|
|
const edges = connectedEdges(mask);
|
|
expect(edges).toEqual([HexEdge.NE, HexEdge.SE, HexEdge.W]);
|
|
});
|
|
});
|
|
|
|
describe('rotateMask', () => {
|
|
it('rotating 6 steps returns original', () => {
|
|
const mask = edgeMask(HexEdge.NE, HexEdge.E);
|
|
expect(rotateMask(mask, 6)).toBe(mask);
|
|
});
|
|
|
|
it('rotates single edge clockwise by 1', () => {
|
|
const mask = edgeMask(HexEdge.NE); // bit 0
|
|
const rotated = rotateMask(mask, 1);
|
|
expect(hasEdge(rotated, HexEdge.E)).toBe(true); // bit 1
|
|
expect(edgeCount(rotated)).toBe(1);
|
|
});
|
|
|
|
it('rotates opposite edges correctly', () => {
|
|
const mask = edgeMask(HexEdge.NE, HexEdge.SW); // bits 0,3
|
|
const rotated = rotateMask(mask, 1);
|
|
expect(hasEdge(rotated, HexEdge.E)).toBe(true);
|
|
expect(hasEdge(rotated, HexEdge.W)).toBe(true);
|
|
expect(edgeCount(rotated)).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('enforceEdgeConstraints', () => {
|
|
it('detects missing continuation on neighbor', () => {
|
|
const hexMap = new HexMap();
|
|
const coord = { q: 0, r: 0 };
|
|
const mask = edgeMask(HexEdge.E);
|
|
|
|
const actions = enforceEdgeConstraints(hexMap, coord, 'road', mask);
|
|
expect(actions).toHaveLength(1);
|
|
expect(actions[0].coord).toEqual({ q: 1, r: 0 }); // E neighbor
|
|
expect(actions[0].edge).toBe(HexEdge.W); // opposite edge
|
|
expect(actions[0].terrainId).toBe('road');
|
|
});
|
|
|
|
it('no action needed when neighbor already has feature', () => {
|
|
const hexMap = new HexMap();
|
|
// Set up neighbor with road on W edge
|
|
hexMap.setFeature({ q: 1, r: 0 }, 'road', edgeMask(HexEdge.W));
|
|
|
|
const actions = enforceEdgeConstraints(
|
|
hexMap, { q: 0, r: 0 }, 'road', edgeMask(HexEdge.E),
|
|
);
|
|
expect(actions).toHaveLength(0);
|
|
});
|
|
});
|