Stage 17 round 5 — backend/correctness bug fixes
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Failing after 12s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Failing after 1s
CI / deploy (pull_request) Has been skipped
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Failing after 12s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Failing after 1s
CI / deploy (pull_request) Has been skipped
- Resign on the opponent's turn: engine ResignSeat(seat) resigns a specific seat (not just toMove); game.Resign bypasses the turn check and forfeits the actor's seat. - Quick-match cancel was a UI no-op (only stopped polling): add the full path (REST /lobby/cancel -> gateway lobby.cancel -> client) and clear the matchmaker's pending result on Cancel, so a cancelled search is dequeued (no 'already queued', no later robot-substituted game). NewGame dequeues on cancel and on abandon. - Lobby win/loss: result.ts ranked by score, so a 0-0 resignation read as a win. The winner now takes rank 1 and the viewer is placed from rank 2 — matching the game-detail screen. - Friend request to a robot: robots no longer block requests; the request stays pending and expires (friendRequestTTL), mirroring a human who ignores it. - Nudge cooldown: ErrNudgeTooSoon now maps to a distinct nudge_too_soon code with a correct message; the chat nudge button disables during the hourly cooldown; the nudge note reads 'Waiting for your move!' (button keeps the Nudge action label). Tests: engine/service off-turn resign, matchmaker cancel-clears-result, friend-to-robot inttest, result.ts 0-0 resignation, nudge_too_soon mapping.
This commit is contained in:
+22
-1
@@ -89,6 +89,20 @@
|
||||
const isMyTurn = $derived(!!view && view.game.status === 'active' && view.game.toMove === view.seat);
|
||||
const gameOver = $derived(!!view && view.game.status !== 'active');
|
||||
const bagEmpty = $derived((view?.bagLen ?? 0) === 0);
|
||||
// Nudge cooldown (one per hour per game, mirrored from the backend): the control is
|
||||
// disabled for an hour after the player's own last nudge. nudgeTick re-evaluates it on a
|
||||
// timer while the chat is open, so it re-enables without waiting for a new message.
|
||||
const nudgeCooldownSecs = 3600;
|
||||
let nudgeTick = $state(0);
|
||||
const nudgeOnCooldown = $derived.by(() => {
|
||||
void nudgeTick;
|
||||
const mine = app.session?.userId ?? '';
|
||||
const last = messages.reduce(
|
||||
(mx, m) => (m.kind === 'nudge' && m.senderId === mine ? Math.max(mx, m.createdAtUnix) : mx),
|
||||
0,
|
||||
);
|
||||
return last > 0 && Date.now() / 1000 - last < nudgeCooldownSecs;
|
||||
});
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -145,6 +159,13 @@
|
||||
else if (e.kind === 'nudge' && e.gameId === id && panel === 'chat') void loadChat();
|
||||
});
|
||||
|
||||
// Tick the nudge cooldown while the chat is open so the control re-enables on time.
|
||||
$effect(() => {
|
||||
if (panel !== 'chat') return;
|
||||
const h = setInterval(() => (nudgeTick += 1), 20000);
|
||||
return () => clearInterval(h);
|
||||
});
|
||||
|
||||
function isCoarse(): boolean {
|
||||
return typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches;
|
||||
}
|
||||
@@ -708,7 +729,7 @@
|
||||
|
||||
{#if panel === 'chat'}
|
||||
<Modal title={t('game.chat')} onclose={() => (panel = 'none')}>
|
||||
<Chat {messages} myId={app.session?.userId ?? ''} {busy} canNudge={!isMyTurn} onsend={sendChat} onnudge={nudge} />
|
||||
<Chat {messages} myId={app.session?.userId ?? ''} {busy} canNudge={!isMyTurn && !nudgeOnCooldown} onsend={sendChat} onnudge={nudge} />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user