フェーズ 7 タイトル画面ともう一度あそぶ

画面をもう 1 つ増やし、「初期状態へ戻す」を関数にまとめる

トップページstep1 概要 / フェーズ 7

ゴール

タイトル画面を足して3 画面にし、何度でもあそべるようにします。 これで step1 のゲームとしての形が完成します。 残るフェーズ 8〜10 は、お化けを増やす・音をつける・公開する、という上積みです。

画面の切りかえ自体はフェーズ 6 と同じ形をもう 1 本増やすだけです。 新しいのは「やり直すときに、変数を最初の状態へ戻す」という話のほうです。

1. 考え方

1-1. 画面が 3 つになっても、やることは同じ

scene に入る値が 1 つ増え、update()draw() の 枝が 1 本増えるだけです。フェーズ 6 で 「値そのものに意味はない」と書いたとおり、番号を振り直す必要はありません。

SCENE_PLAY = 0          # あそんでいる画面
SCENE_GAMEOVER = 1      # ゲームオーバーの画面
SCENE_TITLE = 2         # タイトル画面      ← 足すのはこの 1 行
タイトル SCENE_TITLE 起動したとき 1 回だけ ゲーム SCENE_PLAY ゲームオーバー SCENE_GAMEOVER Enter reset_game() 捕まった gameover_frame Enter(0.5 秒たってから)+ reset_game() ゲームオーバーからはタイトルを経由せず、そのままゲームへ戻る いまのタイトル画面はゲームへ進む以外の道がないので、通っても Enter を 1 回ぶん損するだけ。
ゲームオーバーからは、タイトルを通らずにゲームへ戻る(→ 1-6

1-2. 「やり直す」とは、変数を最初の値に戻すこと

2 回目をあそぶとき、スコアが前回の 12 のままだったり、 お化けが男の子の目の前にいるままだったりしては困ります。

「やり直し」に特別な機能はありません。変数を最初の値に代入し直すだけです。 そしてその「最初の値」は、すでに __init__() に書いてあります。

1-3. __init__() から、初期化だけを取り出す

いまの __init__() は、性質の違う 2 つの仕事をしています。

仕事いつやるか
pyxel.init() / pyxel.load() / pyxel.run() 起動のとき 1 回だけ。2 回目はない
男の子の位置・お化けの位置・スコア・お菓子 あそび始めるたび毎回

混ざっているので、後半だけを reset_game() という名前で取り出します。 やることが変わるわけではありません。置き場所を変えて、 あとから呼び直せるようにするだけです。

時間 → 起動 1 回目を始める 捕まる Enter で 2 回目へ __init__() init / load / run = 画面と素材の用意 ← ここでしか動かない(1 回だけ) reset_game() 男の子・お化け・スコア・お菓子 = 中身の初期化 ← 始めるたびに動く(何回でも) ※ 起動直後にも 1 回呼んでおく。タイトル画面の裏で「置き場所」を作っておくため
__init__() は 1 回だけ、reset_game() は始めるたび

1-4. btnp() を使う(btn() ではない)

フェーズ 2 で出てきた区別が、ここで効いてきます。

意味ここで使うと
btn()押されている(毎フレーム True) Enter を押した指を離す前に、タイトル → ゲーム → …と一気に進んでしまう
btnp()押された瞬間だけ True 1 回押したら 1 回だけ進む。こちらを使います

1-5. 捕まった直後は、キーを受け付けない

逃げているときは矢印キーを連打しています。その勢いで Enter を押していると、 GAME OVER の文字を見る前に、次のゲームが始まってしまいます

フェーズ 6 で入れた「0.5 秒待ってから文字を出す」のと同じ待ち時間を、 キーの受付にも使います。文字が出ていない間は、押しても何も起きないようにします。

同じ数字が 2 か所に出たら、定数へ格上げ 待ちフレーム数の 15 が、 「文字を出す判定」と「キーを受け付ける判定」の 2 か所に必要になりました。 フェーズ 4 で決めた基準どおり、GAMEOVER_WAIT = 15 という名前を付けます。 点滅の 3020 も、タイトルとゲームオーバーの 2 か所で使うので同じように格上げします。

1-6. ゲームオーバーから、どこへ戻すか

「もう一度あそぶ」には、行き先が 2 通り考えられます。

戻り先操作の回数向いている場合
タイトルへ戻す Enter を 2 回(タイトルでもう 1 回押す) タイトルにゲームを始める以外の道があるとき。 難易度を選ぶ、あそび方を読む、ハイスコアを見る、など
そのままゲームへ戻す Enter を 1 回 タイトルが「始める」ボタンでしかないとき。いまはこちら

いまのタイトル画面には、ゲームへ進む以外の道がありません。 そこを経由させるとプレイヤーは意味のない Enter を 1 回よけいに押すことになります。 だからゲームオーバー → Enter → すぐ再挑戦にします。 アーケードゲームでよくある形です。

判断の基準は「そこに分岐があるか」 画面を経由させる価値は、その画面で選べることがあるかどうかで決まります。 選択肢が 1 つしかない画面は、ただの通過点です。
逆に言うと、あとでタイトルに何かを足したくなったら、この判断は変わります。 そのときは「ゲームオーバー → Enter で再挑戦 / Q でタイトルへ」のように キーを 2 つに分けるのが定石です。
    def update_gameover(self):
        """ゲームオーバー画面。少し待ってから、Enter でもう一度始める"""
        if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:
            return

        if pyxel.btnp(pyxel.KEY_RETURN):
            self.reset_game()          # ← タイトルへ行かないので、ここで初期化する
            self.scene = SCENE_PLAY
reset_game() の呼び忘れに注意 タイトルへ戻す形なら初期化は update_title() の 1 か所で済みますが、 直接ゲームへ戻すならこちらにも書く必要があります。 忘れるとスコアが前回のまま、お化けが目の前にいる状態で再開してしまいます。
タイトル画面は起動時にしか出なくなります これは意図した結果です。ゲームの名前を見せる役目は最初の 1 回で果たしています。 ただし後述のハイスコアのように「タイトルに置きたくなるもの」が出てきたときは、 ゲームオーバー画面のほうに置くほうが目に入ります。

2. このフェーズで使う機能

新しい API は pyxel.btnp() だけで、これもフェーズ 2 で一度出ています。

機能役割
pyxel.btnp()キーが押された瞬間を拾う
pyxel.KEY_RETURNEnter キー
メソッドの切り出しreset_game() — 初期化を呼び直せる形にする
if / elif / else3 つの画面へ振り分ける
値を書かない returnその先を実行せずに関数を抜ける(フェーズ 6 で登場)
pyxel.COLOR_YELLOW / COLOR_GRAY色の番号 10 と 13

3. 手順

3-1. 定数を足す

SCENE_PLAY = 0          # あそんでいる画面
SCENE_GAMEOVER = 1      # ゲームオーバーの画面
SCENE_TITLE = 2         # タイトル画面

GAMEOVER_WAIT = 15      # 捕まってから文字を出すまでのフレーム数(0.5 秒)
BLINK_CYCLE = 30        # 点滅 1 周期のフレーム数(1 秒)
BLINK_ON = 20           # そのうち文字が見えているフレーム数

3-2. 点滅の判定を関数にする

draw_center() の下に置きます。class の外です。 self を使わないので外に出せる——フェーズ 6 と同じ基準です。

def is_blink_on():
    """点滅の「見えている」タイミングなら True を返す"""
    return pyxel.frame_count % BLINK_CYCLE < BLINK_ON

3-3. __init__() から初期化を取り出す

いまの __init__()まん中の 6 行を、そのまま reset_game() へ移します。中身は 1 文字も変えません。

    def __init__(self):
        """起動時の設定"""
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Candy Hunt")
        pyxel.load("candy_hunt.pyxres")

        self.scene = SCENE_TITLE        # ← 起動したらタイトルから
        self.reset_game()

        pyxel.run(self.update, self.draw)

    def reset_game(self):
        """ゲームの中身を、最初の状態に戻す"""
        self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2
        self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2
        self.ghost_x = 0
        self.ghost_y = 0
        self.score = 0
        self.place_candy()

3-4. update() の枝を 3 本にする

    def update(self):
        """フレーム毎の更新処理"""
        if self.scene == SCENE_TITLE:
            self.update_title()
        elif self.scene == SCENE_PLAY:
            self.update_play()
        else:
            self.update_gameover()

3-5. タイトルとゲームオーバーの操作を書く

    def update_title(self):
        """タイトル画面。Enter でゲームを始める"""
        if pyxel.btnp(pyxel.KEY_RETURN):
            self.reset_game()
            self.scene = SCENE_PLAY

    def update_gameover(self):
        """ゲームオーバー画面。少し待ってから、Enter でもう一度始める"""
        if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:
            return

        if pyxel.btnp(pyxel.KEY_RETURN):
            self.reset_game()
            self.scene = SCENE_PLAY

どちらも reset_game() を呼んでから SCENE_PLAY にします。 入口が 2 つ(タイトルから/ゲームオーバーから)あっても、 始めるときにやることは同じだからです。 関数に切り出しておいた効きめが、ここで出ます。

3-6. draw() の枝を 3 本にする

タイトルは盤面を描きません。そこだけフェーズ 6 と形が変わります。

    def draw(self):
        """フレーム毎の描画処理"""
        pyxel.cls(pyxel.COLOR_BLACK)

        if self.scene == SCENE_TITLE:
            self.draw_title()
            return

        self.draw_play()

        if self.scene == SCENE_GAMEOVER:
            self.draw_gameover()

    def draw_title(self):
        """タイトル画面を描く"""
        draw_center("CANDY HUNT", 44, pyxel.COLOR_YELLOW)

        if is_blink_on():
            draw_center("PRESS ENTER", 68, pyxel.COLOR_WHITE)

3-7. ゲームオーバーに「次どうすればいいか」を出す

すでに書いてある draw_gameover() を、 定数を使う形に直して、案内の 1 行を足します。

    def draw_gameover(self):
        """ゲームオーバーの文字を重ねて描く"""
        if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:
            return

        if is_blink_on():
            draw_center("GAME OVER", 52, pyxel.COLOR_RED)

        draw_center(f"SCORE {self.score}", 66, pyxel.COLOR_WHITE)
        draw_center("PRESS ENTER TO RETRY", 80, pyxel.COLOR_GRAY)
点滅させるのは 1 つだけにします GAME OVER は点滅、SCOREPRESS ENTER は出しっぱなしです。 いくつも点滅すると、どれを見ればいいのか分からなくなります。 目立たせたいものを 1 つ選ぶ、というのが点滅の使い方です。

4. できあがりのコード

ここまでを反映した全体です。行が増えるより、置き場所が変わるのが中心です。

# candy_hunt.py
# step1 フェーズ7:タイトル画面を足して、もう一度あそべるようにする

import random

import pyxel

SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120

BOY_SIZE = 8            # 男の子の絵の大きさ(縦横とも 8 ドット)
BOY_SPEED = 2           # 1 フレームで進むドット数

CANDY_SIZE = 8          # お菓子の絵の大きさ

GHOST_SIZE = 8          # お化けの絵の大きさ
GHOST_SPEED = 1         # 1 フレームで進むドット数(男の子の半分)

HIT_SIZE = 4            # 当たり判定に使う四角の大きさ(絵の中央だけを見る)
HIT_OFFSET = (BOY_SIZE - HIT_SIZE) // 2     # 絵の左上から、判定の四角までの距離(= 2)

SCENE_PLAY = 0          # あそんでいる画面
SCENE_GAMEOVER = 1      # ゲームオーバーの画面
SCENE_TITLE = 2         # タイトル画面

GAMEOVER_WAIT = 15      # 捕まってから文字を出すまでのフレーム数(0.5 秒)
BLINK_CYCLE = 30        # 点滅 1 周期のフレーム数(1 秒)
BLINK_ON = 20           # そのうち文字が見えているフレーム数


def is_hit(x1, y1, size1, x2, y2, size2):
    """2 つの四角が重なっていれば True を返す"""
    return (x1 < x2 + size2 and x2 < x1 + size1 and
            y1 < y2 + size2 and y2 < y1 + size1)


def draw_center(s, y, col):
    """文字列を画面の横中央に描く"""
    x = (SCREEN_WIDTH - len(s) * 4) // 2     # 4 は 1 文字分のフォント幅
    pyxel.text(x, y, s, col)


def is_blink_on():
    """点滅の「見えている」タイミングなら True を返す"""
    return pyxel.frame_count % BLINK_CYCLE < BLINK_ON


class App:
    def __init__(self):
        """起動時の設定"""
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Candy Hunt")
        pyxel.load("candy_hunt.pyxres")

        self.scene = SCENE_TITLE
        self.reset_game()

        pyxel.run(self.update, self.draw)

    def reset_game(self):
        """ゲームの中身を、最初の状態に戻す"""
        self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2
        self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2
        self.ghost_x = 0
        self.ghost_y = 0
        self.score = 0
        self.place_candy()

    def update(self):
        """フレーム毎の更新処理"""
        if self.scene == SCENE_TITLE:
            self.update_title()
        elif self.scene == SCENE_PLAY:
            self.update_play()
        else:
            self.update_gameover()

    def update_title(self):
        """タイトル画面。Enter でゲームを始める"""
        if pyxel.btnp(pyxel.KEY_RETURN):
            self.reset_game()
            self.scene = SCENE_PLAY

    def update_play(self):
        """あそんでいる間の更新"""
        self.move_boy()
        self.move_ghost()
        self.check_candy()
        self.check_ghost()

    def update_gameover(self):
        """ゲームオーバー画面。少し待ってから、Enter でもう一度始める"""
        if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:
            return

        if pyxel.btnp(pyxel.KEY_RETURN):
            self.reset_game()
            self.scene = SCENE_PLAY

    def move_boy(self):
        """矢印キーで男の子を動かす"""
        if pyxel.btn(pyxel.KEY_LEFT):
            self.boy_x -= BOY_SPEED
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.boy_x += BOY_SPEED
        if pyxel.btn(pyxel.KEY_UP):
            self.boy_y -= BOY_SPEED
        if pyxel.btn(pyxel.KEY_DOWN):
            self.boy_y += BOY_SPEED

        self.boy_x = max(0, min(self.boy_x, SCREEN_WIDTH - BOY_SIZE))
        self.boy_y = max(0, min(self.boy_y, SCREEN_HEIGHT - BOY_SIZE))

    def move_ghost(self):
        """お化けを男の子に近づける"""
        if self.ghost_x < self.boy_x:
            self.ghost_x += GHOST_SPEED
        elif self.ghost_x > self.boy_x:
            self.ghost_x -= GHOST_SPEED

        if self.ghost_y < self.boy_y:
            self.ghost_y += GHOST_SPEED
        elif self.ghost_y > self.boy_y:
            self.ghost_y -= GHOST_SPEED

    def place_candy(self):
        """男の子と重ならない場所へ、お菓子を置きなおす"""
        while True:
            self.candy_x = random.randint(0, SCREEN_WIDTH - CANDY_SIZE)
            self.candy_y = random.randint(0, SCREEN_HEIGHT - CANDY_SIZE)
            if not is_hit(self.boy_x, self.boy_y, BOY_SIZE,
                          self.candy_x, self.candy_y, CANDY_SIZE):
                break

    def check_candy(self):
        """お菓子にしっかり重なったら、スコアを増やして次の場所に置きなおす"""
        if is_hit(self.boy_x + HIT_OFFSET, self.boy_y + HIT_OFFSET, HIT_SIZE,
                  self.candy_x + HIT_OFFSET, self.candy_y + HIT_OFFSET, HIT_SIZE):
            self.score += 1
            self.place_candy()

    def check_ghost(self):
        """お化けに捕まったらゲームオーバーにする"""
        if is_hit(self.boy_x + HIT_OFFSET, self.boy_y + HIT_OFFSET, HIT_SIZE,
                  self.ghost_x + HIT_OFFSET, self.ghost_y + HIT_OFFSET, HIT_SIZE):
            self.scene = SCENE_GAMEOVER
            self.gameover_frame = pyxel.frame_count     # 捕まった時刻を覚えておく

    def draw(self):
        """フレーム毎の描画処理"""
        pyxel.cls(pyxel.COLOR_BLACK)

        if self.scene == SCENE_TITLE:
            self.draw_title()
            return

        self.draw_play()

        if self.scene == SCENE_GAMEOVER:
            self.draw_gameover()

    def draw_title(self):
        """タイトル画面を描く"""
        draw_center("CANDY HUNT", 44, pyxel.COLOR_YELLOW)

        if is_blink_on():
            draw_center("PRESS ENTER", 68, pyxel.COLOR_WHITE)

    def draw_play(self):
        """あそんでいる画面を描く"""
        pyxel.blt(self.candy_x, self.candy_y, 0, 16, 0,
                  CANDY_SIZE, CANDY_SIZE, pyxel.COLOR_BLACK)    # お菓子
        pyxel.blt(self.ghost_x, self.ghost_y, 0, 8, 0,
                  GHOST_SIZE, GHOST_SIZE, pyxel.COLOR_BLACK)    # お化け
        pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
                  BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK)        # 男の子
        pyxel.text(4, 4, f"SCORE {self.score}", pyxel.COLOR_WHITE)               # スコア

    def draw_gameover(self):
        """ゲームオーバーの文字を重ねて描く"""
        if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:    # 0.5 秒は文字を出さない
            return

        if is_blink_on():
            draw_center("GAME OVER", 52, pyxel.COLOR_RED)

        draw_center(f"SCORE {self.score}", 66, pyxel.COLOR_WHITE)
        draw_center("PRESS ENTER TO RETRY", 80, pyxel.COLOR_GRAY)


App()
動かして確かめること

5. コードの解説

起動直後にも reset_game() を呼ぶ理由

最初はタイトル画面なので、男の子もお菓子もまだ描きません。 それでも __init__() で 1 回呼んでおきます。

        self.scene = SCENE_TITLE
        self.reset_game()           # ← タイトルなのに呼ぶ

呼ばないと、self.boy_x などの変数がまだ 1 度も作られていない状態になります。 Python では 代入したときに初めて変数ができるので、 うっかりどこかで読もうとすると AttributeError になります。置き場所を先に作っておくための 1 行です。

place_candy() は男の子の位置を必要とします reset_game() の中で place_candy()最後に呼んでいるのは、 お菓子を「男の子と重ならない場所」に置くために、 先に男の子の位置が決まっていないといけないからです。 順番を入れかえると、前回の位置を基準にしてしまいます。

draw() の途中で return する

        if self.scene == SCENE_TITLE:
            self.draw_title()
            return                  # ← ここで終わり。この下は実行されない

        self.draw_play()

タイトルのときだけ「盤面を描かずに終わる」ので、return で抜けています。 else を使って下をまるごと囲んでも同じ動きになりますが、 インデントが 1 段深くなるぶん読みにくくなります。

「例外的なケースを先に片づけて、あとは本筋だけ書く」—— これは return のよくある使い方で、 フェーズ 6 の draw_gameover() でも同じ形を使いました。

else にしてよい場所、いけない場所

update() の 3 本目は else で受けています。

        if self.scene == SCENE_TITLE:
            self.update_title()
        elif self.scene == SCENE_PLAY:
            self.update_play()
        else:                          # = SCENE_GAMEOVER のとき
            self.update_gameover()

scene には3 つの値しか入らないと分かっているので、 残りは 1 つに決まります。フェーズ 5 の move_ghost()else使えなかったのとは事情が違います。あちらは 「小さい・大きい・等しい」の 3 通りがあり、 else にすると「等しい」が紛れこんでしまうからでした。

どちらでもよいときは、読みやすいほうで elif self.scene == SCENE_GAMEOVER: と書いてもかまいません。 3 つ並べたほうが対称に見えて分かりやすいと感じるなら、そちらでも正解です。

待ち時間が、文字とキーの両方に効いている

同じ GAMEOVER_WAIT を、描くほう受け付けるほうの 2 か所で見ています。

場所していること
draw_gameover()まだ文字を出さない
update_gameover()まだ Enter を受け付けない

この 2 つがそろっていることが大事です。 「見えていないものは押せない」—— 画面に出ていない案内を押せてしまうと、プレイヤーは 「何が起きたのか分からないうちに画面が変わった」と感じます。

そのままゲームへ戻すなら、この待ちはいっそう重要です タイトルへ戻すだけなら、うっかり押しても実害は多くありません。 ですがそのまま次のゲームが始まるとなると、逃げているときの連打が そのままスコアを見る前の再スタートになってしまいます。

