);
}
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(
);
}
}
}
return (
{props.name}
);
}
function Square(props) {
let className = "square";
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 (
{props.value.symbol}
);
}
function MiniGrid(props) {
const mini_grid = props.mini_grid;
const squares = mini_grid.squares;
function grid_square(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 (
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 =
);
}
class Board extends React.Component {
mini_grid(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 (
this.props.onClick(i,j)}
/>
);
}
render() {
return (
);
}
}
function fetch_method_json(method, api = '', data = {}) {
const response = fetch(api, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return response;
}
function fetch_post_json(api = '', data = {}) {
return fetch_method_json('POST', api, data);
}
async function fetch_put_json(api = '', data = {}) {
return fetch_method_json('PUT', api, data);
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
game_info: {},
player_info: {},
other_players: [],
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: "+",
};
}
set_game_info(info) {
this.setState({
game_info: info
});
}
set_player_info(info) {
this.setState({
player_info: info
});
}
set_other_player_info(info) {
const other_players_copy = [...this.state.other_players];
const idx = other_players_copy.findIndex(o => o.id === info.id);
if (idx >= 0) {
other_players_copy[idx] = info;
} else {
other_players_copy.push(info);
}
this.setState({
other_players: other_players_copy
});
}
reset_board() {
this.setState({
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) {
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_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({
mini_grids: new_mini_grids,
moves: new_moves,
next_to_play: next_to_play
});
}
async handle_click(i, j, first_move) {
let move = {
move: [i, j]
};
if (first_move) {
move.assert_first_move = true;
}
const response = await fetch_post_json("move", move);
if (response.status == 200) {
const result = await response.json();
if (! result.legal)
add_message("danger", result.message);
} else {
add_message("danger", `Error occurred sending move`);
}
}
join_team(team) {
fetch_put_json("player", {team: team});
}
render() {
const state = this.state;
const first_move = state.moves.length === 0;
const my_team = state.player_info.team;
var board_active;
let status;
if (this.state.moves.length === 81)
{
status = "Game over";
board_active = false;
}
else if (first_move)
{
if (state.other_players.length == 0) {
status = "You can move or wait for another player to join.";
} else {
let qualifier;
if (state.other_players.length == 1) {
qualifier = "Either";
} else {
qualifier = "Any";
}
status = `${qualifier} player can make the first move.`;
}
board_active = true;
}
else if (my_team === "")
{
status = "You're just watching the game.";
board_active = false;
}
else if (my_team === state.next_to_play)
{
status = "Your turn. Make a move.";
board_active = true;
}
else
{
status = "Waiting for another player to ";
if (state.other_players.length == 0) {
status += "join.";
} else {
status += "move.";
}
board_active = false;
}
return [
,
,