何の話かと言うと
こちらの続きです。今回は、プレイ中のスコアの情報を追加して、手番とスコアの情報を表示する Dashboard コンポーネントを追加します。これで、リバーシの実装は一旦完成です。ぱちぱちぱち。
スコア情報の追加
まずは、スコア情報を保持する状態変数を Layout コンポーネントに追加します。Layout.js を次のようにアップデートします。
1 import React, { useState, useRef } from "react"; 2 import { Container, Stack, 3 Heading, Box, HStack, Button } from "@chakra-ui/react"; 4 5 import { Board } from "./Board"; 6 import { Dashboard } from "./Dashboard"; 7 8 9 export const Layout = (props) => { 10 const [gameID, setGameID] = useState(new Date().getTime()); 11 const [turn, setTurn] = useState("black"); 12 const [scores, setScores] = useState({black: 2, white: 2}); 13 const freeze = useRef(false); 14 15 const restart = () => { 16 setTurn("black"); 17 setScores({black: 2, white: 2}); 18 setGameID(new Date().getTime()); 19 } ... 28 const states = { 29 turn: turn, setTurn: setTurn, 30 scores: scores, setScores: setScores, 31 freeze: freeze, 32 }; 33 34 const element = ( 35 <Container padding="8"> 36 <Stack spacing="1"> 37 <Heading marginLeft="4" fontSize="3xl">Reversi</Heading> 38 <Box> 39 <Board key={gameID} states={states}/> 40 </Box> 41 <Box> 42 <Dashboard states={states}/> 43 </Box> ...
12行目で、黒と白それぞれのスコアを要素とする連想配列を含む状態変数を用意しています。複数の情報を1つの状態変数にまとめたい時は、このような使い方ができます。また、17行目で、リスタート時にスコアを初期値に戻すようにしています。そして、30行目にあるように、この新しい状態変数も states 属性として他のコンポーネントに受け渡すようにします。ここでは、39行目で Board コンポーネントに渡す部分と、42行目で Dashboard コンポーネントに渡す部分があります。Dashboard コンポーネントはこれから用意するもので、現在の番手、スコア、ゲーム終了時のメッセージなどをまとめて表示します。
スコアの計算
スコアの計算処理は、Board コンポーネントの中で行います。新たに手を打つごとに再計算します。Board.js の更新部分は次のとおりです。
... 58 export const Board = (props) => { 59 const size = 8; 60 const fieldRef = useRef(getField(size)); 61 const field = fieldRef.current; 62 63 // Unpack states. 64 const turn = props.states.turn; 65 const setTurn = props.states.setTurn; 66 const freeze = props.states.freeze; 67 const setScores = props.states.setScores; ... 141 // Animation ends. 142 await setTurn(opponent[turn]); 143 } 144 freeze.current = false; 145 updateScore(); 146 } 147 148 const updateScore = () => { 149 const newScore = {black: 0, white:0, blank:0}; 150 for (let y = 0; y < size; y++) { 151 for (let x = 0; x < size; x++) { 152 newScore[field[y][x]] += 1; 153 } 154 } 155 setScores({black: newScore.black, white: newScore.white}); 156 }
67行目で引数 props からスコアを更新する関数を取り出しています。そして、コマを打つ処理が終わった144行目の直後で、スコアを計算する関数 updateScore を呼び出しています。
148〜156行目がスコアの計算です。盤面上のコマの数を数えてスコアを計算し、setScores で新しい値をセットします。このタイミングで Layout コンポーネント以下の再描画が行われて、Dashborad コンポーネントが計算するスコアが更新されます。
Dashboard コンポーネントの実装
ファイル「src/components/Dashboard.js」を新たに次の内容で作成します。
1 import React from "react"; 2 import { Stack, HStack, Box } from "@chakra-ui/react"; 3 4 import black from "../assets/black.png"; 5 import white from "../assets/white.png"; 6 7 8 export const Dashboard = (props) => { 9 // Unpack states. 10 const turn = props.states.turn; 11 const scores = props.states.scores; 12 13 const imageMap = {black: black, white: white}; 14 const style = {width: "24px", height: "24px"}; 15 16 let winner = null; 17 if (scores.black === 0) { 18 winner = "white"; 19 } 20 if (scores.white === 0) { 21 winner = "black"; 22 } 23 if (scores.black + scores.white === 8*8) { 24 if (scores.black > scores.white) { 25 winner = "black"; 26 } else if (scores.white > scores.black) { 27 winner = "white"; 28 } else { 29 winner = "tie"; 30 } 31 } 32 33 var message; 34 switch(winner) { 35 case "black": 36 case "white": 37 message = ( 38 <HStack> 39 <Box>Winner </Box> 40 <Box><img src={imageMap[winner]} alt={winner} style={style}/></Box> 41 </HStack> 42 ); 43 break; 44 case "tie": 45 message = ( 46 <HStack> 47 <Box>Tie </Box> 48 <Box><img src={imageMap.black} alt={black} style={style}/></Box> 49 <Box><img src={imageMap.white} alt={white} style={style}/></Box> 50 </HStack> 51 ); 52 break; 53 default: 54 message = ( 55 <HStack> 56 <Box>Turn </Box> 57 <Box><img src={imageMap[turn]} alt={turn} style={style}/></Box> 58 </HStack> 59 ); 60 } 61 62 const infoElements = ( 63 <Stack spacing="2"> 64 <Box> 65 <HStack> 66 <Box><img src={black} alt="black" style={style}/></Box> 67 <Box>{scores.black} pieces.</Box> 68 </HStack> 69 </Box> 70 <Box> 71 <HStack> 72 <Box><img src={white} alt="white" style={style}/></Box> 73 <Box>{scores.white} pieces.</Box> 74 </HStack> 75 </Box> 76 <Box> 77 {message} 78 </Box> 79 </Stack> 80 ); 81 82 const element = ( 83 <Box style={{marginLeft: 40, marginTop: 10, marginBottom: 10}}> 84 {infoElements} 85 </Box> 86 ); 87 88 return element; 89 }
ここは特に新しいテクニックは使っていません。スコアの値を表示すると同時に、勝ち負けを判定して、勝ち負けが決まった場合はその旨のメッセージ、そうでないときは、現在の手番を表示します。ゲームが終了した時の画面は、こんな感じになります。
ここまでの内容は、下記のリポジトリ(v1.0 ブランチ)で公開しています。
また、ビルドしたバイナリーを GitHub Pages で公開しているので、下記で実際に遊ぶこともできます。
この連載記事は、これで一旦終了です。お疲れさまでした!