PostgreSQLで○×ゲーム（PL/pgSQLサンプルプログラム）

# PL/pgSQLサンプルプログラム

PL/pgSQL入門用のサンプルプログラムとして、○×ゲームを作成しました。

# ○×ゲームの仕組み

まず○×を書くマスを用意する必要がありますが、今回は○×をテーブルに記録します。
テーブルは行番号・列番号・マークの3列用意します。
行番号と列番号でマスを表現し、そのマスに記入されたマークを表現します。

![テーブルとマス目.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70076/7438a2f9-576d-bc9a-8af7-40b0e5a61ae1.png)

レコードそのまま表示してしまうと○×ゲームになりませんが、表示するときにマス目状に表示すればOKです。

# ○×ゲームのテーブル作成
```sql
CREATE TABLE marubatsu_field (
    row_id integer NOT NULL CHECK (1 <= row_id AND row_id <= 3),
    column_id integer NOT NULL CHECK (1 <= column_id AND column_id <= 3),
    mark char(1),
    PRIMARY KEY (row_id, column_id)
);
```

# ○×ゲームの関数

```plsql
CREATE OR REPLACE FUNCTION marubatsu(rownum integer, columnnum integer) RETURNS text AS $$
DECLARE
    row_count integer;
    init_row_num CONSTANT integer := 3;
    init_column_num CONSTANT integer := 3;
    sente_mark marubatsu_field.mark%TYPE;
    gote_rec RECORD;
    display_cur CURSOR FOR
        SELECT row_id, column_id, COALESCE(mark, '　') AS mark 
        FROM marubatsu_field ORDER BY row_id, column_id;
    display_text text := '';
BEGIN -- 実行部の開始
    -- 引数チェック
    IF rownum < 1 OR rownum > 3 THEN
        RAISE EXCEPTION '行指定の数字が有効範囲外を指定しています。';
    ELSIF columnnum < 1 OR columnnum > 3 THEN
        RAISE EXCEPTION '列指定の数字が有効範囲外を指定しています。';
    END IF;

    -- 行が初期化行数じゃなければ初期化
    SELECT COUNT(*) INTO row_count FROM marubatsu_field;
    IF row_count != (init_row_num * init_column_num) THEN
        DELETE FROM marubatsu_field;
        FOR i IN 1 .. init_row_num LOOP
            FOR j IN 1 .. init_column_num LOOP
                INSERT INTO marubatsu_field (row_id, column_id) VALUES (i, j);
            END LOOP;
        END LOOP;
    END IF;

    -- 先手が選んだ位置に○を入れる
    SELECT mark INTO sente_mark FROM marubatsu_field WHERE row_id = rownum AND column_id = columnnum;
    IF sente_mark IS NOT NULL THEN
        -- すでにマークが入っていたらメッセージ
        RAISE EXCEPTION '指定された位置はすでにマークされています。';
    ELSE
        UPDATE marubatsu_field SET mark = '○' WHERE row_id = rownum AND column_id = columnnum;
    END IF;

    -- 後手がマークされていないところをランダムで選択して×を入れる
    BEGIN -- サブブロック開始
        SELECT * INTO gote_rec FROM marubatsu_field WHERE mark is null ORDER BY random() limit 1;
        IF NOT FOUND THEN
            -- 全部埋まっていたら例外を発生させる
            RAISE EXCEPTION SQLSTATE 'ABCDE' USING MESSAGE = 'もう書き込める場所がありません。';
        ELSE
            UPDATE marubatsu_field SET mark = '×' WHERE row_id = gote_rec.row_id AND column_id = gote_rec.column_id;
        END IF;
    EXCEPTION
        -- 全部埋まっていた場合に発生した例外をキャッチしてメッセージを表示
        WHEN SQLSTATE 'ABCDE' THEN
            RAISE NOTICE '%', SQLERRM;
    END; -- サブブロック終了

    -- 全てのレコードを取得して、表示用のテキストを作成する
    FOR rec IN display_cur LOOP
        CASE rec.column_id
            WHEN 1 THEN display_text := display_text || ' ' || rec.mark;
            WHEN 2 THEN display_text := display_text || ' | ' || rec.mark;
            WHEN 3 THEN display_text := display_text || ' | ' || rec.mark || ' ' || chr(10);
        END CASE;
    END LOOP;

    -- 表示用のテキストを返す
    RETURN display_text;

END; -- 実行部の終了
$$ LANGUAGE plpgsql;
```

# おまけ
実は上記のソースには、勝利判定処理がありません。
修正して組み込んでみてもいいかもしれません。
また、あくまでもPL/pgSQLのサンプルなので、機能を説明するために冗長なソースになっているところもあります。もっと簡潔にできるようトライしてもいいでしょう。