🍞

TypeScript で実装した Robots を React でブラウザゲームにしてみた (やっとGUI ver.)

2020/12/13に公開

React 完全に理解した (大嘘)

はじめに

  • 個人的な React 学習の題材として、過去記事 [1] [2] のCUI/ターミナル上で動くゲーム (Robots) をブラウザゲーム化
  • react-hooks の学習のため関数コンポーネントで作成
  • ブラウザゲームは GitHub Pages にて公開

Before (CUI/ターミナル)

ターミナル上でキーボード操作

Screen Shot 2020-12-12 at 12.55.10.png

After (GUI/ブラウザ)

ブラウザ上でマウス操作するゲーム

Screen Shot 2020-12-12 at 19.20.36.png

コンポーネント

コンポーネント構成

React-robots-ts-Diagram.png

  • Game : ゲーム全体(トップ階層)で全体をまとめるコンポーネント
  • Board : 盤面のコンポーネント
  • Square : 盤面を構成する一マスのコンポーネント
  • Control : プレイヤーロボットを操作するコンポーネント
  • Info : レベル、スコア、ステータスのゲーム情報を表示するコンポーネント

Game

components/Robots.tsここを参照
※過去記事の CUI バージョンのロジックを修正して使用

Game.tsx
import React, { useState } from "react";
import Control from "components/Control";
import Board from "components/Board";
import Info from "components/Info";
import {
  width,
  height,
  RobotInfo,
  RobotMove,
  init_robots,
  move_robots,
  is_wipeout,
  check_gameover,
  count_total_dead_enemy,
  calc_bonus,
} from "components/Robots";

export type GameProps = {
  robotList: RobotInfo[];
};

const Game = (props: GameProps) => {
  const [level, setLevel] = useState<number>(1);
  const [score, setScore] = useState<number>(0);
  const [robotList, setRobotList] = useState<RobotInfo[]>(
    init_robots(props.robotList, level)
  );
  //playerが動いたときに描画させるためのupdate/setUpdate
  const [update, setUpdata] = useState<boolean>(false);

  // status
  let status = "Game is ongoing";
  if (check_gameover(robotList)) {
    status = "Game Over...";
  }

  //Game Clear
  let bonus = calc_bonus(level);
  if (is_wipeout(robotList)) {
    // initialize board for next level
    setLevel(level + 1);
    setScore(count_total_dead_enemy(robotList, level + 1) * 10 + bonus);
    setRobotList(init_robots(robotList, level + 1));
    bonus = calc_bonus(level + 1);
  }

  const submit = (move: RobotMove) => {
    // Game Overのときは入力を受け付けない
    if (status === "Game Over...") return;

    // ロボットの動きで他のコンポーネントを強制的に描画させる
    // robotList,setRobotListの持たせ方に問題?
    setUpdata(update ? false : true);
    setRobotList(move_robots(robotList, move));
    setScore(count_total_dead_enemy(robotList, level) * 10 + bonus);
  };

  return (
    <div className="game">
      <div className="game-board">
        <Board robotList={robotList} width={width} height={height} />
      </div>
      <div className="game-info">
        <Control onClick={submit} />
        <Info level={level} score={score} status={status} />
      </div>
    </div>
  );
};

export default Game;

Board

interfaces.ts
export type ISquare = "@" | "+" | "*" | null;
Board.tsx
import React from "react";
import Square from "components/Square";
import { ISquare } from "components/interfaces";
import { RobotInfo, RobotType } from "components/Robots";

interface BoardProps {
  width: number;
  height: number;
  robotList: RobotInfo[];
}

function put_robot(robotList: RobotInfo[], x: number, y: number): ISquare {
  // forEach/find の中でreturnは使ってはいけない?
  // https://www.hanachiru-blog.com/entry/2019/10/31/154305

  let val: ISquare = null;
  robotList.find((element) => {
    if (element.x === x && element.y === y) {
      switch (element.type) {
        case RobotType.Player:
          val = "@";
          break;
        case RobotType.Enemy:
          val = "+";
          break;
        case RobotType.Scrap:
          val = "*";
          break;
        default:
          val = null;
      }
    }
  });

  return val;
}

