Python プログラミング教室

2025年11月29日の実践コースはPythonでスイカゲームのステップ3です

『Scratchで諦めたシリーズ』ということでス〇カゲームに挑戦しています。

前回作ったプログラムを今回のスタート地点にします。まずは以下のコードをコピーして実行してみよう!ファイル名の最後は「.py」にすることをお忘れなく!

import sys                                # 標準ライブラリ:終了処理などに使用
import pygame                             # Pygame:描画と入力

W, H = 400, 600                           # 画面サイズ(幅・高さ)
FPS = 60                                  # フレームレート
DROP_MIN_X = 60                           # 落下可能な最小X(左の壁から少し内側)
DROP_MAX_X = W - 60                       # 落下可能な最大X(右の壁から少し内側)
DROP_Y = 60                               # 投入(落下開始)高さ
TOP_LINE_Y = 100                          # 天井ライン(ゲームオーバー判定用の目安)

# ------------------------------------------------------------
# Player:ユーザが操作できるオブジェクト
# ------------------------------------------------------------
class Player:
    def __init__(self):
        self.aim_x = (DROP_MIN_X + DROP_MAX_X) // 2         # 照準Xの初期位置(中央寄り)
        self.aim_y = DROP_Y                                 # 照準Yは固定(投入高さ)
        self.line_top_y = 0                                 # 照準縦線の開始Y
        self.line_bottom_y = 60                             # 照準縦線の終了Y(見た目用)
        self.line_width = 6                                 # 縦線の太さ
        self.dot_radius = 10                                # ドット(照準点)の半径
        self.color = (140,160,240)                          # 照準の色(淡いブルー)

    def update_from_mouse(self):
        mx, my = pygame.mouse.get_pos()                     # 現在のマウスのX座標を取得 ※Y座標も取得しているが未使用
        self.aim_x = min(max(DROP_MIN_X, mx), DROP_MAX_X)   # マウスXを範囲内に制限して採用

    def draw_aim(self, screen: pygame.Surface):
        pygame.draw.line(screen, self.color, (self.aim_x, self.line_top_y), (self.aim_x, self.line_bottom_y), self.line_width) # 上部の狙いガイド縦線

# ------------------------------------------------------------
# 見た目担当:壁・床・天井ライン・ヒント表示
# ------------------------------------------------------------
class WorldView:
    def __init__(self, screen: pygame.Surface):
        self.screen = screen                                 # 描画先Surfaceを保持

    def draw_base(self):
        self.screen.fill((245, 245, 250))                    # 背景色を塗る(薄いグレー)
        pygame.draw.line(self.screen, (200,200,210), (40, H-40), (W-40, H-40), 20)       # 床ライン(グレー・太線)
        pygame.draw.line(self.screen, (200,200,210), (40, 80), (40, H-30), 20)
        pygame.draw.line(self.screen, (200,200,210), (W-40, 80), (W-40, H-30), 20)
        pygame.draw.line(self.screen, (220,120,120), (50, TOP_LINE_Y), (W - 50, TOP_LINE_Y), 2) # 天井ライン(赤系・細線) 

# ------------------------------------------------------------
# Fruit:落ちてくる果物(仮)オブジェクト
# ------------------------------------------------------------
class Fruit:
    def __init__(self, x, y, radius=20):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = (250, 200, 200)   # 薄いピンク(仮の色)

        # 落下用の変数(今はまだ使わないけど、先に用意しておく)
        self.vy = 0.0                  # たて方向の速さ
        self.gravity = 900.0           # 重力っぽい加速度
        self.is_falling = False        # 最初は「落ちていない」状態

    def update(self, dt: float):
        # 落ちていないときは何もしない
        if not self.is_falling:
            return

        # 重力っぽく、たて方向の速さを増やす
        self.vy += self.gravity * dt

        # 速さを使って、y座標を増やす(下に落ちる)
        self.y += self.vy * dt

        # 床の高さ(だいたい床ラインの少し上)を決める
        floor_y = H - 60

        # 床にぶつかったら止める
        if self.y + self.radius >= floor_y:
            self.y = floor_y - self.radius   # めり込まないように調整
            self.vy = 0.0
            self.is_falling = False

    def draw(self, screen: pygame.Surface):
        pygame.draw.circle(
            screen,
            self.color,
            (int(self.x), int(self.y)),
            self.radius
        )

