React  三目並べでCPUと対戦できるゲームを作成

Reactの公式チュートリアルで三目並べを作った後に
CPUと対戦できるようにします。
チュートリアルでは人対人で勝ち負け判定のある三目並べを作成できる。

チュートアルが終わった後の状態から、始めます。

公式チュートリアル

Reactの公式チュートリアルでは以下を学ぶことができます。

コンポーネントや props、state といった基礎概念について学びます。
ゲームを完成させる:React での開発における非常によくある技法について学びます。
タイムトラベル機能の追加:React 独自の利点について深い洞察が得られます。

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

実装

現状のソース

公式チュートリアルから若干ソースを変更しているため記載します。
変更している点は、ファイルの分割とタイムトラベル機能を1手前までに制限している点です。

Game.js → 勝ち負けを判定したり、盤面の情報を保持している。
Board.js → 9つのマスを表示する
Square.js → 1マスを表示し、「空白」「○」「✖︎」のいずれかを表示

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Game from './components/Game.js'

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
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;
}
import React from 'react';
import Square from './Square.js'

export default 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>
    );
  }
}
import React from 'react';
import Board from './Board.js'

export default class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
      stepNumber: 0,
    };
  }

  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 (this.calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    // 一部追加
    let all_history = history.concat([{
      squares: squares
    }])
    if (all_history.length === 3) {
      all_history.shift();
    }

    this.setState({
      history: all_history,
      stepNumber: all_history.length -1 ,
      xIsNext: !this.state.xIsNext,
    });
  }

  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;
  }

  jumpTo(step) {
    const xIsNext = step !== this.state.stepNumber ? !this.state.xIsNext : this.state.xIsNext;
    this.setState({
      stepNumber: step,
      xIsNext
    });
  }

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = this.calculateWinner(current.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }
    const moves = history.length < 2 ? null :(
      <button onClick={() => this.jumpTo(0)}>1回前へ戻る</button>
  );

    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>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}
import React from 'react';

export default function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

CPUと対戦できるようにする

ランダムで手を打つコンピュータを作成し、対戦できるようにします。

空いているマス(null)を取得します。
indexOfを使用して、打てる手を配列につめていきます。

    // 空いているマスを取得
    const possible_hands = [];
    let hand = squares.indexOf(null);
    while (hand !== -1) {
      possible_hands.push(hand);
      hand = squares.indexOf(null, hand + 1);
    }

空いているマス、つまり打てる場所を取得することができました。
次は打てる手の中からCPUがランダムで手を打つようにします。

   const action_hand = possible_hands[Math.floor(Math.random()*possible_hands.length)];

上記はGame.jsを修正しています。

import React from 'react';
import Board from './Board.js'

export default class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
      stepNumber: 0,
    };
  }

  async handleClick(i) {
    const squares = this.getCurrentBoard();
    if (this.calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    // 一部追加
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    let all_history = history.concat([{
      squares: squares
    }])
    if (all_history.length === 3) {
      all_history.shift();
    }

    await this.setState({
      history: all_history,
      stepNumber: all_history.length -1 ,
      xIsNext: !this.state.xIsNext,
    });
    this.cpuAction(squares);
  }

  // 盤面取得
  getCurrentBoard() {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    return squares;
  }

  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;
  }

  jumpTo(step) {
    const xIsNext = step !== this.state.stepNumber ? !this.state.xIsNext : this.state.xIsNext;
    this.setState({
      stepNumber: step,
      xIsNext : true
    });
  }

  // cpu action
  cpuAction(squares) {

    if (this.calculateWinner(squares)) return;
    let history = this.state.history.slice(0, this.state.stepNumber + 1);

    // 空いているマスを取得
    const possible_hands = [];
    let hand = squares.indexOf(null);
    while (hand !== -1) {
      possible_hands.push(hand);
      hand = squares.indexOf(null, hand + 1);
    }
    // 空いているマスがなければ終了
    if (possible_hands.length === 0) return;
    // 空いているマスのうちランダムで1マスを取得
    const action_hand = possible_hands[Math.floor(Math.random()*possible_hands.length)];

    // 選択した手で盤面を更新
    squares[action_hand] = this.state.xIsNext ? 'X' : 'O';
    history[history.length - 1].squares  = squares;

    this.setState({
      history,
      xIsNext: !this.state.xIsNext,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = this.calculateWinner(current.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }
    const moves = history.length < 2 ? null :(
      <button onClick={() => this.jumpTo(0)}>1回前へ戻る</button>
  );

    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>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

動作

サーバーを起動して、動作してみます。

自分が1手打つと

自動で○を配置してきます。これでCPUが自動で打っています。

最後に

こういう簡単な三目並べのようなゲームで自分でAIと対戦できるようなゲームを作れるようになりたいな。。。

タイトルとURLをコピーしました