diff --git a/src/lib/components/Board.svelte b/src/lib/components/Board.svelte index a9c6478..a74e8bf 100644 --- a/src/lib/components/Board.svelte +++ b/src/lib/components/Board.svelte @@ -12,7 +12,8 @@ legalMovesFrom, movesFrom, squareName, sideOf, kindOf, kingSquare, inCheck, levelOf, frozen, NAMES, SIDE_NAMES, type Move, type Side } from '$lib/game'; - import type { Room } from '$lib/net/room.svelte'; + import { Room } from '$lib/net/room.svelte'; + import { goto } from '$app/navigation'; import { prefs, reducedMotion } from '$lib/prefs.svelte'; let { room }: { room: Room } = $props(); @@ -199,6 +200,23 @@ return g.over.winner === room.me ? 'You win.' : `${g.players[g.over.winner].name} wins.`; }); + /** A game against a drake can be started over at any time: a fresh table, the same side and setup. */ + const drake = $derived(!room.spectating && room.view.seats.some((s) => s.bot && !s.standIn)); + let starting = $state(false); + let fault = $state(''); + async function again() { + starting = true; + fault = ''; + try { + const next = await Room.createWithBot(room.nameOf(room.me!), room.view.options); + await goto(`/join/${next.roomId}`); + } catch (e) { + fault = e instanceof Error ? e.message : 'A new table could not be opened.'; + } finally { + starting = false; + } + } + const pieceCount = (side: Side) => g.cells.filter((p) => p !== '' && sideOf(p) === side).length; @@ -227,7 +245,8 @@ {/if} {#if room.error}

{room.error}

{/if} -

+

{#if drake && !g.over} · {/if}

+ {#if fault}

{fault}

{/if} {#if onGlass && Glass} {:else} @@ -239,7 +258,7 @@ {#if g.over}
- Back to the hall + {#if drake} Back to the hall{:else}Back to the hall{/if}
{:else if yourMove}

Tap a piece, then a square on any board. Tap any piece, yours or not, to read what it is and what it can do. With a keyboard, the arrows move about a board, Page Up and Page Down change boards, Enter picks up and sets down, and Escape cancels.

@@ -366,7 +385,8 @@ margin: 0.8rem auto 0; } - .verdict .commit { + .verdict .commit, + .verdict .quiet { display: inline-block; } diff --git a/src/lib/components/PiecePanel.svelte b/src/lib/components/PiecePanel.svelte index 59af640..dcdd07f 100644 --- a/src/lib/components/PiecePanel.svelte +++ b/src/lib/components/PiecePanel.svelte @@ -50,6 +50,13 @@ return lines; }); const letter = $derived(LETTERS[kind]); + + /** On a phone the panel opens as a summary, and the lore unfolds on request. */ + let more = $state(false); + $effect(() => { + void at; + more = false; + }); diff --git a/src/routes/learn/game/+page.svelte b/src/routes/learn/game/+page.svelte index 41a8d34..9a34fc0 100644 --- a/src/routes/learn/game/+page.svelte +++ b/src/routes/learn/game/+page.svelte @@ -5,7 +5,7 @@ import PiecePanel from '$lib/components/PiecePanel.svelte'; import { CALLOUTS, TOLD_MOVES, toldStates } from '$lib/game/told'; import { narrate } from '$lib/game/chronicle'; - import { notation, kingSquare, inCheck, movesFrom, legalMovesFrom, sideOf } from '$lib/game'; + import { notation, kingSquare, inCheck, movesFrom, legalMovesFrom, sideOf, levelOf } from '$lib/game'; import { prefs, reducedMotion } from '$lib/prefs.svelte'; /** The glass board, loaded the first time it is asked for. */ @@ -49,11 +49,22 @@ function go(n: number) { ply = Math.max(0, Math.min(TOLD_MOVES.length, n)); inspected = null; + showMove(); } function tap(i: number) { inspected = g.cells[i] !== '' && inspected !== i ? i : null; } /** Left and right step the game, unless the keyboard is on a square, where the arrows walk the board. */ + /** On a phone the boards stack below the words, so the board the move was played on is brought into view. */ + function showMove() { + const m = ply > 0 ? last.moves[ply - 1] : null; + if (!m || typeof matchMedia === 'undefined' || !matchMedia('(max-width: 900px)').matches) return; + requestAnimationFrame(() => { + const target = onGlass ? document.querySelector('.boards canvas') : document.querySelector(`.boards g.world[data-level="${levelOf(m.to)}"]`); + target?.scrollIntoView({ block: 'center', behavior: calm ? 'auto' : 'smooth' }); + }); + } + function key(e: KeyboardEvent) { if ((e.target as HTMLElement | null)?.tagName === 'rect') return; if (e.key === 'ArrowRight') go(ply + 1); @@ -97,6 +108,7 @@ {#if ply === TOLD_MOVES.length}Play a drake{/if} + {ply === 0 ? 'Before the first move' : `Move ${ply} of ${TOLD_MOVES.length}`}{#if move} · {notation(move)}{/if}

Jump to a callout: @@ -123,13 +135,47 @@ align-items: start; } + .where { + display: none; + } + @media (max-width: 900px) { .stage { grid-template-columns: minmax(0, 1fr); } - .words { + /* The words come first and scroll with the page; the stickiness that keeps them beside the boards on a wide screen would pin them over the boards here. */ + .stage .words { order: -1; + position: static; + max-height: none; + overflow: visible; + } + + /* Back and Next stay at the foot of the screen with the move's name, so the boards can be looked at without losing the thread. */ + .ways { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 4; + margin: 0; + padding: 0.55rem var(--gutter); + background: var(--slate-deep); + border-top: 1px solid var(--rule); + } + + .where { + display: block; + flex-basis: 100%; + order: -1; + font-size: 0.85rem; + color: var(--bone-dim); + font-variant-numeric: tabular-nums; + } + + main { + padding-bottom: 4.5rem; } }