]> git.cworth.org Git - lmno.games/blobdiff - scribe/scribe.jsx
Highlight last move and legal mini-grids for current move
[lmno.games] / scribe / scribe.jsx
index e7654b417eba2dfdc746a07d91f2a021de4bf2bb..07920773c9f79caa371b178639deaa35fea5a040 100644 (file)
@@ -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 (
     <div className="game-info">
-      <h2>{props.id}</h2>
-      Invite a friend to play by sending this URL: {props.url}
+      <span className="game-id">{props.id}</span>
+      {" "}
+      Share this link to invite a friend:{" "}
+      <span id="game-share-url">{props.url}</span>
+      {" "}
+      <button
+        className="inline"
+        onClick={() => copy_to_clipboard('game-share-url')}
+      >Copy Link</button>
     </div>
   );
 }
@@ -130,7 +266,7 @@ function PlayerInfo(props) {
 
   return (
     <div className="player-info">
-      <h2>Players</h2>
+      <span className="players-header">Players: </span>
       {props.player.name}
       {props.player.team ? ` (${props.player.team})` : ""}
       {props.first_move ? "" : " "}
@@ -146,6 +282,50 @@ 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(
+          <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";
 
@@ -155,6 +335,10 @@ function Square(props) {
     className += " open";
   }
 
+  if (props.last_move) {
+    className += " last-move";
+  }
+
   const onClick = props.active ? props.onClick : null;
 
   return (
@@ -165,147 +349,116 @@ function Square(props) {
   );
 }
 
-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);
     return (
       <Square
         value={value}
-        active={this.props.active && ! value}
-        onClick={() => this.props.onClick(i,j)}
+        active={props.active}
+        last_move={last_move}
+        onClick={() => props.onClick(j)}
       />
     );
   }
 
-  render() {
-    return (
-      <div>
-        <div className="board-row">
-          {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)}
-        </div>
-        <div className="board-row">
-          {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)}
-        </div>
-        <div className="board-row">
-          {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)}
-        </div>
+  /* 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)
+      occupied++;
+  });
 
-        <div className="board-row">
-        </div>
+  let class_name = "mini-grid";
+  if (props.active && occupied < 9)
+    class_name += " active";
 
-        <div className="board-row">
-          {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)}
-        </div>
-        <div className="board-row">
-          {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)}
-        </div>
-        <div className="board-row">
-          {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)}
-        </div>
+  return (
+    <div className={class_name}>
+      {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)}
+    </div>
+  );
+}
 
-        <div className="board-row">
-        </div>
+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 active = false;
+    if (this.props.active) {
+      active = true;
+      if (this.props.last_two_moves[0]) {
+        /* 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)
+            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);
+      }
+    }
 
-        <div className="board-row">
-          {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)}
-        </div>
-        <div className="board-row">
-          {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)}
-        </div>
-        <div className="board-row">
-          {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)}
-        </div>
+    /* 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 (
+      <MiniGrid
+        squares={squares}
+        active={active}
+        last_moves={last_moves}
+        onClick={(j) => this.props.onClick(i,j)}
+      />
+    );
+  }
 
+  render() {
+    return (
+      <div className="board-container">
+        <div className="board">
+          {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)}
+        </div>
       </div>
     );
   }
@@ -337,9 +490,9 @@ 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(null)),
+      moves: [],
+      next_to_play: "+",
     };
   }
 
@@ -375,12 +528,13 @@ class Game extends React.Component {
   }
 
   receive_move(move) {
-    if (this.state.moves === 81) {
+    if (this.state.moves.length === 81) {
       return;
     }
     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_moves = [...this.state.moves, move];
     let next_to_play;
     if (this.state.next_to_play === "+")
       next_to_play = "o";
@@ -388,7 +542,7 @@ class Game extends React.Component {
       next_to_play = "+";
     this.setState({
       squares: new_squares,
-      moves: this.state.moves + 1,
+      moves: new_moves,
       next_to_play: next_to_play
     });
   }
@@ -416,7 +570,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 +635,23 @@ class Game extends React.Component {
           <Board
             active={board_active}
             squares={state.squares}
+            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>
     ];
   }