# ------------------------------------------------------------
# ゲーム全体
# ------------------------------------------------------------
class Game:
    def __init__(self):
        pygame.init()                                        # Pygame初期化
        self.screen = pygame.display.set_mode((W, H))        # ウィンドウ生成
        pygame.display.set_caption("Suika Step 1")           # タイトル設定
        self.clock = pygame.time.Clock()                     # 時間管理(FPS制御)
        self.worldView = WorldView(self.screen)              # 見た目管理を構築
        self.player = Player()                               # ユーザ操作クラス(Aim)
        # とりあえず1個だけフルーツを作っておく
        self.current_fruit = Fruit(self.player.aim_x, self.player.aim_y)

    def handle_events(self) -> bool:
        for e in pygame.event.get():                         # イベントループ開始
            if e.type == pygame.QUIT:                        # 閉じるボタンが押されたら
                return False                                 # ループ終了を指示
            if e.type == pygame.KEYDOWN:                     # キーボードが押されたら
                if e.key == pygame.K_ESCAPE:                 # ESCキーなら
                    return False                             # ループ終了を指示
                if e.key == pygame.K_SPACE:                  # スペースキーが押された
                    # スペースキーで落下開始
                    if self.current_fruit is not None:
                        self.current_fruit.is_falling = True
        return True                                          # 続行する

    def update(self, dt: float):
        self.player.update_from_mouse()                      # マウス位置からAimのXを更新
        # フルーツがまだ落ちていない間は、Aimにくっつけておく
        if self.current_fruit is not None and not self.current_fruit.is_falling:
            self.current_fruit.x = self.player.aim_x
            self.current_fruit.y = self.player.aim_y

        # 落下中の処理(is_falling が True のときだけ動く)
        if self.current_fruit is not None:
            self.current_fruit.update(dt)

    def draw(self):
        self.worldView.draw_base()                           # 背景・壁・天井ラインを描画
        self.player.draw_aim(self.screen)                    # Aim(照準)を描画
        # フルーツを描画
        if self.current_fruit is not None:
            self.current_fruit.draw(self.screen)
        pygame.display.flip()                                # 画面を更新(ダブルバッファ反映)

    def run(self):
        running = True                                       # メインループの継続フラグ
        while running:                                       # メインループ開始
            dt = self.clock.tick(FPS) / 1000.0               # 経過時間(秒)を取得
            running = self.handle_events()                   # 入力イベント処理(継続可否)
            self.update(dt)                                  # 状態更新(Aim/物理)
            self.draw()                                      # 描画
        pygame.quit()                                        # Pygameの終了処理
        sys.exit()                                           # プロセスを終了

if __name__ == "__main__":
    Game().run()                                             # ゲームを起動

ステップ3 フルーツを何個も落とせるようにする+簡単なゲームオーバー

ステップ3-0:前回のおさらい

Player / WorldView / Fruit / Game の役割をもう一度確認
is_falling フラグで「落ちている / 落ちていない」を切り替えていることを解説
update が「毎フレーム呼ばれているメインの処理」であることを再確認

読み方のコツ

この講座では何度かプログラミングの上手な読み方をお伝えしています。
1行1行読むのではなく、全体像を把握するように塊ごとにどんなことをしているのかを大雑把に読んでいきます。

ある程度理解できたら少しずつプログラムを修正していきましょう。

ステップ3-1:Fruit が「着地した」ことを覚えておく

狙い:「状態をフラグで覚えておく」という考え方の練習。

1)Fruit クラスにフラグを追加

class Fruit:
    def __init__(self, x, y, radius=20):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = (250, 200, 200)   # 薄いピンク(仮の色)

        # 落下用の変数
        self.vy = 0.0
        self.gravity = 900.0
        self.is_falling = False

        # ★追加:このフルーツがもう着地したかどうか
        self.has_landed = False

2)床に当たったときに has_landed = True にする

    def update(self, dt: float):
        if not self.is_falling:
            return

        self.vy += self.gravity * dt
        self.y += self.vy * dt

        floor_y = H - 60

        if self.y + self.radius >= floor_y:
            self.y = floor_y - self.radius
            self.vy = 0.0
            self.is_falling = False

            # ★追加:着地フラグをオン
            self.has_landed = True

ここまでで、

  • 今まで通り床で止まって消える
  • さらに「着地したよ」(has_landed = True)という情報がフルーツ自身の中に残る
    という状態になります。

※見た目の動きはまだ変わっていません。

ステップ3-2:落ちたフルーツをリストにためる

Pythonのリストを使って「たくさんのオブジェクトを管理する」練習。

