X-Git-Url: https://git.cworth.org/git?p=lmno.games;a=blobdiff_plain;f=scribe%2Fscribe.jsx;h=d8f6afc9bf334b9ff523626854ff6883bfa870a4;hp=e7654b417eba2dfdc746a07d91f2a021de4bf2bb;hb=7274cc0cc286ead5e767155101658074353afb14;hpb=e52910fa1933db903b039a0f4f32b6470b3100d4 diff --git a/scribe/scribe.jsx b/scribe/scribe.jsx index e7654b4..d8f6afc 100644 --- a/scribe/scribe.jsx +++ b/scribe/scribe.jsx @@ -1,8 +1,8 @@ function team_symbol(team) { if (team === "+") - return "🞥"; + return "+"; else - return "🞇"; + return "o"; } function undisplay(element) { @@ -26,7 +26,9 @@ const events = new EventSource("events"); events.onerror = function(event) { if (event.target.readyState === EventSource.CLOSED) { + setTimeout(() => { add_message("danger", "Connection to server lost."); + }, 1000); } }; @@ -77,14 +79,148 @@ events.addEventListener("game-state", event => { * 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"); + tmp.setAttribute("value", document.getElementById(id).innerHTML); + document.body.appendChild(tmp); + tmp.select(); + document.execCommand("copy"); + document.body.removeChild(tmp); +} + function GameInfo(props) { if (! props.id) return null; return (
-

{props.id}

- Invite a friend to play by sending this URL: {props.url} + {props.id} + {" "} + Share this link to invite a friend:{" "} + {props.url} + {" "} +
); } @@ -130,7 +266,7 @@ function PlayerInfo(props) { return (
-

Players

+ Players: {props.player.name} {props.player.team ? ` (${props.player.team})` : ""} {props.first_move ? "" : " "} @@ -146,166 +282,195 @@ function PlayerInfo(props) { ); } +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} +
+ + + {glyph_dots} + + +
+
+ ); +} + 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 (
- {props.value} + {props.value.symbol}
); } -class Board extends React.Component { - render_square(i,j) { - const value = this.props.squares[i][j]; +function MiniGrid(props) { + function grid_square(j) { + const value = props.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 ( this.props.onClick(i,j)} + active={square_active} + last_move={last_move} + onClick={() => props.onClick(j)} /> ); } - render() { - return ( -
-
- {this.render_square(0,0)} - {this.render_square(0,1)} - {this.render_square(0,2)} - {" "} - {this.render_square(1,0)} - {this.render_square(1,1)} - {this.render_square(1,2)} - {" "} - {this.render_square(2,0)} - {this.render_square(2,1)} - {this.render_square(2,2)} -
-
- {this.render_square(0,3)} - {this.render_square(0,4)} - {this.render_square(0,5)} - {" "} - {this.render_square(1,3)} - {this.render_square(1,4)} - {this.render_square(1,5)} - {" "} - {this.render_square(2,3)} - {this.render_square(2,4)} - {this.render_square(2,5)} -
-
- {this.render_square(0,6)} - {this.render_square(0,7)} - {this.render_square(0,8)} - {" "} - {this.render_square(1,6)} - {this.render_square(1,7)} - {this.render_square(1,8)} - {" "} - {this.render_square(2,6)} - {this.render_square(2,7)} - {this.render_square(2,8)} -
+ /* 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; + props.squares.forEach(element => { + if (element.symbol) + occupied++; + }); -
-
+ let class_name = "mini-grid"; + if (props.active && occupied < 9) + class_name += " active"; -
- {this.render_square(3,0)} - {this.render_square(3,1)} - {this.render_square(3,2)} - {" "} - {this.render_square(4,0)} - {this.render_square(4,1)} - {this.render_square(4,2)} - {" "} - {this.render_square(5,0)} - {this.render_square(5,1)} - {this.render_square(5,2)} -
-
- {this.render_square(3,3)} - {this.render_square(3,4)} - {this.render_square(3,5)} - {" "} - {this.render_square(4,3)} - {this.render_square(4,4)} - {this.render_square(4,5)} - {" "} - {this.render_square(5,3)} - {this.render_square(5,4)} - {this.render_square(5,5)} -
-
- {this.render_square(3,6)} - {this.render_square(3,7)} - {this.render_square(3,8)} - {" "} - {this.render_square(4,6)} - {this.render_square(4,7)} - {this.render_square(4,8)} - {" "} - {this.render_square(5,6)} - {this.render_square(5,7)} - {this.render_square(5,8)} -
+ return ( +
+ {grid_square(0)} + {grid_square(1)} + {grid_square(2)} + {grid_square(3)} + {grid_square(4)} + {grid_square(5)} + {grid_square(6)} + {grid_square(7)} + {grid_square(8)} +
+ ); +} -
-
+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.squares[target].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); + } + } -
- {this.render_square(6,0)} - {this.render_square(6,1)} - {this.render_square(6,2)} - {" "} - {this.render_square(7,0)} - {this.render_square(7,1)} - {this.render_square(7,2)} - {" "} - {this.render_square(8,0)} - {this.render_square(8,1)} - {this.render_square(8,2)} -
-
- {this.render_square(6,3)} - {this.render_square(6,4)} - {this.render_square(6,5)} - {" "} - {this.render_square(7,3)} - {this.render_square(7,4)} - {this.render_square(7,5)} - {" "} - {this.render_square(8,3)} - {this.render_square(8,4)} - {this.render_square(8,5)} -
-
- {this.render_square(6,6)} - {this.render_square(6,7)} - {this.render_square(6,8)} - {" "} - {this.render_square(7,6)} - {this.render_square(7,7)} - {this.render_square(7,8)} - {" "} - {this.render_square(8,6)} - {this.render_square(8,7)} - {this.render_square(8,8)} -
+ /* 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 squares = this.props.squares[i]; + return ( + this.props.onClick(i,j)} + /> + ); + } + render() { + return ( +
+
+ {this.mini_grid(0)} + {this.mini_grid(1)} + {this.mini_grid(2)} + {this.mini_grid(3)} + {this.mini_grid(4)} + {this.mini_grid(5)} + {this.mini_grid(6)} + {this.mini_grid(7)} + {this.mini_grid(8)} +
); } @@ -337,9 +502,12 @@ class Game extends React.Component { game_info: {}, player_info: {}, other_players: [], - squares: Array(9).fill(null).map(() => Array(9).fill(null)), - moves: 0, - next_to_play: "+" + squares: [...Array(9)].map(() => Array(9).fill({ + symbol: null, + glyph: false + })), + moves: [], + next_to_play: "+", }; } @@ -374,21 +542,220 @@ class Game extends React.Component { }); } + 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; + new_squares[mini_grid_index][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_squares[mini_grid_index], position); + const is_glyph = this.is_glyph(connected); + + for (let i = 0; i < 9; i++) { + if (connected[i]) + new_squares[mini_grid_index][i].glyph = is_glyph; + } + + /* 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, + moves: new_moves, next_to_play: next_to_play }); } @@ -416,7 +783,7 @@ class Game extends React.Component { 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; @@ -481,9 +848,23 @@ class Game extends React.Component { this.handle_click(i, j, first_move)} />
+
, +
+ { + scribe_glyphs.map(glyph => { + return ( + + ); + }) + }
]; }