A table may ask each seat to rank its choices before the deal, in secret; the start line keeps the rankings and create reads them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
1 parent
eca8c92964
commit
8c99126aee
12 files changed
+186
-9
No files matched your search
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import TableTalk from './TableTalk.svelte';
|
||||
import { game } from '$lib/game';
|
||||
import type { Room } from '$lib/net/room.svelte';
|
||||
|
||||
let { room, link, copied, copyLink }: { room: Room; link: string; copied: boolean; copyLink: () => void } = $props();
|
||||
@@ -8,6 +10,28 @@
|
||||
const hostName = $derived(v.seats.find((s) => s.id === v.host)?.name ?? 'the host');
|
||||
const keeperSeated = $derived(v.seats.some((s) => s.name.toLowerCase() === v.keeper.toLowerCase()));
|
||||
|
||||
// A table that asks for rankings: this seat's, best first, kept on the server as it changes.
|
||||
const choices = $derived(game.rankable?.(v.options) ?? null);
|
||||
const people = $derived(v.seats.filter((s) => !s.bot).length);
|
||||
const labelOf = (value: string) => choices?.find((c) => c.value === value)?.label ?? value;
|
||||
let order = $state<string[]>([]);
|
||||
// Follows the server's copy; the local order is read untracked, so a click is not undone before its reply.
|
||||
$effect(() => {
|
||||
if (!choices) return;
|
||||
const values = choices.map((c) => c.value);
|
||||
const mine = v.yourRanking;
|
||||
if (mine && mine.length === values.length) order = [...mine];
|
||||
else if (untrack(() => order.length) !== values.length) order = values;
|
||||
});
|
||||
/** Move the choice at `i` to place `j`, the others closing up behind it. */
|
||||
async function move(i: number, j: number) {
|
||||
if (j < 0 || j >= order.length || j === i) return;
|
||||
const next = [...order];
|
||||
next.splice(j, 0, ...next.splice(i, 1));
|
||||
order = next;
|
||||
await room.rank(next);
|
||||
}
|
||||
|
||||
/** The hour where the keeper lives, in words, so a caller knows whether to wait. */
|
||||
let minute = $state(0);
|
||||
$effect(() => {
|
||||
@@ -62,6 +86,28 @@
|
||||
{#if v.rematchOf}
|
||||
<p class="muted">A rematch of table {v.rematchOf}: the same company, seats held for those not yet back.</p>
|
||||
{/if}
|
||||
{#if choices && !room.spectating}
|
||||
<section class="ranking" aria-label="Your ranking">
|
||||
<h3>Your ranking</h3>
|
||||
<p class="muted small">
|
||||
Best first. When the game begins, the deal reads everyone's ranking; nobody sees another player's.
|
||||
{#if !v.yourRanking}Until you move one, you have no preference and take what is left.{:else}Your ranking is kept.{/if}
|
||||
</p>
|
||||
<ol>
|
||||
{#each order as value, i (value)}
|
||||
<li>
|
||||
<span class="place">{i + 1}.</span>
|
||||
<span class="choice">{labelOf(value)}</span>
|
||||
<button type="button" class="quiet" aria-label="Move {labelOf(value)} to the top" title="To the top" disabled={i === 0} onclick={() => move(i, 0)}>⤒</button>
|
||||
<button type="button" class="quiet" aria-label="Move {labelOf(value)} up" disabled={i === 0} onclick={() => move(i, i - 1)}>↑</button>
|
||||
<button type="button" class="quiet" aria-label="Move {labelOf(value)} down" disabled={i === order.length - 1} onclick={() => move(i, i + 1)}>↓</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{#if !v.yourRanking}<button type="button" class="quiet" onclick={() => room.rank(order)}>Keep this order</button>{/if}
|
||||
{#if people > 1}<p class="muted small">{v.ranked} of the {people} players {v.ranked === 1 ? 'has' : 'have'} ranked.</p>{/if}
|
||||
</section>
|
||||
{/if}
|
||||
{#if !room.spectating}
|
||||
<div class="ways">
|
||||
{#if room.seatEmpty && room.isHost}
|
||||
@@ -170,6 +216,45 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ranking {
|
||||
max-width: 26rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.ranking h3 {
|
||||
margin: 0 0 0.2rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.ranking ol {
|
||||
list-style: none;
|
||||
margin: 0.4rem 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ranking li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.15rem 0;
|
||||
}
|
||||
|
||||
.ranking .place {
|
||||
width: 1.6rem;
|
||||
color: var(--bone-dim);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ranking .choice {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ranking .quiet {
|
||||
min-width: 2.6rem;
|
||||
min-height: 2.4rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
.keeper {
|
||||
margin-top: 0.8rem;
|
||||
max-width: 38em;
|
||||
|
||||
Reference in new issue
Block a user