1)Game にリストを持たせる

class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((W, H))
        pygame.display.set_caption("Suika Step 3")
        self.clock = pygame.time.Clock()
        self.worldView = WorldView(self.screen)
        self.player = Player()

        # ★追加:これまでに落ちたフルーツを入れておく箱(リスト)
        self.fruits = []

        # 今操作しているフルーツ
        self.current_fruit = Fruit(self.player.aim_x, self.player.aim_y)

        # ★追加:ゲームオーバーフラグ
        self.game_over = False

        # ★追加:文字表示用フォント
        self.font = pygame.font.SysFont(None, 40)

2)update の中で「着地したら箱に入れる」

    def update(self, dt: float):
        # Aim をマウスに合わせる
        self.player.update_from_mouse()

        # ゲームオーバーになったら後の処理はスキップでもOK(ここでは動かさない)
        if self.game_over:
            return

        # 落ちていない間は Aim にくっつけておく
        if self.current_fruit is not None and not self.current_fruit.is_falling:
            self.current_fruit.x = self.player.aim_x
            self.current_fruit.y = self.player.aim_y

        # 落下中の処理
        if self.current_fruit is not None:
            self.current_fruit.update(dt)

            # ★ここが今回のポイント:
            #   current_fruit が着地したら、リストに移して新しいフルーツを作る
            if self.current_fruit.has_landed:
                self.fruits.append(self.current_fruit)

                # 新しいフルーツを用意(次に落とすやつ)
                self.current_fruit = Fruit(self.player.aim_x, self.player.aim_y)

                # (ゲームオーバー判定はこのあとで)

3)draw で「たまったフルーツ」を全部描画

    def draw(self):
        self.worldView.draw_base()
        self.player.draw_aim(self.screen)

        # ★これまでに落ちたフルーツを全部描画
        for f in self.fruits:
            f.draw(self.screen)

        # 今操作中のフルーツも描画
        if self.current_fruit is not None:
            self.current_fruit.draw(self.screen)

        # ★ゲームオーバー時の表示
        if self.game_over:
            text = self.font.render("GAME OVER", True, (200, 50, 50))
            rect = text.get_rect(center=(W // 2, H // 2))
            self.screen.blit(text, rect)

        pygame.display.flip()

この時点で:

  • スペースで落とす
  • 床に着いたら「箱」に入る
  • 次のフルーツが自動的に出てくる
    という動きになります(まだゲームオーバーはなし)。

ステップ3-3:フルーツが○個たまったらゲームオーバーにする

「条件分岐+カウンター」でゲームの終了判定を作る。

本格的な「天井ラインまで積もったらゲームオーバー」は、
当たり判定や積み上げロジックが必要なので、
ここではシンプルに

フルーツが 10 個たまったらゲームオーバー

にしておくと、ちょうど良い難易度です。

1)定数を追加

MAX_FRUITS = 10  # 何個落としたらゲームオーバーにするか

2)着地した直後に個数をチェック

さきほどの update の中の着地処理の続きに追記します。

            if self.current_fruit.has_landed:
                self.fruits.append(self.current_fruit)

                # ★ゲームオーバー判定
                if len(self.fruits) >= MAX_FRUITS:
                    # 規定個数に達したらゲームオーバー
                    self.game_over = True
                    self.current_fruit = None
                    return  # ここで update を終わらせてもOK

                # まだゲームオーバーでなければ次のフルーツ
                self.current_fruit = Fruit(self.player.aim_x, self.player.aim_y)

3)ゲームオーバー中はスペースキーを無効にする

    def handle_events(self) -> bool:
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                return False
            if e.type == pygame.KEYDOWN:
                if e.key == pygame.K_ESCAPE:
                    return False
                if e.key == pygame.K_SPACE:
                    # ★ゲームオーバー中は何もしない
                    if self.game_over:
                        continue
                    # ★「落ちていないときだけ」落下開始にする
                    if self.current_fruit is not None and not self.current_fruit.is_falling:
                        self.current_fruit.is_falling = True
        return True

ここまでできると、

  • 10回フルーツを落とすと「GAME OVER」と表示される
  • それ以上はもう落とせない
    という「一応ゲームとして遊べる形」になります。

ステップ3-3:今後は何をしてけばいいか考えよう

次に何を追加したいのかは各自で考えて意見を出してみよう。
今回出た意見に合わせて次回追加する内容を考えたいと思います。

-Python, プログラミング教室