チュートリアルで「難しい」と言ってくるパズルゲーム【ユウゴウパズル】
上の動画を見て、Pythonプログラミングで作ってみたくなりました。
以前、PythonでAIとコンピュータ対戦できる「三目並べドットコム」を作りました。
「ユウゴウパズル」はゼリーがジャンプしながら左右に移動するので、天井にぶつかって移動できなかったり、半分の高さの床は乗り越えられたりします。
それをゼロから作るのは難しいので、ユウゴウパズルの前身でもある「ゼリーのパズル」をPythonで作ってみます。
#BitSummit で使う #ゼリーのパズル のポスターが描けました。イェイ。 pic.twitter.com/n8odrThxbb
— たつなみ (@qrostar) March 3, 2014
ゼリーのパズルは、ゼリーをみぎやひだりに動かして同じ色のものをぜんぶくっつけるとクリアという簡単ルールです。
見た目はシンプルですが、簡単そうに見えてけっこう難しめです。
ユウゴウパズルとは?
ユウゴウパズルはシンプルな2Dパズルゲームです。ゼリーを左右に動かして、同じ色で全てくっつければクリアです。シンプルに見えて、やりごたえのあるパズルゲームです。良質なパズルを探している人におすすめです。
1人でじっくりリラックスしながら楽しむのもいいです。また、友人と一緒に考えて盛り上がるのもよしです。
ゲーム作者のたつなみさん(@qroster)が、TwitterとInstagramで毎週投稿されている『すこしずるいパズル』もかなり面白いです。
\毎週日曜/
パズル/謎解き投稿
ちいかわ×たつなみコラボの『ちいかわパズル』も発売中です。
Pythonプログラミング
HTMLとCSSで画面を表示する
HTMLで盤面を作る
今回は、盤面を縦10マス×横15マスで作りました。
IDでマスの位置を指定します。
ID「0000」が縦1マス目・縦1マス目で、ID「0914」は縦10マス目・縦15マス目になります。
ブロックの色は、赤をクラス「red」・青をクラス「blue」・黄をクラス「yellow」で指定します。
好きな位置にクラスを追加すると、その位置のマスにCSSで色が付くようにします。
盤面情報「squares-box」に、15×10個のマス目情報「squares」を入れます。
クラス「squares-box」を使って、CSSで盤面のサイズや位置を設定するためです。
「index.html」のソースコードです。
<!DOCTYPE html>
<html>
<body>
<link rel="stylesheet" href="static/puzzle.css">
<div class="squares-box">
<div id="0000" class="square kabe"></div>
<div id="0001" class="square kabe"></div>
<div id="0002" class="square kabe"></div>
<div id="0003" class="square kabe"></div>
<div id="0004" class="square kabe"></div>
<div id="0005" class="square kabe"></div>
<div id="0006" class="square kabe"></div>
<div id="0007" class="square kabe"></div>
<div id="0008" class="square kabe"></div>
<div id="0009" class="square kabe"></div>
<div id="0010" class="square kabe"></div>
<div id="0011" class="square kabe"></div>
<div id="0012" class="square kabe"></div>
<div id="0013" class="square kabe"></div>
<div id="0014" class="square kabe"></div>
<div id="0100" class="square kabe"></div>
<div id="0101" class="square"></div>
<div id="0102" class="square"></div>
:
:
:
<div id="0911" class="square kabe"></div>
<div id="0912" class="square kabe"></div>
<div id="0913" class="square kabe"></div>
<div id="0914" class="square kabe"></div>
</div>
</body>
</html>
CSSでサイズ・位置・色を設定する
「width」「height」でサイズを指定します。
「display」「flex-wrap」「margin」で位置を指定します。
「background-color」で背景色、「border-radius」で角を丸めています。
display: flex;
その直下の要素を横並びにします。(これがないと縦並びになります。)
flex-wrap: wrap;
要素を複数行に折り返します。(これがないとずっと横に並びます。)
margin: 0 auto;
要素を中央揃えにします。(これがないと左寄せになります。)
「puzzle.css」のソースコード
.squares-box {
width: 375px;
height: 250px;
display: flex;
flex-wrap: wrap;
margin: 0 auto;
}
.square {
width: 25px;
height: 25px;
}
.kabe {
background-color: #999;
}
.red {
border-radius: 20%;
background-color: #ff0000;
}
.blue {
border-radius: 20%;
background-color: #00b0f0;
}
.yellow {
border-radius: 20%;
background-color: #fd7e00;
}
Pythonでブロックを移動する
※実際にブロックを動かせます。
やりたいこと
・クリック(タップ)でブロックを選ぶ
・選んだブロックより右側をクリック(タップ)で右へ移動する
・選んだブロックより左側をクリック(タップ)で左へ移動する
・移動したブロックの下に、壁や別のブロックがなければ落下する
盤面情報をリスト(二次元配列)で作る
self.field = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,2,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,1,1,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,4,3,0,0,3,0,0,0,0,0,4,0,1],
[1,1,1,1,2,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]
1は壁、ブロックの色は2赤・3青・4黄で、15×10のリストを作ります。
盤面情報を画面に表示する
def dislay(self):
# 画面の表示
for square in document.select(".square"):
square.classList.remove("kabe")
square.classList.remove("red")
square.classList.remove("blue")
square.classList.remove("yellow")
for i in range(10):
for j in range(15):
if self.field[i][j] == 1:
document[format(i, "02") + format(j, "02")].classList.add("kabe")
if self.field[i][j] == 2:
document[format(i, "02") + format(j, "02")].classList.add("red")
if self.field[i][j] == 3:
document[format(i, "02") + format(j, "02")].classList.add("blue")
if self.field[i][j] == 4:
document[format(i, "02") + format(j, "02")].classList.add("yellow")
return
「square.classList.remove」で、一度すべてのクラスを削除します。
盤面情報が1の位置に「kabe」を、2の位置に「red」、3の位置に「blue」、4の位置に「yellow」をクラスに追加します。
CSSで追加したクラスの色を付けています。
ブロックを選択
# 選択したブロックを調べる
if event.target.classList.contains("red") or event.target.classList.contains("blue") or event.target.classList.contains("yellow"):
# 選択したブロック
self.x = int(event.target.id[-2:])
self.y = int(event.target.id[:2])
self.z = self.field[self.y][self.x]
クリックした要素のクラスが「red」「blue」「yellow」だったときに、ID名から座標を、盤面情報から色を取得します。
ブロックを左右移動
# 元のブロックを消す
self.check_field[self.y][self.x] = 0
# ブロックを右へ移動する
self.check_field[self.y][self.x + 1] = self.z
# ブロックを左へ移動する
self.check_field[self.y][self.x - 1] = self.z
「self.check_field」はブロックの盤面情報です。移動するブロックの盤面情報の値を変更します。
壁や別のブロックがあれば移動しない
def isHit(self):
# 移動チェック
for i in range(10):
for j in range(15):
if self.check_field[i][j] > 1 and self.field[i][j] > 0:
return True
return False
「self.check_field」がブロック情報で、「self.field」が盤面情報です。
移動したあとのブロック情報が2以上(ブロックがある)、かつ盤面情報が1以上(壁かブロック)のとき「True」を返し、移動したあとのブロックが何にもぶつかってなければ「False」を返します。
ブロックの落下処理
def fall(self):
# 落下処理
self.n = 1
while True:
# 白紙の盤面
self.check_field = copy.deepcopy(self.clear_field)
# 移動した位置に値を入れる
self.check_field[self.y + self.n][self.x] = self.z
# 移動チェック
if self.isHit():
if self.n > 1:
# ブロックを消す
self.field[self.y][self.x] = 0
# ブロックを下へ移動する
self.field[self.y + self.n - 1][self.x] = self.z
return
# ブロックが下にぶつかってなければ繰り返す
self.n += 1
壁か別のブロックにぶつかるまでブロックを下に移動します。
「self.check_field」がブロック情報で、「self.field」が盤面情報です。
「while True:」を使って、ブロックが下にぶつかるまで繰り返します。
「self.isHit()」がTrueのときブロックがぶつかったので、一つ上の位置にブロックを移動します。
「puzzle.py」のソースコード
from browser import document, timer
import copy
class main():
def __init__(self):
# フィールドの初期設定
self.clear_field = [
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
self.field = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,2,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,1,1,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,4,3,0,0,3,0,0,0,0,0,4,0,1],
[1,1,1,1,2,0,1,1,0,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]
# フィールドの表示
self.dislay()
# 初期設定
self.flg = 0
self.x = 0
self.y = 0
self.z = 0
# マウスイベント
document["body"].bind('mouseup', self.release)
def dislay(self):
# 画面の表示
for square in document.select(".square"):
square.classList.remove("kabe")
square.classList.remove("red")
square.classList.remove("blue")
square.classList.remove("yellow")
for i in range(10):
for j in range(15):
if self.field[i][j] == 1:
document[format(i, "02") + format(j, "02")].classList.add("kabe")
if self.field[i][j] == 2:
document[format(i, "02") + format(j, "02")].classList.add("red")
if self.field[i][j] == 3:
document[format(i, "02") + format(j, "02")].classList.add("blue")
if self.field[i][j] == 4:
document[format(i, "02") + format(j, "02")].classList.add("yellow")
return
def isHit(self):
# 移動チェック
for i in range(10):
for j in range(15):
if self.check_field[i][j] > 1 and self.field[i][j] > 0:
return True
return False
# --------------------------------------
# プレイモード
# --------------------------------------
def release(self, event):
# ブロック移動中は受け付けない
if self.flg == 1:
return
# 選択したブロックを調べる
if event.target.classList.contains("red") or event.target.classList.contains("blue") or event.target.classList.contains("yellow"):
# 外枠を消す
document[format(self.y, "02") + format(self.x, "02")].style.width = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "none"
# 選択したブロック
self.x = int(event.target.id[-2:])
self.y = int(event.target.id[:2])
self.z = self.field[self.y][self.x]
# 外枠を表示する
document[format(self.y, "02") + format(self.x, "02")].style.width = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "dotted 2px #ffff00"
return
elif event.target.classList.contains("square"):
#ブロックを移動する
if self.x == 0:
return
elif int(event.target.id[-2:]) > self.x:
# 右に移動
self.move_right()
return
elif int(event.target.id[-2:]) < self.x:
# 左に移動
self.move_left()
return
def move_right(self):
# ブロック移動中
self.flg = 1
# 白紙の盤面
self.check_field = copy.deepcopy(self.clear_field)
# 移動した位置に値を入れる
self.check_field[self.y][self.x + 1] = self.z
# 移動チェック
if self.isHit():
# ブロック移動中を解除
self.flg = 0
return
# ブロックを消す
self.field[self.y][self.x] = 0
# ブロックを右へ移動する
self.field[self.y][self.x + 1] = self.z
self.dislay()
# 外枠を消す
document[format(self.y, "02") + format(self.x, "02")].style.width = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "none"
self.x += 1
# 外枠を表示する
document[format(self.y, "02") + format(self.x, "02")].style.width = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "dotted 2px #ffff00"
# 落下チェック
timer.set_timeout(self.fall, 100)
return
def move_left(self):
# ブロック移動中
self.flg = 1
# 白紙の盤面
self.check_field = copy.deepcopy(self.clear_field)
# 移動した位置に値を入れる
self.check_field[self.y][self.x - 1] = self.z
# 移動チェック
if self.isHit():
# ブロック移動中を解除
self.flg = 0
return
# ブロックを消す
self.field[self.y][self.x] = 0
# ブロックを左へ移動する
self.field[self.y][self.x - 1] = self.z
self.dislay()
# 外枠を消す
document[format(self.y, "02") + format(self.x, "02")].style.width = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "none"
self.x -= 1
# 外枠を表示する
document[format(self.y, "02") + format(self.x, "02")].style.width = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "dotted 2px #ffff00"
# 落下チェック
timer.set_timeout(self.fall, 100)
return
def fall(self):
# 落下処理
self.n = 1
while True:
# 白紙の盤面
self.check_field = copy.deepcopy(self.clear_field)
# 移動した位置に値を入れる
self.check_field[self.y + self.n][self.x] = self.z
# 移動チェック
if self.isHit():
if self.n > 1:
# ブロックを消す
self.field[self.y][self.x] = 0
# ブロックを下へ移動する
self.field[self.y + self.n - 1][self.x] = self.z
self.dislay()
# 外枠を消す
document[format(self.y, "02") + format(self.x, "02")].style.width = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "25px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "none"
self.y += self.n - 1
# 外枠を表示する
document[format(self.y, "02") + format(self.x, "02")].style.width = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.height = "21px"
document[format(self.y, "02") + format(self.x, "02")].style.border = "dotted 2px #ffff00"
# ブロック移動中を解除
self.flg = 0
return
# ブロックが下にぶつかってなければ繰り返す
self.n += 1
if __name__ == '__main__':
main()
今後の課題
Pythonを始めたころに作ったのがテトリスだったので、ブロックの移動自体はそんなに難しくなかったです。
今回作ったゼリーのパズルは、次の部分が未完成です。
・ほかのブロックを押せない
・くっついたブロックの処理
・上のブロックの落下処理
・一つ戻る処理
・クリアの処理
今後の課題が山積みですが、とりあえず完成させてみたいです。
こちらの「三目並べドットコム」もよろしくお願いします。
コメント