90eaf4964b
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Live play now exchanges per-variant alphabet indices instead of concrete letters (rack out; submit-play, evaluate, exchange, word-check in). The client caches each variant's (index, letter, value) table behind StateRequest.include_alphabet and renders the rack and blank chooser from it, dropping the hardcoded value/alphabet tables. History, the durable journal and GCG stay decoded concrete characters (ARCHITECTURE §9.1, unchanged). - pkg/fbs: new AlphabetEntry + PlayTile; StateView.rack -> [ubyte] + alphabet; StateRequest.include_alphabet; SubmitPlay/Eval tiles -> [PlayTile]; Exchange tiles + CheckWord word -> [ubyte] (committed Go + TS regenerated). - engine: AlphabetTable + a cached per-variant codec (LetterForIndex/EncodeRack/ DecodeTiles/DecodeWord) + BlankIndex sentinel; Go parity test. - backend server edge maps index<->letter (new thin game.Service.GameVariant); game.Service domain methods, engine.Game and the robot keep one letter-based play path. The gateway forwards indices verbatim (no alphabet table). - ui: lib/alphabet.ts in-memory cache; codec encodes/decodes indices; premiums.ts is geometry-only; the mock seeds a fixture table; the UI normalises display to upper case (codec + cache), leaving placement/board/checkword unchanged. Parity moved to the Go engine.AlphabetTable test; premiums.ts loses its value tables. Discharges TODO-4.
79 lines
2.0 KiB
Svelte
79 lines
2.0 KiB
Svelte
<script lang="ts">
|
|
import type { RackSlot } from '../lib/placement';
|
|
import { BLANK } from '../lib/placement';
|
|
import { valueForLetter } from '../lib/alphabet';
|
|
import type { Variant } from '../lib/model';
|
|
|
|
let {
|
|
slots,
|
|
variant,
|
|
selected,
|
|
ondown,
|
|
}: {
|
|
slots: RackSlot[];
|
|
variant: Variant;
|
|
selected: number | null;
|
|
ondown: (e: PointerEvent, index: number) => void;
|
|
} = $props();
|
|
|
|
// Used slots are hidden (the rack shifts left, freeing room on the right for the
|
|
// MakeMove control); the slot still exists in the model for per-tile recall.
|
|
const visible = $derived(slots.filter((s) => !s.used));
|
|
</script>
|
|
|
|
<div class="rack">
|
|
{#each visible as slot (slot.index)}
|
|
<button
|
|
class="tile"
|
|
class:selected={selected === slot.index}
|
|
data-rack-index={slot.index}
|
|
onpointerdown={(e) => ondown(e, slot.index)}
|
|
>
|
|
<span class="letter">{slot.letter === BLANK ? '' : slot.letter}</span>
|
|
{#if slot.letter !== BLANK}<span class="val">{valueForLetter(variant, slot.letter)}</span>{/if}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<style>
|
|
.rack {
|
|
display: flex;
|
|
gap: 5px;
|
|
align-items: center;
|
|
/* Reserve one tile's height so an empty rack (e.g. a finished game) keeps the
|
|
footer the same size as during play — no layout jump between states. */
|
|
min-height: min(12.5vw, 46px);
|
|
}
|
|
.tile {
|
|
position: relative;
|
|
flex: 0 0 auto;
|
|
width: min(12.5vw, 46px);
|
|
aspect-ratio: 1;
|
|
background: var(--tile-bg);
|
|
color: var(--tile-text);
|
|
border: none;
|
|
border-radius: 5px;
|
|
box-shadow: inset 0 -3px 0 var(--tile-edge);
|
|
font-weight: 700;
|
|
font-size: 1.4rem;
|
|
touch-action: none;
|
|
user-select: none;
|
|
}
|
|
.tile.selected {
|
|
outline: 3px solid var(--accent);
|
|
outline-offset: -3px;
|
|
}
|
|
.letter {
|
|
position: absolute;
|
|
top: 8%;
|
|
left: 14%;
|
|
}
|
|
.val {
|
|
position: absolute;
|
|
right: 4px;
|
|
bottom: 1px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
}
|
|
</style>
|