]> git.cworth.org Git - empires-server/blob - scribe.js
Add some autofocus attributes to several forms
[empires-server] / scribe.js
1 const express = require("express");
2 const Game = require("./game.js");
3
4 class Scribe extends Game {
5   constructor(id) {
6     super(id);
7     this.teams = [{id:0, name:"+"}, {id:1, name:"o"}];
8     this.state = {
9       moves: [],
10       squares: Array(9).fill(null).map(() => Array(9).fill(null)),
11       team_to_play: this.teams[0],
12     };
13   }
14
15   find_connected_recursive(recursion_state, position) {
16
17     if (position < 0 || position >= 9)
18       return;
19
20     if (recursion_state.visited[position])
21       return;
22
23     recursion_state.visited[position] = true;
24
25     if (recursion_state.mini_grid[position] !== recursion_state.target)
26       return;
27
28     recursion_state.connected[position] = true;
29
30     /* Left */
31     if (position % 3 !== 0)
32       this.find_connected_recursive(recursion_state, position - 1);
33     /* Right */
34     if (position % 3 !== 2)
35       this.find_connected_recursive(recursion_state, position + 1);
36     /* Up */
37     this.find_connected_recursive(recursion_state, position - 3);
38     /* Down */
39     this.find_connected_recursive(recursion_state, position + 3);
40   }
41
42   /* Find all cells within a mini-grid that are 4-way connected to the
43    * given cell. */
44   find_connected(mini_grid, position) {
45     const connected = Array(9).fill(false);
46
47     /* If the given cell is empty then there is nothing connected. */
48     if (mini_grid[position] === null)
49       return connected;
50
51     const cell = mini_grid[position];
52
53     let recursion_state = {
54       mini_grid: mini_grid,
55       connected: connected,
56       visited: Array(9).fill(false),
57       target: cell,
58     };
59     this.find_connected_recursive(recursion_state, position);
60
61     return connected;
62   }
63
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);
68
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.).
72      */
73     let min_row = 2;
74     let min_col = 2;
75     let max_row = 0;
76     let max_col = 0;
77     let count = 0;
78
79     for (let i = 0; i < 9; i++) {
80       const row = Math.floor(i/3);
81       const col = i % 3;
82
83       if (! connected[i])
84         continue;
85
86       count++;
87
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);
92     }
93
94     const width = max_col - min_col + 1;
95     const height = max_row - min_row + 1;
96
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];
102
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);
109
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)
115     {
116       two_corners_in_a_line = true;
117     }
118
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)
124     {
125       zero_corners_in_a_line = true;
126     }
127
128     /* Now we have the information we need to determine glyphs. */
129     let is_glyph = undefined;
130     switch (count) {
131     case 1:
132       /* Single */
133       is_glyph = true;
134       break;
135     case 2:
136       /* Double */
137       is_glyph = true;
138       break;
139     case 3:
140       /* Line */
141       is_glyph = (width === 3 || height === 3);
142       break;
143     case 4:
144       /* Pipe, Squat-T, and 4-block, but not Tetris S */
145       is_glyph = two_corners_in_a_line;
146       break;
147     case 5:
148       if (width !== 3 || height !== 3 || ! connected[4])
149       {
150         /* Pentomino P and U are not glyphs (not 3x3) */
151         /* Pentomino V is not a glyph (center not connected) */
152         is_glyph = false;
153       }
154       else if (corners_count === 0 || two_corners_in_a_line)
155       {
156         /* Pentomino X is glyph Cross (no corners) */
157         /* Pentomino T is glyph T (has a row or column with 2 corners) */
158         is_glyph = true;
159       } else {
160         /* The corner counting above excludes pentomino F, W, and Z
161          * which are not glyphs. */
162         is_glyph = false;
163       }
164       break;
165     case 6:
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)
169         is_glyph = true;
170       else
171         is_glyph = false;
172       break;
173     case 7:
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)
178         is_glyph = true;
179       else
180         is_glyph = false;
181       break;
182     case 8:
183       /* Ottoman or O */
184       if (corners_count === 4)
185         is_glyph = true;
186       else
187         is_glyph = false;
188       break;
189     case 9:
190       is_glyph = true;
191       break;
192     }
193   }
194
195   /* Returns true if move was legal and added, false otherwise. */
196   add_move(player, move) {
197
198     const state = this.state;
199     const i = move[0];
200     const j = move[1];
201     const result = super.add_move(player, move);
202
203     /* If the generic Game class can reject this move, then we don't
204      * need to look at it any further. */
205     if (! result.legal)
206       return result;
207
208     /* Ensure move is legal by Scribe rules. First, if this is only
209      * the first or second move, then any move is legal.
210      */
211     if (state.moves.length >= 2) {
212       const prev = state.moves.slice(-2, -1)[0];
213
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. */
217         let count = 0;
218         for (let square of state.squares[prev[1]]) {
219           if (square)
220             count++;
221         }
222         if (count != 9) {
223           return { legal: false,
224                    message: "Move is inconsistent with your previous move" };
225         }
226       }
227
228     }
229
230     /* Cannot move to an occupied square. */
231     if (state.squares[i][j])
232     {
233       return { legal: false,
234                message: "Square is already occupied" };
235     }
236
237     state.squares[i][j] = state.team_to_play;
238     state.moves.push(move);
239
240     this.detect_glyph(i, j);
241
242     if (state.team_to_play.id === 0)
243       state.team_to_play = this.teams[1];
244     else
245       state.team_to_play = this.teams[0];
246
247     return { legal: true };
248   }
249 }
250
251 Scribe.router = express.Router();
252
253 Scribe.meta = {
254   name: "Scribe",
255   identifier: "scribe",
256   options: {
257     allow_guest: true
258   }
259 };
260
261 exports.Game = Scribe;