6. もう一歩(やらなくても大丈夫)

点滅の「間」を毎回そろえる

いまの点滅は pyxel.frame_count をそのまま見ているので、 捕まったタイミングによって、文字が出るまでの間がずれます。 実際に測ると 0.50 秒のときも 0.83 秒のときもあります (待ちの 0.5 秒が明けた瞬間、たまたま点滅の「消えている側」だと、そのぶん延びるため)。

毎回きっちりそろえたいなら、捕まった時刻からの経過で点滅させます。

    def draw_gameover(self):
        """ゲームオーバーの文字を重ねて描く"""
        elapsed = pyxel.frame_count - self.gameover_frame       # 捕まってからの経過
        if elapsed < GAMEOVER_WAIT:
            return

        if (elapsed - GAMEOVER_WAIT) % BLINK_CYCLE < BLINK_ON:  # 待ち明けを起点に点滅
            draw_center("GAME OVER", 52, pyxel.COLOR_RED)
            draw_center("PRESS ENTER TO RETRY", 80, pyxel.COLOR_GRAY)

        draw_center(f"SCORE {self.score}", 66, pyxel.COLOR_WHITE)

elapsed という変数に入れておくと、同じ引き算を 2 回書かずに済み、 式の意味も読み取りやすくなります。

- GAMEOVER_WAIT を引くところが肝心です ただ elapsed % BLINK_CYCLE と書くと、間の長さは毎回 0.50 秒でそろいますが、 最初のひと光りが 5 フレーム(0.17 秒)しか続きません。 待ちが明けた瞬間、点滅の周期はすでに 15 フレームぶん進んでいて、 「見えている 20 フレーム」の残り 5 フレームに飛びこむからです。
待ちが明けた時点を 0 と数え直すと、最初から 20 フレーム(0.67 秒) しっかり光ります。実際に並べるとこうなります( が見えているフレーム)。
elapsed % BLINK_CYCLE             ・・・・・・・・・・・・・・・●●●●●・・・・・・・・・・●●●●●●●●●●
(elapsed - GAMEOVER_WAIT) % ...    ・・・・・・・・・・・・・・・●●●●●●●●●●●●●●●●●●●●・・・・・
案内の文字も一緒に点滅させるかどうか PRESS ENTER TO RETRYif の中に入れると、 GAME OVER と息を合わせて光ります。2 つが同時に動くぶんには 「どちらを見ればいいか分からない」にはならず、ひとかたまりの合図として読めます。 外に出して出しっぱなしにすれば、案内はいつでも読めます。好みで選んでください。
どちらが良いというものではありません 全体で点滅がそろっているほうが気持ちよい、という考え方もあります。 遊んでみて、しっくりくるほうを選んでください。

