test(simulation): ✅ Add tests for polar hex grid neighbor calculations and rendering in simulation module
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2d90723253
commit
fcd67fff4f
1 changed files with 276 additions and 0 deletions
|
|
@ -0,0 +1,276 @@
|
|||
/**
|
||||
* Tests for the polar hex grid layout and data mapping.
|
||||
* Verifies that equator and polar views produce consistent tile→data mappings
|
||||
* and that the geodesic ring structure has correct tile counts and positions.
|
||||
*/
|
||||
import {
|
||||
terrainColorJS,
|
||||
buildEquatorLayout,
|
||||
buildPolarLayout,
|
||||
polarTileCount,
|
||||
computeHexColors,
|
||||
} from '@magic-civ/guide-engine/components/climate-sim/polarHexGrid.js'
|
||||
import { GRID_WIDTH, GRID_HEIGHT, HEX_W, HEX_H } from '@magic-civ/engine-ts'
|
||||
|
||||
// ── terrainColorJS ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('terrainColorJS', () => {
|
||||
it('returns ocean color for encoded value 0', () => {
|
||||
const [r, g, b] = terrainColorJS(0)
|
||||
expect(r).toBeCloseTo(0.24, 2)
|
||||
expect(g).toBeCloseTo(0.47, 2)
|
||||
expect(b).toBeCloseTo(0.82, 2)
|
||||
})
|
||||
|
||||
it('returns ice color for encoded value 4/24', () => {
|
||||
const [r, g, b] = terrainColorJS(4 / 24)
|
||||
expect(r).toBeCloseTo(0.88, 2)
|
||||
expect(g).toBeCloseTo(0.94, 2)
|
||||
expect(b).toBeCloseTo(1.00, 2)
|
||||
})
|
||||
|
||||
it('returns grassland color for encoded value 9/24', () => {
|
||||
const [r, g, b] = terrainColorJS(9 / 24)
|
||||
expect(r).toBeCloseTo(0.38, 2)
|
||||
expect(g).toBeCloseTo(0.72, 2)
|
||||
expect(b).toBeCloseTo(0.35, 2)
|
||||
})
|
||||
|
||||
it('clamps out-of-range values to last color', () => {
|
||||
const [r, g, b] = terrainColorJS(1.5)
|
||||
// Should clamp to index 24 (last entry)
|
||||
expect(r).toBeCloseTo(0.30, 2)
|
||||
expect(g).toBeCloseTo(0.50, 2)
|
||||
expect(b).toBeCloseTo(0.80, 2)
|
||||
})
|
||||
})
|
||||
|
||||
// ── polarTileCount ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('polarTileCount', () => {
|
||||
it('returns 1 for ring 0 (single pole tile)', () => {
|
||||
expect(polarTileCount(0)).toBe(1)
|
||||
})
|
||||
|
||||
it('returns 7 for 1 ring (center + 6)', () => {
|
||||
expect(polarTileCount(1)).toBe(7)
|
||||
})
|
||||
|
||||
it('returns 19 for 2 rings (1 + 6 + 12)', () => {
|
||||
expect(polarTileCount(2)).toBe(19)
|
||||
})
|
||||
|
||||
it('returns 37 for 3 rings (1 + 6 + 12 + 18)', () => {
|
||||
expect(polarTileCount(3)).toBe(37)
|
||||
})
|
||||
|
||||
it('matches formula 1 + 3*N*(N+1)', () => {
|
||||
for (let n = 0; n <= 12; n++) {
|
||||
expect(polarTileCount(n)).toBe(1 + 3 * n * (n + 1))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ── buildEquatorLayout ──────────────────────────────────────────────────────
|
||||
|
||||
describe('buildEquatorLayout', () => {
|
||||
const layout = buildEquatorLayout()
|
||||
|
||||
it('produces exactly 40×24 = 960 tiles', () => {
|
||||
expect(layout.tiles.length).toBe(GRID_WIDTH * GRID_HEIGHT)
|
||||
})
|
||||
|
||||
it('has hexR = HEX_W / 2', () => {
|
||||
expect(layout.hexR).toBe(HEX_W / 2)
|
||||
})
|
||||
|
||||
it('maps each tile to its own unique (col, row)', () => {
|
||||
const seen = new Set<string>()
|
||||
for (const tile of layout.tiles) {
|
||||
const key = `${tile.col},${tile.row}`
|
||||
expect(seen.has(key)).toBe(false)
|
||||
seen.add(key)
|
||||
}
|
||||
expect(seen.size).toBe(GRID_WIDTH * GRID_HEIGHT)
|
||||
})
|
||||
|
||||
it('all cols are in [0, 39] and all rows are in [0, 23]', () => {
|
||||
for (const tile of layout.tiles) {
|
||||
expect(tile.col).toBeGreaterThanOrEqual(0)
|
||||
expect(tile.col).toBeLessThan(GRID_WIDTH)
|
||||
expect(tile.row).toBeGreaterThanOrEqual(0)
|
||||
expect(tile.row).toBeLessThan(GRID_HEIGHT)
|
||||
}
|
||||
})
|
||||
|
||||
it('positions first tile at hexCenter(0, 0)', () => {
|
||||
const first = layout.tiles[0]!
|
||||
expect(first.x).toBeCloseTo(HEX_W / 2, 1)
|
||||
expect(first.y).toBeCloseTo(HEX_H / 2, 1)
|
||||
expect(first.col).toBe(0)
|
||||
expect(first.row).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ── buildPolarLayout ────────────────────────────────────────────────────────
|
||||
|
||||
describe('buildPolarLayout', () => {
|
||||
const canvasW = 726
|
||||
const canvasH = 490
|
||||
|
||||
describe('north pole', () => {
|
||||
const layout = buildPolarLayout('north', 5, canvasW, canvasH)
|
||||
|
||||
it('has correct tile count for 5 rings', () => {
|
||||
expect(layout.tiles.length).toBe(polarTileCount(5))
|
||||
})
|
||||
|
||||
it('center tile maps to row 0 (north pole)', () => {
|
||||
const center = layout.tiles[0]!
|
||||
expect(center.row).toBe(0)
|
||||
expect(center.x).toBeCloseTo(canvasW / 2, 1)
|
||||
expect(center.y).toBeCloseTo(canvasH / 2, 1)
|
||||
})
|
||||
|
||||
it('ring 1 tiles map to row 1', () => {
|
||||
// Tiles 1-6 are ring 1
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
expect(layout.tiles[i]!.row).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('ring N tiles map to row N', () => {
|
||||
// Ring 2 starts at index 7 (1 + 6), has 12 tiles
|
||||
for (let i = 7; i < 7 + 12; i++) {
|
||||
expect(layout.tiles[i]!.row).toBe(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('all data coords are within grid bounds', () => {
|
||||
for (const tile of layout.tiles) {
|
||||
expect(tile.col).toBeGreaterThanOrEqual(0)
|
||||
expect(tile.col).toBeLessThan(GRID_WIDTH)
|
||||
expect(tile.row).toBeGreaterThanOrEqual(0)
|
||||
expect(tile.row).toBeLessThan(GRID_HEIGHT)
|
||||
}
|
||||
})
|
||||
|
||||
it('ring 1 columns are evenly distributed across the 40-column grid', () => {
|
||||
const ring1Cols = layout.tiles.slice(1, 7).map((t) => t.col)
|
||||
// 6 tiles should cover different parts of the 40-column range
|
||||
const uniqueCols = new Set(ring1Cols)
|
||||
expect(uniqueCols.size).toBe(6)
|
||||
// Columns should span a wide range (not all clustered)
|
||||
const min = Math.min(...ring1Cols)
|
||||
const max = Math.max(...ring1Cols)
|
||||
expect(max - min).toBeGreaterThan(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('south pole', () => {
|
||||
const layout = buildPolarLayout('south', 3, canvasW, canvasH)
|
||||
|
||||
it('center tile maps to row 23 (south pole)', () => {
|
||||
expect(layout.tiles[0]!.row).toBe(GRID_HEIGHT - 1)
|
||||
})
|
||||
|
||||
it('ring 1 tiles map to row 22', () => {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
expect(layout.tiles[i]!.row).toBe(GRID_HEIGHT - 2)
|
||||
}
|
||||
})
|
||||
|
||||
it('rows decrease as rings increase (moving toward equator)', () => {
|
||||
const ring2Row = layout.tiles[7]!.row
|
||||
const ring3Row = layout.tiles[7 + 12]!.row
|
||||
expect(ring2Row).toBe(GRID_HEIGHT - 3)
|
||||
expect(ring3Row).toBe(GRID_HEIGHT - 4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ring clamping', () => {
|
||||
it('clamps rows when numRings exceeds grid height', () => {
|
||||
const layout = buildPolarLayout('north', 30, canvasW, canvasH)
|
||||
for (const tile of layout.tiles) {
|
||||
expect(tile.row).toBeGreaterThanOrEqual(0)
|
||||
expect(tile.row).toBeLessThan(GRID_HEIGHT)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── computeHexColors ────────────────────────────────────────────────────────
|
||||
|
||||
describe('computeHexColors', () => {
|
||||
it('produces 7 vertices × 3 channels per hex', () => {
|
||||
const dataCoords = [{ col: 0, row: 0 }, { col: 1, row: 1 }]
|
||||
const texB = new Float32Array(GRID_WIDTH * GRID_HEIGHT * 4)
|
||||
const colors = computeHexColors(dataCoords, texB)
|
||||
expect(colors.length).toBe(2 * 7 * 3)
|
||||
})
|
||||
|
||||
it('all 7 vertices of a hex share the same color', () => {
|
||||
const dataCoords = [{ col: 5, row: 3 }]
|
||||
const texB = new Float32Array(GRID_WIDTH * GRID_HEIGHT * 4)
|
||||
// Set terrain at (5, 3) to grassland (index 9, encoded as 9/24)
|
||||
const texIdx = (3 * GRID_WIDTH + 5) * 4
|
||||
texB[texIdx + 2] = 9 / 24
|
||||
const colors = computeHexColors(dataCoords, texB)
|
||||
const [r0, g0, b0] = [colors[0]!, colors[1]!, colors[2]!]
|
||||
for (let v = 1; v < 7; v++) {
|
||||
expect(colors[v * 3]).toBe(r0)
|
||||
expect(colors[v * 3 + 1]).toBe(g0)
|
||||
expect(colors[v * 3 + 2]).toBe(b0)
|
||||
}
|
||||
})
|
||||
|
||||
it('equator and polar views read the same data for the same (col, row)', () => {
|
||||
// Create a texB with known terrain at specific positions
|
||||
const texB = new Float32Array(GRID_WIDTH * GRID_HEIGHT * 4)
|
||||
// Set row 0 col 20 to ice (index 4)
|
||||
texB[(0 * GRID_WIDTH + 20) * 4 + 2] = 4 / 24
|
||||
// Set row 1 col 10 to forest (index 10)
|
||||
texB[(1 * GRID_WIDTH + 10) * 4 + 2] = 10 / 24
|
||||
|
||||
// Equator tile at (20, 0)
|
||||
const equatorColors = computeHexColors([{ col: 20, row: 0 }], texB)
|
||||
// Polar center tile also maps to (20, 0) for north pole
|
||||
const polarColors = computeHexColors([{ col: 20, row: 0 }], texB)
|
||||
|
||||
// Same (col, row) → identical colors
|
||||
expect(equatorColors[0]).toBe(polarColors[0])
|
||||
expect(equatorColors[1]).toBe(polarColors[1])
|
||||
expect(equatorColors[2]).toBe(polarColors[2])
|
||||
})
|
||||
})
|
||||
|
||||
// ── data consistency between equator and polar ──────────────────────────────
|
||||
|
||||
describe('equator-polar data consistency', () => {
|
||||
it('polar center tile reads from the same texB index as equator row-0 tile', () => {
|
||||
const equator = buildEquatorLayout()
|
||||
const polar = buildPolarLayout('north', 1, 726, 490)
|
||||
|
||||
// Find equator tile at (col=20, row=0) — same col as polar center
|
||||
const equatorTile = equator.tiles.find((t) => t.col === 20 && t.row === 0)!
|
||||
const polarCenter = polar.tiles[0]!
|
||||
|
||||
// Both should map to the same grid position
|
||||
expect(polarCenter.col).toBe(equatorTile.col)
|
||||
expect(polarCenter.row).toBe(equatorTile.row)
|
||||
})
|
||||
|
||||
it('polar ring 1 tiles all reference row 1 data', () => {
|
||||
const polar = buildPolarLayout('north', 1, 726, 490)
|
||||
const ring1 = polar.tiles.slice(1, 7)
|
||||
|
||||
for (const tile of ring1) {
|
||||
expect(tile.row).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('south pole center maps to last row', () => {
|
||||
const polar = buildPolarLayout('south', 1, 726, 490)
|
||||
expect(polar.tiles[0]!.row).toBe(GRID_HEIGHT - 1)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue