データを増やす。コードは、ほとんど増やさない
ステージを 10 個にします。クリアしたら Enter で次へ、
10 個ぜんぶ終わったら 全クリアです。
ここでうれしいのは、頭を使って書くコードが 25 行しかないことです。
残りはぜんぶ地図のデータ。フェーズ 1 で
load_stage() を「文字の地図をもらって、遊べる状態を作る係」にしておいたので、
渡す地図を変えるだけでステージが変わります。
フェーズ 5 の 218 行に足して、できあがりは 356 行になります。
いまは STAGE1 という名前を load_stage() に直接渡しています。
これを 10 個まとめたリストにして、「いま何番目か」を数ひとつで覚える形に変えます。
load_stage() が毎回まっさらに作り直しますステージが 1 つだったときは、クリアしたらそこで終わりでした。 これからはその先があります。
self.stage_no と self.all_cleared の 2 つだけですALL CLEAR! が出るのではなく、
いったん CLEAR! が出て、Enter を押すと ALL CLEAR! になります。| 機能 | 役割 |
|---|---|
リストの中にリスト |
STAGES は「地図(文字列のリスト)」を 10 個並べたもの。
MOVES と同じ「並べて、番号で引く」形 |
len(STAGES) |
ステージの個数。10 と直接書かないので、増やしても直す場所がない |
pyxel.btnp(pyxel.KEY_RETURN) |
Enter キー。step1 のタイトル画面と同じ |
| f 文字列 | f"STAGE {self.stage_no + 1}/{len(STAGES)}"。step1 のスコア表示と同じ |
class の外の関数 |
draw_center()。self を使わないので外に置く |
7 段階です。3-1 の入力がいちばん長いので、気長にいきましょう。
STAGE1 の下に、STAGE2 から STAGE10 まで並べます。
10 個とも、解けることを確かめてあります(確かめ方は 7 に書きました)。
STAGE2 = [ # 横にも押せる
"########",
"#......#",
"#.$..O.#",
"#.@....#",
"#.$..O.#",
"#......#",
"########",
]
STAGE3 = [ # 縦と横を組み合わせる
"#########",
"#.O...O.#",
"#.......#",
"#..$....#",
"#....$..#",
"#..@....#",
"#.......#",
"#########",
]
STAGE4 = [ # 壁があるので、押す側へ回り込む
"##########",
"#........#",
"#.O.##.O.#",
"#...##...#",
"#..$..$..#",
"#........#",
"#...@....#",
"##########",
]
STAGE5 = [ # 段が仕切られていて、行き来に回り道がいる
"##########",
"#........#",
"#O..$....#",
"#...##...#",
"#O..$....#",
"#...##...#",
"#O..$....#",
"#....@...#",
"##########",
]
STAGE6 = [ # どれから動かすか、順番を考える
"##########",
"#........#",
"#.O.O.O..#",
"#........#",
"#.$......#",
"#..$.....#",
"#...$....#",
"#....@...#",
"##########",
]
STAGE7 = [ # 細い通路。押せる向きが決まってしまう
"##########",
"#....#...#",
"#.$..#.O.#",
"#....#...#",
"#.$......#",
"#....#...#",
"#.@..#.O.#",
"#....#...#",
"##########",
]
STAGE8 = [ # 真ん中のゴールへ、四方から運ぶ
"############",
"#..........#",
"#.$......$.#",
"#....##....#",
"#...OOOO...#",
"#....##....#",
"#.$......$.#",
"#.....@....#",
"############",
]
STAGE9 = [ # 中央の壁を大きく回る
"###########",
"#.........#",
"#.OO..$$..#",
"#.........#",
"#...###...#",
"#.........#",
"#.OO..$$..#",
"#....@....#",
"###########",
]
STAGE10 = [ # 仕上げ。四隅へ運ぶ
"###########",
"#O.......O#",
"#.........#",
"#...$.$...#",
"#..#...#..#",
"#...$.$...#",
"#.........#",
"#O...@...O#",
"###########",
]
# と . を 1 つ間違えるだけで、解けない地図になります。
写したあとで 検算ツールを走らせると、その場で分かります。
STAGE10 のすぐ下です。並べた順が、遊ぶ順になります。
STAGES = [STAGE1, STAGE2, STAGE3, STAGE4, STAGE5,
STAGE6, STAGE7, STAGE8, STAGE9, STAGE10]
STAGES の下、class App: の手前に書きます。
def draw_center(text, y, color):
"""文字を、画面の横まんなかに描く"""
x = (SCREEN_WIDTH - len(text) * 4) // 2
pyxel.text(x, y, text, color)
class の外なのか
この関数は self を1 度も使いません。
画面の幅と文字列だけで答えが出ます。is_hit() と draw_center() を外に置きました。
基準は同じで、「ゲームの状態を見ないなら、外に出す」です。
フェーズ 5 では 1 か所でしか使わなかったので draw() の中に直接書いていましたが、
2 か所から使うことになったので、名前をつけて外に出します。
__init__() に、進み具合を 2 つ足す def __init__(self):
"""起動時の設定"""
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Sokoban")
pyxel.load("sokoban.pyxres")
self.stage_no = 0 # いま何番目のステージか(0 から数える) ← 足す
self.all_cleared = False # 10 個ぜんぶ終わったか ← 足す
self.load_stage(STAGES[self.stage_no]) # ← STAGE1 から書き換える
pyxel.run(self.update, self.draw)
load_stage() の中に書いてはいけません
self.stage_no は __init__() に書きます。
load_stage() に書くと、R を押すたびに 1 面に戻ってしまいます。walk_timer や cleared はステージごとに 0 / False へ。
stage_no はステージをまたいで覚えておく数なので、外側です。
next_stage() を足す
is_cleared() のうしろに書きます。
def next_stage(self):
"""次のステージへ進む。最後だったら全クリア"""
if self.stage_no + 1 < len(STAGES):
self.stage_no += 1
self.load_stage(STAGES[self.stage_no])
else:
self.all_cleared = True
update() の先頭を書き換える
R の 3 行を差し替えて、その前後にブロックを 2 つ足します。
def update(self):
"""フレーム毎の更新処理"""
if self.all_cleared: # ぜんぶ終わった。Enter で 1 面から ← 足す
if pyxel.btnp(pyxel.KEY_RETURN):
self.stage_no = 0
self.all_cleared = False
self.load_stage(STAGES[self.stage_no])
return
if pyxel.btnp(pyxel.KEY_R): # いつでも、このステージをやり直せる
self.load_stage(STAGES[self.stage_no]) # ← STAGE1 から書き換える
return
if self.cleared: # クリアした。Enter で次のステージへ ← 足す
if pyxel.btnp(pyxel.KEY_RETURN):
self.next_stage()
return
if self.walk_timer > 0: # 歩いている途中(ここから下はそのまま)
and not self.cleared は、そのまま残します
「if self.cleared: return を足したのだから、もういらないのでは?」と思うかもしれません。
いります。return が効くのは、フレームの初めからクリアしていたときだけです。
いままさに、このフレームでクリアしたときは通り抜けてしまいます。
フェーズ 5 の 6-2 と同じ話です。
draw() の表示を書き換える
draw() のいちばん下、フェーズ 5 で書いた 5 行を差し替えます。
# 画面の上の表示
if self.all_cleared:
draw_center("ALL CLEAR! ENTER", 3, pyxel.COLOR_YELLOW)
else:
pyxel.text(2, 3, f"STAGE {self.stage_no + 1}/{len(STAGES)}",
pyxel.COLOR_GRAY)
if self.cleared:
draw_center("CLEAR! ENTER", 3, pyxel.COLOR_YELLOW)
地図が 113 行、コードが 25 行の追加です。
# sokoban.py
# step2 フェーズ6:ステージを 10 個にする
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.#",
"#..$.$.#",
"#..@...#",
"########",
]
STAGE2 = [ # 横にも押せる
"########",
"#......#",
"#.$..O.#",
"#.@....#",
"#.$..O.#",
"#......#",
"########",
]
STAGE3 = [ # 縦と横を組み合わせる
"#########",
"#.O...O.#",
"#.......#",
"#..$....#",
"#....$..#",
"#..@....#",
"#.......#",
"#########",
]
STAGE4 = [ # 壁があるので、押す側へ回り込む
"##########",
"#........#",
"#.O.##.O.#",
"#...##...#",
"#..$..$..#",
"#........#",
"#...@....#",
"##########",
]
STAGE5 = [ # 段が仕切られていて、行き来に回り道がいる
"##########",
"#........#",
"#O..$....#",
"#...##...#",
"#O..$....#",
"#...##...#",
"#O..$....#",
"#....@...#",
"##########",
]
STAGE6 = [ # どれから動かすか、順番を考える
"##########",
"#........#",
"#.O.O.O..#",
"#........#",
"#.$......#",
"#..$.....#",
"#...$....#",
"#....@...#",
"##########",
]
STAGE7 = [ # 細い通路。押せる向きが決まってしまう
"##########",
"#....#...#",
"#.$..#.O.#",
"#....#...#",
"#.$......#",
"#....#...#",
"#.@..#.O.#",
"#....#...#",
"##########",
]
STAGE8 = [ # 真ん中のゴールへ、四方から運ぶ
"############",
"#..........#",
"#.$......$.#",
"#....##....#",
"#...OOOO...#",
"#....##....#",
"#.$......$.#",
"#.....@....#",
"############",
]
STAGE9 = [ # 中央の壁を大きく回る
"###########",
"#.........#",
"#.OO..$$..#",
"#.........#",
"#...###...#",
"#.........#",
"#.OO..$$..#",
"#....@....#",
"###########",
]
STAGE10 = [ # 仕上げ。四隅へ運ぶ
"###########",
"#O.......O#",
"#.........#",
"#...$.$...#",
"#..#...#..#",
"#...$.$...#",
"#.........#",
"#O...@...O#",
"###########",
]
STAGES = [STAGE1, STAGE2, STAGE3, STAGE4, STAGE5,
STAGE6, STAGE7, STAGE8, STAGE9, STAGE10]
def draw_center(text, y, color):
"""文字を、画面の横まんなかに描く"""
x = (SCREEN_WIDTH - len(text) * 4) // 2
pyxel.text(x, y, text, color)
class App:
def __init__(self):
"""起動時の設定"""
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Sokoban")
pyxel.load("sokoban.pyxres")
self.stage_no = 0 # いま何番目のステージか(0 から数える)
self.all_cleared = False # 10 個ぜんぶ終わったか
self.load_stage(STAGES[self.stage_no])
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 self.all_cleared: # ぜんぶ終わった。Enter で 1 面から
if pyxel.btnp(pyxel.KEY_RETURN):
self.stage_no = 0
self.all_cleared = False
self.load_stage(STAGES[self.stage_no])
return
if pyxel.btnp(pyxel.KEY_R): # いつでも、このステージをやり直せる
self.load_stage(STAGES[self.stage_no])
return
if self.cleared: # クリアした。Enter で次のステージへ
if pyxel.btnp(pyxel.KEY_RETURN):
self.next_stage()
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 next_stage(self):
"""次のステージへ進む。最後だったら全クリア"""
if self.stage_no + 1 < len(STAGES):
self.stage_no += 1
self.load_stage(STAGES[self.stage_no])
else:
self.all_cleared = True
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.all_cleared:
draw_center("ALL CLEAR! ENTER", 3, pyxel.COLOR_YELLOW)
else:
pyxel.text(2, 3, f"STAGE {self.stage_no + 1}/{len(STAGES)}",
pyxel.COLOR_GRAY)
if self.cleared:
draw_center("CLEAR! ENTER", 3, pyxel.COLOR_YELLOW)
App()
cd step2
python sokoban.py
| やってみること | こうなれば成功 |
|---|---|
| 起動する | 画面の左上に STAGE 1/10 と出る。盤面はいままでと同じ |
| 1 面を解く | CLEAR! ENTER が出る |
Enter を押す | 2 面が出る。左上が STAGE 2/10 になり、盤面の形も変わる |
| 3 面へ進む | 盤面が大きくなり、位置も真ん中のままずれない |
2 面の途中で R | 2 面の最初に戻る(1 面には戻らない) |
Enter を押しっぱなし | 1 つだけ進む。一気に飛ばない |
| 10 面を解く | CLEAR! ENTER → Enter → ALL CLEAR! ENTER |
全クリアで Enter | 1 面に戻る。最初から遊び直せる |
| 面 | 大きさ | 荷物 | 押し | 矢印 | ねらい |
|---|---|---|---|---|---|
| 1 | 8×6 | 2 | 2 | 7 | 押すことを覚える |
| 2 | 8×7 | 2 | 6 | 13 | 横にも押せる |
| 3 | 9×8 | 2 | 7 | 20 | 縦と横を組み合わせる |
| 4 | 10×8 | 2 | 6 | 22 | 壁を回り込む |
| 5 | 10×9 | 3 | 9 | 40 | 仕切りがあって遠回り |
| 6 | 10×9 | 3 | 12 | 35 | どれから動かすか |
| 7 | 10×9 | 2 | 16 | 34 | 細い通路 |
| 8 | 12×9 | 4 | 18 | 76 | 真ん中のゴールへ四方から |
| 9 | 11×9 | 4 | 20 | 82 | 中央の壁を大きく回る |
| 10 | 11×9 | 4 | 20 | 57 | 四隅へ運ぶ |
0 から数える番号と、1 から数える表示self.stage_no = 0 # 中では 0 から
f"STAGE {self.stage_no + 1}/{len(STAGES)}" # 見せるときだけ +1
リストの番号は 0 から始まります。STAGES[0] が 1 面です。
でも遊ぶ人に「STAGE 0」と見せるのは変なので、描くときだけ +1 します。
この +1 は、なるべく 1 か所にまとめます。
あちこちで足したり引いたりすると、どちらの数なのか分からなくなります。
「中はいつも 0 から。画面に出すときだけ +1」と決めておくと迷いません。
10 と書かずに len(STAGES) と書くif self.stage_no + 1 < len(STAGES): # 「まだ次がある?」
ここを if self.stage_no + 1 < 10: と書いても、いまは同じように動きます。
でも11 個目を足した瞬間に、10 面で全クリアになってしまいます。
len(STAGES) と書いておけば、地図を 1 つ足すだけで、
どこも直さずに 11 ステージのゲームになります。
画面の STAGE 3/10 の 10 も、同じ理由で len(STAGES) です。
GHOST_MOVE_EVERY[len(self.ghosts)] という書き方をしました。
いま何体いるかを、リストの長さから取り出すやり方です。
自分で数を書くと、本体とその数の 2 か所を、いつもそろえる義務が生まれます。
数えさせれば、その義務は消えます。
update() の頭に、早い return が 3 つ並ぶ
update() の上半分は、「ふつうの操作ではないもの」を先に片づけて帰る形になりました。
| 順番 | 見るもの | なぜその順か |
|---|---|---|
| 1 | 全クリアしているか | いちばん特別な状態。ここでは R も矢印も意味がない |
| 2 | R が押されたか |
歩いている途中でも効かせたいので、歩きの処理より前 |
| 3 | ステージをクリアしたか | Enter だけを受け付ける。矢印はもう見ない |
| 4 | ふつうの操作 | 歩きを進めて、キーを見る |
特別な場合を上から順に return で落としていくと、いちばん下に
「ふつうのとき」だけが残ります。
if を何重にも入れ子にするより、ずっと読みやすくなります。
R の判定を if self.cleared: のあとに置くと、
クリアしたあとに R が効かなくなります
(cleared の return で引き返してしまうため)。return は読みやすい反面、並べた順がそのまま優先順位になります。
足すときは「これは、どれより先に見るべきか」を必ず考えてください。
| 置き場所 | いつ実行されるか | 何を置くか |
|---|---|---|
__init__() | 起動のとき 1 回だけ | stage_no、all_cleared |
load_stage() | ステージが変わるたび(R も含む) |
tiles、boxes、player_x/y、
walk_timer、pushing、cleared、ox/oy |
update() の中 | 毎フレーム | 途中で変わる値(walk_timer を増やす、など) |
新しい変数を足すときは、まずこの表のどこに置くかを決めます。
迷ったら「R を押したとき、これは戻ってほしいか?」と考えてください。
戻ってほしいなら load_stage()、覚えていてほしいなら __init__() です。
ここからはおまけです。10 個の地図はもう用意してありますが、 作りたくなったときのために、考え方と道具を置いておきます。
倉庫番の地図づくりでいちばん大事なのは、「荷物が二度と動かせなくなる形」を知ることです。
作った地図が本当に解けるのかを、手で確かめるのは大変です。
リポジトリの .todo/solve.py が調べてくれます。
python .todo/solve.py # step2/sokoban.py の地図をぜんぶ調べる
python .todo/solve.py 7 # 7 番目だけ調べて、解き方も表示する
D:\claude\learning_pyxel\step2\sokoban.py から 10 個のステージを読みました
STAGE1 8x6 荷物2/ゴール2 ✅ 押し 2 回 / 矢印 7 回
STAGE2 8x7 荷物2/ゴール2 ✅ 押し 6 回 / 矢印 13 回
...
STAGE10 11x9 荷物4/ゴール4 ✅ 押し 20 回 / 矢印 57 回
10 個中 10 個が解けました
sokoban.py を実行せずに読んでいるので、走らせてもゲームは始まりません。
地図を書き換えたら、そのまま走らせるだけです。
| 出てくるもの | 意味 |
|---|---|
| ✅ 押し ○ 回 / 矢印 ○ 回 | 解けます。押しは荷物を押した回数、矢印はそのときの矢印キーの回数。 押しが多いほど頭を使い、矢印が多いほど歩き回ります |
| ❌ 解けません | どう動かしてもゴールに全部は乗りません。作り直しです |
| ⚠ 荷物 ○ 個 / ゴール ○ 個 | 数が合っていません。$ か O の打ち間違いです |
| ⚠ ○ 通りまで調べても… | 盤面が広すぎるか、荷物が多すぎます。少し小さくしてください |
load_stage() は、ゴールの上に荷物や主人公が最初から乗っている状態を読めません。
ふつうの倉庫番では *(ゴールの上の荷物)や +(ゴールの上の主人公)という
文字を使いますが、いまは対応していないので使わずに作ってください。load_stage() の if に
2 つ足して、mark = "O" を残すようにするだけです。
| 症状 | 原因と対処 |
|---|---|
R を押すと 1 面に戻ってしまう |
self.load_stage(STAGE1) のままです。
STAGES[self.stage_no] に書き換えてください |
クリアしても Enter で進まない |
if self.cleared: のブロックを書いていないか、
update() のいちばん下に置いています。
歩きの処理より前です |
Enter を 1 回押したら 2〜3 面まとめて進む |
btn になっています。btnp です |
10 面を解いても ALL CLEAR! にならない |
next_stage() の if が
<= になっていませんか。< です。
stage_no + 1 < len(STAGES) |
IndexError: list index out of range |
同じ原因です。STAGES[10] を読もうとしています(番号は 0〜9) |
| 2 面に進むと、盤面が画面からはみ出す/ずれる | load_stage() の ox / oy の計算を消していませんか。
毎回そこで計算しているから、大きさが変わっても真ん中に来ます |
IndexError が、ある面だけで出る |
その地図の行の長さがそろっていません。
短い行があると、tiles[y][x] でその行だけはみ出します |
| ある面が、どうしても解けない | 写し間違いかもしれません。python .todo/solve.py で確かめてください
(7-2) |
左上の表示が STAGE 0/10 から始まる |
self.stage_no + 1 の + 1 を書いていません |
| ステージが変わっても表示が変わらない | f"..." の f を書き忘れていませんか。
f がないと、{} がそのまま文字として出ます |
全クリアのあと Enter を押しても何も起きない |
self.all_cleared = False に戻していません。
True のままなので、ずっと全クリア画面です |
NameError: name 'draw_center' is not defined |
def draw_center(...) を class App: の中に書いています。
外(手前)です |
これでゲームとして遊べるものが完成しました。10 面あって、解けて、やり直せて、 全部クリアできます。
残り 5 つのフェーズは、遊びやすさと仕上げです。 次はタイトル画面とステージ選択——起動していきなり 1 面が始まるのではなく、 どの面から遊ぶかを選べるようにします。
self.scene に
「いまどの画面か」を入れて、update() と draw() で振り分けます。
今回はその復習です。stage_no をそのまま動かせばよいので、じつは
もう半分できています。