1 const express = require("express");
2 const Game = require("./game.js");
4 class Scribe extends Game {
7 this.teams = [{id:0, name:"+"}, {id:1, name:"o"}];
10 squares: Array(9).fill(null).map(() => Array(9).fill(null)),
11 team_to_play: this.teams[0],
15 find_connected_recursive(recursion_state, position) {
17 if (position < 0 || position >= 9)
20 if (recursion_state.visited[position])
23 recursion_state.visited[position] = true;
25 if (recursion_state.mini_grid[position] !== recursion_state.target)
28 recursion_state.connected[position] = true;
31 if (position % 3 !== 0)
32 this.find_connected_recursive(recursion_state, position - 1);
34 if (position % 3 !== 2)
35 this.find_connected_recursive(recursion_state, position + 1);
37 this.find_connected_recursive(recursion_state, position - 3);
39 this.find_connected_recursive(recursion_state, position + 3);
42 /* Find all cells within a mini-grid that are 4-way connected to the
44 find_connected(mini_grid, position) {
45 const connected = Array(9).fill(false);
47 /* If the given cell is empty then there is nothing connected. */
48 if (mini_grid[position] === null)
51 const cell = mini_grid[position];
53 let recursion_state = {
56 visited: Array(9).fill(false),
59 this.find_connected_recursive(recursion_state, position);
64 /* Detect whether the given cell belongs to a glyph. */
65 detect_glyph(mini_grid_index, position) {
66 const mini_grid = this.state.squares[mini_grid_index];
67 const connected = this.find_connected(mini_grid, position);
69 /* Now that we have a set of connected cells, let's collect some
70 * stats on them, (width, height, number of cells, configuration
71 * of corner cells, etc.).
79 for (let i = 0; i < 9; i++) {
80 const row = Math.floor(i/3);
88 min_row = Math.min(row, min_row);
89 min_col = Math.min(col, min_col);
90 max_row = Math.max(row, max_row);
91 max_col = Math.max(col, max_col);
94 const width = max_col - min_col + 1;
95 const height = max_row - min_row + 1;
97 /* Corners, (top-left, top-right, bottom-left, and bottom-right) */
98 const tl = connected[3 * min_row + min_col];
99 const tr = connected[3 * min_row + max_col];
100 const bl = connected[3 * max_row + min_col];
101 const br = connected[3 * max_row + max_col];
103 const count_true = (acc, val) => acc + (val ? 1 : 0);
104 const corners_count = [tl, tr, bl, br].reduce(count_true, 0);
105 const top_corners_count = [tl, tr].reduce(count_true, 0);
106 const bottom_corners_count = [bl, br].reduce(count_true, 0);
107 const left_corners_count = [tl, bl].reduce(count_true, 0);
108 const right_corners_count = [tr, br].reduce(count_true, 0);
110 let two_corners_in_a_line = false;
111 if (top_corners_count === 2 ||
112 bottom_corners_count === 2 ||
113 left_corners_count === 2 ||
114 right_corners_count === 2)
116 two_corners_in_a_line = true;
119 let zero_corners_in_a_line = false;
120 if (top_corners_count === 0 ||
121 bottom_corners_count === 0 ||
122 left_corners_count === 0 ||
123 right_corners_count === 0)
125 zero_corners_in_a_line = true;
128 /* Now we have the information we need to determine glyphs. */
129 let is_glyph = undefined;
141 is_glyph = (width === 3 || height === 3);
144 /* Pipe, Squat-T, and 4-block, but not Tetris S */
145 is_glyph = two_corners_in_a_line;
148 if (width !== 3 || height !== 3 || ! connected[4])
150 /* Pentomino P and U are not glyphs (not 3x3) */
151 /* Pentomino V is not a glyph (center not connected) */
154 else if (corners_count === 0 || two_corners_in_a_line)
156 /* Pentomino X is glyph Cross (no corners) */
157 /* Pentomino T is glyph T (has a row or column with 2 corners) */
160 /* The corner counting above excludes pentomino F, W, and Z
161 * which are not glyphs. */
166 /* 6-Block has widht or height of 2. */
167 /* Bomber, Chair, and J have 3 corners occupied. */
168 if (width === 2 || height === 2 || corners_count === 3)
174 /* Earring and U have no center square occupied */
175 /* H has 4 corners occupied */
176 /* House has a row or column with 0 corners occupied */
177 if ((! connected[4]) || corners_count === 4 || zero_corners_in_a_line)
184 if (corners_count === 4)
195 /* Returns true if move was legal and added, false otherwise. */
196 add_move(player, move) {
198 const state = this.state;
201 const result = super.add_move(player, move);
203 /* If the generic Game class can reject this move, then we don't
204 * need to look at it any further. */
208 /* Ensure move is legal by Scribe rules. First, if this is only
209 * the first or second move, then any move is legal.
211 if (state.moves.length >= 2) {
212 const prev = state.moves.slice(-2, -1)[0];
214 /* Then check for mini-grid compatibility with move from two moves ago. */
215 if (move[0] != prev[1]) {
216 /* This can still be legal if the target mini grid is full. */
218 for (let square of state.squares[prev[1]]) {
223 return { legal: false,
224 message: "Move is inconsistent with your previous move" };
230 /* Cannot move to an occupied square. */
231 if (state.squares[i][j])
233 return { legal: false,
234 message: "Square is already occupied" };
237 state.squares[i][j] = state.team_to_play;
238 state.moves.push(move);
240 this.detect_glyph(i, j);
242 if (state.team_to_play.id === 0)
243 state.team_to_play = this.teams[1];
245 state.team_to_play = this.teams[0];
247 return { legal: true };
251 Scribe.router = express.Router();
255 identifier: "scribe",
261 exports.Game = Scribe;