この記事はPostgreSQL Advent Calendar 2018の16日目の記事です。
はじめに
誰かさんの影響で、私も PostgreSQL でクダラナイことをやってみたくなりました。
冬の風物詩
冬といえば、やっぱり 軍人将棋 ですよね!
軍人将棋(ぐんじんしょうぎ)とは、通常2人で行うボードゲームの一種。軍事将棋(ぐんじしょうぎ)、または行軍将棋(こうぐんしょうぎ)とも呼ばれる。軍隊の階級や兵種を元にした駒を用いて盤上にて競う。欧米圏ではストラテゴ(Stratego)という名称の同種のボードゲームがプレイされている。
Wikipediaより
小さい頃、父を審判役に姉と競い合ったのを思い出します。
ということで、PostgreSQLを使って 軍人将棋 を実現してみます。
(審判役もPostgreSQLにやらせれば不要になるので一石二鳥だし!)
ルール
軍人将棋を知らない方のためにサクッとルールをまとめると、、、
- 16種類23個の駒を盤面に置く(相手に見えないように裏返して)
- 16種類の駒は各々動き方が決まってるので、それにしたがって動かす
- 相手の駒の位置に動かしたらバトル開始!
- 審判は公平に判断する(16種類の駒の勝ち負けは下図のように決まってる)
- 相手の動ける駒を全て負かすor偉い駒で総司令部を占拠したらゲーム終了
って感じです。
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
軍旗は後ろの駒と同じ強さになるので、ちょっと面倒くさい。
実現方法
相手の駒が見えてしまうと、ゲームが成立しないのでユーザ分けたり、権限を整理したりする必要があります。
ちなみに、パーティションで盤面テーブルを作成してますが、PostgreSQL10だとパーティションキー(player)をまたぐ更新ができません。PostgreSQL11からはOKなので、持っててよかったPostgreSQL11です。
実装例
細々とルールを盛り込んでいったら、コードもそこそこの量になってしまったので、ポイントのみ解説します。
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
で実現してます。ちゃんと相手の駒が見えない(?になってる)ので、軍人将棋に独特の 心理戦 を再現できました!
実際の駒は2から3文字(大将とか飛行機とか)になり表示が崩れてしまうので、盤上の駒は全角英字で表現してるのが切ないですが、まぁなんとなくそれっぽい動きをするし、まぁいっかって感じです。
おわりに
ということで16日目でした。明日17日目はホヤホヤのsawada_masahikoさんの登場です!