台パンができるリバーシをつくった

# 台パンリバーシ<sup> 3D</sup>

## はじめに
みなさんはじめまして。τomoと申します。
IT系の専門学校に通う1年生の19歳です。

今回は学校の進級制作作品として展示する用に台パンができるリバーシ[^1]を作成しました( アイデア自体は高校の時に考えたものですが ) 。

また，進級制作作品展？では自分のブースを自由に使えるとのことなので，リアルタイムで遊んでくれた人のスコアをランキングとして表示するページも作成しました ( セキュリティガバがあるかもしれないのでイベントが終わり次第データベースと切り離す予定です ) 。

JavaScriptに翻弄されながら1人でゲームを作った記録として記事に残そうと思います。

:::note alert
この記事は私が所属している学校とは関係ない非公式なものです。
学校に対する，本記事及び作品に関するお問い合わせ等はご遠慮ください。
:::

**[『台パンリバーシ<sup> 3D</sup>』はこちらから ( 不定期更新 )](https://reversi.syntck.com "台パンリバーシ3D")**
**[GitHub repo はこちらから](https://github.com/Tomo1151/Reversi-offline)**

## どういうゲーム？
3Dで遊べるリバーシ ( シングルプレイ ) です。
自分が不利になっていくにつれ，いらいらが溜まっていきます(画面右のゲージ)。

![スクリーンショット 2024-03-06 2.59.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570882/f0be51fd-7f2b-3e90-6d7a-3c5fb3ccda49.png)

プレイヤーの忍耐力の限界を越えると台パンを行うことができます。

![スクリーンショット 2024-02-28 13.43.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570882/65fc7d68-601b-fe0e-159f-fb25bef7d335.png)

カーソルまたは指で台パンしたいところを選択しクリックまたはタップで台パンを繰り出すことができます。
台パンを行うと範囲内の石が確率でひっくり返ります ( 敵の石の方がひっくり返りやすくなっています ) 。

台パンを駆使しながらリバーシで勝利を目指しましょう。

## 目標
今回作品制作にあたって，以下のことを目標として掲げました。

1. みんなで笑いながら遊べるようなクソゲーを作る [コンセプト]
2. WEB技術を用いる [進級作品の制約]
3. ブラウザ上で完結する
4. PCでもスマホでも遊べる
5. 使用ライブラリは最低限
6. (一時的に)リアルタイムでランキングを保存 / 更新できる
7. 台パンができる
8. 3Dなリバーシ
9. 強めの敵

:::note info

このゲームのアイデアを思いついたのは高校生の時でしたが，その時は「P2Pでオンライン対戦ができるといいな」と思っていました ( ゲームサーバを運用することは金銭的にも難しいため，Minecraft JEのマルチプレイのようなマルチプレイを目指していました ) 。

UPnPを用いてオンラインでオセロのデータをやりとりする形式で開発を進めていたのですが ( ネットワーク越しにオセロとして遊ぶことが出来るようなモノは出来ていました )，セキュリティの怪しさ故に配布できるものでは無いと判断しその計画は頓挫しました。

そのため，今回はコンピュータ対戦を主とする，”ブラウザ上で完結する”形式で作ることになりました。
いつかまた余裕がある時にオンライン対戦には挑戦したいと思っています。
:::

## 使った技術
WEB技術だけでゲームを作ります。ゲームエンジンはありません。
ゲームの進行から描画まで全てをJavaScript (ESM) で行っています。
### ゲーム本体
* HTML / CSS
* JavaScript
    * Three.js

### ランキング機能
* PHP
* MySQL

## 開発の流れ
まず一番最初にリバーシ自体のロジックを確認するために，2Dの\<table\>リバーシを作成しました。
サクッとリバーシ自体のロジックを作った上でThree.jsに乗せていく形です。
Three.js自体は以前から触っていてある程度は理解していたので，3Dのリバーシを完成させるのはそこまで時間がかかりませんでした。

3Dリバーシ自体が完成したタイミングで敵を作成し始めました。
色々な記事・ブログを漁り，とても苦労しながら実装しました。

面倒そうで後回しにしていた台パン機能は敵のロジックを作り終えてから作成しました。

そしてカメラ。
台パンモードへの移行，戻す動作でカメラをいい感じに動かしてやる必要がありました。いい感じに目的地まで動かして止めるような機能はデフォルトでついてない ( と思う... ) ため，自力でどうにかしました。とてもク⚪︎なコードであることは承知の上，動けば ( ダブルミーニング ) 良いの精神で乗り越えました。

カメラを動かすのはどうにかなりましたが，台パン時にカメラをどうするかの問題がありました。当初，台パンの瞬間に数秒間揺らすようなものを想定していたのですが，どうも手前で変なコードを書いてしまったため，実現が不可能そう ( やる気のなさと数学の弱さ故 ) だと気づきました。
そこで，瞬間的にカメラのlookAtをランダムでずらすことで”それっぽさ”は出たんじゃないかなと思っています。

ここまで来るとだいぶ”台パン” + ”3D” + ”リバーシ”が出来てきました。
あとは細かいところをブラッシュアップしていく作業です。

ゲーム本編がある程度できたタイミングはイベント１週間前です。

ここからイベントに向けてランキング機能を作りました。
PHPのセッションを用いて簡単なCSRF対策を行い，JS から fetch API でスコアを登録できるようにする必要があります。
自分のサイトでは PHP + MySQL で簡易CMSを作成していたのである程度は覚えていましたが，久しぶりの PHP はかなり苦痛でした。
ランキング自体はテーブル一つでどうにかなるので簡単に作りました。

`id (主キー), name (プレイヤー名，表示用), score (ゲームスコア), mode (ゲームモード), result (ゲームの勝敗), time (対局時間), registered_at (レコードの登録日時)`

を記録していくだけの簡単なお仕事です。
ここまで作ってイベント本番を迎えました。

## クラス構成
中途半端で怪しいOOPをしているので，以下のようなクラス構成になっています。

![スクリーンショット 2024-03-06 2.32.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570882/a4295929-fb1a-cb9f-b5fe-79195411fd53.png)

- CameraManager : Three.jsのカメラ (OrbitControls) を管理
- DOMManager : ゲームの進行に合わせてDOMの操作を行う
- Enemy.js (extends Player) : 敵に関する全部
- Event.js : ゲームイベントの定義
- GameManager (extends THREE.EventDispatcher) : ゲームの進行を行う
- Logger.js : ゲームのログを管理
- Object.js : オセロ盤や石の情報
- Player.js : 操作するプレイヤーに関する情報
- RendererManager : Three.jsのレンダラーを管理
- Search.js (WebWorker) : 敵の打つ手を探索するワーカースレッド
- Section : タイトル画面やリザルト画面，ゲーム画面の管理
- SectionManager : セクションの管理
- Utils : よく使う系の関数をまとめただけ

Javaを書く友人の影響を多大に受けながら育ったので，JSらしからぬ書き方をしているような気がしています。
GMについては本当は implements THREE.EventDispatcher みたいなことをしたかったのですが，インターフェースとかはJSにないっぽいのでこうなりました (こういうのって継承より実装の方が合ってると思うのですがわかりません) 。

## どうやって動いているのか
超イベントドリブンな感じで動いています。
GMがイベントをリッスンし指示を出す形です。

1. GM が turn_notice のイベントをディスパッチ
1. turn_notice をリッスンしている Player ( Enemy ) が任意の位置に石を置く ( put_notice )
1. GM が put_notice のタイミングで石を置かれた位置を検証，成功 or 失敗のレンスポンスを返す
1. 各々がそのレスポンスを確認し，confirm する

基本的な動きはこれだけです。
今回は3Dなリバーシ故アニメーション等を待つ必要があるため，そういったことのタイミングなどもイベントで管理しています。

石を置くアニメーション等はThree.jsで行うと面倒になりそうだったため，3Dモデルにデータとして持たせました。

盤のマスはBoxGeometryで作成しています。これにマスの番号をプロパティで持たせRaycasterで選択されたマスを判別しています。
クリック時に選択されていたマスを置くマスとしてイベントを生成したりしています。

試合終了時にスコアを計算し表示しています。
どうしてもクライアントサイドにゲーム進行をすべて投げているため，不正データのランキング登録は弾けなさそうでした。そこで最低限，サーバサイドでも受け取った結果からスコア計算をし検証するようにしました。
また，最低限のセキュリティ対策としてPHPのセッションを用いたCSRF対策をしました。正規のページのコンソールから偽データをPOSTされると怪しいですが，適当に作ったフォームからは送信できないようになっているはずです。

## 敵について
このゲームの仕様上，プレイヤーには一度イライラしてもらう必要があります。
そのため，やわな敵ではゲームが成り立ちません。
そのため，現時点では単純な NegaMax アルゴリズムで5手先まで読み，リスクが一番少ない場所へ置く戦法をとっています。
また評価マップは盤面の状況に応じて少し変化するようにしました。
筆者はリバーシがとても弱いのでほとんど勝てません。

6手先以上を読もうとすると少しターンが長引いてしまい，画面が固まることになってしまいました ( 描画と探索を同じスレッドで行っていたため ) 。
そこで WebWorker を用いてワーカースレッドにて探索処理を行うように変えました。

また，もともと NegaAlpha アルゴリズムによって探索を効率化していたのですが，なぜか目に見えるレベルで敵が弱くなってしまったため，NegaMax に戻したという経緯があります ( 時間がなかったのと，デバッグが非常に大変だったため ) 。

## 大変だったこと
- 超イベントドリブンだったこと。
→ 基本的にあらゆる処理がイベント発火に合わせて動く仕組みにならざるを得ないため，処理の順番があやふや。
- 探索アルゴ
→ そもそも筆者は数学が弱い，デバッグがとても大変で非効率的
- スマホ対応
→ イベント本番はみんなスマホでまわるため，スマホで遊べるようにする必要があった。デバッグが地獄
- デバッグ
- デバッグ
- デバッグ

## 感想 ・ まとめ
そこそこ時間を掛けつつ一つのゲームをある程度形にできてよかった。また，ランキング機能のおかげでたくさんの技術を使うことができた。コンセプト上，クライアントサイドで全てを行うため制約が多く大変であったが，PCでもスマホでもその場でサイトに飛んですぐ遊んでもらえる形にできたのは良かったと思う。
リバーシのひっくり返す部分や，敵の最適な置き場所を探索する部分など，アルゴリズムも勉強になった。
また，PHPのセッション管理やJSのWebWorker等，あまり触れたことのなかった部分も触ることができたのも良かった。
絶対にテストが足りていないため怪しい部分は潜在的にたくさん存在していそうなのが少し心残りではある。
台パンできるリバーシというコンセプト自体にそこそこの自信があり少なくとも自分は面白いと思っているため，今後気が向いたらオンライン対戦とかにも手を出したいと思った。

けど最近はたくさんThree.jsを使ったのでもうお腹いっぱい。そろそろいいかな。
TypeScript と React をやらねば。

[^1]: いわゆるオセロのことです。オセロ・Othelloは登録商標らしい？権利関係がごちゃごちゃしているようなのでリバーシという名称を用いています。