diff --git a/ui/src/game/Game.svelte b/ui/src/game/Game.svelte index 14ee04a..3cc54c6 100644 --- a/ui/src/game/Game.svelte +++ b/ui/src/game/Game.svelte @@ -20,6 +20,7 @@ import type { EvalResult, MoveRecord, MoveResult, StateView, Tile } from '../lib/model'; import { lastMoveCells, replay } from '../lib/board'; import { badgeKind } from '../lib/unread'; + import { seatMedal } from '../lib/result'; import { historyGrid } from '../lib/history'; import { centre, premiumGrid } from '../lib/premiums'; import { variantNameKey, usesStarBlank, BLANK_STAR } from '../lib/variants'; @@ -1496,7 +1497,7 @@ {#if badge}{/if} {#each view.game.seats as s (s.seat)}
-
{seatName(s)}
+
{#if gameOver && isLocalGameId(id)}{/if}{seatName(s)}
{#if blockConfirm[s.seat]}{t('game.blockShort')}{:else if addConfirm[s.seat]}{t('game.addFriendShort')}{:else}{s.score}{/if}
@@ -1622,7 +1623,9 @@
{view.bagLen === 0 ? t('game.bagEmpty') : t('game.bag', { n: view.bagLen })} {#if gameOver} - {resultText()} + + {#if !isLocalGameId(id)}{resultText()}{/if} {:else if placement.pending.length === 0} {view.game.hotseat ? t('hotseat.turnOf', { name: hotseatName(view.game.toMove) }) : isMyTurn ? t('game.yourTurn') : turnLabel()} {/if} @@ -1865,6 +1868,10 @@ overflow: hidden; text-overflow: ellipsis; } + /* The finished-game place medal on a local (offline) seat plaque, left of the name. */ + .medal { + margin-right: 3px; + } .sc { font-weight: 700; font-variant-numeric: tabular-nums; diff --git a/ui/src/lib/result.test.ts b/ui/src/lib/result.test.ts index 52c6147..0c49376 100644 --- a/ui/src/lib/result.test.ts +++ b/ui/src/lib/result.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { resultBadge } from './result'; +import { resultBadge, seatMedal } from './result'; import type { GameView, Seat } from './model'; const seat = (s: number, accountId: string, score: number, isWinner = false): Seat => ({ @@ -75,3 +75,29 @@ describe('resultBadge', () => { expect(resultBadge(second, 'me')).toEqual({ key: 'result.place2', emoji: '🥈' }); }); }); + +describe('seatMedal', () => { + it('is empty for a game still in progress', () => { + expect(seatMedal(game([seat(0, 'a', 5), seat(1, 'b', 3)], 'active'), 0)).toBe(''); + }); + + it('finished two-player: winner gets the trophy, the other second place', () => { + const g = game([seat(0, 'a', 300, true), seat(1, 'b', 200)]); + expect(seatMedal(g, 0)).toBe('🏆'); + expect(seatMedal(g, 1)).toBe('🥈'); + }); + + it('finished draw (no winner): every seat gets the medal', () => { + const g = game([seat(0, 'a', 200), seat(1, 'b', 200)]); + expect(seatMedal(g, 0)).toBe('🏅'); + expect(seatMedal(g, 1)).toBe('🏅'); + }); + + it('finished four-player: places each seat by score', () => { + const g = game([seat(0, 'a', 100), seat(1, 'b', 400, true), seat(2, 'c', 300), seat(3, 'd', 200)]); + expect(seatMedal(g, 1)).toBe('🏆'); // winner + expect(seatMedal(g, 2)).toBe('🥈'); // 2nd by score + expect(seatMedal(g, 3)).toBe('🥉'); // 3rd + expect(seatMedal(g, 0)).toBe('🏅'); // 4th + }); +}); diff --git a/ui/src/lib/result.ts b/ui/src/lib/result.ts index 19360e2..8ceabb1 100644 --- a/ui/src/lib/result.ts +++ b/ui/src/lib/result.ts @@ -30,3 +30,20 @@ export function resultBadge(game: GameView, myId: string): ResultBadge { if (rank === 3) return { key: 'result.place3', emoji: '🥉' }; return { key: 'result.place4', emoji: '🏅' }; } + +/** + * seatMedal returns a per-SEAT place emoji for a finished game (empty while it is still in progress), + * so a local (offline) game can show each player's medal on their plaque instead of a single + * viewer-centric "you won/lost". The winner takes the trophy; the rest place by score (a draw — no + * winner — gives every seat the medal). Mirrors the ranking in resultBadge, keyed by seat not viewer. + */ +export function seatMedal(game: GameView, seat: number): string { + if (game.status !== 'finished') return ''; + const s = game.seats.find((x) => x.seat === seat); + if (!s) return ''; + if (s.isWinner) return '🏆'; + if (!game.seats.some((x) => x.isWinner)) return '🏅'; // a draw: no winner + const ahead = game.seats.filter((x) => !x.isWinner && x.seat !== seat && x.score > s.score).length; + const rank = 2 + ahead; + return rank === 2 ? '🥈' : rank === 3 ? '🥉' : '🏅'; +} diff --git a/ui/src/screens/Lobby.svelte b/ui/src/screens/Lobby.svelte index 6189512..1d1d103 100644 --- a/ui/src/screens/Lobby.svelte +++ b/ui/src/screens/Lobby.svelte @@ -353,9 +353,11 @@ >{/each} - + {#key blinkNonce.get(g.id) ?? 0} - {resultBadge(g, myId).emoji} + {isLocalGameId(g.id) ? '' : resultBadge(g, myId).emoji} {/key} {#if group.finished || g.hotseat}