import { describe, it, expect } from 'vitest'; import { axialToPixel, pixelToAxial, axialRound, getNeighbor, getNeighbors, axialDistance, coordKey, parseCoordKey, hexVertices, hexEdgeMidpoints, closestEdge, } from '@core/coords'; import { HexEdge } from '@core/types'; describe('axialToPixel / pixelToAxial roundtrip', () => { const size = 32; it('origin hex maps to origin pixel', () => { const p = axialToPixel({ q: 0, r: 0 }, size); expect(p.x).toBeCloseTo(0); expect(p.y).toBeCloseTo(0); }); it('roundtrips integer coordinates', () => { const cases = [ { q: 0, r: 0 }, { q: 1, r: 0 }, { q: 0, r: 1 }, { q: -2, r: 3 }, { q: 5, r: -3 }, ]; for (const coord of cases) { const pixel = axialToPixel(coord, size); const back = pixelToAxial(pixel, size); expect(back.q).toBe(coord.q); expect(back.r).toBe(coord.r); } }); it('respects origin offset', () => { const origin = { x: 100, y: 200 }; const coord = { q: 2, r: 1 }; const pixel = axialToPixel(coord, size, origin); const back = pixelToAxial(pixel, size, origin); expect(back.q).toBe(coord.q); expect(back.r).toBe(coord.r); }); }); describe('axialRound', () => { it('rounds fractional coords to nearest hex', () => { const result = axialRound({ q: 0.3, r: 0.1 }); expect(result.q).toBe(0); expect(result.r).toBe(0); }); it('handles mid-boundary correctly', () => { const result = axialRound({ q: 0.7, r: -0.2 }); expect(Number.isInteger(result.q)).toBe(true); expect(Number.isInteger(result.r)).toBe(true); // Cube constraint: q + r + s = 0 const s = -result.q - result.r; expect(Number.isInteger(s)).toBe(true); }); }); describe('neighbors', () => { it('returns correct neighbor for each edge', () => { const origin = { q: 3, r: 4 }; expect(getNeighbor(origin, HexEdge.NE)).toEqual({ q: 4, r: 3 }); expect(getNeighbor(origin, HexEdge.E)).toEqual({ q: 4, r: 4 }); expect(getNeighbor(origin, HexEdge.SE)).toEqual({ q: 3, r: 5 }); expect(getNeighbor(origin, HexEdge.SW)).toEqual({ q: 2, r: 5 }); expect(getNeighbor(origin, HexEdge.W)).toEqual({ q: 2, r: 4 }); expect(getNeighbor(origin, HexEdge.NW)).toEqual({ q: 3, r: 3 }); }); it('getNeighbors returns 6 neighbors', () => { const neighbors = getNeighbors({ q: 0, r: 0 }); expect(neighbors).toHaveLength(6); // All should be distance 1 for (const n of neighbors) { expect(axialDistance({ q: 0, r: 0 }, n)).toBe(1); } }); }); describe('axialDistance', () => { it('same hex is distance 0', () => { expect(axialDistance({ q: 2, r: 3 }, { q: 2, r: 3 })).toBe(0); }); it('adjacent hexes are distance 1', () => { expect(axialDistance({ q: 0, r: 0 }, { q: 1, r: 0 })).toBe(1); }); it('diagonal distance', () => { expect(axialDistance({ q: 0, r: 0 }, { q: 3, r: -3 })).toBe(3); }); }); describe('coordKey / parseCoordKey', () => { it('roundtrips', () => { const coord = { q: -5, r: 12 }; expect(parseCoordKey(coordKey(coord))).toEqual(coord); }); }); describe('hexVertices', () => { it('produces 6 vertices', () => { const verts = hexVertices(0, 0, 32); expect(verts).toHaveLength(6); }); it('vertices are equidistant from center', () => { const size = 32; const verts = hexVertices(10, 20, size); for (const v of verts) { const dist = Math.hypot(v.x - 10, v.y - 20); expect(dist).toBeCloseTo(size, 5); } }); }); describe('closestEdge', () => { // Flat-top hex: vertices at 0,60,120,180,240,300 degrees // Edge midpoints are between consecutive vertices: // midpoint 0 (v0-v1) at ~30° → NE direction // midpoint 1 (v1-v2) at ~90° → top (NW in our enum) // midpoint 2 (v2-v3) at ~150° → upper-left // midpoint 3 (v3-v4) at ~210° → lower-left // midpoint 4 (v4-v5) at ~270° → bottom // midpoint 5 (v5-v0) at ~330° → lower-right // The mapping between midpoint index and HexEdge enum depends // on how we defined the enum. Our vertex-based midpoints go: // index 0 → NE(30°), 1 → NW(90°)... etc // So a point at x=30,y=0 (0°) is closest to midpoint 0 (NE) or 5 (SE-ish) it('point to the upper-right → NE edge', () => { const center = { x: 0, y: 0 }; // NE midpoint is at ~(-30°) in screen coords = upper-right const point = { x: 24, y: -14 }; expect(closestEdge(center, 32, point)).toBe(HexEdge.NE); }); it('point to the right → E edge', () => { const center = { x: 0, y: 0 }; // E midpoint is at ~30° = right-downish const point = { x: 28, y: 10 }; expect(closestEdge(center, 32, point)).toBe(HexEdge.E); }); it('point directly below → SE edge', () => { const center = { x: 0, y: 0 }; const point = { x: 0, y: 30 }; expect(closestEdge(center, 32, point)).toBe(HexEdge.SE); }); it('point directly above → NW edge', () => { const center = { x: 0, y: 0 }; const point = { x: 0, y: -30 }; expect(closestEdge(center, 32, point)).toBe(HexEdge.NW); }); it('returns a valid edge (0-5)', () => { const center = { x: 100, y: 100 }; const point = { x: 120, y: 95 }; const edge = closestEdge(center, 32, point); expect(edge).toBeGreaterThanOrEqual(0); expect(edge).toBeLessThanOrEqual(5); }); });