webRTCでカードゲームを実装したい

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/0326e721-d5b4-f213-b15f-c41621accba9.png" width="50%"/>
初めまして、iteyanと申します。本記事では、表題の通りwebRTCを使ったカードゲームの実装方法を考えていきたいと思います。
タグに[skyway](https://webrtc.ecl.ntt.com/)が入っているのは、私が実装している環境で使用しているだけです。簡単に使えてお世話になっていますので、皆さんもぜひどうぞ。
前提として、ある程度webRTCの知識があることが望ましいです。本記事を読むのに技術的な知識は不要ですが、P2P通信を知っている上で話をさせていただきます。

この記事で書くこと

+ カードゲームに必要なモデルの定義
+ 上記を実現するための手法

この記事で書かない事

+ 具体的なコード

# カードゲームに必要なものを考える
カードゲームとひとくくりに言っても、プレイ人数やルールによって必要とされるものは様々です。具体的にイメージしたいという人はUNOを頭に浮かべて考えてみましょう。
ゲーム中に使われるカードは準備されているもので固定されていて、自分でカードを購入してデッキを構築するタイプのゲームではありません。

## 自分のカードは自分だけに見える
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/1c174e0f-211c-655a-5de2-6ed36a2f62b1.png" width="50%"/>
特に説明はいらないと思います。多くのカードゲームの基本的なルールですね。以降、**カードの秘匿**と呼称します。

## 山札があり、誰もその中身を知ることができない
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/81baa917-e712-989e-8c99-5a9008334cf4.png" width="50%"/>

これも深い説明はいらないと思います。七並べなど、山札がないゲームもあると思います。以降、**山札**と呼称します。

## 共通の場があり、プレイヤー全員で場を共有する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/a3403ca2-1cdf-cd5a-e094-c0d1b561b1eb.png" width="50%"/>
カードを出したり、ゲームによっては点数を表示したり、サイコロの結果を表示する場です。この場は全員で同じものが見えていないといけません。以降、**共通の場**と呼称します。

***
大きく三つ取り上げました。以上の三点を実現できれば、基本的なカードゲームは実装可能なはずです。それでは、考えていきましょう。

# webRTCで実現したい
これまでの条件を踏まえると、多くの人が一番最初に思いつくのはサーバーを使って各プレイヤーと通信を行い、場と山札とプレイヤーの状況を監視し続けるという古き良き手法でしょう。メリットデメリットはあると思いますが、あえてP2Pを使いたい。サーバーを極力使いたくないというのが、私のモチベーションでした。

## （不採用）プレイヤーに山を作らせて配る
このパターンでは、GMまたは代表するプレイヤーを決めて、そのプレイヤーが山とプレイヤーのカードを決めます。私が最初に考えたのもコレです。
### 実現できること

+ 共通の場

### 実現できないこと

+ カードの秘匿
+ 山札

webRTCでは全員に通信が発信されますので、通信の内容を見ると誰に何のカードが配られたか、山札にどんなカードが残っているのかがわかってしまいますね。難読化などである程度誤魔化すことはできるでしょうが、堅牢とは言い難いです。

## （不採用）サーバーで山と配られるカードを作成する
このパターンでは、サーバーで作られた山と自分の手札を最初にダウンロードし、プレイします。
### 実現できること
+ 共通の場

### 実現できないこと
+ カードの秘匿（山札が共有されているので、初期の手札以外は推測できる）
+ 山札

最初の案とあんまり変わらないですね。

## （採用）サーバーで暗号化された山をプレイヤー分だけ作り、プレイヤーには他のプレイヤーの山を持たせる
採用となった案です。何を言っているのかわからないと思いますので、順に書いてみます。

### サーバー側の流れ

#### 山を作る
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/6127af14-49bd-0c3e-64a8-067f70e4b075.jpeg" width="80%"/>
具体的にはカードを配列に入れて、シャッフルをおこないます。
#### プレイヤーの数だけ山を複製する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/80de1f16-87ab-c649-a169-fe92a13c0a88.jpeg" width="80%"/>
同一の山がいくつか作成されます。
#### 山の数だけ復号可能な暗号鍵を作成し、山を暗号化する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/44efa42f-211e-9444-f21a-5be2f752436d.jpeg" width="80%"/>
各山を異なる暗号鍵で暗号化します。
#### プレイヤーに渡すセットを作る
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/243f9394-1037-d440-9362-fb5641fc8282.jpeg" width="80%"/>
ここで渡されるのは、自分用の復号鍵と他プレイヤーの山です。以降、**セット**と呼称します。自分の山を他のプレイヤーに持たせるのがコツです。

### プレイヤー間の流れ
#### セットのダウンロード
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/55f2c519-9954-b2db-e609-d24512485c43.jpeg" width="80%"/>
サーバー側の処理が終わったところで、プレイヤーは自分のセットをダウンロードします。全員がセットをダウンロードしたら、ゲームが開始されます。
#### 相手の手札の作成
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/56d21d16-2930-6903-cb77-43dbe0f159b4.jpeg" width="80%"/>
各プレイヤーは、セットから自分以外のプレイヤーの手札を取り出し、該当するプレイヤーに渡してあげます。GM的な役割を持たせてあげてもいいですし、プレイヤー間の順序などを決めてもスムースでしょう。
渡すカードは暗号化されていて、自分の持っている復号鍵とは合致しないので、どんなカードを渡したのかはわかりません。この通信は共有されますが、送信先以外のプレイヤーは鍵が一致しないのでどんなカードがやりとりされているかはわかりません。
#### 手札の受領と復号
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/9a6e07e5-271d-448f-af5b-cf8f49878e08.jpeg" width="80%"/>
手札を受け取ったプレイヤーは、自分の復号鍵を使って暗号化されたカードを復号します。この時、初めてどんなカードを受け取ったかが判明します。
#### 共通の場にカードを出す
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/981928/5c6b5ea7-b645-1d4e-20a7-bbc78bf8c48e.jpeg" width="80%"/>

共通の場に出すカードは、復号化されたものを使用します。全プレイヤーは、各クライアントで場に出されたカードに対しての処理をおこないます。
#### 以降ループ
セットから相手にカードを渡す→復号→カードが判明→カードを共通の場に出す
このループでカードゲームが成立するようになります！

### 実現できること

+ 共通の場
+ カードの秘匿
+ 山札

### イカサマの判定
この手法では、カードを受け取ったときの処理が各クライアントに任されています。当然懸念すべきこととして、そこには不正が介入する余地がありますが、この手法においてはゲーム終了後に自身の暗号鍵を公開することで、全員がどんなカードを受け渡した。処理を正しく行ったのかが再現可能です。つまり、[完全情報ゲーム](https://ja.wikipedia.org/wiki/%E5%AE%8C%E5%85%A8%E6%83%85%E5%A0%B1%E3%82%B2%E3%83%BC%E3%83%A0)の後ろ向き帰納法が使用可能になります。

# 終わりに
本記事は以上となります。結局サーバーを使ってるじゃないというご意見もあるでしょうが、最初にセットを作ってあげた後は結果報告などを記録すればいいだけなので、サーバーの負荷はグッと減るのではないでしょうか。
ゲーム中にサイコロを振りたいという場合は、サーバー側で何百回かサイコロを振ってあげて山札と一緒に入れてもいいでしょう。
誰かの参考やインスピレーションになれば、幸いです。