アジの開きを閉じる。

競プロ(AtCoder)中心のブログ

【ゲーム】HTTPステータスコードで神経衰弱するゲームを開発しました

Reactで何か作りたいけどネタがない,,,
そういえばHTTPステータスコード忘れてきてるな,,,

せや!!!

\\爆誕//
HTTPステータスコード神経衰弱(プレイページ)
開発リポジトリ

プレイ中の画像
プレイ中の画像

目次

どんなゲーム?

HTTPステータスコードはWEB開発の基本中の基本です.
そんなHTTPステータスコードを楽しみながら覚えられるようなゲームを作りました.
以下の3つのモードで遊ぶことができます.

  • CPUと対戦: コンピュータと対戦できます.強さは「弱い」「普通」「強い」から選べます.
  • 2人で対戦: ローカルで2人で対戦できます.
  • レーニング: 1人でプレイできます.

工夫したところ

今回は実装練習重視だったのと,個人的にシンプルなUIが好きなので,UIはAnt Designをそのまま使っています.
画面は「モード選択」「ゲーム設定」「プレイ画面」の3つの画面を作成し,以下のことを意識しました.

  • 画面の共通化: どのモードでも画面構成はほぼ同じなので,選択したモードやゲーム設定をContextに持たせて画面の出し分けを行い,実装量を減らしました.
  • UIとロジックの分離: 実装していると,カードの出し分けを行うUIの処理と神経衰弱のゲームロジックの処理が混在した巨大なファイルが出来上がってしまいました.そこで,UI処理はpage.tsxのそのままに,ゲームロジックをカスタムフックに切り出して分離しました.かなり実装しやすくなりました.

開発で難しかったところ

2つあります.

その1: 再レンダリングのタイミング

実装では,カードの表・裏の状態はopenedというboolean型配列のstateで管理していました.
カードのonClick関数が走ると,openedをtrueに変更することでそのカードが裏返るCSSを当て,再レンダリングされた瞬間から500msかけて裏返る(ように見える)ようになっています.
1枚目のカード選択はそれで良いのですが,2枚目のカード選択後は,裏返ってから,ペアならばカードが除去されペアでなければ再び裏返るようにしないといけません.

Reactは基本的にstateを更新する処理が実行されると,すぐに再レンダリングしてしまいます.
そのためonClick関数の中でopenedをfalseに戻したり,カードが場にあるかを管理する別のstateを更新してしまうと,カードが表向きになるモーションが起こらなかったり,表向きになる前に除去されたりしてしまいます.

理想は「onClickが走る→カードの再レンダリング→裏返るモーション→カードの再レンダリング→再び裏返るモーションor除去」です.
つまり,onClickが走るタイミングと2回目の再レンダリングには時差があります.

そこで今回は,onClickの中でsetTimeoutを呼び出し,1500ms後に再度stateを更新して再レンダリングするようにして実現しました.

setState関数が実行するタイミングや,setState関数に値では無くコールバック関数を渡した場合の挙動などが勉強になりました.

CPUの連続した選択もsetTimeoutをネストした1ターン分の処理を,再帰で呼び出して実現しています.(setTimeoutのネストは正直好きではないですが,,,)

その2: CPUのアルゴリズム

CPUのカード選択のアルゴリズムは思っていたより大変でした.
過去に裏返った既知のカードの情報から強さに応じて選択の仕方を変える実装が難しかったです.

ペアを作るかどうかは生成した乱数が強さに応じた閾値を超えるかどうかで判定すれば良いです.*1
ただペアを作らなかった場合にわざと外す2枚を選ぶのか,未知のカードの中から選ぶのかは場の状況に依ります.(すべて既知ならわざと外さないといけないし,未知のカードがあるならまずはそこから1枚選ぶ)

結局は人間の行動を模倣しているのですが,場の状況と確率的判定と組み合わせた選択をコードに書き起こすのに苦労しました.
もしかしたら上手いまとめ方があるのかもしれません.
プレイ中,CPUのターンが1枚選んで終わってしまうことがあります.発生条件や原因はよく分かっていません.アルゴリズムのバグではないはずです.バグってたらごめん!

また,UIへの反映も工夫したところです.
先述の通りstateの更新と再レンダリングは密接な関係があるので,先にCPUの選択するカードをすべて決定してから,setTimeoutを呼び出す関数を再帰して1手ずつ描画しました.*2
競プロやってなかったら「そうだ,京都,行こう.」のノリで「そうだ,再帰,実装しよう.」なんて思わないぞ!!!?!

既知のカードの情報はUIには直接関係ないのでstateではなくrefで管理することも意識しました.

最後に

せっかくなのでGitHub ActionsでGitHub PagesのBuild&Deployも行ってみました.
deployブランチを変更するだけでBuild&Deployしてくれるのはかなり楽で嬉しいですね.

次は何作ろうかな~!!(ネタが無い)

*1:実装では閾値を超えると選択しない.

*2:今思えばrefは常にリアルタイムの値を取得できるので,「CPUのターンで先にすべての選択を決定してからUIに反映」じゃなくても良かったかも.