Tutorial: Intro To Reactをやったメモ

追加課題のメモです。初心者です。

1. Display the location for each move in the format (col, row) in the move history list.

@@ -49,6 +49,8 @@ class Game extends React.Component {
     this.state = {
       history: [{
         squares: Array(9).fill(null),
+        col: null,
+        row: null,
       }],
       stepNumber: 0,
       xIsNext: true,
@@ -66,6 +68,8 @@ class Game extends React.Component {
     this.setState({
       history: history.concat([{
         squares: squares,
+        col: (i % 3) + 1,
+        row: Math.floor(i / 3) + 1,
       }]),
       stepNumber: history.length,
       xIsNext: !this.state.xIsNext,
@@ -86,7 +90,7 @@ class Game extends React.Component {
 
     const moves = history.map((step, move) => {
       const desc = move ?
-        'Go to move #' + move :
+        'Go to move #' + move + ' (' + step.col + ',' + step.row + ')' :
         'Go to game start';
       return (
         <li key={move}>

状態にcolrowを持つように変更。

2. Bold the currently selected item in the move list.

index.js

@@ -92,9 +92,10 @@ class Game extends React.Component {
       const desc = move ?
         'Go to move #' + move + ' (' + step.col + ',' + step.row + ')' :
         'Go to game start';
+      const textStyle = (move === this.state.stepNumber) ? 'bold'  : null;
       return (
         <li key={move}>
-          <button onClick={() => this.jumpTo(move)}>{desc}</button>
+          <button className={textStyle} onClick={() => this.jumpTo(move)}>{desc}</button>
         </li>
       );
     });

style.css

@@ -48,3 +48,7 @@ ol, ul {
 .game-info {
   margin-left: 20px;
 }
+
+.bold {
+  font-weight: bold;
+}

classNameを切り替え。

3. Rewrite Board to use two loops to make the squares instead of hardcoding them.

@@ -16,30 +16,21 @@ class Board extends React.Component {
       <Square
         value={this.props.squares[i]}
         onClick={() => this.props.onClick(i)}
+        key={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>
-    );
+    let squares = [];
+    for(let row = 0; row < 3; row++) {
+      let rows = [];
+      for(let col = 0; col < 3; col++) {
+        rows.push(this.renderSquare(row*3 + col));
+      }
+      squares.push(<div className="board-row" key={row}>{rows}</div>);
+    }
+    return (<div>{squares}</div>);
   }
 }

keyを追加。

4. Add a toggle button that lets you sort the moves in either ascending or descending order.

@@ -45,6 +45,7 @@ class Game extends React.Component {
       }],
       stepNumber: 0,
       xIsNext: true,
+      isDescendingOrder: false,
     };
   }
 
@@ -74,12 +75,18 @@ class Game extends React.Component {
     });
   }
 
+  toggleOrder() {
+    this.setState({
+      isDescendingOrder: !this.state.isDescendingOrder,
+    });
+  }
+
   render() {
     const history = this.state.history;
     const current = history[this.state.stepNumber];
     const winner = calculateWinner(current.squares);
 
-    const moves = history.map((step, move) => {
+    let moves = history.map((step, move) => {
       const desc = move ?
         'Go to move #' + move + ' (' + step.col + ',' + step.row + ')' :
         'Go to game start';
@@ -90,6 +97,14 @@ class Game extends React.Component {
         </li>
       );
     });
+    let movesList;
+    if(this.state.isDescendingOrder) {
+      moves.reverse();
+      movesList = <ol reversed>{moves}</ol>;
+    }
+    else {
+      movesList = <ol>{moves}</ol>;
+    }
 
     let status;
     if(winner) {
@@ -108,7 +123,8 @@ class Game extends React.Component {
         </div>
         <div className="game-info">
           <div>{status}</div>
-          <ol>{moves}</ol>
+          <button onClick={() => this.toggleOrder()}>toggle</button>
+          {movesList}
         </div>
       </div>
     );

状態にisDescendingOrderを追加。<ol>まで逆順にする必要はなかったかもしれない。

5. When someone wins, highlight the three squares that caused the win.

index.js

@@ -3,8 +3,9 @@ import ReactDOM from 'react-dom';
 import './style.css';
 
 function Square(props) {
+  const buttonClass = props.isHighlight ? "square highlight" : "square";
   return (
-    <button className="square" onClick={props.onClick}>
+    <button className={buttonClass} onClick={props.onClick}>
       {props.value}
     </button>
   );
@@ -17,6 +18,7 @@ class Board extends React.Component {
         value={this.props.squares[i]}
         onClick={() => this.props.onClick(i)}
         key={i}
+        isHighlight={this.props.highlightLine.includes(i)}
       />
     );
   }
@@ -84,7 +86,8 @@ class Game extends React.Component {
   render() {
     const history = this.state.history;
     const current = history[this.state.stepNumber];
-    const winner = calculateWinner(current.squares);
+    const w = calculateWinner(current.squares);
+    const [winner, line] = (w) ? w : [null, [null]];
 
     let moves = history.map((step, move) => {
       const desc = move ?
@@ -119,6 +122,7 @@ class Game extends React.Component {
           <Board 
             squares={current.squares}
             onClick={(i) => this.handleClick(i)}
+            highlightLine={line}
           />
         </div>
         <div className="game-info">
@@ -152,7 +156,7 @@ function calculateWinner(squares) {
   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 [squares[a], lines[i]];
     }
   }
   return null;

style.css

@@ -40,6 +40,10 @@ ol, ul {
   background: #ddd;
 }
 
+.highlight {
+  color: #ff0000;
+}
+
 .game {
   display: flex;
   flex-direction: row;

calculateWinnerでハイライトするラインを返すように修正。そのラインをBoardpropsに渡す。squareの添字がラインに含まれていたらハイライトするようにhighlightclassNameに追加する。

6. When no one wins, display a message about the result being a draw.

@@ -113,7 +113,11 @@ class Game extends React.Component {
     if(winner) {
       status = 'Winner: ' + winner;
     } else {
-      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+      if(this.state.stepNumber === 9) {
+        status = 'Draw';
+      } else {
+        status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+      }
     }
 
     return (

あれ、なんかこれ簡単じゃね?と思って書いたけど、これだと全てのマスが埋まった時にDrawと表示されるだけなので、"When no one wins,"じゃない。

@@ -55,7 +55,9 @@ class Game extends React.Component {
     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]) {
+    if(calculateWinner(squares) ||
+       squares[i] ||
+       isDraw(squares, this.state.xIsNext)) {
       return;
     }
     squares[i] = this.state.xIsNext ? 'X' : 'O';
@@ -113,7 +115,11 @@ class Game extends React.Component {
     if(winner) {
       status = 'Winner: ' + winner;
     } else {
-      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+      if(isDraw(current.squares, this.state.xIsNext)) {
+        status = 'Draw';
+      } else {
+        status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+      }
     }
 
     return (
@@ -160,4 +166,19 @@ function calculateWinner(squares) {
     }
   }
   return null;
-} 
+}
+
+function isDraw(squares, xIsNext) {
+  if(calculateWinner(squares)) {
+    return false;
+  }
+
+  for(let i = 0; i < squares.length; i++) {
+    if(squares[i] === null) {
+      let sqrs = squares.slice();
+      sqrs[i] = xIsNext ? 'X' : 'O';
+      if(isDraw(sqrs, !xIsNext) === false) return false;
+    }
+  }
+  return true;
+}

isDraw()を追加。引き分けの時にtrue, それ以外(どちらかが勝てるパターンが残っている場合)はfalseを返す。

全探索でざっくり512パターン。イミュータブルが大事だよ、と書かれていたのでslice()してるけど、計算量とかメモリってどのくらいまでなら許容されるのかなぁ。