The table on a phone: the masthead keeps the title, who is playing and the room code, and folds the rest behind a Table button; the piece panel opens as a summary and unfolds the lore on request; the told game brings the move's board into view and keeps Back and Next at the foot of the screen with the move's name; a game against a drake can be started over at any time and played again when it ends
The masthead on a 390-pixel screen was 300 pixels tall, with seven buttons before the status line; it is 132 now. The piece panel was 30 percent of the screen with the lore scrolling inside; the summary is a third less, and the lore is a tap away. The told game's words were sticky on a phone as on a wide screen, and pinned themselves over the boards; they scroll with the page now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
7dd113b1ff
commit
36508f29ca
4 files changed
+152
-6
No files matched your search
@@ -12,7 +12,8 @@
|
|||||||
legalMovesFrom, movesFrom, squareName, sideOf, kindOf, kingSquare, inCheck, levelOf, frozen, NAMES, SIDE_NAMES,
|
legalMovesFrom, movesFrom, squareName, sideOf, kindOf, kingSquare, inCheck, levelOf, frozen, NAMES, SIDE_NAMES,
|
||||||
type Move, type Side
|
type Move, type Side
|
||||||
} from '$lib/game';
|
} 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';
|
import { prefs, reducedMotion } from '$lib/prefs.svelte';
|
||||||
|
|
||||||
let { room }: { room: Room } = $props();
|
let { room }: { room: Room } = $props();
|
||||||
@@ -199,6 +200,23 @@
|
|||||||
return g.over.winner === room.me ? 'You win.' : `${g.players[g.over.winner].name} wins.`;
|
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;
|
const pieceCount = (side: Side) => g.cells.filter((p) => p !== '' && sideOf(p) === side).length;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -227,7 +245,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if room.error}<p class="warning">{room.error}</p>{/if}
|
{#if room.error}<p class="warning">{room.error}</p>{/if}
|
||||||
|
|
||||||
<p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button></p>
|
<p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button>{#if drake && !g.over} · <button type="button" class="link plain" disabled={starting} onclick={again}>Start over against a fresh drake</button>{/if}</p>
|
||||||
|
{#if fault}<p class="warning">{fault}</p>{/if}
|
||||||
{#if onGlass && Glass}
|
{#if onGlass && Glass}
|
||||||
<Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} />
|
<Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} />
|
||||||
{:else}
|
{:else}
|
||||||
@@ -239,7 +258,7 @@
|
|||||||
|
|
||||||
{#if g.over}
|
{#if g.over}
|
||||||
<div class="verdict">
|
<div class="verdict">
|
||||||
<a class="commit" href="/">Back to the hall</a>
|
{#if drake}<button type="button" class="commit" disabled={starting} onclick={again}>Play the drake again</button> <a class="quiet" href="/">Back to the hall</a>{:else}<a class="commit" href="/">Back to the hall</a>{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if yourMove}
|
{:else if yourMove}
|
||||||
<p class="muted small how">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.</p>
|
<p class="muted small how">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.</p>
|
||||||
@@ -366,7 +385,8 @@
|
|||||||
margin: 0.8rem auto 0;
|
margin: 0.8rem auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verdict .commit {
|
.verdict .commit,
|
||||||
|
.verdict .quiet {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,13 @@
|
|||||||
return lines;
|
return lines;
|
||||||
});
|
});
|
||||||
const letter = $derived(LETTERS[kind]);
|
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;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="lore" aria-label="About this piece">
|
<aside class="lore" aria-label="About this piece">
|
||||||
@@ -62,6 +69,8 @@
|
|||||||
<button type="button" class="dismiss" title="Close" aria-label="Close" onclick={close}>×</button>
|
<button type="button" class="dismiss" title="Close" aria-label="Close" onclick={close}>×</button>
|
||||||
</header>
|
</header>
|
||||||
<p class="now">{situation.join(' ')}</p>
|
<p class="now">{situation.join(' ')}</p>
|
||||||
|
<button type="button" class="link plain unfold" aria-expanded={more} onclick={() => (more = !more)}>{more ? 'Less' : 'How it moves, why it is here, how to play it'}</button>
|
||||||
|
<div class="rest" class:open={more}>
|
||||||
<p>{lore.is}</p>
|
<p>{lore.is}</p>
|
||||||
<h3>How it moves</h3>
|
<h3>How it moves</h3>
|
||||||
<p>{lore.moves}</p>
|
<p>{lore.moves}</p>
|
||||||
@@ -69,6 +78,7 @@
|
|||||||
<p>{lore.why}</p>
|
<p>{lore.why}</p>
|
||||||
<h3>How to play it</h3>
|
<h3>How to play it</h3>
|
||||||
<p>{lore.play}</p>
|
<p>{lore.play}</p>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -107,6 +117,38 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unfold {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.lore {
|
||||||
|
padding-top: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unfold {
|
||||||
|
display: inline;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest.open {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.now {
|
.now {
|
||||||
color: var(--bone);
|
color: var(--bone);
|
||||||
border-left: 2px solid var(--ember);
|
border-left: 2px solid var(--ember);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
let error = $state('');
|
let error = $state('');
|
||||||
let copied = $state(false);
|
let copied = $state(false);
|
||||||
let reporting = $state(false);
|
let reporting = $state(false);
|
||||||
|
let tableOpen = $state(false);
|
||||||
let prefsOpen = $state(false);
|
let prefsOpen = $state(false);
|
||||||
/** A phrase minted to carry this seat to another device, while it lasts. */
|
/** A phrase minted to carry this seat to another device, while it lasts. */
|
||||||
let phrase = $state<{ phrase: string; expiresAt: number } | null>(null);
|
let phrase = $state<{ phrase: string; expiresAt: number } | null>(null);
|
||||||
@@ -190,6 +191,10 @@
|
|||||||
{#if room.view.audience > 0}
|
{#if room.view.audience > 0}
|
||||||
<span class="audience" title="the Peanut Gallery">{room.view.audience} in the gallery{room.view.galleryTalk ? ', talking' : ''}</span>
|
<span class="audience" title="the Peanut Gallery">{room.view.audience} in the gallery{room.view.galleryTalk ? ', talking' : ''}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="quiet more" aria-expanded={tableOpen} onclick={() => (tableOpen = !tableOpen)}>Table {tableOpen ? '▴' : '▾'}</button>
|
||||||
|
<div class="rest">
|
||||||
|
{#if room}
|
||||||
{#if !watching}
|
{#if !watching}
|
||||||
<button type="button" class="quiet" title="A phrase that brings this seat to another device" onclick={transfer}>Transfer seat</button>
|
<button type="button" class="quiet" title="A phrase that brings this seat to another device" onclick={transfer}>Transfer seat</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -204,6 +209,7 @@
|
|||||||
<button type="button" class="quiet" title="Something behaved unexpectedly? Tell the keeper." onclick={() => (reporting = true)}>Report</button>
|
<button type="button" class="quiet" title="Something behaved unexpectedly? Tell the keeper." onclick={() => (reporting = true)}>Report</button>
|
||||||
{/if}
|
{/if}
|
||||||
<a class="quiet" href="/">The hall</a>
|
<a class="quiet" href="/">The hall</a>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -311,6 +317,32 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On a phone the masthead keeps the title, who is playing and the room code; the rest waits behind one button. */
|
||||||
|
.more {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.more {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more[aria-expanded='true'] + .rest {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.quiet.room b {
|
.quiet.room b {
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -389,6 +421,12 @@
|
|||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.masthead {
|
.masthead {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.6rem var(--gutter) 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standfirst {
|
||||||
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
import PiecePanel from '$lib/components/PiecePanel.svelte';
|
import PiecePanel from '$lib/components/PiecePanel.svelte';
|
||||||
import { CALLOUTS, TOLD_MOVES, toldStates } from '$lib/game/told';
|
import { CALLOUTS, TOLD_MOVES, toldStates } from '$lib/game/told';
|
||||||
import { narrate } from '$lib/game/chronicle';
|
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';
|
import { prefs, reducedMotion } from '$lib/prefs.svelte';
|
||||||
|
|
||||||
/** The glass board, loaded the first time it is asked for. */
|
/** The glass board, loaded the first time it is asked for. */
|
||||||
@@ -49,11 +49,22 @@
|
|||||||
function go(n: number) {
|
function go(n: number) {
|
||||||
ply = Math.max(0, Math.min(TOLD_MOVES.length, n));
|
ply = Math.max(0, Math.min(TOLD_MOVES.length, n));
|
||||||
inspected = null;
|
inspected = null;
|
||||||
|
showMove();
|
||||||
}
|
}
|
||||||
function tap(i: number) {
|
function tap(i: number) {
|
||||||
inspected = g.cells[i] !== '' && inspected !== i ? i : null;
|
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. */
|
/** 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) {
|
function key(e: KeyboardEvent) {
|
||||||
if ((e.target as HTMLElement | null)?.tagName === 'rect') return;
|
if ((e.target as HTMLElement | null)?.tagName === 'rect') return;
|
||||||
if (e.key === 'ArrowRight') go(ply + 1);
|
if (e.key === 'ArrowRight') go(ply + 1);
|
||||||
@@ -97,6 +108,7 @@
|
|||||||
<button type="button" class="quiet" disabled={ply === 0} onclick={() => go(ply - 1)}>Back</button>
|
<button type="button" class="quiet" disabled={ply === 0} onclick={() => go(ply - 1)}>Back</button>
|
||||||
<button type="button" class="commit small" disabled={ply === TOLD_MOVES.length} onclick={() => go(ply + 1)}>Next move</button>
|
<button type="button" class="commit small" disabled={ply === TOLD_MOVES.length} onclick={() => go(ply + 1)}>Next move</button>
|
||||||
{#if ply === TOLD_MOVES.length}<a class="quiet" href="/">Play a drake</a>{/if}
|
{#if ply === TOLD_MOVES.length}<a class="quiet" href="/">Play a drake</a>{/if}
|
||||||
|
<span class="where">{ply === 0 ? 'Before the first move' : `Move ${ply} of ${TOLD_MOVES.length}`}{#if move} · {notation(move)}{/if}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted small marks">
|
<p class="muted small marks">
|
||||||
Jump to a callout:
|
Jump to a callout:
|
||||||
@@ -123,13 +135,47 @@
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.where {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.stage {
|
.stage {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user