import { describe, expect, it } from 'vitest'; import { BLANK, cellOccupied, isBlankSlot, newPlacement, place, placementFromHint, rackView, recallAt, recallIndex, reorderIndices, reset, toSubmit, } from './placement'; const rack = ['A', 'Q', BLANK, 'N', 'I', 'W', 'E']; describe('placement state machine', () => { it('places a tile and marks the rack slot used', () => { const p = place(newPlacement(rack), 0, 7, 7); expect(p.pending).toHaveLength(1); expect(rackView(p)[0].used).toBe(true); expect(rackView(p)[1].used).toBe(false); }); it('rejects reusing a slot or an occupied cell', () => { let p = place(newPlacement(rack), 0, 7, 7); p = place(p, 0, 7, 8); // same slot -> no-op expect(p.pending).toHaveLength(1); p = place(p, 1, 7, 7); // occupied cell -> no-op expect(p.pending).toHaveLength(1); }); it('requires a letter for a blank slot', () => { const noLetter = place(newPlacement(rack), 2, 7, 7); expect(noLetter.pending).toHaveLength(0); const withLetter = place(newPlacement(rack), 2, 7, 7, 'x'); expect(withLetter.pending[0]).toMatchObject({ letter: 'X', blank: true }); }); it('recalls a tile by cell', () => { let p = place(newPlacement(rack), 0, 7, 7); p = recallAt(p, 7, 7); expect(p.pending).toHaveLength(0); expect(reset(place(p, 0, 7, 7)).pending).toHaveLength(0); }); it('builds a sorted submit payload and returns null when empty', () => { let p = place(newPlacement(rack), 1, 7, 9); p = place(p, 0, 7, 7); const sub = toSubmit(p); expect(sub?.tiles.map((t) => t.col)).toEqual([7, 9]); expect(toSubmit(newPlacement(rack))).toBeNull(); }); it('recalls a tile by rack index and reports occupied cells / blank slots', () => { let p = place(newPlacement(rack), 0, 7, 7); p = place(p, 1, 7, 8); expect(cellOccupied(p, 7, 7)).toBe(true); expect(cellOccupied(p, 6, 6)).toBe(false); p = recallIndex(p, 0); expect(p.pending.map((t) => t.rackIndex)).toEqual([1]); expect(isBlankSlot(newPlacement(rack), 2)).toBe(true); // '?' slot expect(isBlankSlot(newPlacement(rack), 0)).toBe(false); }); it('submits a single tile as a one-tile payload', () => { const sub = toSubmit(place(newPlacement(rack), 0, 7, 7)); expect(sub?.tiles).toHaveLength(1); }); }); describe('placementFromHint', () => { it('maps hint letters and blanks onto rack slots', () => { const p = placementFromHint( [ { row: 7, col: 7, letter: 'C', blank: false }, { row: 7, col: 8, letter: 'A', blank: false }, { row: 7, col: 9, letter: 'B', blank: true }, ], ['C', 'A', BLANK, 'T'], ); expect(p.pending).toHaveLength(3); expect(p.pending[0]).toMatchObject({ rackIndex: 0, letter: 'C', blank: false }); expect(p.pending[2]).toMatchObject({ rackIndex: 2, letter: 'B', blank: true }); }); it('falls back to a blank slot when the hint letter is not in the rack', () => { const p = placementFromHint([{ row: 7, col: 7, letter: 'Z', blank: false }], ['A', BLANK]); expect(p.pending).toHaveLength(1); expect(p.pending[0]).toMatchObject({ rackIndex: 1, letter: 'Z', blank: true }); }); it('skips hint tiles once the rack is exhausted', () => { const p = placementFromHint( [ { row: 7, col: 7, letter: 'A', blank: false }, { row: 7, col: 8, letter: 'B', blank: false }, ], ['A'], ); expect(p.pending.map((t) => t.letter)).toEqual(['A']); }); }); describe('reorderIndices', () => { it('lifts an element and drops it at the given slot among the others', () => { expect(reorderIndices(4, 0, 2)).toEqual([1, 2, 0, 3]); expect(reorderIndices(4, 3, 0)).toEqual([3, 0, 1, 2]); expect(reorderIndices(4, 1, 1)).toEqual([0, 1, 2, 3]); // back to identity expect(reorderIndices(3, 0, 99)).toEqual([1, 2, 0]); // slot clamped to the end }); });