(2018.9.9. 公開)
つながったコマを盤上から消し去る手順を考えてみる。まず、列ごとに上から落ちてくる処理。
この処理を行うには、次のように進める。
y1
, y2
とする。y1
, y2
を一番下の位置(yy - 1
、yy
は縦に並べるコマの数)にセットする。y1
の位置のコマが「残る」ものなら、そのコマを y2
の位置に移動して、y2
を1減らす。コマが「消える」ものなら、何もしない。y1
を1減らして、0より小さくなるまで繰り返す。
この処理でコマを移動する時、移動先のコマは常に「すでに処理済み」の位置なので、board
配列の中身を直接書き換えても構わない。(もし、未処理の位置に書き込む必要がある時は、移動先の配列を別に用意しておく必要がある。)
コードは下のようになる。上の手順が理解できれば、コードを理解することは難しくないだろう。
-- === コマを下に詰める ===
function fill_down()
for x = 0, xx - 1 do -- 各列について処理
local y2 = yy - 1 -- 下から順に詰める
for y1 = yy - 1, 0, -1 do
local c = board[y1 * xx + x + 1]
if c < 100 then
-- (x, y1)のコマが生きていれば(x, y2)に移す
if y2 ~= y1 then -- y2 == y1 の時は同じ場所だから動かさなくていい
board[y2 * xx + x + 1] = c
end
y2 = y2 - 1 -- これは y2 == y1 の時も必要
end
end
-- 全部のコマを移し終わったら、それより上は0で埋める
while y2 >= 0 do
board[y2 * xx + x + 1] = 0
y2 = y2 - 1
end
end
end
次に、空いた列を左に詰める処理。列が空いているかどうかは、一番下にコマがあるかどうかを調べればよい。
処理方法は、下に詰める場合とよく似ている。今度は、「移す前の列」を x1
, 「移す先の列」を x2
で表す。
x1
, x2
として、両方とも0にセットする。x1
列の一番下のコマが0でなければ、列全体を x2
に移して、x2
を1増やす。コマが0なら、何もしない。x1
を1増やして、xx
(横に並ぶコマの数)に達するまで繰り返す。コードは下の通り。
-- === 列を左に詰める ===
function fill_left()
local x2 = 0
for x1 = 0, xx - 1 do
local c = board[(yy - 1) * xx + x1 + 1]
if c > 0 then
-- x1 列を x2 列に動かす
if x2 ~= x1 then -- x2 == x1 の時は同じ場所だから動かさなくていい
for y = 0, yy - 1 do
board[y * xx + x2 + 1] = board[y * xx + x1 + 1]
end
end
x2 = x2 + 1 -- これは x2 == x1 の場合も必要
end
end
-- 全部の列を詰め終わったら、それより右は0で埋める
while x2 < xx do
-- 空にする
for y = 0, yy - 1 do
board[y * xx + x2 + 1] = 0
end
x2 = x2 + 1
end
end
盤面の書き換えには、スプライトバッチの中身を書き換える。特定のスプライトだけを置き換える SpriteBatch:set()
関数を使う方法と、いったん中身をクリアして最初から作り直す方法がある。前者の方が負荷は少ないが、この処理は「コマを消した時」しか呼ばれないので、負荷をそれほど意識することはない。クリアして作り直す方法で書けば、最初に盤面を初期化する時にも流用できるので、この方法で書くことにする。スプライトの番号を board
のキーと合わせるため、コマが存在しないところには「ダミーのスプライト(大きさを0にする)」を置く。
また、残りコマ数の計算もここで行う。
-- === 盤面の更新 ===
function update_board()
batch:clear() -- スプライトバッチをクリア
for i = 1, ntiles do -- 種類ごとの残数をクリア
rest[i] = 0
end
num = 0 -- 全体の残数をクリア
for y = 0, yy - 1 do
for x = 0, xx - 1 do
local c = board[y * xx + x + 1] % 100 -- %100 は実は不要だが念のため
if c == 0 then
batch:add(quads[1], 0, 0, 0, 0, 0) -- ダミー(表示しないスプライト)
else
batch:add(quads[c], x * 32, (y + 1) * 32)
rest[c] = rest[c] + 1 -- この種類の残数を+1
num = num + 1 -- 全体の残数を+1
end
end
end
end
結局、コマを消す処理は下のようになる。最初に、得点を加算する。N 個のコマを消した時の得点は (N–1)2 としている。次いで、コマを消して下に詰める・列を左に詰める作業を行い、盤面を更新して、マークを付け直す。
-- === コマを消す ===
function erase_tiles()
-- === 消したコマの得点を加算 ===
point = point + (cont - 1) * (cont - 1)
-- === コマを消して詰める ===
fill_down() -- マークされたコマを消して下に詰める
fill_left() -- 列を左に詰める
update_board() -- 盤面を書き直す
mark() -- マークを付け直す
end
cont
というのは「連続したコマの数」で、mark()
関数で計算している。
ゲームが進行できるようになると、「最初からやり直し」と「別の盤面でやり直し」の機能が欲しくなる。盤面初期化の処理を関数にして、「最初と同じ盤面を再現」および「新しい盤面を作成」の処理ができるようにしよう。
「最初と同じ盤面を再現」するのに最初に思いつく方法は、「盤面のデータを別の配列にコピーしておく」ことだろう。もちろんこれでも良いのだが、もう1つのやり方として、「乱数のタネを保存しておく」方法もある。Love2D の乱数には「タネ」と呼ばれる数値があって、同じタネから始めると同じ乱数が生成される。
タネは整数値で、love.math.setRandomSeed(seed)
というコードで設定できる。よく使われるのは、「現在時刻」をタネとして使うものである。ただし、love.timer.getTime()
が返す値をそのままタネとして使うと、1秒以内に2回「新しい盤面を作成」したとき、同じタネが使われてしまう。love.timer.getTime()
の値を 1000 倍(ミリ秒単位)にすれば、まず同じタネが使われることはない。
初期化を関数として実装すると、下のようになる。引数の retry
が「真」なら、保存しておいたタネをセットして、同じ盤面が再現されるようにする。
-- === 盤面初期化 ===
function init_board(retry)
if not retry then
randomSeed = love.timer.getTime() * 1000 -- タネを新しく作る(ミリ秒単位の現在時刻)
end
love.math.setRandomSeed(randomSeed) -- 保存してあるタネを使う
for i = 1, xx * yy do -- xx*yy 回繰り返し
board[i] = love.math.random(ntiles) -- ランダムにコマを配置
end
update_board() -- スプライトバッチ、残りコマ数を更新
-- === 現在位置初期化 ===
cx = 0
cy = 0
mark() -- マークをつける
end
love.keypressed()
に追記して、リターンキー、"R", "N" の処理を付け加える。
function love.keypressed(key, scancode, isrepeat)
local dx, dy = 0, 0
if key == "right" then -- 右矢印キーが押されている
dx = 1
elseif key == "left" then -- 左矢印キーが押されている
dx = -1
elseif key == "up" then -- 上矢印キーが押されている
dy = -1
elseif key == "down" then -- 下矢印キーが押されている
dy = 1
elseif key == "return" then -- リターンキーが押されている
if cont > 1 then -- 2つ以上連続したコマがある
erase_tiles() -- コマを消す
end
elseif key == "r" then -- "R" キーが押されている
init_board(true) -- 盤面の初期化(前回と同じ盤面)
point = 0 -- 得点をリセット
elseif key == "n" then -- "N" キーが押されている
init_board() -- 盤面の初期化(新しい画面)
point = 0 -- 得点をリセット
end
if dx ~= 0 or dy ~= 0 then
-- 新しい位置を計算する。画面からはみ出したら、反対側から出てくる
cx = (cx + dx) % xx
cy = (cy + dy) % yy
mark() -- マークをつけ直す
end
end
以上を合わせたのが次のプログラム。
-- サンプルプログラム 101-03 main.lua
-- fruits32.png, ipag.ttf が必要
function set_screen_size()
width = 640 -- ゲーム画面の横幅
height = 480 -- ゲーム画面の高さ
love.window.setMode(width, height) -- これが効くなら問題なし
love.window.setTitle("さめがめ on LÖVE")
swidth = love.graphics.getWidth() -- 実画面の横幅
sheight = love.graphics.getHeight() -- 実画面の高さ
transx, transy = 0, 0
scale = 1
if swidth ~= width or sheight ~= height then
-- setMode が効いてない場合
if swidth / sheight > width / height then -- 実画面の方が横長
scale = sheight / height -- 縦方向で倍率を決める
transx = math.floor((swidth - width * scale) / 2) -- 空白の幅
transy = 0
else
scale = swidth / width -- 横方向で倍率を決める
transx = 0
transy = math.floor((sheight - height * scale) / 2) -- 空白の高さ
end
end
love.graphics.setBackgroundColor(0.82, 0.82, 0.82) -- ゲーム画面の外は灰色の枠
end
function love.load()
-- === 画面サイズ設定 ===
set_screen_size() -- 画面の大きさを設定
xx = math.floor(width / 32) -- 盤面の幅
yy = math.floor(height / 32) - 1 -- 盤面の高さ
love.keyboard.setKeyRepeat(true) -- キーのオートリピートを有効にする
-- === フォント指定 (IPAゴシック 16ポイント) ===
font = love.graphics.newFont("ipag.ttf", 16)
love.graphics.setFont(font)
-- === 画像関連初期化 ===
ntiles = 5 -- コマの種類
tiles = love.graphics.newImage("fruits32.png") -- コマ画像を読み込む
wid0, high0 = tiles:getDimensions() -- コマ画像のサイズ
batch = love.graphics.newSpriteBatch(tiles) -- スプライトバッチ
quads = {} -- Quad を保持しておくテーブル
for i = 1, ntiles do -- Quad をコマの種類分だけ作る
quads[i] = love.graphics.newQuad((i - 1) * 32, 0, 32, 32, wid0, high0)
end
-- === 初期盤面を作る ===
board = {} -- 盤面
rest = {} -- 種類ごとの残りコマ数
init_board() -- 最初の盤面を作る
point = 0 -- 得点を0にする
end
-- === 盤面初期化 ===
function init_board(retry)
if not retry then
randomSeed = love.timer.getTime() * 1000 -- タネを新しく作る(ミリ秒単位の現在時刻)
end
love.math.setRandomSeed(randomSeed) -- 保存してあるタネを使う
for i = 1, xx * yy do -- xx*yy 回繰り返し
board[i] = love.math.random(ntiles) -- ランダムにコマを配置
end
update_board() -- スプライトバッチ、残りコマ数を更新
-- === 現在位置初期化 ===
cx = 0
cy = 0
mark() -- マークをつける
end
-- === 盤面の更新 ===
function update_board()
batch:clear() -- スプライトバッチをクリア
for i = 1, ntiles do -- 種類ごとの残数をクリア
rest[i] = 0
end
num = 0 -- 全体の残数をクリア
for y = 0, yy - 1 do
for x = 0, xx - 1 do
local c = board[y * xx + x + 1] % 100 -- %100 は実は不要だが念のため
if c == 0 then
batch:add(quads[1], 0, 0, 0, 0, 0) -- ダミー(表示しないスプライト)
else
batch:add(quads[c], x * 32, (y + 1) * 32)
rest[c] = rest[c] + 1 -- この種類の残数を+1
num = num + 1 -- 全体の残数を+1
end
end
end
end
-- (x, y) とつながっているコマをマークする(再帰呼び出し)
function mark_from_here(x, y)
local c, n
c = board[y * xx + x + 1]
board[y * xx + x + 1] = c + 100
n = 1
if x > 0 and board[y * xx + x] == c then
-- 左のマスに同じコマがある
n = n + mark_from_here(x - 1, y)
end
if x < xx - 1 and board[y * xx + x + 2] == c then
-- 右のマスに同じコマがある
n = n + mark_from_here(x + 1, y)
end
if y > 0 and board[(y - 1) * xx + x + 1] == c then
-- 上のマスに同じコマがある
n = n + mark_from_here(x, y - 1)
end
if y < yy - 1 and board[(y + 1) * xx + x + 1] == c then
-- 下のマスに同じコマがある
n = n + mark_from_here(x, y + 1)
end
return n
end
-- (cx, cy) とつながっているコマをマークする
function mark()
-- === すべてのマークを外す ===
for i = 1, xx * yy do
board[i] = board[i] % 100
end
-- === (cx, cy) にコマがあれば、そことつながっているコマをマークする ===
if board[cy * xx + cx + 1] ~= 0 then
cont = mark_from_here(cx, cy) -- つながっているコマの数
else
cont = 0
end
end
-- マークされているコマの位置を塗る
function show_mark()
local r = 8
for x = 0, xx - 1 do
for y = 0, yy - 1 do
local i = y * xx + x + 1
if board[i] > 100 then
local x1, y1 = x * 32, (y + 1) * 32
love.graphics.rectangle("fill", x1, y1, 32, 32, r, r)
if x > 0 and board[i - 1] > 100 then
love.graphics.rectangle("fill", x1, y1, r, 32)
end
if x < xx - 1 and board[i + 1] > 100 then
love.graphics.rectangle("fill", x1 + 32 - r, y1, r, 32)
end
if y > 0 and board[i - xx] > 100 then
love.graphics.rectangle("fill", x1, y1, 32, r)
end
if y < yy - 1 and board[i + xx] > 100 then
love.graphics.rectangle("fill", x1, y1 + 32 - r, 32, r)
end
end
end
end
end
-- === コマを下に詰める ===
function fill_down()
for x = 0, xx - 1 do -- 各列について処理
local y2 = yy - 1 -- 下から順に詰める
for y1 = yy - 1, 0, -1 do
local c = board[y1 * xx + x + 1]
if c < 100 then
-- (x, y1)のコマが生きていれば(x, y2)に移す
if y2 ~= y1 then -- y2 == y1 の時は同じ場所だから動かさなくていい
board[y2 * xx + x + 1] = c
end
y2 = y2 - 1 -- これは y2 == y1 の時も必要
end
end
-- 全部のコマを移し終わったら、それより上は0で埋める
while y2 >= 0 do
board[y2 * xx + x + 1] = 0
y2 = y2 - 1
end
end
end
-- === 列を左に詰める ===
function fill_left()
local x2 = 0
for x1 = 0, xx - 1 do
local c = board[(yy - 1) * xx + x1 + 1]
if c > 0 then
-- x1 列を x2 列に動かす
if x2 ~= x1 then -- x2 == x1 の時は同じ場所だから動かさなくていい
for y = 0, yy - 1 do
board[y * xx + x2 + 1] = board[y * xx + x1 + 1]
end
end
x2 = x2 + 1 -- これは x2 == x1 の場合も必要
end
end
-- 全部の列を詰め終わったら、それより右は0で埋める
while x2 < xx do
-- 空にする
for y = 0, yy - 1 do
board[y * xx + x2 + 1] = 0
end
x2 = x2 + 1
end
end
-- === コマを消す ===
function erase_tiles()
-- === 消したコマの得点を加算 ===
point = point + (cont - 1) * (cont - 1)
-- === コマを消して詰める ===
fill_down() -- マークされたコマを消して下に詰める
fill_left() -- 列を左に詰める
update_board() -- 盤面を書き直す
mark() -- マークを付け直す
end
function love.keypressed(key, scancode, isrepeat)
local dx, dy = 0, 0
if key == "right" then -- 右矢印キーが押されている
dx = 1
elseif key == "left" then -- 左矢印キーが押されている
dx = -1
elseif key == "up" then -- 上矢印キーが押されている
dy = -1
elseif key == "down" then -- 下矢印キーが押されている
dy = 1
elseif key == "return" then -- リターンキーが押されている
if cont > 1 then -- 2つ以上連続したコマがある
erase_tiles() -- コマを消す
end
elseif key == "r" then -- "R" キーが押されている
init_board(true) -- 盤面の初期化(前回と同じ盤面)
point = 0 -- 得点をリセット
elseif key == "n" then -- "N" キーが押されている
init_board() -- 盤面の初期化(新しい画面)
point = 0 -- 得点をリセット
end
if dx ~= 0 or dy ~= 0 then
-- 新しい位置を計算する。画面からはみ出したら、反対側から出てくる
cx = (cx + dx) % xx
cy = (cy + dy) % yy
mark() -- マークをつけ直す
end
end
function love.draw()
-- === 描画範囲を設定 ===
love.graphics.setScissor(transx, transy, width * scale, height * scale)
love.graphics.translate(transx, transy) -- 原点移動
love.graphics.scale(scale, scale) -- 拡大率を設定
-- === 盤面を塗りつぶす ===
love.graphics.setColor(0.39, 0.42, 0.92)
love.graphics.rectangle("fill", 0, 0, width, 32) -- 点数、残りコマ数など
love.graphics.setColor(0.47, 0.50, 1.00)
love.graphics.rectangle("fill", 0, 32, width, height - 32) -- 盤面
-- === スコア・残りコマ数など表示 ===
love.graphics.setColor(1, 1, 1)
love.graphics.print(string.format("スコア %-d", point), 4, 8)
love.graphics.print(string.format("残り %-d", num), 120, 8)
for i = 1, ntiles do
love.graphics.draw(tiles, quads[i], 140 + i * 60, 0)
love.graphics.print(string.format("%-d", rest[i]), 140 + 36 + i * 60, 8)
end
love.graphics.print("[R]etry [N]ew", 220 + ntiles * 60, 8)
-- === マークの表示 ===
love.graphics.setColor(0.39, 1, 1) -- 水色
show_mark()
-- === 現在位置の表示 ===
love.graphics.setColor(1, 1, 1) -- 白色
love.graphics.rectangle("fill", cx * 32, (cy + 1) * 32, 32, 32, 8, 8) -- 角丸四角形を塗りつぶす
-- === コマの表示 ===
love.graphics.draw(batch, 0, 0)
-- === 描画範囲をリセット ===
love.graphics.setScissor()
end
一応ゲームとして成立するようになった。
目次