サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

React + ES6 入門 02: マルバツゲームの勝ちを決める


「React + ES6 入門」はReact公式サイトの「TUTORIAL」をもとに、ReactおよびJSXの構文に加え、ECMAScript 6(ECMAScript 2015)についても解説します。「React + ES6 入門 01: ゲームの盤面をつくる」では、9マスのマルバツゲーム盤面をつくり、クリックしたマス目に「X」を示しました。さらに、「X」と「O」が交互に出るようにし、並びから勝ちを決めます。

01 9マスの値を盤面のコンポーネントにまとめる

今回、「X」と「O」が交互につけられるようになり、勝ちもわかりますので、マルバツゲームとしてひとまずかたちにはなります(サンプル001)。「React + ES6 入門 01」のコード002「9マスのゲーム盤面をつくる」に手を加えてゆきましょう。

サンプル001■React 15.5.3 + ES6: Tic tac toe minimal refined

勝ちを決めるには、9つのマス目の値をまとめて捉えていなければなりません。そのため、盤面のコンポーネント(Board)のstateに、配列(squares)を加えます。初期値は、以下のようにコンストラクタとして呼び出されるconstructor()メソッドで与えます。メソッド本体から、継承するスーパークラスのコンストラクタを呼び出すのがsuper()です。この後でなければ、this参照はできません。

盤面のコンポーネント(Board)には、クリックのハンドラ(handleClick())も加えます。その本体で配列(squares)はArray.slice()メソッドにより複製してから値を書き替えて、setState()メソッドで上書きするかたちにしています。オブジェクトは複雑になると階層が深くなることもあり、じかに書き替えると影響が予想しにくくなるからです。また、Reactが画面を描き直すときも、範囲がはっきりするので効率的です(「Why Immutability Is Important」参照)。


class Board extends React.Component {
	constructor() {
		super();
		this.state = {
			squares: new Array(9)
		};
	}

	handleClick(i) {
		const squares = this.state.squares.slice();
		squares[i] = 'X';
		this.setState({squares: squares});
	}

}

それぞれのマス目に示す値も、クリックのハンドラも、盤面のコンポーネント(Board)がもちます。そこで、マス目の要素を返すメソッド(renderSquare())は、つぎのようにふたつの変数(valueとonClick)にそれぞれのJavaScriptコードを与えました。すると、マス目のコンポーネント(Square)は、propsプロパティから値を参照し、ハンドラのメソッド(handleClick())が呼び出せます。


class Square extends React.Component {
	render() {
		return (
			/* <button className="square" onClick={() => this.setState({value: 'X'})}>
				{this.state && this.state.value} */
			<button className="square" onClick={() => this.props.onClick()}>
				{this.props.value}
			</button>
		);
	}
}
class Board extends React.Component {

	renderSquare(i) {
		// return <Square value={i} />;
		return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
	}

}

02 関数コンポーネントを使う

マス目のコンポーネント(Square)はみずから値をもたなくなったので、もはやstateプロパティも要りません。このような場合、コンポーネントは返す要素を戻り値とした関数で定められます。propsプロパティもなくなる代わりに、親から与えられた変数を納めたオブジェクトが関数の引数として受け取れます(「【React】コンポーネントをシンプルに書くためのStateless Functional Componentsについて」参照)。マス目のコンポーネントは、つぎのように書き替えればよいのです。


// class Square extends React.Component {
function Square(props) {
	// render() {
	return (
		/* <button className="square" onClick={() => this.props.onClick()}>
			{this.props.value} */
		<button className="square" onClick={() => props.onClick()}>
			{props.value}
		</button>
	);
	// }
}

これで9マスの値を盤面のコンポーネント(Board)がまとめて扱えるようになりました。動きは前出の「React + ES6 入門 01」コード002と変わりません。けれど、9つのマス目の値を配列に納めていますので、その要素を調べることにより勝ちが決められます。ここまでのJavaScriptの手直しは、以下のコード001にまとめました。

コード001■9マスの値を盤面のコンポーネントでまとめて扱う


function Square(props) {
	return (
		<button className="square" onClick={() => props.onClick()}>
			{props.value}
		</button>
	);
}
class Board extends React.Component {
	constructor() {
		super();
		this.state = {
			squares: new Array(9)
		};
	}
	renderSquare(i) {
		return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
	}
	handleClick(i) {
		const squares = this.state.squares.slice();
		squares[i] = 'X';
		this.setState({squares: squares});
	}
	render() {
		const status = 'Next player: X';
		return (
			<div>
				<div className="status">{status}</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>
		);
	}
}
ReactDOM.render(
	<Board />,
	document.getElementById('container')
);

03 XとOを交互につける

ゲームの盤面はマス目9つで組み立てます。盤面はつぎのように新たなクラス(Board)で定めました。JSXの構文の中に波かっこ{}を使うと、JavaScriptの式が書けます。つまり、変数(プロパティ)を参照したり、関数(メソッド)が呼び出せるのです。render()メソッドは、const宣言した定数(status)の値を<div>要素のテキストとして差し込み、3コマから1行をつくる<div>要素の中からメソッド(renderSquare())を呼び出します。そして、メソッドからマス目(Square)の要素をひとつ受け取って加えているのです。


