From 606ccffa42d62bf4b9595f4125bd7bb5fad150ae Mon Sep 17 00:00:00 2001 From: Business Goose Date: Wed, 23 Sep 2026 12:43:35 -0700 Subject: [PATCH] game: keep the player cards in their column, and rebuild the final score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A grid column's automatic minimum is its content's min-content width, and a handle has no break opportunity, so jevgo.labs.isaaccorbrey.com widened the players card past its share of .game-info — far enough to run into the score card. The columns are minmax(min(250px, 100%), 1fr) now, the cards may shrink (min-width: 0), and handles wrap rather than overflow. The final score was a mint-green banner reading 'Black: 31 / White: 47 / Winner: goose.art'. It is now a scoreboard: a proportional black/white bar, one row per player with their stone, handle and score, the winner's row tinted, and a summary line giving the margin. The leader is read from the two numbers rather than the stored winner, so the card cannot contradict itself, and a level score reads as a drawn game. --- src/routes/game/[id]/+page.svelte | 150 +++++++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 14 deletions(-) diff --git a/src/routes/game/[id]/+page.svelte b/src/routes/game/[id]/+page.svelte index 8b2c428..c8ec84f 100644 --- a/src/routes/game/[id]/+page.svelte +++ b/src/routes/game/[id]/+page.svelte @@ -121,10 +121,23 @@ const gameBoardSize = $derived(gameRecord?.boardSize ?? data.boardSize); const gamePlayerOne = $derived(gameRecord?.playerOne ?? data.creatorDid); const gamePlayerTwo = $derived(gameRecord?.playerTwo ?? data.playerTwoDid); - const gameWinner = $derived(gameRecord?.winner ?? null); const gameBlackScore = $derived(gameRecord?.blackScore ?? null); const gameWhiteScore = $derived(gameRecord?.whiteScore ?? null); + // Final score presentation: the shares drive the bar, the margin the summary. + // The leader is read from the numbers rather than the stored winner, so the + // card cannot contradict itself. + const scoreTotal = $derived((gameBlackScore ?? 0) + (gameWhiteScore ?? 0)); + const blackShare = $derived(scoreTotal > 0 ? ((gameBlackScore ?? 0) / scoreTotal) * 100 : 50); + const scoreMargin = $derived(Math.abs((gameBlackScore ?? 0) - (gameWhiteScore ?? 0))); + const scoreLeader = $derived( + gameBlackScore === null || gameWhiteScore === null || gameBlackScore === gameWhiteScore + ? null + : gameBlackScore > gameWhiteScore + ? gamePlayerOne + : gamePlayerTwo + ); + // Derive game status from PDS data (moves, resigns, scores) const gameStatus = $derived.by(() => { const status = @@ -1309,12 +1322,40 @@ {:else if gameBlackScore !== null && gameWhiteScore !== null}
-

Final Scores

-

⚫ Black: {gameBlackScore}

-

⚪ White: {gameWhiteScore}

- {#if gameWinner} -

🏆 Winner: {gameWinner === gamePlayerOne ? playerOneHandle : playerTwoHandle}

- {/if} +

Final Score

+ + + +
+ + + {playerOneHandle}{data.session?.did === gamePlayerOne ? ' (you)' : ''} + + {gameBlackScore} +
+ +
+ + + {playerTwoHandle}{data.session?.did === gamePlayerTwo ? ' (you)' : ''} + + {gameWhiteScore} +
+ +

+ {#if scoreMargin === 0} + Drawn game + {:else} + 🏆 {scoreLeader === gamePlayerOne ? playerOneHandle : playerTwoHandle} wins by {scoreMargin} + {/if} +

{/if} {/if} @@ -2001,12 +2042,17 @@ .game-info { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + /* minmax(min(250px, 100%), 1fr), not minmax(250px, 1fr): a grid column's + automatic minimum is its content's min-content width, and a player's handle + has no break opportunity, so a long one (jevgo.labs.isaaccorbrey.com) used + to widen its card past its share and into the score card. */ + grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); gap: 1rem; margin-bottom: 2rem; } .info-card { + min-width: 0; background: var(--sky-white); border-radius: 1rem; padding: 1.5rem; @@ -2073,6 +2119,7 @@ align-items: center; gap: 0.75rem; flex: 1; + min-width: 0; text-align: center; } @@ -2178,6 +2225,9 @@ font-weight: 600; font-size: 1rem; transition: color 0.2s; + /* handles contain no break opportunity, so allow one rather than overflow */ + overflow-wrap: anywhere; + min-width: 0; } .player-name:hover { @@ -2329,15 +2379,87 @@ + /* Final score: the result reads from the two rows, so the card itself stays + quiet and only the winner's row is tinted. */ .score-card { - background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); - border: 2px solid #34d399; + border-left: 4px solid var(--sky-apricot); } - .winner-text { - font-weight: bold; - color: #059669; - margin-top: 0.5rem; + .score-bar { + display: flex; + height: 10px; + margin: 0.9rem 0 0.6rem; + border-radius: 999px; + overflow: hidden; + box-shadow: inset 0 1px 2px rgba(53, 78, 97, 0.2); + } + + .score-bar-seg { + height: 100%; + } + + .score-bar-seg.black { + background: linear-gradient(180deg, #4c4c55, #14141a); + } + + .score-bar-seg.white { + background: linear-gradient(180deg, #ffffff, #d6d2ca); + } + + .score-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.5rem; + border-radius: 0.5rem; + } + + .score-row.won { + background: rgba(5, 150, 105, 0.1); + } + + .score-marker { + flex: none; + width: 0.85rem; + height: 0.85rem; + border-radius: 50%; + } + + .score-marker.black { + background: radial-gradient(circle at 34% 30%, #6e6e72, #0e0e12); + } + + .score-marker.white { + background: radial-gradient(circle at 34% 30%, #ffffff, #cfcbc4); + box-shadow: inset 0 0 0 1px rgba(90, 96, 110, 0.35); + } + + .score-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--sky-slate-dark); + font-weight: 600; + } + + .score-row.won .score-name { + font-weight: 700; + } + + .score-points { + flex: none; + font-size: 1.25rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--sky-slate-dark); + } + + .score-card .score-summary { + margin: 0.75rem 0 0; + color: var(--sky-slate); + font-weight: 600; } .score-input-section { -- knotmirror 0.0.1