Add a simple tictactoe game, implemented with React
authorCarl Worth <cworth@cworth.org>
Mon, 25 May 2020 20:37:10 +0000 (13:37 -0700)
committerCarl Worth <cworth@cworth.org>
Tue, 26 May 2020 03:53:14 +0000 (20:53 -0700)
This isn't a proper LMNO game, (it's not networked at all), but it's a
starting point for seeing how we might structure a React-based client
(and we can develop this into a proper LMNO game).

In fact, the source code here came directly from the React tutorial
here:

https://reactjs.org/tutorial/tutorial.html

Note that I've not pulled in the 1000+ npm modules that would have
come from using create-react-app as recommended in that tutorial. As
can be seen here, by example, none of them are needed. The only build
tool requireed is something to compile JSX and we've got that working
via the Debian-provided babel packages (as seen in the most recent
commits here).

tictactoe/Makefile [new file with mode: 0644]
tictactoe/index.html [new file with mode: 0644]
tictactoe/tictactoe.css [new file with mode: 0644]
tictactoe/tictactoe.jsx [new file with mode: 0644]

diff --git a/tictactoe/Makefile b/tictactoe/Makefile
new file mode 100644 (file)
index 0000000..9f07401
--- /dev/null
@@ -0,0 +1,12 @@
+# Defer all targets up to the upper-level
+#
+# This requires two recipes. The first to cover the case of no
+# explicit target specifed (so when invoked as "make" we call "make"
+# at the upper-level) and then a .DEFAULT recipe to pass any explicit
+# target up as well, (so that an invocation of "make foo" results in a
+# call to "make foo" above.
+all:
+       $(MAKE) -C ..
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/tictactoe/index.html b/tictactoe/index.html
new file mode 100644 (file)
index 0000000..6a6d289
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+    <title>Tic-tac-toe</title>
+
+    <script src="/react.js"></script>
+    <script src="/react-dom.js"></script>
+    <link rel="stylesheet" href="tictactoe.css" type="text/css" />
+    <script type="module" src="tictactoe.js"></script>
+  </head>
+  <body>
+    <div id="tictactoe"></div>
+  </body>
+</html>
diff --git a/tictactoe/tictactoe.css b/tictactoe/tictactoe.css
new file mode 100644 (file)
index 0000000..0e56082
--- /dev/null
@@ -0,0 +1,50 @@
+body {
+  font: 14px "Century Gothic", Futura, sans-serif;
+  margin: 20px;
+}
+
+ol, ul {
+  padding-left: 30px;
+}
+
+.board-row:after {
+  clear: both;
+  content: "";
+  display: table;
+}
+
+.status {
+  margin-bottom: 10px;
+}
+
+.square {
+  background: #fff;
+  border: 1px solid #999;
+  float: left;
+  font-size: 24px;
+  font-weight: bold;
+  line-height: 34px;
+  height: 34px;
+  margin-right: -1px;
+  margin-top: -1px;
+  padding: 0;
+  text-align: center;
+  width: 34px;
+}
+
+.square:focus {
+  outline: none;
+}
+
+.kbd-navigation .square:focus {
+  background: #ddd;
+}
+
+.game {
+  display: flex;
+  flex-direction: row;
+}
+
+.game-info {
+  margin-left: 20px;
+}
diff --git a/tictactoe/tictactoe.jsx b/tictactoe/tictactoe.jsx
new file mode 100644 (file)
index 0000000..eff7eff
--- /dev/null
@@ -0,0 +1,144 @@
+function Square(props) {
+  return (
+    <button className="square" onClick={props.onClick}>
+      {props.value}
+    </button>
+  );
+}
+
+class Board extends React.Component {
+  renderSquare(i) {
+    return (
+      <Square
+        value={this.props.squares[i]}
+        onClick={() => this.props.onClick(i)}
+      />
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        <div className="board-row">
+          {this.renderSquare(0)}
+          {this.renderSquare(1)}
+          {this.renderSquare(2)}
+        </div>
+        <div className="board-row">
+          {this.renderSquare(3)}
+          {this.renderSquare(4)}
+          {this.renderSquare(5)}
+        </div>
+        <div className="board-row">
+          {this.renderSquare(6)}
+          {this.renderSquare(7)}
+          {this.renderSquare(8)}
+        </div>
+      </div>
+    );
+  }
+}
+
+class Game extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      history: [
+        {
+          squares: Array(9).fill(null)
+        }
+      ],
+      stepNumber: 0,
+      xIsNext: true
+    };
+  }
+
+  handleClick(i) {
+    const history = this.state.history.slice(0, this.state.stepNumber + 1);
+    const current = history[history.length - 1];
+    const squares = current.squares.slice();
+    if (calculateWinner(squares) || squares[i]) {
+      return;
+    }
+    squares[i] = this.state.xIsNext ? "X" : "O";
+    this.setState({
+      history: history.concat([
+        {
+          squares: squares
+        }
+      ]),
+      stepNumber: history.length,
+      xIsNext: !this.state.xIsNext
+    });
+  }
+
+  jumpTo(step) {
+    this.setState({
+      stepNumber: step,
+      xIsNext: (step % 2) === 0
+    });
+  }
+
+  render() {
+    const history = this.state.history;
+    const current = history[this.state.stepNumber];
+    const winner = calculateWinner(current.squares);
+
+    const moves = history.map((step, move) => {
+      const desc = move ?
+        'Go to move #' + move :
+        'Go to game start';
+      return (
+        <li key={move}>
+          <button onClick={() => this.jumpTo(move)}>{desc}</button>
+        </li>
+      );
+    });
+
+    let status;
+    if (winner) {
+      status = "Winner: " + winner;
+    } else {
+      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
+    }
+
+    return (
+      <div className="game">
+        <div className="game-board">
+          <Board
+            squares={current.squares}
+            onClick={i => this.handleClick(i)}
+          />
+        </div>
+        <div className="game-info">
+          <div>{status}</div>
+          <ol>{moves}</ol>
+        </div>
+      </div>
+    );
+  }
+}
+
+// ========================================
+
+ReactDOM.render(<Game />, document.getElementById("tictactoe"));
+
+function calculateWinner(squares) {
+  const lines = [
+    [0, 1, 2],
+    [3, 4, 5],
+    [6, 7, 8],
+    [0, 3, 6],
+    [1, 4, 7],
+    [2, 5, 8],
+    [0, 4, 8],
+    [2, 4, 6]
+  ];
+  for (let i = 0; i < lines.length; i++) {
+    const [a, b, c] = lines[i];
+    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
+      return squares[a];
+    }
+  }
+  return null;
+}