PostgreSQL Advent Calendar 2012 12/14 です。
ライフゲームとは「生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム」のことです。
碁盤目状に配置されたセルが、隣接するセルとの関係により、誕生・生存・死滅していく様子をシミュレーションします。
ルールは下記参考
ライフゲーム - Wikipedia
このライフゲームをSQLを使って実装してみます。
データ構造
セルの集合を下記のように表現します。
・1つのセルにつき、1行のレコード
・セルは3つの属性(列)を持つ
integer x:碁盤の左端から右に何個目か
integer y:碁盤の上端から下に何個目か
integer alive:生存状態(生きているセルなら1、死んでいるセルなら0)
データはこれだけです。
初期化
碁盤のサイズ、生存しているセルの割合、を決めてランダムに生成
CREATE TABLE cells AS SELECT x::integer, y::integer ,CASE WHEN random() < 0.3 THEN 1 -- 3割のセルが生存 ELSE 0 END::integer AS alive FROM generate_series(1,5) t1(x) -- 横のサイズが5 ,generate_series(1,5) t2(y) -- 縦のサイズが5 ; /* SELECT * FROM cells; x | y | alive ---+---+------- 1 | 1 | 1 1 | 2 | 0 1 | 3 | 0 1 | 4 | 0 1 | 5 | 0 2 | 1 | 0 2 | 2 | 0 2 | 3 | 1 2 | 4 | 1 2 | 5 | 0 3 | 1 | 1 3 | 2 | 1 3 | 3 | 1 3 | 4 | 1 3 | 5 | 0 4 | 1 | 0 4 | 2 | 0 4 | 3 | 0 4 | 4 | 0 4 | 5 | 0 5 | 1 | 0 5 | 2 | 0 5 | 3 | 0 5 | 4 | 1 5 | 5 | 0 (25 rows) */
描画
初期化したセルから碁盤を描画してみます。
aliveの値をテキストに集約して0,1の文字列で表現しています。
SELECT string_agg(alive_y, E'¥n' ORDER BY y) AS board FROM( SELECT y ,string_agg(alive::text, '' ORDER BY x) AS alive_y FROM cells GROUP BY y )t ; /* board ----------------------------------- 10100¥n00100¥n01100¥n01101¥n00000 (1 row) 10100 00100 01100 01101 00000 */
次世代の生成
まずはSQLを
SELECT x, y ,( SELECT CASE WHEN c1.alive = 0 AND c2.alives = 3 THEN 1 -- 誕生 WHEN c1.alive = 1 AND (c2.alives = 2 OR c2.alives = 3) THEN 1 -- 生存 ELSE 0 -- 死滅 END FROM( SELECT sum(c2.alive) as alives FROM cells c2 WHERE NOT(c1.x = c2.x AND c1.y = c2.y) -- 自分自身を除く AND c1.x BETWEEN c2.x - 1 AND c2.x + 1 -- 横の範囲 AND c1.y BETWEEN c2.y - 1 AND c2.y + 1 -- 縦の範囲 )c2 )::integer AS alive FROM cells c1 ; /* x | y | alive ---+---+------- 1 | 1 | 0 1 | 2 | 0 1 | 3 | 0 1 | 4 | 0 1 | 5 | 0 2 | 1 | 1 2 | 2 | 0 2 | 3 | 0 2 | 4 | 1 2 | 5 | 0 3 | 1 | 0 3 | 2 | 1 3 | 3 | 0 3 | 4 | 1 3 | 5 | 0 4 | 1 | 0 4 | 2 | 1 4 | 3 | 0 4 | 4 | 1 4 | 5 | 0 5 | 1 | 0 5 | 2 | 0 5 | 3 | 0 5 | 4 | 0 5 | 5 | 0 (25 rows) 01000 00110 00000 01110 00000 */
ちょっと解説
次世代のaliveを生成しているサブクエリを具体的なケースに置き換えてみます。
x=2, y=3のセルは次世代で生存しているでしょうか?
サブクエリをc1.x=2, c1.y=3, c1.alive=1で置換したものが下記になります。
SELECT CASE WHEN 1 = 0 AND c2.alives = 3 THEN 1 -- 誕生 WHEN 1 = 1 AND (c2.alives = 2 OR c2.alives = 3) THEN 1 -- 生存 ELSE 0 -- 死滅 END FROM( -- ここのサブクエリで周囲の生存セル数を計算 SELECT sum(c2.alive) as alives FROM cells c2 WHERE NOT(2 = c2.x AND 3 = c2.y) -- 自分自身を除く AND 2 BETWEEN c2.x - 1 AND c2.x + 1 -- 横の範囲 AND 3 BETWEEN c2.y - 1 AND c2.y + 1 -- 縦の範囲 )c2具体的なケースができたら、後は全てのセルに対して同様に処理してやればOKです。
状態を保存する場合はUPDATEするなり、CREATE TABLEするなりしてやりましょう。
まとめ
ライフゲーム?それ、SQLでもできるよ。
Advent Calendar
明日はsakamotomsさんです。
おまけ1
任意の碁盤の状態からcellsを初期化したい場合
CREATE OR REPLACE FUNCTION board_to_cells(board text) RETURNS TABLE(x integer, y integer, alive integer) AS $$ SELECT row_number() OVER(PARTITION BY y)::integer as x ,y::integer ,alive::integer FROM( SELECT row_number() OVER() AS y ,regexp_split_to_table(alive_y, '') AS alive FROM( SELECT regexp_split_to_table($1, '\n') AS alive_y )t )t $$ LANGUAGE SQL; /* SELECT * FROM board_to_cells(E'010\n100\n100'); */
おまけ2
パフォーマンス実験
手元の環境にて、初期状態が100×100, 生存率3割で試したところ、次世代の計算にかかる時間が20秒強ほど。 もっと高速な実装を考えてみるのも面白いかも。