めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

数学関連の書き物

同人誌

私の執筆部分の PDF を公開しています。

Riemann幾何学ユーザーのための情報幾何学入門

私の執筆部分の PDF を公開しています。(同人誌に掲載したものから少しだけ加筆修正しています。)

ガロア理論入門

私の執筆部分の PDF を公開しています。

量子光学理論入門

パソコン風のテキストゲームを簡単に作れる React のゲームエンジン

何の話かと言うと

昔懐かしいパソコン風のテキストゲームを簡単に作れるゲームエンジンを React で作りました。こんな感じのブラウザ上で遊べるゲームが作成できます。

ここから遊べます。)

利用手順

まず、こちらの記事に従って、React の開発環境を準備します。

enakai00.hatenablog.com

作業用ディレクトリーにゲームエンジンをダウンロードして、必要なパッケージをインストールします。

% 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」に記載されており、このファイルを書き換えるだけでオリジナルのゲームが作成できます。

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]

その後は、ゲーム内で使用するサブルーチンを用意します。この例では、次のような関数を定義しています。

  • ゲームの初期化(initGame)
  • ゲームオーバー画面(gameover)
  • 自機の移動(moveBike)
  • 障害物の表示(putBlock)

これらはすべて、async キーワードをつけて非同期関数として定義します。関数内では、次の同期処理が利用できます。

  • await refresh() :画面を再描画して、描画が終わるまで待つ。
  • await sleep() :指定時間(ミリ秒単位)何もせずに待つ。
    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      }


その他の部分は、ボイラープレートなのでそのままにしておいてください。

その他の補助変数・補助関数

その他には、画面描画に関連する、次の補助変数、補助関数が利用できます。

  • 2次元配列 screen

screen[y][x] には、座標 (x, y) に表示する「キャラクター、文字色、背景色」の情報が下記の連想配列として保存されています。(画面サイズはお約束の 40 × 24 です。)

screen[y][x] = {
  char: "", color: "white", bgColor: "black"
};

これを書き換えて、await refresh() を実行すると画面が書き換えられます。逆に、screen[y][x] を読み出せば現在表示されている「キャラクター、文字色、背景色」がわかります。(パソコンで言うところの VRAM みたいなものです。)

  • 関数 print

文字列を表示する際は、screen を個別に書き換えるのは面倒です。print で指定位置から文字列を書き込むことができます。

print(screen, x, y, str, "white", "black");

第1引数は、書き換える配列として screen を指定します。座標 (x, y) から文字列 str を書き込みます。その後ろは文字色と背景色で、省略時は、"white", "black" になります。半角文字は、自動で全角文字に変換されます。

ちなみに、背景色を付けて " "(スペース)を表示すると、冒頭のゲームで使っているような塗りつぶしたブロックが表現できます。

  • 関数 clearScreen

配列 screen を渡すと初期化して画面をクリアします。

clearScreen(screen);

初期状態では、screen[y][x].char === ""(スペースではなく空文字列)になっていますので、" "(スペース)を表示した場所は、初期状態とは別物になります。

  • 関数 randInt

画面表示とは関係ありませんが、ゲームでは必須の整数の乱数が得られます。

randInt(min, max);

min 以上、max 未満の整数値が得られます。

ゲームの公開方法

開発したゲームは、GitHub Pages で公開することができます。下記の手順を参考にしてください。

enakai00.hatenablog.com

バックエンドエンジニアのための(かどうかは本当はよく分からないけど、とにかく書いてみる)React 入門(パート8)

何の話かと言うと

enakai00.hatenablog.com

こちらの続きです。今回は、プレイ中のスコアの情報を追加して、手番とスコアの情報を表示する 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.com

また、ビルドしたバイナリーを GitHub Pages で公開しているので、下記で実際に遊ぶこともできます。

enakai00.github.io

この連載記事は、これで一旦終了です。お疲れさまでした!