From 9258e9882dabf791ebac6b3eae127d38d92a7991 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sat, 7 Mar 2026 08:08:04 -0500 Subject: [PATCH] letterrip: Implement server-side validation of words Now that we have the dictionary on the server side, (for purpose of final scoring) we also implement validation of words on every tile placement on the server side. This lets us drop the dictionary from the client side so there is no possibility of disagreement between client and server on which words are valid. --- letterrip.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/letterrip.js b/letterrip.js index 770c309..e90a165 100644 --- a/letterrip.js +++ b/letterrip.js @@ -426,6 +426,69 @@ class LetterRip extends Game { } response.sendStatus(200); + + this.send_board_state(session_id); + } + + /* Analyze a player's board and send validation state via SSE. */ + send_board_state(session_id) { + const player = this.players_by_session[session_id]; + if (!player) return; + + const tiles = this.state.player_tiles[session_id]; + if (!tiles) return; + + const board = this.state.player_boards[session_id] || {}; + const analysis = LetterRip.analyze_grid(board); + + /* Invalid cells: in an invalid word or isolated (not in any word). */ + const invalid = new Set(); + const in_word = new Set(); + for (const w of analysis.words) { + for (const cell of w.cells) { + const key = cell.r + "," + cell.c; + in_word.add(key); + if (!w.valid) invalid.add(key); + } + } + for (const key of Object.keys(board)) { + if (!in_word.has(key)) invalid.add(key); + } + + /* Unconnected cells (not reachable from the first tile via BFS). */ + const unconnected = new Set(); + const occupied = Object.keys(board); + if (occupied.length > 1) { + const pos_set = new Set(occupied); + const visited = new Set([occupied[0]]); + const queue = [occupied[0]]; + while (queue.length > 0) { + const cur = queue.shift(); + const [cr, cc] = cur.split(",").map(Number); + for (const n of [(cr-1)+","+cc, (cr+1)+","+cc, + cr+","+(cc-1), cr+","+(cc+1)]) { + if (pos_set.has(n) && !visited.has(n)) { + visited.add(n); + queue.push(n); + } + } + } + for (const key of occupied) { + if (!visited.has(key)) unconnected.add(key); + } + } + + const all_valid = analysis.words.length > 0 + && analysis.words.every(w => w.valid); + const all_connected = unconnected.size === 0; + + const data = JSON.stringify({ + invalid_cells: [...invalid], + unconnected_cells: [...unconnected], + all_valid, + all_connected + }); + player.send(`event: board-state\ndata: ${data}\n\n`); } handle_events(request, response) { @@ -447,6 +510,11 @@ class LetterRip extends Game { response.write(`event: board\ndata: ${JSON.stringify(board)}\n\n`); } + /* Send current board validation state. */ + if (tiles) { + this.send_board_state(session_id); + } + /* Send current bag count. */ response.write(`event: dealt\ndata: ${JSON.stringify({ remaining: this.state.bag.length -- 2.45.2