const Board = (props: BoardProps) => {
  return (
    <div>
      {[...Array(props.height)].map((_, i) => {
        return (
          <div className="board-row" key={i}>
            {[...Array(props.width)].map((_, j) => {
              return (
                <Square value={put_robot(props.robotList, j, i)} key={j} />
              );
            })}
          </div>
        );
      })}
    </div>
  );
};

export default Board;

Square

Square.tsx
import React from "react";
import { ISquare } from "components/interfaces";

interface SquareProps {
  value: ISquare;
}
const Square = (props: SquareProps) => {
  return <button className="square">{props.value}</button>;
};

export default Square;

Control

Control.tsx
import React from "react";
import "../index.css";
import { RobotMove } from "components/Robots";

interface ControlProps {
  onClick: Function;
}

const Control = (props: ControlProps) => {
  return (
    <React.Fragment>
      <label>
        <h4>Move:</h4>
      </label>
      <div>
        <button
          type="submit"
          onClick={(e) => props.onClick(RobotMove.UpperLeft)}
        >
          y
        </button>
        <button type="submit" onClick={(e) => props.onClick(RobotMove.Up)}>
          k
        </button>
        <button
          type="submit"
          onClick={(e) => props.onClick(RobotMove.UpperRight)}
        >
          u
        </button>
      </div>
      <div>
        <button type="submit" onClick={(e) => props.onClick(RobotMove.Left)}>
          h
        </button>
        <button type="submit" onClick={(e) => props.onClick(RobotMove.Wait)}>
          w
        </button>
        <button type="submit" onClick={(e) => props.onClick(RobotMove.Right)}>
          l
        </button>
      </div>
      <div>
        <button
          type="submit"
          onClick={(e) => props.onClick(RobotMove.LowerLeft)}
        >
          b
        </button>
        <button type="submit" onClick={(e) => props.onClick(RobotMove.Down)}>
          j
        </button>
        <button
          type="submit"
          onClick={(e) => props.onClick(RobotMove.LowerRight)}
        >
          n
        </button>
      </div>
      <div>
        <button
          type="submit"
          onClick={(e) => props.onClick(RobotMove.Teleport)}
        >
          t
        </button>
      </div>
    </React.Fragment>
  );
};

export default Control;

Info

Info.tsx
import React, { Fragment } from "react";

type InfoProps = {
  level: number;
  score: number;
  status: string;
};

const Info = (props: InfoProps) => {
  return (
    <React.Fragment>
      <label>
        <h4>Legend:</h4>
      </label>
      <div>
        <label>+ : robot</label>
      </div>
      <div>
        <label>* : junk heap</label>
      </div>
      <div>
        <label>@ : you</label>
      </div>
      <label>
        <h4>Level & Score:</h4>
      </label>
      <div>
        <label>level : {props.level}</label>
      </div>
      <div>
        <label>score : {props.score}</label>
      </div>
      <label>
        <h4>Status:</h4>
      </label>
      <div>
        <label>{props.status}</label>
      </div>
    </React.Fragment>
  );
};

export default Info;

ソース

おわりに

  • Reactの基本の基はわかった(ということにしておく・・・)
  • しかし、まだ useState を上手く使えていない気もする (というよりも設計が上手くできてない?)
  • 実は three.js を使って派手に仕立てる野望があったが断念[3]
  • 誰かまともな CSS をあててほしい・・・上記の GitHub で PR 受付中 (他力本願)
脚注
  1. 昔懐かしの Robots を TypeScript で実装する (CUI ver.) ↩︎

  2. TypeScript で実装した Robots をリファクタリングしてみた (まだCUI ver.) ↩︎

  3. 参考例:three.js x React x redux で3Dオセロゲームを作った ↩︎

Discussion