class Board extends React.Component {
	constructor() {

		this.state = {
			squares: new Array(9),
			xIsNext: true
		};
	}

}


class Board extends React.Component {

	handleClick(i) {

		// squares[i] = 'X';
		squares[i] = this.state.xIsNext ? 'X' : 'O';
		// this.setState({squares: squares});
		this.setState({
			squares: squares,
			xIsNext: !this.state.xIsNext
		});
	}
	render() {
		// const status = 'Next player: X';
		const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

	}

}

図001■XとOが交互に表れる

図001

04 勝ちを調べる

9マスの値の配列から勝ちの3つの並びがあるかどうか、つぎの関数(calculateWinner())でたしかめられます。勝ちの3つのマス目の組み合わせは、定数(lines)に配列で納めておきます。関数は引数(squares)に受け取った9つの値の配列から、勝ちのいずれかの組み合わせがあるかどうかfor文で順に調べるのです。勝ちがあったらその印(「X」か「O」)、なかったらnullが返されます。なお、配列の要素を配列のかたちで書いた変数にそれぞれ納める分割代入は、ECMAScript 6で採り入れられました。


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],
	];
	const length = lines.length;
	for (let i = 0; i < length; i++) {
		const [a, b, c] = lines[i];
		const player = squares[a];
		if (player && player === squares[b] && player === squares[c]) {
			return player;
		}
	}
	return null;
}

盤面のコンポーネント(Board)は、つぎのようにrender()メソッドで前掲関数(calculateWinner())から勝ちが見つかったら勝者を示します(図002)。もっとも、これだけでは勝ちが決まっても、続けて印がつけられます。それだけでなく、すでに選ばれたマス目もクリックで印が変えられてしまうのです。そこで、クリックのハンドラ(handleClick())にコードを書き加え、勝ちが決まったあとや、すでに選ばれたマス目は盤面が変わらないようにしました。


class Board extends React.Component {

	handleClick(i) {
		const squares = this.state.squares.slice();
		if (calculateWinner(squares) || squares[i]) {
			return;
		}

	}
	render() {
		const winner = calculateWinner(this.state.squares);
		let status;
		if (winner) {
			status = 'Winner: ' + winner;
		} else {
			status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
		}

}

図002■勝者が示された

図002

05 クリックを止める考え方

盤面のコンポーネント(Board)でクリックのハンドラ(handleClick())は、クリックから進めてよいかどうかを、つぎのような3行のif文で判定していました。コードは短いものの、クリックのたびに関数(calculateWinner())で勝ちの並びがあるかどうか調べるのは効率がよくありません。


class Board extends React.Component {

	handleClick(i) {

		if (calculateWinner(squares) || squares[i]) {
			return;
		}

	}

}

盤面のコンポーネント(Board)のstateプロパティに、つぎのように勝負がついたかどうかのフラグ(isFinished)を加えれば、その後の関数(calculateWinner())の呼び出しが避けられます。if文は3つに増えても、クリックのハンドラ(handleClick())は、勝負が終わればフラグの値だけ見てただちに処理を抜けます。配列(squares)の複製もしません。その分負荷は下がるはずです。


class Board extends React.Component {
	constructor() {

		this.state = {

			isFinished: false
		};
	}

	handleClick(i) {
		if (this.state.isFinished) {
			return;
		}
		const squares = this.state.squares.slice();
		// if (calculateWinner(squares) || squares[i]) {
		if (calculateWinner(squares)) {
			this.setState({isFinished: true});
			return;
		}
		if (squares[i]) {
			return;
		}

}

これでマルバツゲームとして、ひとまずかたちになりました。書きあがったスクリプトは、つぎのコード002にまとめます。

コード002■マルバツゲームで勝ちを決める


function Square(props) {
	return (
		<button className="square" onClick={() => props.onClick()}>
			{props.value}
		</button>
	);
}
class Board extends React.Component {
	constructor() {
		super();
		this.state = {
			squares: new Array(9),
			xIsNext: true,
			isFinished: false
		};
	}
	renderSquare(i) {
		return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
	}
	handleClick(i) {
		if (this.state.isFinished) {
			return;
		}
		const squares = this.state.squares.slice();
		if (calculateWinner(squares)) {
			this.setState({isFinished: true});
			return;
		}
		if (squares[i]) {
			return;
		}
		squares[i] = this.state.xIsNext ? 'X' : 'O';

		this.setState({
			squares: squares,
			xIsNext: !this.state.xIsNext
		});
	}
	render() {
		const winner = calculateWinner(this.state.squares);
		let status;
		if (winner) {
			status = 'Winner: ' + winner;
		} else {
			status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
		}
		return (
			<div>
				<div className="status">{status}</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>
		);
	}
}
ReactDOM.render(
	<Board />,
	document.getElementById('container')
);
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],
	];
	const length = lines.length;
	for (let i = 0; i < length; i++) {
		const [a, b, c] = lines[i];
		const player = squares[a];
		if (player && player === squares[b] && player === squares[c]) {
			return player;
		}
	}
	return null;
}


作成者: 野中文雄
作成日: 2017年4月10日


Copyright © 2001-2016 Fumio Nonaka.  All rights reserved.