* Game and supporting classes *
*********************************************************/
+const scribe_glyphs = [
+ {
+ name: "Single",
+ squares: [1,0,0,
+ 0,0,0,
+ 0,0,0]
+ },
+ {
+ name: "Double",
+ squares: [1,1,0,
+ 0,0,0,
+ 0,0,0]
+ },
+ {
+ name: "Line",
+ squares: [1,1,1,
+ 0,0,0,
+ 0,0,0]
+ },
+ {
+ name: "Pipe",
+ squares: [0,0,1,
+ 1,1,1,
+ 0,0,0]
+ },
+ {
+ name: "Squat-T",
+ squares: [1,1,1,
+ 0,1,0,
+ 0,0,0]
+ },
+ {
+ name: "4-block",
+ squares: [1,1,0,
+ 1,1,0,
+ 0,0,0]
+ },
+ {
+ name: "T",
+ squares: [1,1,1,
+ 0,1,0,
+ 0,1,0]
+ },
+ {
+ name: "Cross",
+ squares: [0,1,0,
+ 1,1,1,
+ 0,1,0]
+ },
+ {
+ name: "6-block",
+ squares: [1,1,1,
+ 1,1,1,
+ 0,0,0]
+ },
+ {
+ name: "Bomber",
+ squares: [1,1,1,
+ 0,1,1,
+ 0,0,1]
+ },
+ {
+ name: "Chair",
+ squares: [0,0,1,
+ 1,1,1,
+ 1,0,1]
+ },
+ {
+ name: "J",
+ squares: [0,0,1,
+ 1,0,1,
+ 1,1,1]
+ },
+ {
+ name: "Earring",
+ squares: [0,1,1,
+ 1,0,1,
+ 1,1,1]
+ },
+ {
+ name: "House",
+ squares: [0,1,0,
+ 1,1,1,
+ 1,1,1]
+ },
+ {
+ name: "H",
+ squares: [1,0,1,
+ 1,1,1,
+ 1,0,1]
+ },
+ {
+ name: "U",
+ squares: [1,0,1,
+ 1,0,1,
+ 1,1,1]
+ },
+ {
+ name: "Ottoman",
+ squares: [1,1,1,
+ 1,1,1,
+ 1,0,1]
+ },
+ {
+ name: "O",
+ squares: [1,1,1,
+ 1,0,1,
+ 1,1,1]
+ },
+ {
+ name: "9-block",
+ squares: [1,1,1,
+ 1,1,1,
+ 1,1,1]
+ }
+];
+
function copy_to_clipboard(id)
{
const tmp = document.createElement("input");
);
}
+function Glyph(props) {
+
+ const glyph_dots = [];
+
+ let last_square = 0;
+ for (let i = 0; i < 9; i++) {
+ if (props.squares[i])
+ last_square = i;
+ }
+
+ const height = Math.floor(20 * (Math.floor(last_square / 3) + 1));
+
+ const viewbox=`0 0 60 ${height}`;
+
+ for (let row = 0; row < 3; row++) {
+ for (let col = 0; col < 3; col++) {
+ if (props.squares[3 * row + col]) {
+ let cy = 10 + 20 * row;
+ let cx = 10 + 20 * col;
+ glyph_dots.push(
+ <circle
+ key={3 * row + col}
+ cx={cx}
+ cy={cy}
+ r="8"
+ />
+ );
+ }
+ }
+ }
+
+ return (<div className="glyph-and-name">
+ {props.name}
+ <div className="glyph">
+ <svg viewBox={viewbox}>
+ <g fill="#287789">
+ {glyph_dots}
+ </g>
+ </svg>
+ </div>
+ </div>
+ );
+}
+
function Square(props) {
let className = "square";
- if (props.value) {
+ if (props.value.symbol) {
className += " occupied";
} else if (props.active) {
className += " open";
}
+ if (props.value.glyph) {
+ if (props.value.symbol === '+')
+ className += " glyph-plus";
+ else
+ className += " glyph-o";
+ }
+
+ if (props.last_move) {
+ className += " last-move";
+ }
+
const onClick = props.active ? props.onClick : null;
return (
<div className={className}
onClick={onClick}>
- {props.value}
+ {props.value.symbol}
</div>
);
}
function MiniGrid(props) {
+
+ const mini_grid = props.mini_grid;
+ const squares = mini_grid.squares;
+
function grid_square(j) {
- const value = props.squares[j];
+ const value = squares[j];
+ const last_move = props.last_moves.includes(j);
+
+ /* Even if the grid is active, the square is only active if
+ * unoccupied. */
+ const square_active = (props.active && (value.symbol === null));
+
return (
<Square
value={value}
- active={props.active}
+ active={square_active}
+ last_move={last_move}
onClick={() => props.onClick(j)}
/>
);
}
+ /* Even if my parent thinks I'm active because of the last move, I
+ * might not _really_ be active if I'm full. */
+ let occupied = 0;
+ mini_grid.squares.forEach(element => {
+ if (element.symbol)
+ occupied++;
+ });
+
+ let class_name = "mini-grid";
+ if (props.active && occupied < 9)
+ class_name += " active";
+
+ let winner = null;
+ if (mini_grid.winner) {
+ winner = <div className="winner">{mini_grid.winner}</div>;
+ }
+
return (
- <div className="mini-grid">
+ <div className={class_name}>
{grid_square(0)}
{grid_square(1)}
{grid_square(2)}
{grid_square(6)}
{grid_square(7)}
{grid_square(8)}
+ {winner}
</div>
);
}
class Board extends React.Component {
mini_grid(i) {
- const squares = this.props.squares[i];
+ /* This mini grid is active only if both:
+ *
+ * 1. It is our turn (this.props.active === true)
+ *
+ * 2. One of the following conditions is met:
+ *
+ * a. This is this players first turn (last_two_moves[0] === null)
+ * b. This mini grid corresponds to this players last turn
+ * c. The mini grid that corresponds to the players last turn is full
+ */
+ let grid_active = false;
+ if (this.props.active) {
+ grid_active = true;
+ if (this.props.last_two_moves.length > 1) {
+ /* First index (0) gives us our last move, (that is, of the
+ * last two moves, it's the first one, so two moves ago).
+ *
+ * Second index (1) gives us the second number from that move,
+ * (that is, the index within the mini-grid that we last
+ * played).
+ */
+ const target = this.props.last_two_moves[0][1];
+ let occupied = 0;
+ this.props.mini_grids[target].squares.forEach(element => {
+ if (element.symbol)
+ occupied++;
+ });
+ /* If the target mini-grid isn't full then this grid is
+ * only active if it is that target. */
+ if (occupied < 9)
+ grid_active = (i === target);
+ }
+ }
+
+ /* We want to highlight each of the last two moves (both "+" and
+ * "o"). So we filter the last two moves that have a first index
+ * that matches this mini_grid and pass down their second index
+ * be highlighted.
+ */
+ const last_moves = this.props.last_two_moves.filter(move => move[0] === i)
+ .map(move => move[1]);
+
+ const mini_grid = this.props.mini_grids[i];
return (
<MiniGrid
- squares={squares}
- active={this.props.active}
+ mini_grid={mini_grid}
+ active={grid_active}
+ last_moves={last_moves}
onClick={(j) => this.props.onClick(i,j)}
/>
);
game_info: {},
player_info: {},
other_players: [],
- squares: Array(9).fill(null).map(() => Array(9).fill(null)),
- moves: 0,
- next_to_play: "+"
+ mini_grids: [...Array(9)].map(() => ({
+ score_plus: null,
+ score_o: null,
+ winner: null,
+ squares: Array(9).fill({
+ symbol: null,
+ glyph: false
+ })
+ })),
+ moves: [],
+ next_to_play: "+",
};
}
});
}
+ find_connected_recursive(recursion_state, position) {
+
+ if (position < 0 || position >= 9)
+ return;
+
+ if (recursion_state.visited[position])
+ return;
+
+ recursion_state.visited[position] = true;
+
+ if (recursion_state.mini_grid[position].symbol !== recursion_state.target)
+ return;
+
+ recursion_state.connected[position] = true;
+
+ /* Left */
+ if (position % 3 !== 0)
+ this.find_connected_recursive(recursion_state, position - 1);
+ /* Right */
+ if (position % 3 !== 2)
+ this.find_connected_recursive(recursion_state, position + 1);
+ /* Up */
+ this.find_connected_recursive(recursion_state, position - 3);
+ /* Down */
+ this.find_connected_recursive(recursion_state, position + 3);
+ }
+
+ /* Find all cells within a mini-grid that are 4-way connected to the
+ * given cell. */
+ find_connected(mini_grid, position) {
+ const connected = Array(9).fill(false);
+
+ /* If the given cell is empty then there is nothing connected. */
+ if (mini_grid[position] === null)
+ return connected;
+
+ const cell = mini_grid[position].symbol;
+
+ let recursion_state = {
+ mini_grid: mini_grid,
+ connected: connected,
+ visited: Array(9).fill(false),
+ target: cell,
+ };
+ this.find_connected_recursive(recursion_state, position);
+
+ return connected;
+ }
+
+ /* Determine whether a connected group of cells is a glyph.
+ *
+ * Here, 'connected' is a length-9 array of Booleans, true
+ * for the connected cells in a mini-grid.
+ */
+ is_glyph(connected) {
+
+ /* Now that we have a set of connected cells, let's collect some
+ * stats on them, (width, height, number of cells, configuration
+ * of corner cells, etc.).
+ */
+ let min_row = 2;
+ let min_col = 2;
+ let max_row = 0;
+ let max_col = 0;
+ let count = 0;
+
+ for (let i = 0; i < 9; i++) {
+ const row = Math.floor(i/3);
+ const col = i % 3;
+
+ if (! connected[i])
+ continue;
+
+ count++;
+
+ min_row = Math.min(row, min_row);
+ min_col = Math.min(col, min_col);
+ max_row = Math.max(row, max_row);
+ max_col = Math.max(col, max_col);
+ }
+
+ const width = max_col - min_col + 1;
+ const height = max_row - min_row + 1;
+
+ /* Corners, (top-left, top-right, bottom-left, and bottom-right) */
+ const tl = connected[3 * min_row + min_col];
+ const tr = connected[3 * min_row + max_col];
+ const bl = connected[3 * max_row + min_col];
+ const br = connected[3 * max_row + max_col];
+
+ const count_true = (acc, val) => acc + (val ? 1 : 0);
+ const corners_count = [tl, tr, bl, br].reduce(count_true, 0);
+ const top_corners_count = [tl, tr].reduce(count_true, 0);
+ const bottom_corners_count = [bl, br].reduce(count_true, 0);
+ const left_corners_count = [tl, bl].reduce(count_true, 0);
+ const right_corners_count = [tr, br].reduce(count_true, 0);
+
+ let two_corners_in_a_line = false;
+ if (top_corners_count === 2 ||
+ bottom_corners_count === 2 ||
+ left_corners_count === 2 ||
+ right_corners_count === 2)
+ {
+ two_corners_in_a_line = true;
+ }
+
+ let zero_corners_in_a_line = false;
+ if (top_corners_count === 0 ||
+ bottom_corners_count === 0 ||
+ left_corners_count === 0 ||
+ right_corners_count === 0)
+ {
+ zero_corners_in_a_line = true;
+ }
+
+ /* Now we have the information we need to determine glyphs. */
+ switch (count) {
+ case 1:
+ /* Single */
+ return true;
+ case 2:
+ /* Double */
+ return true;
+ case 3:
+ /* Line */
+ return (width === 3 || height === 3);
+ case 4:
+ /* Pipe, Squat-T, and 4-block, but not Tetris S */
+ return two_corners_in_a_line;
+ case 5:
+ if (width !== 3 || height !== 3 || ! connected[4])
+ {
+ /* Pentomino P and U are not glyphs (not 3x3) */
+ /* Pentomino V is not a glyph (center not connected) */
+ return false;
+ }
+ else if (corners_count === 0 || two_corners_in_a_line)
+ {
+ /* Pentomino X is glyph Cross (no corners) */
+ /* Pentomino T is glyph T (has a row or column with 2 corners) */
+ return true;
+ } else {
+ /* The corner counting above excludes pentomino F, W, and Z
+ * which are not glyphs. */
+ return false;
+ }
+ break;
+ case 6:
+ /* 6-Block has width or height of 2. */
+ /* Bomber, Chair, and J have 3 corners occupied. */
+ if (width === 2 || height === 2 || corners_count === 3)
+ return true;
+ return false;
+ case 7:
+ /* Earring and U have no center square occupied */
+ /* H has 4 corners occupied */
+ /* House has a row or column with 0 corners occupied */
+ if ((! connected[4]) || corners_count === 4 || zero_corners_in_a_line)
+ return true;
+ return false;
+ case 8:
+ /* Ottoman or O */
+ if (corners_count === 4)
+ return true;
+ return false;
+ case 9:
+ return true;
+ }
+
+ /* Should be unreachable */
+ return false;
+ }
+
receive_move(move) {
- if (this.state.moves === 81) {
+ const mini_grid_index = move[0];
+ const position = move[1];
+
+ /* Don't allow any moves after the board is full */
+ if (this.state.moves.length === 81) {
return;
}
+
+ /* Set the team's symbol into the board state. */
const symbol = team_symbol(this.state.next_to_play);
- const new_squares = this.state.squares.map(arr => arr.slice());
- new_squares[move[0]][move[1]] = symbol;
+ const new_mini_grids = this.state.mini_grids.map(obj => {
+ const new_obj = {...obj};
+ new_obj.squares = obj.squares.slice();
+ return new_obj;
+ });
+ const new_mini_grid = new_mini_grids[mini_grid_index];
+ new_mini_grid.squares[position] = {
+ symbol: symbol,
+ glyph: false
+ };
+
+ /* With the symbol added to the squares, we need to see if this
+ * newly-placed move forms a glyph or not. */
+ const connected = this.find_connected(new_mini_grid.squares, position);
+ const is_glyph = this.is_glyph(connected);
+
+ /* Either set (or clear) the glyph Boolean for each connected square. */
+ for (let i = 0; i < 9; i++) {
+ if (connected[i])
+ new_mini_grid.squares[i].glyph = is_glyph;
+ }
+
+ /* If this is the last cell of played in a mini-grid then it's
+ * time to score it. */
+ const occupied = new_mini_grid.squares.reduce(
+ (acc, val) => acc + (val.symbol !== null ? 1 : 0), 0);
+ if (occupied === 9) {
+ for (let i = 0; i < 9; i++) {
+ if (new_mini_grid.squares[i].glyph) {
+ if (new_mini_grid.squares[i].symbol === '+')
+ new_mini_grid.score_plus++;
+ else
+ new_mini_grid.score_o++;
+ }
+ }
+ if (new_mini_grid.score_plus > new_mini_grid.score_o)
+ new_mini_grid.winner = '+';
+ else
+ new_mini_grid.winner = 'o';
+ }
+
+ /* And append the move to the list of moves. */
+ const new_moves = [...this.state.moves, move];
+
+ /* Finally, compute the next player to move. */
let next_to_play;
if (this.state.next_to_play === "+")
next_to_play = "o";
else
next_to_play = "+";
+
+ /* And shove all those state modifications toward React. */
this.setState({
- squares: new_squares,
- moves: this.state.moves + 1,
+ mini_grids: new_mini_grids,
+ moves: new_moves,
next_to_play: next_to_play
});
}
render() {
const state = this.state;
- const first_move = state.moves === 0;
+ const first_move = state.moves.length === 0;
const my_team = state.player_info.team;
var board_active;
<div className="game-board">
<Board
active={board_active}
- squares={state.squares}
+ mini_grids={state.mini_grids}
+ last_two_moves={state.moves.slice(-2)}
onClick={(i,j) => this.handle_click(i, j, first_move)}
/>
</div>
+ </div>,
+ <div key="glyphs" className="glyphs">
+ {
+ scribe_glyphs.map(glyph => {
+ return (
+ <Glyph
+ key={glyph.name}
+ name={glyph.name}
+ squares={glyph.squares}
+ />
+ );
+ })
+ }
</div>
];
}