Board Game on PostgreSQL!

この記事は[PostgreSQL Advent Calendar 2018](https://qiita.com/advent-calendar/2018/postgresql)の16日目の記事です。

# はじめに
誰かさんの影響で、私も PostgreSQL でクダラナイことをやってみたくなりました。

# 冬の風物詩
冬といえば、やっぱり **軍人将棋** ですよね！

>軍人将棋（ぐんじんしょうぎ）とは、通常2人で行うボードゲームの一種。軍事将棋（ぐんじしょうぎ）、または行軍将棋（こうぐんしょうぎ）とも呼ばれる。軍隊の階級や兵種を元にした駒を用いて盤上にて競う。欧米圏ではストラテゴ（Stratego）という名称の同種のボードゲームがプレイされている。
>[Wikipediaより](https://ja.wikipedia.org/wiki/%E8%BB%8D%E4%BA%BA%E5%B0%86%E6%A3%8B)

小さい頃、父を審判役に姉と競い合ったのを思い出します。
ということで、PostgreSQLを使って *軍人将棋* を実現してみます。
(審判役もPostgreSQLにやらせれば不要になるので一石二鳥だし！）

# ルール
軍人将棋を知らない方のためにサクッとルールをまとめると、、、
- 16種類23個の駒を盤面に置く（相手に見えないように裏返して）
- 16種類の駒は各々動き方が決まってるので、それにしたがって動かす
- 相手の駒の位置に動かしたらバトル開始！
- 審判は公平に判断する(16種類の駒の勝ち負けは下図のように決まってる)
- 相手の動ける駒を全て負かすor偉い駒で総司令部を占拠したらゲーム終了
って感じです。

```sql:勝敗表（０は勝ち、１は引き分け、２は負け。9は条件次第)
  koma  | taisho | cyusho | shosho | plane | tank | taisa | cyusa | shosa | taii | cyui | shoi | kihei | kohei | spy | mine | flag 
--------+--------+--------+--------+-------+------+-------+-------+-------+------+------+------+-------+-------+-----+------+------
 大将   |      1 |      0 |      0 |     0 |    0 |     0 |     0 |     0 |    0 |    0 |    0 |     0 |     0 |   2 |    2 |    9
 中将   |      2 |      1 |      0 |     0 |    0 |     0 |     0 |     0 |    0 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 少将   |      2 |      2 |      1 |     0 |    0 |     0 |     0 |     0 |    0 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 飛行機 |      2 |      2 |      2 |     1 |    0 |     0 |     0 |     0 |    0 |    0 |    0 |     0 |     0 |   0 |    0 |    9
 タンク |      2 |      2 |      2 |     2 |    1 |     0 |     0 |     0 |    0 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 大佐   |      2 |      2 |      2 |     2 |    2 |     1 |     0 |     0 |    0 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 中佐   |      2 |      2 |      2 |     2 |    2 |     2 |     1 |     0 |    0 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 少佐   |      2 |      2 |      2 |     2 |    2 |     2 |     2 |     1 |    0 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 大尉   |      2 |      2 |      2 |     2 |    2 |     2 |     2 |     2 |    1 |    0 |    0 |     0 |     0 |   0 |    2 |    9
 中尉   |      2 |      2 |      2 |     2 |    2 |     2 |     2 |     2 |    2 |    1 |    0 |     0 |     0 |   0 |    2 |    9
 少尉   |      2 |      2 |      2 |     2 |    2 |     2 |     2 |     2 |    2 |    2 |    1 |     0 |     0 |   0 |    2 |    9
 騎兵   |      2 |      2 |      2 |     2 |    2 |     2 |     2 |     2 |    2 |    2 |    2 |     1 |     0 |   0 |    2 |    9
 工兵   |      2 |      2 |      2 |     2 |    2 |     2 |     2 |     2 |    2 |    2 |    2 |     2 |     1 |   0 |    0 |    9
 スパイ |      0 |      2 |      2 |     2 |    2 |     2 |     2 |     2 |    2 |    2 |    2 |     2 |     2 |   1 |    2 |    9
 地雷   |      0 |      0 |      0 |     2 |    0 |     0 |     0 |     0 |    0 |    0 |    0 |     0 |     2 |   0 |    1 |    9
 軍旗   |      9 |      9 |      9 |     9 |    9 |     9 |     9 |     9 |    9 |    9 |    9 |     9 |     9 |   9 |    9 |    9
```

軍旗は後ろの駒と同じ強さになるので、ちょっと面倒くさい。

# 実現方法
相手の駒が見えてしまうと、ゲームが成立しないのでユーザ分けたり、権限を整理したりする必要があります。
![board.png](https://qiita-image-store.s3.amazonaws.com/0/151699/84bfb7e0-eb26-de7b-d42c-a50f7cac0c78.png)

ちなみに、パーティションで盤面テーブルを作成してますが、PostgreSQL10だとパーティションキー(player)をまたぐ更新ができません。PostgreSQL11からはOKなので、持っててよかったPostgreSQL11です。

# 実装例
細々とルールを盛り込んでいったら、コードもそこそこの量になってしまったので、ポイントのみ解説します。

```sql:judge.sql
CREATE OR REPLACE FUNCTION update_internal(target_player_id int, target_koma_id int, target_pos_x int, target_pos_y int) RETURNS VOID AS $$
BEGIN
	DELETE FROM all_koma WHERE pos_x = target_pos_x AND pos_y = target_pos_y;
	INSERT INTO all_koma VALUES (target_player_id, target_koma_id, target_pos_x, target_pos_y);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE OR REPLACE FUNCTION judge() RETURNS trigger AS $$
DECLARE
	col_name	TEXT;
	row_num		INT;
	judge_num	INT;
	back_pos_y	INT;
	back_koma_id	INT;
BEGIN
	-- NEW:チャレンジャー、OLD:元の駒
	SELECT koma_col INTO col_name FROM vs_rule WHERE koma_id = NEW.koma_id;
	row_num  = OLD.koma_id;
	EXECUTE 'SELECT ' || col_name || ' FROM vs_rule WHERE koma_id = ' || row_num INTO judge_num;
	IF judge_num = 0 THEN
		-- 元の勝ち:何もしない
		return NULL;
	ELSIF judge_num = 1 THEN
		-- 相打ち:空にする
		PERFORM update_internal(0, 98, OLD.pos_x, OLD.pos_y);
	ELSIF judge_num = 2 THEN
		-- チャレンジャーの勝ち:NEWで上書き
		PERFORM update_internal(NEW.player_id, NEW.koma_id, NEW.pos_x, NEW.pos_y);
	ELSIF judge_num = 9 THEN
		-- 軍旗駒に対する条件チェック
		IF OLD.player_id = 1 THEN
			back_pos_y = OLD.pos_y - 1;
		ELSIF OLD.player_id = 2 THEN
			back_pos_y = OLD.pos_y + 1;
		END IF;
		IF back_pos_y < 0 OR back_pos_y > 8 THEN
			PERFORM update_internal(NEW.player_id, NEW.koma_id, NEW.pos_x, NEW.pos_y);
		ELSE
			SELECT koma_id INTO back_koma_id FROM all_koma WHERE pos_x = OLD.pos_x AND pos_y = back_pos_y;
			row_num  = back_koma_id;
			EXECUTE 'SELECT ' || col_name || ' FROM vs_rule WHERE koma_id = ' || row_num INTO judge_num;
			IF judge_num = 0 THEN
				return NULL;
			ELSIF judge_num = 1 THEN
				PERFORM update_internal(0, 98, OLD.pos_x, OLD.pos_y);
			ELSIF judge_num = 2 THEN
				PERFORM update_internal(NEW.player_id, NEW.koma_id, NEW.pos_x, NEW.pos_y);
			END IF;
		END IF;
	END IF;
	return NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER koma_judge_p0 BEFORE UPDATE ON p0_koma
FOR EACH ROW EXECUTE PROCEDURE judge();
CREATE TRIGGER koma_judge_p1 BEFORE UPDATE ON p1_koma
FOR EACH ROW EXECUTE PROCEDURE judge();
CREATE TRIGGER koma_judge_p2 BEFORE UPDATE ON p2_koma
FOR EACH ROW EXECUTE PROCEDURE judge();
```

update_internal関数で盤上の駒を更新します。UPDATEだと無限ループしちゃいそうなので、実際はDELETE＋INSERTで更新してます。
また、この関数は各プレイヤーが直接呼び出すこともある(移動後のマスを空にする。ちなみに空は駒ID=98です)ので、「SECURITY DEFINER」をつけてスーパーユーザ権限で実行されるようになってます。

judgeトリガ関数で勝敗表を参照してどっちの駒を残すか決定してます。この関数を各パーティションテーブル(p0_koma, p1_koma, p2_koma)に仕掛けることで、審判不要の世界が形成されてます。

# Let's play!
本当は駒の移動についてもきちんとルールに則って実装したかったけど、面倒なので、そこは紳士的にゲームを楽しむべし！という方針にしました。
盤面の表示は、みんな大好き`¥crosstabview`で実現してます。ちゃんと相手の駒が見えない（？になってる）ので、軍人将棋に独特の **心理戦** を再現できました！
<img width="241" alt="play.png" src="https://qiita-image-store.s3.amazonaws.com/0/151699/a8634ba1-a8bd-27a5-4aa0-43fa6db79a4f.png">

実際の駒は２から3文字（大将とか飛行機とか）になり表示が崩れてしまうので、盤上の駒は全角英字で表現してるのが切ないですが、まぁなんとなくそれっぽい動きをするし、まぁいっかって感じです。

# おわりに
ということで16日目でした。明日17日目はホヤホヤの[sawada_masahiko](https://qiita.com/sawada_masahiko)さんの登場です！