Pin moves to v0.5.6, package version to 0.5.6:0. One packaging change of its own: Games in Progress reported "seat 0" while the game's own screens now say "Seat 1", so an administrator and a player would have been describing different chairs. Same +1, same reason. Verified on phoenix.local as an update from 0.5.5:0. The served client carries all three of the release's fixes: the lobby's seat numbers go through seatLabel, .bs-name.bs-turn has lost the font-weight that made the current player's name unreadable at 11px, and the west-to-east seating line is gated to Day 1 Stage 1.
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import { i18n } from '../i18n'
|
|
import { sdk } from '../sdk'
|
|
import { listGames } from '../serverApi'
|
|
import type { GameRow } from '../serverApi'
|
|
|
|
/** "3 Aug 2026, 14:32" — a timestamp a person reads, not an epoch they decode. */
|
|
function when(ms: number): string {
|
|
return new Date(ms).toLocaleString('en-GB', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
})
|
|
}
|
|
|
|
function describe(g: GameRow): string {
|
|
const who = g.playerNames.join(', ')
|
|
if (g.state === 'lobby') {
|
|
return [
|
|
i18n('Waiting to start — nobody has played a turn yet.'),
|
|
`${i18n('Seats')} (${g.playerCount}): ${who}`,
|
|
`${i18n('Created')}: ${when(g.createdAt)}`,
|
|
].join('\n')
|
|
}
|
|
const waiting =
|
|
g.status === 'finished'
|
|
? i18n('Finished — this game is over and waits on nobody.')
|
|
: g.waitingOn
|
|
? // +1 for the same reason the game's own screens do it: seats are zero-based internally
|
|
// and one-based to a person, and an administrator reading "seat 0" against a player
|
|
// reading "Seat 1" is two people describing different chairs.
|
|
`${i18n('Waiting on')}: ${g.waitingOn.name} (${i18n('seat')} ${g.waitingOn.seat + 1})`
|
|
: `${i18n('Waiting on')}: ${i18n('nobody — the Mainline Phase is running itself')}`
|
|
return [
|
|
`${i18n('Players')} (${g.playerCount}): ${who}`,
|
|
`${i18n('Day')} ${g.day}, ${i18n('Stage')} ${g.stage} — ${g.phase}`,
|
|
waiting,
|
|
`${i18n('Started')}: ${when(g.createdAt)}`,
|
|
`${i18n('Last move')}: ${g.lastMoveAt === undefined ? i18n('unknown') : when(g.lastMoveAt)}`,
|
|
].join('\n')
|
|
}
|
|
|
|
/**
|
|
* Read-only, and separate from Manage Game on purpose: this is the one you run without thinking,
|
|
* so it must not be the one that can delete anything.
|
|
*
|
|
* `only-running` because none of this exists on disk in readable form. A save is the seed plus the
|
|
* intents, so "whose turn is it" is only answerable by replaying the game through the engine —
|
|
* which lives in the game repo, not in this package. The running server has already done that work
|
|
* and is asked for the answer.
|
|
*/
|
|
export const gamesInProgress = sdk.Action.withoutInput(
|
|
'games-in-progress',
|
|
|
|
async () => ({
|
|
name: i18n('Games in Progress'),
|
|
description: i18n(
|
|
'List every game and lobby on this server, and where each one has got to',
|
|
),
|
|
warning: null,
|
|
allowedStatuses: 'only-running',
|
|
group: null,
|
|
visibility: 'enabled',
|
|
}),
|
|
|
|
async ({ effects }) => {
|
|
const games = await listGames(effects)
|
|
if (games.length === 0) {
|
|
return {
|
|
version: '1',
|
|
title: i18n('Games in Progress'),
|
|
message: i18n('There are no games on this server.'),
|
|
result: null,
|
|
}
|
|
}
|
|
return {
|
|
version: '1',
|
|
title: i18n('Games in Progress'),
|
|
message: `${games.length} ${i18n('game(s) on this server. Use Manage Game to save one for later, or to end it.')}`,
|
|
result: {
|
|
type: 'group',
|
|
value: games.map((g) => ({
|
|
type: 'single' as const,
|
|
name: g.gameCode ?? g.gameId,
|
|
description: null,
|
|
value: describe(g),
|
|
masked: false,
|
|
copyable: false,
|
|
qr: false,
|
|
})),
|
|
},
|
|
}
|
|
},
|
|
)
|