From: Carl Worth Date: Sat, 7 Mar 2026 04:08:17 +0000 (-0500) Subject: letterrip: Display scores at the end of the game X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=e7ae105ea8d510ecde4cd256215b39bc642f7bb1;p=lmno.games letterrip: Display scores at the end of the game The server now computes a score for each player and sends them out, so display the scores and the board for each player. --- diff --git a/letterrip/letterrip.css b/letterrip/letterrip.css index 653df63..6fb309a 100644 --- a/letterrip/letterrip.css +++ b/letterrip/letterrip.css @@ -270,3 +270,85 @@ font-style: italic; color: #555; } + +/* Scoreboard */ +.scoreboard h2 { + margin-bottom: 0.5em; +} + +.player-result { + margin-bottom: 2em; +} + +.player-score-line { + display: flex; + align-items: baseline; + gap: 0.5em; + margin-bottom: 0.5em; + font-size: 1.1em; +} + +.player-score-line .rank { + font-weight: bold; + color: #666; +} + +.player-score-line .player-name { + font-weight: bold; +} + +.player-score-line .player-score { + margin-left: auto; + font-weight: bold; + color: #27ae60; +} + +.player-score-line .player-score.negative { + color: #c0392b; +} + +/* Scored grid (read-only, compact) */ +.scored-grid { + display: inline-grid; + gap: 0; + --scored-cell-size: 36px; +} + +.scored-cell { + width: var(--scored-cell-size); + height: var(--scored-cell-size); + display: flex; + align-items: center; + justify-content: center; +} + +.scored-grid .tile { + width: 32px; + height: 32px; + font-size: 16px; + cursor: default; +} + +.scored-grid-empty { + color: #999; + font-style: italic; +} + +/* Unscored tiles (in results display) */ +.tile.unscored { + opacity: 0.4; +} + +/* Unplaced tiles shown below scored grid */ +.result-rack { + display: flex; + gap: 4px; + margin-top: 0.5em; +} + +.result-rack .tile { + width: 32px; + height: 32px; + font-size: 16px; + cursor: default; +} diff --git a/letterrip/letterrip.jsx b/letterrip/letterrip.jsx index ee76f40..bc30d25 100644 --- a/letterrip/letterrip.jsx +++ b/letterrip/letterrip.jsx @@ -305,6 +305,86 @@ function BlankTileModal(props) { ]; } +function ScoredGrid(props) { + const keys = Object.keys(props.grid); + if (keys.length === 0) { + return
No tiles placed
; + } + + let minR = Infinity, maxR = -Infinity, minC = Infinity, maxC = -Infinity; + for (const key of keys) { + const [r, c] = key.split(",").map(Number); + if (r < minR) minR = r; + if (r > maxR) maxR = r; + if (c < minC) minC = c; + if (c > maxC) maxC = c; + } + + const cells = []; + for (let r = minR; r <= maxR; r++) { + for (let c = minC; c <= maxC; c++) { + const key = r + "," + c; + const cell = props.grid[key]; + if (cell) { + let className = "tile"; + if (cell.isBlank) className += " blank"; + if (!cell.scored) className += " unscored"; + cells.push( +
+
{cell.letter}
+
+ ); + } else { + cells.push(
); + } + } + } + + const cols = maxC - minC + 1; + return ( +
+ {cells} +
+ ); +} + +function ScoreBoard(props) { + const sorted = [...props.results].sort((a, b) => b.score - a.score); + return ( +
+

Final Scores

+ {sorted.map((result, i) => ( +
+
+ #{i + 1} + {result.name} + + {result.score} {result.score === 1 || result.score === -1 + ? "point" : "points"} + +
+ + {result.rack.length > 0 ? ( +
+ {result.rack.map((letter, j) => { + const is_blank = (letter === "_"); + return ( +
+ {is_blank ? "\u00A0" : letter} +
+ ); + })} +
+ ) : null} +
+ ))} +
+ ); +} + function Tile(props) { const { letter, isBlank, invalid, unconnected, selected, onDragStart, onDragEnd, onClick, onTouchStart } = props; @@ -341,7 +421,7 @@ class Game extends React.Component { stuck_players: [], bag_remaining: null, game_over: false, - winner: null, + results: null, blank_pending: null, drag_source: null, drag_over_cell: null, @@ -485,7 +565,7 @@ class Game extends React.Component { } receive_game_over(data) { - this.setState({ game_over: true, winner: data.winner }); + this.setState({ game_over: true, results: data.results || null }); } /***************************************************** @@ -908,6 +988,15 @@ class Game extends React.Component { render() { const state = this.state; + + /* Show scoreboard when results are in. */ + if (state.results) { + return [ + , + + ]; + } + const analysis = analyze_grid(state.grid); const invalid_cells = get_invalid_cells(state.grid, analysis); const unconnected_cells = get_unconnected_cells(state.grid, analysis); @@ -925,7 +1014,7 @@ class Game extends React.Component { state.game_over ? (
- {state.winner} wins! Game over. + Game over! Calculating scores...
) : null,