});
}
-/*********************************************************
- * Word detection and validation *
- *********************************************************/
-
-/* Find all words on the grid (horizontal and vertical runs of 2+).
- * Returns { words: [{cells, word, valid}], all_valid, all_connected }
- */
-function analyze_grid(grid) {
- const occupied = Object.keys(grid);
- if (occupied.length === 0) {
- return { words: [], all_valid: true, all_connected: true };
- }
-
- /* Parse all occupied positions. */
- const positions = occupied.map(key => {
- const [r, c] = key.split(",").map(Number);
- return { r, c, key };
- });
-
- const pos_set = new Set(occupied);
-
- /* Find horizontal words. */
- const words = [];
- const visited_h = new Set();
- const visited_v = new Set();
-
- for (const { r, c } of positions) {
- /* Horizontal word starting from leftmost cell of this run. */
- const hkey = r + "," + c;
- if (!visited_h.has(hkey)) {
- /* Find start of horizontal run. */
- let sc = c;
- while (pos_set.has(r + "," + (sc - 1))) sc--;
- /* Find end. */
- let ec = sc;
- while (pos_set.has(r + "," + (ec + 1))) ec++;
- /* Mark visited. */
- for (let cc = sc; cc <= ec; cc++) visited_h.add(r + "," + cc);
- if (ec > sc) {
- const cells = [];
- let word = "";
- for (let cc = sc; cc <= ec; cc++) {
- const cell = grid[r + "," + cc];
- cells.push({ r, c: cc });
- word += cell.letter;
- }
- const valid = window.TWL.has(word);
- words.push({ cells, word, valid });
- }
- }
-
- /* Vertical word starting from topmost cell of this run. */
- const vkey = r + "," + c;
- if (!visited_v.has(vkey)) {
- let sr = r;
- while (pos_set.has((sr - 1) + "," + c)) sr--;
- let er = sr;
- while (pos_set.has((er + 1) + "," + c)) er++;
- for (let rr = sr; rr <= er; rr++) visited_v.add(rr + "," + c);
- if (er > sr) {
- const cells = [];
- let word = "";
- for (let rr = sr; rr <= er; rr++) {
- const cell = grid[rr + "," + c];
- cells.push({ r: rr, c });
- word += cell.letter;
- }
- const valid = window.TWL.has(word);
- words.push({ cells, word, valid });
- }
- }
- }
-
- /* Check connectivity via BFS. */
- const start = positions[0].key;
- const visited = new Set([start]);
- const queue = [start];
- while (queue.length > 0) {
- const cur = queue.shift();
- const [cr, cc] = cur.split(",").map(Number);
- const neighbors = [
- (cr-1)+","+cc, (cr+1)+","+cc,
- cr+","+(cc-1), cr+","+(cc+1)
- ];
- for (const n of neighbors) {
- if (pos_set.has(n) && !visited.has(n)) {
- visited.add(n);
- queue.push(n);
- }
- }
- }
- const all_connected = visited.size === occupied.length;
- const all_valid = words.length > 0 && words.every(w => w.valid);
-
- return { words, all_valid, all_connected };
-}
-
-/* Determine which cells are in invalid words or isolated. */
-function get_invalid_cells(grid, analysis) {
- const invalid = new Set();
-
- for (const w of analysis.words) {
- if (!w.valid) {
- for (const cell of w.cells) {
- invalid.add(cell.r + "," + cell.c);
- }
- }
- }
-
- /* Mark isolated tiles (not part of any word). */
- const in_word = new Set();
- for (const w of analysis.words) {
- for (const cell of w.cells) {
- in_word.add(cell.r + "," + cell.c);
- }
- }
- for (const key of Object.keys(grid)) {
- if (!in_word.has(key)) {
- invalid.add(key);
- }
- }
-
- return invalid;
-}
-
-/* Find which cells are not connected to the main component. */
-function get_unconnected_cells(grid, analysis) {
- if (analysis.all_connected) return new Set();
-
- const occupied = Object.keys(grid);
- if (occupied.length === 0) return new Set();
-
- const pos_set = new Set(occupied);
- const start = occupied[0];
- const visited = new Set([start]);
- const queue = [start];
- while (queue.length > 0) {
- const cur = queue.shift();
- const [cr, cc] = cur.split(",").map(Number);
- const neighbors = [
- (cr-1)+","+cc, (cr+1)+","+cc,
- cr+","+(cc-1), cr+","+(cc+1)
- ];
- for (const n of neighbors) {
- if (pos_set.has(n) && !visited.has(n)) {
- visited.add(n);
- queue.push(n);
- }
- }
- }
- const unconnected = new Set();
- for (const key of occupied) {
- if (!visited.has(key)) unconnected.add(key);
- }
- return unconnected;
-}
-
/*********************************************************
* Compute grid render bounds *
*********************************************************/
drag_over_cell: null,
rack_drag_over: false,
selected: null,
- touch_pos: null
+ touch_pos: null,
+ board_state: { invalid_cells: [], unconnected_cells: [],
+ all_valid: true, all_connected: true }
};
}
});
}
+ receive_board_state(data) {
+ this.setState({ board_state: data });
+ }
+
receive_game_over(data) {
this.setState({ game_over: true, results: data.results || null });
}
];
}
- const analysis = analyze_grid(state.grid);
- const invalid_cells = get_invalid_cells(state.grid, analysis);
- const unconnected_cells = get_unconnected_cells(state.grid, analysis);
+ const bs = state.board_state;
+ const invalid_cells = new Set(bs.invalid_cells);
+ const unconnected_cells = new Set(bs.unconnected_cells);
const all_placed = state.rack.length === 0 && state.tiles.length > 0;
- const can_complete = all_placed && analysis.all_valid && analysis.all_connected;
+ const can_complete = all_placed && bs.all_valid && bs.all_connected;
return [
<GameInfo key="gi" id={state.game_info.id} url={state.game_info.url} />,
state.joined ? (
<div key="board-rack" className="board-and-rack">
- {this.render_board(analysis, invalid_cells, unconnected_cells)}
+ {this.render_board(invalid_cells, unconnected_cells)}
{this.render_rack(can_complete)}
{state.blank_pending ? (
<BlankTileModal
) : null}
{Object.keys(state.grid).length > 0 && !can_complete ? (
<div className="status">
- {!analysis.all_valid ? "Some words are not valid." :
- !analysis.all_connected ? "All tiles must be connected." :
+ {!bs.all_valid ? "Some words are not valid." :
+ !bs.all_connected ? "All tiles must be connected." :
state.rack.length > 0 ? "Place all your tiles to complete." :
null}
</div>
);
}
- render_board(analysis, invalid_cells, unconnected_cells) {
+ render_board(invalid_cells, unconnected_cells) {
const { grid, drag_over_cell, drag_source, selected } = this.state;
const bounds = grid_bounds(grid);
const rows = [];
window.game.receive_stuck(JSON.parse(event.data));
});
+events.addEventListener("board-state", event => {
+ window.game.receive_board_state(JSON.parse(event.data));
+});
+
events.addEventListener("game-over", event => {
window.game.receive_game_over(JSON.parse(event.data));
});
events.addEventListener("game-state", event => {
- /* game-state is sent by the base class but we don't use it for
- * board state (that's all client-side). We rely on the 'tiles'
- * event for reconnection recovery instead. */
+ /* game-state is sent by the base class but we don't use it
+ * directly. We rely on the 'tiles' and 'board' events for
+ * reconnection recovery instead. */
});