書籍
同人誌
私の執筆部分の PDF を公開しています。
私の執筆部分の PDF を公開しています。(同人誌に掲載したものから少しだけ加筆修正しています。)
私の執筆部分の PDF を公開しています。
私の執筆部分の PDF を公開しています。
私の執筆部分の PDF を公開しています。(同人誌に掲載したものから少しだけ加筆修正しています。)
私の執筆部分の PDF を公開しています。
まず、こちらの記事に従って、React の開発環境を準備します。
作業用ディレクトリーにゲームエンジンをダウンロードして、必要なパッケージをインストールします。
% cd ~/Documents % git clone https://github.com/enakai00/react_textgame % cd react_textgame % yarn install
冒頭の「あるけあるけゲーム」がデフォルトで入っており、下記のコマンドで実行できます。
% yarn start ---- Output ---- Compiled successfully! You can now view react_textgame in the browser. Local: http://localhost:3000 On Your Network: http://192.168.1.56:3000 Note that the development build is not optimized. To create a production build, use npm run build. webpack compiled successfully
ゲーム本体はファイル「src/Main.js」に記載されており、このファイルを書き換えるだけでオリジナルのゲームが作成できます。
110行目に下記のような記述があります。ここには、ゲームで使用するキーを並べて指定します。これらのキーの On/Off を検知するハンドラーが用意されて、変数 KeyPress からキーの状態が読み出せるようになります。
109 // Define keys used in the game. 110 const keys = ["s", "i", "j", "l", "m"];
たとえば、"s" キーを押していれば、KeyPress["s"] === ture、押していなければ、KeyPress["s"] === false となります。
ゲーム本体は、7行目から始まる関数「game」の中に書きます。
6 // Your code here! 7 const game = async (screen, refresh, keyPress, exit) => { ... 105 }
大雑把な構造は次の通りです。
はじめに、ゲーム全体で使用するグローバル変数を定義します。
8 // Global game variables. 9 const bike = {x: 0, y: 0, direction: 0, score: 0} 10 const bikeChar = ["┻", "┣", "┳", "┫"] 11 const dirVector = [[0, -1], [1, 0], [0, 1], [-1, 0]] // [dx, dy]
その後は、ゲーム内で使用するサブルーチンを用意します。この例では、次のような関数を定義しています。
これらはすべて、async キーワードをつけて非同期関数として定義します。関数内では、次の同期処理が利用できます。
13 const initGame = async () => { 14 // Initialize bike status. 15 bike.x = 20; ... 47 } 48 49 const gameover = async () => { 50 print(screen, 15, 10, " GAME OVER ", "black", "white"); 51 await refresh(); 52 await sleep(5000); 53 } 54 55 const moveBike = async () => { 56 if (keyPress["i"]) { 57 bike.direction = 0; 58 } ... 80 } 81 82 const putBlock = async () => { 83 const x = randInt(1, 39); 84 const y = randInt(2, 22); 85 if (randInt(0, 2) === 0) { 86 print(screen, x, y, " ", "yellow", "yellow"); 87 } 88 }
最後にこれらのサブルーチンを順に呼び出すループをメインループとして用意します。
91 // main loop 92 var finished; 93 while (true) { 94 finished = false; 95 await initGame(); 96 while (!finished) { 97 if (exit.current) return; 98 await moveBike(); 99 await putBlock(); 100 await refresh(); 101 await sleep(100); 102 } 103 await gameover(); 104 } 105 }
変数 finished でゲームオーバーかどうかの判定を行っており、サブルーチン内部で「finshed = true」に設定するとゲームオーバーになります。個々のサブルーチンは、await を付けて、同期的に順番に実行していきましょう。(昔のパソコンのプログラムは、すべて同期実行でしたから。)
また、画面の左下に「Reset」ボタンの機能が用意されていますが、これを押すと「exit.current=true」がセットされるので、これを検知して関数「game」を終了(return)させる必要があります。97行目はそのための処理になります。同様に、ゲーム開始時に「S」キーが押されるのを待つループの中でも、これを検知して return するようにしてあります。
34 while (true) { 35 if (exit.current) return; 36 if (keyPress["s"]) { 37 break; 38 } 39 await sleep(100); 40 }
その他の部分は、ボイラープレートなのでそのままにしておいてください。
その他には、画面描画に関連する、次の補助変数、補助関数が利用できます。
screen[y][x] には、座標 (x, y) に表示する「キャラクター、文字色、背景色」の情報が下記の連想配列として保存されています。(画面サイズはお約束の 40 × 24 です。)
screen[y][x] = { char: "", color: "white", bgColor: "black" };
これを書き換えて、await refresh() を実行すると画面が書き換えられます。逆に、screen[y][x] を読み出せば現在表示されている「キャラクター、文字色、背景色」がわかります。(パソコンで言うところの VRAM みたいなものです。)
文字列を表示する際は、screen を個別に書き換えるのは面倒です。print で指定位置から文字列を書き込むことができます。
print(screen, x, y, str, "white", "black");
第1引数は、書き換える配列として screen を指定します。座標 (x, y) から文字列 str を書き込みます。その後ろは文字色と背景色で、省略時は、"white", "black" になります。半角文字は、自動で全角文字に変換されます。
ちなみに、背景色を付けて " "(スペース)を表示すると、冒頭のゲームで使っているような塗りつぶしたブロックが表現できます。
配列 screen を渡すと初期化して画面をクリアします。
clearScreen(screen);
初期状態では、screen[y][x].char === ""(スペースではなく空文字列)になっていますので、" "(スペース)を表示した場所は、初期状態とは別物になります。
画面表示とは関係ありませんが、ゲームでは必須の整数の乱数が得られます。
randInt(min, max);
min 以上、max 未満の整数値が得られます。
こちらの続きです。今回は、プレイ中のスコアの情報を追加して、手番とスコアの情報を表示する 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 コンポーネントが計算するスコアが更新されます。
ファイル「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 で公開しているので、下記で実際に遊ぶこともできます。
この連載記事は、これで一旦終了です。お疲れさまでした!