]> git.cworth.org Git - lmno.games/blobdiff - scribe/scribe.jsx
Score each mini glyph and render the winner for each
[lmno.games] / scribe / scribe.jsx
index 07920773c9f79caa371b178639deaa35fea5a040..75742ca8ec160513fbde5f962298d747d12586df 100644 (file)
@@ -329,12 +329,19 @@ function Glyph(props) {
 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";
   }
@@ -344,19 +351,28 @@ function Square(props) {
   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)}
       />
@@ -366,8 +382,8 @@ function MiniGrid(props) {
   /* 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)
+  mini_grid.squares.forEach(element => {
+    if (element.symbol)
       occupied++;
   });
 
@@ -375,6 +391,11 @@ function MiniGrid(props) {
   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={class_name}>
       {grid_square(0)}
@@ -386,6 +407,7 @@ function MiniGrid(props) {
       {grid_square(6)}
       {grid_square(7)}
       {grid_square(8)}
+      {winner}
     </div>
   );
 }
@@ -402,10 +424,10 @@ class Board extends React.Component {
      *    b. This mini grid corresponds to this players last turn
      *    c. The mini grid that corresponds to the players last turn is full
      */
-    let active = false;
+    let grid_active = false;
     if (this.props.active) {
-      active = true;
-      if (this.props.last_two_moves[0]) {
+      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).
          *
@@ -415,14 +437,14 @@ class Board extends React.Component {
          */
         const target = this.props.last_two_moves[0][1];
         let occupied = 0;
-        this.props.squares[target].forEach(element => {
-          if (element)
+        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)
-          active = (i === target);
+          grid_active = (i === target);
       }
     }
 
@@ -434,11 +456,11 @@ class Board extends React.Component {
     const last_moves = this.props.last_two_moves.filter(move => move[0] === i)
           .map(move => move[1]);
 
-    const squares = this.props.squares[i];
+    const mini_grid = this.props.mini_grids[i];
     return (
       <MiniGrid
-        squares={squares}
-        active={active}
+        mini_grid={mini_grid}
+        active={grid_active}
         last_moves={last_moves}
         onClick={(j) => this.props.onClick(i,j)}
       />
@@ -490,7 +512,15 @@ class Game extends React.Component {
       game_info: {},
       player_info: {},
       other_players: [],
-      squares: [...Array(9)].map(() => Array(9).fill(null)),
+      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: "+",
     };
@@ -527,21 +557,244 @@ 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) {
+    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,
+      mini_grids: new_mini_grids,
       moves: new_moves,
       next_to_play: next_to_play
     });
@@ -634,7 +887,7 @@ class Game extends React.Component {
         <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)}
           />