解けたことを知らせる。詰んだら、いつでも最初から
いまは荷物をゴールに乗せても何も起こりません。壁ぎわに押しつけて詰んだら、
ウィンドウを閉じるしかありません。ここでゲームとして成立させます。
すべての荷物がゴールに乗ったらクリア、R キーでいつでも最初から——の 2 つです。
フェーズ 4 のコードに 26 行ほど書き足して、できあがりは 218 行になります。
tiles)・主人公・荷物(boxes)の 3 つに
分けて持つと決めました。あのとき「荷物がゴールから出たとき、床に戻し忘れる不具合を
構造で避けるため」と書いたのを覚えているでしょうか。
クリア判定は、その決定のおかげで 1 行で書けます。
フェーズ 4 を動かしたあと、主人公の目を黒くしたいという話になりました。
ところが黒は描いても出てきません。透過色(colkey)に黒を指定していたからです。
pyxel.blt() の最後の引数は「この 1 色だけは描かない」という指定です。
主人公の絵は背景を黒で塗っていたので、黒=背景として抜いていました。
目も同じ黒で描いたので、目も背景と区別されずに抜けてしまうわけです。
sokoban.pyxres のドットです。背景だった黒だけをグレーに塗り替え、目の黒はそのまま残しました
直し方は 2 段階です。リソースエディタで、絵の背景をグレー(色番号 13)に塗り替える。
そしてコード側の pyxel.COLOR_BLACK を pyxel.COLOR_GRAY に変える——
主人公を描いている blt の 1 か所だけです。
pyxel.blt(px, py, 0, self.player_dir * TILE, v, TILE, TILE, pyxel.COLOR_GRAY)
blt を呼ぶたびに指定するものなので、
絵ごとに違う色を指定してかまいません。
リンゴには黒を使っていないので、背景も黒のまま・指定も COLOR_BLACK のままで困りません。
タイル(床・壁・ゴール)はそもそも透過色を渡していません——
8×8 をまるごと敷き詰めるので、抜く必要がないからです。
ゴールの位置は tiles に残っています。
地図の O は、荷物や主人公とちがってどこへも動きません。
だから調べることは 1 つだけです。
荷物のいるマスの地形が "O" かどうか。
これを荷物 1 つずつについて調べて、全部そろっていればクリアです。
boxes と tiles を別々に持っているから、荷物の下の地形がいつでも分かりますO を $ で上書きしてしまいます。
すると「もとがゴールだったマス」が分からなくなり、
ゴールから押し出したときに床に戻せません。O は地形」「$ は別のリスト」と分けました。
設計の良さは、こういう先の場面で効いてきます。
調べる中身は決まりました。次はいつ調べるかです。これで画面の印象が変わります。 候補は 2 つあります。
| 調べる場所 | どうなるか |
|---|---|
try_move() の中(キーを押した瞬間) |
盤面の上ではもう乗っているので、STAGE CLEAR! が先に出ます。
リンゴがまだ 8 フレームかけてすべっている途中に文字が出て、気が早い感じになります |
update() の「着いた」ところ(すべり終わった瞬間) |
リンゴがゴールに収まったのと同時に文字が出ます。こちらを選びます |
フェーズ 4 で作った「盤面の位置」と「見た目の位置」の差が、そのまま出てくる話です。 盤面の判定はキーを押した瞬間、見せ方は着いた瞬間——この使い分けを覚えておいてください。
R でステージの最初に戻したい。まず思いつくのは、
始めの状態を控えておいて、それを戻すやり方です。
# 最初に控えを取っておいて……
self.backup = self.boxes
# R が押されたら、戻す
self.boxes = self.backup
ところが、これは 1 文字も動きません。
フェーズ 3 の 6-2「リストは中身が共有される」で見た話が、
今度は困る側で出てきます。
load_stage() は最初から「文字の地図 → 遊べる状態」を作る係なので、
もう一度呼ぶだけで済みます。load_stage(STAGES[n]) を呼ぶだけになります。
| 機能 | 役割 |
|---|---|
all(…) |
ぜんぶ True かを調べる Python の関数。1 つでも False があれば False |
… for … in …(ジェネレータ式) |
for を後ろに書く書き方。「リストの全員について、この値」をひとことで表す |
not |
True と False をひっくり返す。not self.cleared =「まだクリアしていない」 |
pyxel.btnp(pyxel.KEY_R) |
R キーを押した瞬間だけ。矢印キーの btn と使い分ける |
pyxel.text(x, y, 文字, 色) |
文字を描く。step1 のスコア表示と同じもの |
return(値を返さない) |
update() を途中で切り上げる。step1 フェーズ 6 で出てきた使い方 |
6 段階です。3-2 から 3-6 は続きものなので、最後まで書いてから実行してください。
リソースエディタ(pyxel edit sokoban.pyxres)を開き、
イメージバンク 0 の u = 32, v = 0 に 1 枚描きます。
場所は フェーズ 1 の配置図のとおり、タイルの段のいちばん右です。
| 位置 | 描くもの |
|---|---|
u = 24, v = 0 | ふつうのリンゴ(もうある) |
u = 32, v = 0 | ゴールに乗ったリンゴ(これを描く) |
u = 24 のリンゴをコピーして右に貼り付け、少し明るくするのが手軽です。
ふちを白でひと巻きする、上にキラッと 2 ドット足す、実の色を 1 段明るい色に置き換える——
どれでも伝わります。COLOR_BLACK のままです)。
U_FLOOR = 0
U_WALL = 8
U_GOAL = 16
U_BOX = 24
U_BOX_ON_GOAL = 32 # ゴールに乗っている荷物 ← 足す
load_stage() に、クリアの旗を足す
self.pushing = False のうしろに 1 行です。
self.walk_timer = 0 # 0 なら止まっている。1〜8 は歩いている途中
self.pushing = False # いまの一歩で、荷物を押しているか
self.cleared = False # このステージをクリアしたか ← 足す
__init__() ではなく load_stage() に書きます
self.cleared = False を __init__() に書くと、
起動したときの 1 回しか初期化されません。
やり直すたびに False に戻ってほしいので、load_stage() の中です。load_stage() は毎回やることの置き場です。
move_box() と try_move() のあいだに書きます。
どちらも調べて答えるだけで、盤面を変えません。
def on_goal(self, box):
"""その荷物が、ゴールのマスに乗っているか"""
return self.tiles[box[1]][box[0]] == "O"
def is_cleared(self):
"""すべての荷物がゴールに乗ったか"""
return all(self.on_goal(box) for box in self.boxes)
on_goal() はクリア判定だけのものではありません。
draw() でも「この荷物は光っている絵にするか」を決めるのに使います(3-6)。
2 か所から呼ばれるので、名前をつけて独立させる価値があります。is_cleared() のほうは on_goal() を荷物の数だけ並べたもの、
という関係です。
update() を書き換える
先頭に 4 行、到着したところに 2 行、そして最後の if の条件を書き換えます。
def update(self):
"""フレーム毎の更新処理"""
if pyxel.btnp(pyxel.KEY_R): # いつでも、最初からやり直せる ← 足す
self.load_stage(STAGE1)
return
if self.walk_timer > 0: # 歩いている途中
self.walk_timer += 1
if self.walk_timer > MOVE_FRAMES: # 隣のマスに着いた
self.walk_timer = 0
self.from_x = self.player_x # 見た目を、いまいるマスに合わせる
self.from_y = self.player_y
self.pushing = False
if self.is_cleared(): # 着いた、その瞬間に調べる ← 足す
self.cleared = True
# 止まっていて、まだクリアしていないときだけ、キーを見る ← 条件を足す
if self.walk_timer == 0 and not self.cleared:
self.handle_key()
and not self.cleared を忘れないでください
フェーズ 4 の 6-1 で、
「着いたその同じフレームで、次の一歩に入れる」のがなめらかさの決め手だと書きました。
ここではそれが裏目に出ます。cleared を立てたその同じフレームに handle_key() が呼ばれ、
もう一歩進んでしまいます。せっかく乗せたリンゴを、そのまま押し出してしまうのです。
くわしくは 6-2 を見てください。
draw() を 2 か所書き換える
荷物の blt を 3 行ぶんふくらませ、draw() のいちばん下に 5 行足します。
u = U_BOX
if self.on_goal(box): # ゴールに乗っているあいだは別の絵 ← 足す
u = U_BOX_ON_GOAL
pyxel.blt(bx, by, 0, u, V_TILE, TILE, TILE, pyxel.COLOR_BLACK)
U_BOX と直接書いていたところが u に変わっている点に注意してください。
そして、いちばん下です。
# クリアしたら、画面の上に知らせる
if self.cleared:
msg = "STAGE CLEAR!"
x = (SCREEN_WIDTH - len(msg) * 4) // 2
pyxel.text(x, 3, msg, pyxel.COLOR_YELLOW)
y = 3 は、空けておいた場所です
フェーズ 1 で HUD_HEIGHT = 12 と決め、画面の上 12 ドットを盤面に使わずに空けてありました。
そこへ書いています。だから盤面に文字がかぶりません。(SCREEN_WIDTH - len(msg) * 4) // 2 は step1 と同じです。
Pyxel の標準フォントは1 文字 4 ドット送りなので、文字数 × 4 が文字列の幅になります。
"STAGE CLEAR!" は 12 文字 = 48 ドット、x = 56 です。
書き足したのは定数 1 つ、load_stage() の 1 行、update() の 6 行、
新しい on_goal() と is_cleared()、そして draw() の 2 か所です。
# sokoban.py
# step2 フェーズ5:クリア判定と、やり直し
import pyxel
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
TILE = 8 # 1 マスの大きさ(ドット)
HUD_HEIGHT = 12 # 画面の上、手数などを出すために空けておく高さ
MOVE_FRAMES = 8 # 隣のマスへ移りきるまでにかけるフレーム数
# イメージバンク 0 の、切り出し位置
V_TILE = 0 # タイルの段
V_PLAYER_A = 8 # 主人公の段(コマ A)
V_PLAYER_B = 16 # 主人公の段(コマ B・足を入れ替えた絵)
U_FLOOR = 0
U_WALL = 8
U_GOAL = 16
U_BOX = 24
U_BOX_ON_GOAL = 32 # ゴールに乗っている荷物
# 向き。この番号がそのまま主人公の絵の切り出し位置になる
DIR_DOWN = 0
DIR_UP = 1
DIR_LEFT = 2
DIR_RIGHT = 3
# 向きごとの、1 歩ぶんのずれ(x のずれ, y のずれ)
MOVES = [
(0, 1), # 下
(0, -1), # 上
(-1, 0), # 左
(1, 0), # 右
]
# ステージの地図
# # 壁 . 床 O ゴール $ 荷物 @ 主人公
STAGE1 = [
"########",
"#......#",
"#..O.O.#",
"#..$.$.#",
"#..@...#",
"########",
]
class App:
def __init__(self):
"""起動時の設定"""
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Sokoban")
pyxel.load("sokoban.pyxres")
self.load_stage(STAGE1)
pyxel.run(self.update, self.draw)
def load_stage(self, rows):
"""文字の地図を読んで、壁・主人公・荷物に分ける"""
self.tiles = []
self.boxes = []
self.player_dir = DIR_DOWN # はじめは下を向いている
self.walk_timer = 0 # 0 なら止まっている。1〜8 は歩いている途中
self.pushing = False # いまの一歩で、荷物を押しているか
self.cleared = False # このステージをクリアしたか
for y in range(len(rows)):
line = []
for x in range(len(rows[y])):
mark = rows[y][x]
if mark == "@":
self.player_x = x
self.player_y = y
mark = "." # 主人公の足元は床
elif mark == "$":
self.boxes.append([x, y])
mark = "." # 荷物の下も床
line.append(mark)
self.tiles.append(line)
self.from_x = self.player_x # 見た目の出発マス
self.from_y = self.player_y
# 盤面を画面の中央に置くための、ずらし幅
board_w = len(self.tiles[0]) * TILE
board_h = len(self.tiles) * TILE
self.ox = (SCREEN_WIDTH - board_w) // 2
self.oy = HUD_HEIGHT + (SCREEN_HEIGHT - HUD_HEIGHT - board_h) // 2
def update(self):
"""フレーム毎の更新処理"""
if pyxel.btnp(pyxel.KEY_R): # いつでも、最初からやり直せる
self.load_stage(STAGE1)
return
if self.walk_timer > 0: # 歩いている途中
self.walk_timer += 1
if self.walk_timer > MOVE_FRAMES: # 隣のマスに着いた
self.walk_timer = 0
self.from_x = self.player_x # 見た目を、いまいるマスに合わせる
self.from_y = self.player_y
self.pushing = False
if self.is_cleared(): # 着いた、その瞬間に調べる
self.cleared = True
# 止まっていて、まだクリアしていないときだけ、キーを見る
if self.walk_timer == 0 and not self.cleared:
self.handle_key()
def handle_key(self):
"""矢印キーを見て、押された向きへ 1 歩進もうとする"""
if pyxel.btn(pyxel.KEY_DOWN):
self.try_move(DIR_DOWN)
elif pyxel.btn(pyxel.KEY_UP):
self.try_move(DIR_UP)
elif pyxel.btn(pyxel.KEY_LEFT):
self.try_move(DIR_LEFT)
elif pyxel.btn(pyxel.KEY_RIGHT):
self.try_move(DIR_RIGHT)
def has_box(self, x, y):
"""そのマスに荷物があるかどうかを答える"""
for box in self.boxes:
if box[0] == x and box[1] == y:
return True
return False
def move_box(self, x, y, dx, dy):
"""(x, y) にある荷物を、(dx, dy) だけ動かす"""
for box in self.boxes:
if box[0] == x and box[1] == y:
box[0] += dx
box[1] += dy
return
def on_goal(self, box):
"""その荷物が、ゴールのマスに乗っているか"""
return self.tiles[box[1]][box[0]] == "O"
def is_cleared(self):
"""すべての荷物がゴールに乗ったか"""
return all(self.on_goal(box) for box in self.boxes)
def try_move(self, direction):
"""その向きへ動けるか調べて、動けるときだけ 1 マス進む"""
self.player_dir = direction # 動けなくても、向きだけは変える
dx, dy = MOVES[direction]
next_x = self.player_x + dx
next_y = self.player_y + dy
if self.tiles[next_y][next_x] == "#": # 行き先が壁なら
return # 何もしないで戻る
if self.has_box(next_x, next_y): # 行き先に荷物があるなら
far_x = next_x + dx # 荷物の、1 つ先のマス
far_y = next_y + dy
if self.tiles[far_y][far_x] == "#": # その先が壁なら押せない
return
if self.has_box(far_x, far_y): # その先に別の荷物があっても押せない
return
self.move_box(next_x, next_y, dx, dy)
self.pushing = True # この一歩は、荷物を押している
self.player_x = next_x
self.player_y = next_y
self.walk_timer = 1 # ここから 8 フレームかけて歩く
def slide_pos(self, origin, from_cell, to_cell):
"""出発マスから到着マスへ、いまどこまで来たかをドットで返す"""
moved = (to_cell - from_cell) * TILE * self.walk_timer // MOVE_FRAMES
return origin + from_cell * TILE + moved
def draw(self):
"""描画処理"""
pyxel.cls(pyxel.COLOR_BLACK)
# 壁を、上の行から 1 マスずつ描く
for y in range(len(self.tiles)):
for x in range(len(self.tiles[y])):
mark = self.tiles[y][x]
if mark == "#":
u = U_WALL
elif mark == "O":
u = U_GOAL
else:
u = U_FLOOR
pyxel.blt(self.ox + x * TILE, self.oy + y * TILE,
0, u, V_TILE, TILE, TILE)
# 荷物。押している 1 つだけは、主人公といっしょにすべる
dx, dy = MOVES[self.player_dir]
push_x = self.player_x + dx # 押した荷物は、主人公の目の前にいる
push_y = self.player_y + dy
for box in self.boxes:
if self.pushing and box[0] == push_x and box[1] == push_y:
bx = self.slide_pos(self.ox, box[0] - dx, box[0])
by = self.slide_pos(self.oy, box[1] - dy, box[1])
else:
bx = self.ox + box[0] * TILE
by = self.oy + box[1] * TILE
u = U_BOX
if self.on_goal(box): # ゴールに乗っているあいだは別の絵
u = U_BOX_ON_GOAL
pyxel.blt(bx, by, 0, u, V_TILE, TILE, TILE, pyxel.COLOR_BLACK)
# 主人公
px = self.slide_pos(self.ox, self.from_x, self.player_x)
py = self.slide_pos(self.oy, self.from_y, self.player_y)
v = V_PLAYER_A
if self.walk_timer >= MOVE_FRAMES // 2: # 歩きの後半は、足を入れ替える
v = V_PLAYER_B
pyxel.blt(px, py, 0, self.player_dir * TILE, v, TILE, TILE, pyxel.COLOR_GRAY)
# クリアしたら、画面の上に知らせる
if self.cleared:
msg = "STAGE CLEAR!"
x = (SCREEN_WIDTH - len(msg) * 4) // 2
pyxel.text(x, 3, msg, pyxel.COLOR_YELLOW)
App()
cd step2
python sokoban.py
| やってみること | こうなれば成功 |
|---|---|
| リンゴ 1 個をゴールに乗せる | そのリンゴが光っている絵に変わる。まだ STAGE CLEAR! は出ない |
| 乗せたリンゴを、もう一度押して外す | ふつうのリンゴの絵に戻る。ゴールの目印も消えていない |
| 2 個ともゴールに乗せる | リンゴが収まったその瞬間に画面の上に STAGE CLEAR! が出る |
| 最後の一歩を、キーを押しっぱなしで決める | クリアしたところで止まる。そのまま先へ歩いていかない |
| クリアしたあと、矢印キーを押す | もう動かない(向きも変わらない) |
R を押す |
リンゴと主人公が最初の位置に戻る。STAGE CLEAR! も消える |
歩いている途中で R を押す |
すべりが止まって、すぐ初期状態になる |
R を押しっぱなしにする |
1 回だけやり直る。押し続けても連続でリセットされない |
| リンゴを壁ぎわに押しつけて詰ませる | R で立て直せる。ウィンドウを閉じなくてよくなった |
if self.cleared and pyxel.frame_count % 30 < 20:
とすると、0.67 秒ついて 0.33 秒消えます(step1 フェーズ 7 と同じ手です)pyxel.text(2, 3, "R:RETRY", pyxel.COLOR_GRAY) のように左上に小さく出すと、
R があることが遊ぶ人に伝わりますsum(1 for box in self.boxes if self.on_goal(box)) で乗っている個数が出ます。
f"{乗った数} / {len(self.boxes)}" と表示すると、あと何個かが分かります
(sum は all と同じ「for を後ろに書く」形です)all() と、for を後ろに書く書き方 return all(self.on_goal(box) for box in self.boxes)
はじめて見ると、for が後ろにあるのが不思議に見えると思います。
これは「リストの全員について、この値を出す」を 1 行で書く書き方です。
いつもの for で書くと、こうなります。
# ぜんぶ書き出した版(同じ意味)
def is_cleared(self):
for box in self.boxes:
if not self.on_goal(box):
return False # 1 つでも乗っていなければ、その場でおしまい
return True # 最後まで来たなら、ぜんぶ乗っていた
どちらでも正しく動きます。
今回 all() を選んだのは、読んだとおりの意味になるからです。
「all(すべて)が on_goal(ゴールに乗っている)」と、
英語の文のように読めます。
all([self.on_goal(box) for box in self.boxes]) と
角かっこをつけても動きます。こちらは「リスト内包表記」といって、
先に True/False のリストを作ってから all() に渡します。False が出た時点で、残りを調べずに打ち切ります。
荷物 2 個では差は出ませんが、短く書けて、しかも無駄がないのでこちらを使います。
all() は、空っぽのとき True です
all([]) は True を返します(実測で確認しました)。
「反例が 1 つもない」=「すべて成り立つ」という決まりです。$ を書き忘れたときに、これで気づけます。
and not self.cleared を書かないと何が起きるか、
実際に走らせて 1 フレームずつ記録しました。
最後の一歩を ↑ を押しっぱなしで決めたときの動きです。
| フレーム | boxes | 主人公 | walk_timer | cleared |
|---|---|---|---|---|
| 1 | [3,2] [5,2] | (5, 3) | 1 | False |
| 2 〜 7 | [3,2] [5,2] | (5, 3) | 2 〜 7 | False |
| 8 | [3,2] [5,2] | (5, 3) | 8 | False |
| 9 | [3,2] [5,1] | (5, 2) | 1 | True |
9 フレーム目を見てください。cleared が True になった、その同じフレームで
リンゴが [5,2] から [5,1] へ動いています。
ゴールから押し出してしまったのです。結果はこうなります。
STAGE CLEAR! と出ているのに、リンゴはゴールの上にない
(is_cleared() は False なのに、cleared は True のまま)
理由は、フェーズ 4 でわざとそう作ったところにあります。
update() の 2 つの if は else ではないので、
「着いた」と「次のキーを見る」が同じフレームに起きます。
なめらかに歩かせるための仕組みが、そのままここへ効いてきます。
if self.walk_timer > 0:
...
self.walk_timer = 0 # ← 0 になる
if self.is_cleared():
self.cleared = True # ← 同じフレームで True になる
if self.walk_timer == 0 and not self.cleared: # ← だからここで止める
self.handle_key()
self.pushing(フェーズ 4)でも同じ話をしました。
旗は、立てる場所・下ろす場所・見る場所の 3 つをセットで考えるものです。self.cleared は——立てるのが update() の到着のところ、
下ろすのが load_stage()、見るのが update() の入力と draw() の表示。
この 4 か所がそろって、はじめて正しく動きます。
self.cleared をやめて、必要なときに毎回 is_cleared() を呼ぶ
作りにもできます(if self.walk_timer == 0 and not self.is_cleared():)。
1 フレームに 2 回ほど余分に数えるだけなので、速さは問題になりません。is_cleared() は盤面の位置で答えるので、
リンゴがすべり終わるのを待たずに True になり、文字が早く出てしまいます。
② フェーズ 8 で「クリアした瞬間から何フレーム経ったか」を使って演出を作ります。
そのとき「瞬間」を捉えられる形にしておきたいからです。
1-3 の図を、実際に動かして確かめた結果です。すべて実測です。
>>> boxes = [[3, 3], [5, 3]]
>>> backup = boxes # 名札を増やすだけ
>>> backup is boxes
True
>>> boxes[0][1] -= 1 # リンゴを 1 マス押した
>>> backup
[[3, 2], [5, 3]] # 控えも動いている
>>> boxes = [[3, 3], [5, 3]]
>>> backup = boxes[:] # 外側だけ新しく作る(浅いコピー)
>>> backup is boxes
False # 外側は別もの
>>> backup[0] is boxes[0]
True # でも中身は同じもの
>>> boxes[0][1] -= 1
>>> backup
[[3, 2], [5, 3]] # やはり控えも動いている
boxes[:] のほかに list(boxes) と boxes.copy() も
ありますが、3 つとも同じ「浅いコピー」です。中の [x, y] は共有されます。
ちゃんと控えを取るなら、中身まで作り直します。
>>> boxes = [[3, 3], [5, 3]]
>>> backup = [box[:] for box in boxes] # 1 つずつコピーして、新しいリストに
>>> backup[0] is boxes[0]
False # 中身も別もの
>>> boxes[0][1] -= 1
>>> backup
[[3, 3], [5, 3]] # 控えは無事
import copy して copy.deepcopy(boxes) と書く方法もあり、結果は同じです。
STAGE1 は文字列のリストです。文字列は1 文字も書き換えられません。
>>> STAGE1[1][3] = "."
TypeError: 'str' object does not support item assignment
だから load_stage(STAGE1) を何度呼んでも、
いつも同じ初期状態が出てきます。tiles も同じ注意がいります
self.tiles はリストのリストなので、
self.tiles[:] では行が共有されます。
いまは tiles を書き換えていないので問題になりませんが、
「二次元リストの控えは、浅いコピーでは取れない」と頭に入れておいてください。
R だけ btnp にする理由
フェーズ 4 で矢印キーを btnp から btn に変えました。
なのに R は btnp です。これは矛盾ではありません。
| キー | 使うもの | なぜ |
|---|---|---|
| 矢印 | btn |
押しっぱなしで歩き続けてほしい。歩いている間は見に行かないので暴走しない |
R | btnp |
1 回で足りる。押しっぱなしのあいだ毎フレーム作り直しても、無駄なだけ |
もし R を btn にすると、押しているあいだずっと初期状態に戻され続けます。
R を押しながら矢印を押しても1 歩も進めません——毎フレーム return で
引き返してしまうからです。btnp なら 1 フレーム目だけ効き、
2 フレーム目からは矢印が通ります(これも実測で確認しました)。
R の判定を update() のいちばん上に置く理由
歩いている途中でも効かせたいからです。
もし下に置くと、if self.walk_timer == 0 … の中に入ってしまい、
すべっているあいだは R が無視されます。return でその下をぜんぶ飛ばしているので、
作り直した直後の状態に、同じフレームで歩きの処理が割り込むこともありません。
「例外的な操作は、いちばん上で片づけて return」——覚えやすい型です。
u = U_BOX
if self.on_goal(box):
u = U_BOX_ON_GOAL
ここで大事なのは、「乗った」「外した」を覚えていないことです。 毎フレーム、いまの位置を見て、その場で絵を決めています。
だからゴールから押し出したときに「絵を戻す」処理がいりません。 覚えていないものは、戻し忘れることもありません。 これはフェーズ 1 で「地形と荷物を分けて持つ」と決めたことの、そのままの成果です。
u の選び方・slide_pos() の位置)……
ずれようがない。忘れる心配がないwalk_timer・pushing・cleared)……
計算では出せないことだけ。「いつ始まったか」「どっちを押したか」など| 症状 | 原因と対処 |
|---|---|
起動した瞬間から STAGE CLEAR! が出ている |
地図に $(荷物)がありません。all([]) は True なので、
荷物 0 個は「ぜんぶ乗っている」と判定されます(6-1) |
荷物を乗せても STAGE CLEAR! が出ない |
地図の O の数と $ の数が合っていませんか。
荷物のほうが多いと、永久にクリアできません |
STAGE CLEAR! が出ているのに、リンゴがゴールに乗っていない |
and not self.cleared を書いていません。
クリアした同じフレームに、もう一歩進んでいます(6-2) |
| クリアしたあとも歩けてしまう | 同じ原因です。if self.walk_timer == 0 and not self.cleared: を確かめてください |
| クリアの文字が、リンゴが着く前に出る | if self.is_cleared(): を try_move() の中に書いています。
update() の到着したところに移してください(1-2) |
| 乗せた荷物の絵が変わらない | u = U_BOX のままで、blt の引数が
U_BOX と直接書かれていませんか。u を渡します |
| 乗せた荷物が消える | u = 32, v = 0 に絵を描いていません。
何もない場所を切り出すと、黒(透過色)だけが並びます。3-1 に戻ってください |
R を押しても何も起きない |
pyxel.KEY_R の綴りを確かめてください。
また、update() のいちばん上にありますか |
R を押すと、矢印キーが効かなくなる |
btn(pyxel.KEY_R) になっています。btnp です
(6-4) |
R でやり直しても、クリアの文字が消えない |
self.cleared = False を load_stage() ではなく
__init__() に書いています |
AttributeError: 'App' object has no attribute 'cleared' |
self.cleared = False を書いていません。
load_stage() の中、self.pushing = False のうしろです |
TypeError: 'bool' object is not iterable |
all(self.on_goal(box)) のように、for の部分を書き忘れています。
all(self.on_goal(box) for box in self.boxes) です |
IndexError: list index out of range |
on_goal() で self.tiles[box[0]][box[1]] と、
x と y を逆にしていませんか。tiles[box[1]][box[0]] です |
| 文字が画面からはみ出す/左に寄る | 中央ぞろえの式が (SCREEN_WIDTH - len(msg)) // 2 になっていませんか。
* 4 が必要です(1 文字 4 ドット) |
| 主人公の目が黒くならない/体に穴があく | 透過色の指定を見直してください。絵の背景をグレーにしたら、
blt の最後も pyxel.COLOR_GRAY です(この節) |
これで1 ステージぶんのゲームが完成しました。解ける、勝てる、やり直せる—— ゲームとして必要なものは、ぜんぶそろっています。
次はステージを 10 個にします。
ここでうれしいのは、ほとんど書き足すものがないことです。
load_stage() が「文字の地図をもらって、遊べる状態を作る」形になっているので、
地図を並べたリストを用意して、番号で渡すだけです。
STAGES = [STAGE1, STAGE2, …] と並べ、self.stage_no で
何番目かを覚えます。load_stage() が毎回計算しているので、何マスでも勝手に真ん中に来ます。