SQLer 生島勘富 のブログ

RDB・SQLの話題を中心に情報発信をしています。

CODE VS 2.1 をSQLでやるとどうなる6

 前回の続きです。今回は消すブロックを選択するところまで作ります。

 作るのは http://codevswc.jp/jpn/rule.html これです。

 是非、ルールをよく読んで、「自分なら SQL でこういう方針で作る」
 「Javaなら、.Netなら、Rubyなら…… こういう方針で作る」
 と想像しながら読んでください。

 多分、多くの人が、「『消すブロックを選択するところ』が一番、SQLでできない」と考えるところじゃないかと思っています。が、この辺の処理は極めてSQL的です。

 最初の方で作った4方向、8方向へオフセットするテーブルを使いますから、勘のいい人はアレを観た時点で分かったと思いますが……。

現状のデータでは分かりにくいので新たなデータでやります

 このSQL http://www.g1sys.co.jp/TestB.txt を流して確認すると、

-- 確認
SELECT * FROM DispBoard10(-1, 0, 0, 0)
ORDER BY Row DESC;

 図の様に返ってきます。

 以降、これをベースに考えます。
 色が変わっているブロックを選択出来れば成功です。

4方向か8方向か

 まずは消すブロックをカウントするのですけれど、今、落としたブロックは分かるわけですから、落としたブロックだけを8方向に合算して行き、合計が条件と一致する(現在のゲームでは10)になれば消すブロックと考えれば良いことです。

 しかし、図の様に赤枠の4つのブロックが投下されたとすると、

(1, 0)の位置にある[5]から右下方向に合算して行って(0, 1)の[5]を見つけ、「消すブロック」と決定することができます。
 さらに、(0, 0)の位置の[1]、(1, 1)の位置の[11]はどの方向合算していっても消せませんが、(0, 1)の[5]を左上に合算をして行ったとき、既に選択済みの(1, 0)の[5]が二重に選択されます。単純な個数なら重複して抽出し、後から重複を消せばすむ話ですが、ルールからオレンジの(0, 1)の[5]は二重計上しなければ行けません。

 この複雑さを回避するために、全部の数字を4方向にチェックしていくのが一般的と思います。(ちなみに8方向に合算していくと、必ず二重計上され無駄です)

 いくつかフラグを足せばSQLでも、落としたブロックだけをチェックするだけでできなくはないのですが、解説が大変なので4方向に合算していくやり方にしました(苦笑)

 恐らく、パフォーマンス的にはそんなに変わらないと思います。

 と言うわけで、再帰SQLを使って4方向に合算していきます。

再帰SQLとCROSS JOIN

WITH
	cb (GameID, SimulationID, Turn, Row, Col
		, ERow, ECol, Num, Amount, Cnt, Angle, Memo) -- 4方向に合算された結果
	AS(
	SELECT
		GameID
		, SimulationID
		, Turn
		, Row
		, Col
		, Row AS ERow
		, Col AS ECol
		, Num
		, Num AS amount
		, 1 AS cnt
		, 0 --調べている位置
		, CAST(N'調査' AS nvarchar(8)) AS Memo
	FROM Board
	WHERE GameID =-1 AND Turn = 0	AND Num < 10 
	UNION ALL
	SELECT 
		cb.GameID
		, cb.SimulationID
		, cb.Turn
		, cb.Row			-- 調査元の行
		, cb.Col		-- 調査元の列
		, bd.Row		-- 調査先の行
		, bd.Col		-- 調査先の列
		, bd.Num
		, cb.Amount + bd.Num
		, Cnt + 1 
		, dr.Angle
		, dr.Memo
	FROM 
		Board AS bd
		INNER JOIN (cb
		CROSS JOIN Direction4 AS dr)
			ON bd.Row = cb.Row + (dr.RowOffset * Cnt)
			AND bd.Col = cb.Col + (dr.ColOffset * Cnt)
			AND bd.GameID = cb.GameID
			AND bd.SimulationID = cb.SimulationID
			AND bd.turn = cb.turn
	WHERE
		cb.amount + bd.num <= 10
	)
-- SELECT * FROM cb; -- として、以下をコメントアウトすれば4方向に合算した結果が確認できる
	, eb (GameID, SimulationID, Turn, Row, Col
		, ERow, ECol, Num, Amount, Cnt, Angle, Memo) -- 消すブロック
	AS(
	SELECT * 
	FROM cb AS bd
		WHERE EXISTS (
			SELECT * FROM cb 
			WHERE bd.row = cb.row 
				AND bd.col = cb.col
				AND (bd.angle = 0 OR bd.angle = cb.angle)
				AND cb.amount = 10
			)
	) -- WITHの終わり
-- SELECT * FROM eb; -- でより詳しい情報が表示されます。
SELECT ERow, ECol, Num FROM eb;	

 実行すれば、(0, 1)の[5]が重複して、都合5つのブロックが選択されるはずです。

 つづく