Add support for detecting glyph shapes
authorCarl Worth <cworth@cworth.org>
Tue, 7 Jul 2020 05:58:12 +0000 (22:58 -0700)
committerCarl Worth <cworth@cworth.org>
Sat, 19 Sep 2020 19:51:13 +0000 (12:51 -0700)
And color the background of each cell that is part fo a glyph in a
player-specific color.

Note: I'm not at all convinced that this is a good way to style
things, but the actual functionality is here now, and I'll be happy to
improve the specific styling in the future.

scribe/scribe.css
scribe/scribe.jsx

index aba532e04b0bd49f65ccb33163c96ab5d77187ba..3bac47b40a2df6f5882cc10f63b04852898a8d4e 100644 (file)
@@ -1,3 +1,9 @@
+:root {
+    /* Colors for the two players. */
+    --o-color: #c2dfff;
+    --plus-color: #fdd7e4;
+}
+
 /* We want our board to be the largest square that can
  * fit. Unfortunately, it requires a bit of CSS magic to make that
  * happen. We can set a width easily enough, but what we can't easily
 
     border-radius: 6px;
     border: 3px solid #999;
+
+    position:relative;
 }
 
 .mini-grid.active {
     border: 3px solid var(--accent-color-bright);
 }
 
+.mini-grid.o-wins .square, .mini-grid.plus-wins .square {
+    opacity: 0.6;
+}
+
+.mini-grid .winner {
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    line-height: 0;
+    font-size: calc(min(35vw, .35 * var(--page-max-width)));
+    font-weight: bold;
+    opacity: 0.20;
+}
+
 .square {
     display: flex;
     justify-content: center;
     cursor: default;
 }
 
+.square.glyph-o {
+    background: var(--o-color);
+}
+
+.square.glyph-plus {
+    background: var(--plus-color);
+}
+
 .square.open:hover {
     background-color: var(--accent-color-bright);
 }
index b736f78fceb813756fcfa3a684773dae186493c9..d8f6afc9bf334b9ff523626854ff6883bfa870a4 100644 (file)
@@ -542,15 +542,176 @@ class Game extends React.Component {
     });
   }
 
-  find_connected(mini_grid_index, position) {
-    /* TODO: Implement actual finding of connected squares here. */
-    let connected = Array(9).fill(false);
+  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;
   }
 
-  detect_glyph(connected) {
-    /* TODO: Implement actual glyph detection here. */
+  /* 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;
   }
 
@@ -573,8 +734,8 @@ class Game extends React.Component {
 
     /* 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(mini_grid_index, position);
-    const is_glyph = this.detect_glyph(connected);
+    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])