ハイスコアを覚えておく

reset_game()消してはいけない値、という例になります。

    def __init__(self):
        ...
        self.high_score = 0         # ← reset_game ではなく __init__ に置く
        self.scene = SCENE_TITLE
        self.reset_game()
    def check_ghost(self):
        """お化けに捕まったらゲームオーバーにする"""
        if is_hit(...):
            self.scene = SCENE_GAMEOVER
            self.gameover_frame = pyxel.frame_count
            self.high_score = max(self.high_score, self.score)   # 最高記録を更新
    def draw_gameover(self):
        """ゲームオーバーの文字を重ねて描く"""
        if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:
            return

        if is_blink_on():
            draw_center("GAME OVER", 52, pyxel.COLOR_RED)

        draw_center(f"SCORE {self.score}", 66, pyxel.COLOR_WHITE)
        draw_center(f"HI-SCORE {self.high_score}", 76, pyxel.COLOR_YELLOW)
        draw_center("PRESS ENTER TO RETRY", 90, pyxel.COLOR_GRAY)
タイトルではなく、ゲームオーバー画面に出します 1-6 で決めたとおり、タイトルは起動時にしか出ません。 毎回目に入るのはゲームオーバー画面のほうなので、 記録を見せたい場所はこちらです。

どこに変数を置くかが、そのまま「いつ消えるか」を決めます。 reset_game() に入れれば毎回 0 に戻り、 __init__() に入れればアプリを閉じるまで残ります。

アプリを閉じても残したい場合 ファイルに書き出す必要があります。step1 では扱いませんが、 Python の標準機能(open() で書いて読む)だけでできます。

7. うまくいかないときは

症状見るところ
起動するといきなりゲームが始まる __init__()self.sceneSCENE_PLAY のままです。SCENE_TITLE にします
Enter を押すとタイトルとゲームを行き来してしまう btn() になっています。btnp() にします
2 回目のスコアが 0 から始まらない update_gameover()self.reset_game() を 呼んでいるか確認します。self.scene を変えるだけでは戻りません。 update_title() と 2 か所あります
2 回目にお化けが目の前から始まる 同じく reset_game() の呼び忘れです。 ghost_x / ghost_y が前回のまま残っています
タイトル画面に男の子やお化けが出ている draw()return を書き忘れています。 draw_title() のあと、そのまま draw_play() まで進んでいます
ゲームオーバーで Enter が効かない update()else の中で self.update_gameover() を呼んでいるか確認します。 あるいは 0.5 秒の待ち時間中に押しています(仕様どおりです)
AttributeError: ... 'gameover_frame' フェーズ 6 の check_ghost()self.gameover_frame = pyxel.frame_count を書き忘れています
AttributeError: ... 'boy_x' __init__()self.reset_game() を書き忘れています。 定義しただけでは呼ばれません
タイトルの文字が中央からずれる draw_center() ではなく pyxel.text() を 直接呼んでいませんか

8. 次のフェーズ

これでゲームとして一周する形ができました。あそんで、捕まって、 Enter 1 回でまた始められます

次のフェーズ 8 ではお菓子を集めるほどお化けが増えます(最大 4 体)。 ここでリストが登場します。ghost_x / ghost_y という 変数 2 つで 1 体を持つやり方は、4 体になると破綻するからです。 「同じものがいくつもある」ときはリストにする——step1 最後の山場です。

お化けの速さは、ここで見直します 備忘録 #chase-speed に書いたとおり、 GHOST_SPEED = 11 体でも全力で逃げて 5 秒という速さです。 4 体になったら明らかに厳しいので、フェーズ 8 で 速さ・増えるペース・出てくる場所をまとめて調整します。