次の DEMO を見に行く
Python

【Python】もぐら叩きゲームを作ってみた(初心者プログラミング)

takahide

おはようございます。タカヒデです。

本日はPythonで「もぐら叩きゲーム」を作ってみました。

STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。

こんな人にオススメ
  • プログラミング初心者
  • 何から始めればよいか分からない
  • まずは簡単なゲームを作って興味を持ってみたい

ぜひ実際にコードを打ちながら作成してみてください。

「もぐら叩きゲーム」完成イメージ

まずは、「もぐら叩きゲーム」完成後の最終的なコードと完成イメージです。

import tkinter as tk
import random

class WhackAMole(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("もぐらたたき")
        self.resizable(False, False)
        self.geometry("520x680")

        # 状態
        self.game_time_sec = 30          # 制限時間
        self.remaining = self.game_time_sec
        self.score = 0
        self.best = 0
        self.running = False
        self.current_mole = None
        self.current_hole_index = None
        self.spawn_after_id = None
        self.timer_after_id = None
        self.mole_lifetime_ms = 900      # もぐらが出ている時間
        self.spawn_interval_ms = 950     # 次の出現までの間隔

        # 上部UI
        top = tk.Frame(self, padx=12, pady=8)
        top.pack(fill="x")
        self.score_var = tk.StringVar(value="Score: 0")
        self.time_var = tk.StringVar(value=f"Time: {self.game_time_sec}")
        self.best_var = tk.StringVar(value="Best: 0")
        tk.Label(top, textvariable=self.score_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left")
        tk.Label(top, textvariable=self.time_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left", padx=18)
        tk.Label(top, textvariable=self.best_var, font=("Yu Gothic UI", 14)).pack(side="right")

        # プログレスバー(残り時間の可視化)
        self.bar = tk.Canvas(self, width=496, height=16, bg="#eee", highlightthickness=0)
        self.bar.pack(pady=(0, 10))
        self.bar_rect = self.bar.create_rectangle(0, 0, 496, 16, fill="#4aa3ff", width=0)

        # キャンバス(穴とモグラ)
        self.canvas = tk.Canvas(self, width=496, height=496, bg="#1b1f2a", highlightthickness=0)
        self.canvas.pack(padx=12, pady=4)

        # 下部ボタン
        bottom = tk.Frame(self, pady=8)
        bottom.pack()
        self.start_btn = tk.Button(bottom, text="スタート", font=("Yu Gothic UI", 14, "bold"),
                                   width=12, command=self.start_game)
        self.start_btn.grid(row=0, column=0, padx=6)
        self.reset_btn = tk.Button(bottom, text="リセット", font=("Yu Gothic UI", 12),
                                   width=10, command=self.reset_game, state="disabled")
        self.reset_btn.grid(row=0, column=1, padx=6)
        self.hint_lbl = tk.Label(self, text="ルール:出てきたモグラをクリック。制限時間は30秒。",
                                 font=("Yu Gothic UI", 12))
        self.hint_lbl.pack()

        # 穴の配置(3x3)
        self.holes = self._build_holes()

        # モグラクリック用タグバインド
        self.canvas.tag_bind("mole", "<Button-1>", self.hit)

    # 穴を描く(戻り値は中心座標のリスト)
    def _build_holes(self):
        holes = []
        padding = 36
        grid = 3
        cell = (496 - padding*2) // grid  # セルサイズ
        r_outer = int(cell*0.42)
        r_inner = int(cell*0.35)

        for gy in range(grid):
            for gx in range(grid):
                cx = padding + cell//2 + gx*cell
                cy = padding + cell//2 + gy*cell

                # 穴の縁(外側リング)
                self.canvas.create_oval(cx-r_outer, cy-r_outer, cx+r_outer, cy+r_outer,
                                        fill="#16202d", outline="#0d1117", width=2)
                # 穴の中(暗い円)
                self.canvas.create_oval(cx-r_inner, cy-r_inner, cx+r_inner, cy+r_inner,
                                        fill="#0f1722", outline="#0b1220", width=2)
                holes.append((cx, cy))
        return holes

    def start_game(self):
        if self.running:
            return
        # 初期化
        self.running = True
        self.score = 0
        self.remaining = self.game_time_sec
        self.score_var.set(f"Score: {self.score}")
        self.time_var.set(f"Time: {self.remaining}")
        self._update_bar()
        self.start_btn.config(state="disabled")
        self.reset_btn.config(state="normal")
        self.hint_lbl.config(text="狙ってクリック。素早さがカギ。")
        # ゲームループ開始
        self._schedule_spawn(early=True)
        self._tick_timer()

    def reset_game(self):
        # 進行中でも初期化
        self._cancel_after()
        self._clear_mole()
        self.running = False
        self.score = 0
        self.remaining = self.game_time_sec
        self.spawn_interval_ms = 950
        self.mole_lifetime_ms = 900
        self.score_var.set("Score: 0")
        self.time_var.set(f"Time: {self.game_time_sec}")
        self._update_bar()
        self.start_btn.config(state="normal")
        self.reset_btn.config(state="disabled")
        self.hint_lbl.config(text="リセット完了。スタートで再開。")

    def _tick_timer(self):
        if not self.running:
            return
        self.time_var.set(f"Time: {self.remaining}")
        self._update_bar()
        if self.remaining <= 0:
            self._game_over()
            return
        # 難易度を少しずつ上げる(5秒ごとに短縮)
        if self.remaining % 5 == 0:
            self.spawn_interval_ms = max(700, int(self.spawn_interval_ms * 0.93))
            self.mole_lifetime_ms = max(550, int(self.mole_lifetime_ms * 0.94))
        self.remaining -= 1
        self.timer_after_id = self.after(1000, self._tick_timer)

    def _update_bar(self):
        # 残り時間バーの幅を更新
        full = 496
        w = int(full * max(0, self.remaining) / self.game_time_sec)
        self.bar.coords(self.bar_rect, 0, 0, w, 16)

    def _schedule_spawn(self, early=False):
        if not self.running:
            return
        delay = 300 if early else self.spawn_interval_ms
        self.spawn_after_id = self.after(delay, self._spawn_mole)

    def _spawn_mole(self):
        if not self.running:
            return

        # 既存のモグラを消す(見逃し)
        self._clear_mole()

        # 新しい穴を選ぶ(前回と同じ場所は避ける)
        indices = list(range(len(self.holes)))
        if self.current_hole_index is not None and len(indices) > 1:
            indices.remove(self.current_hole_index)
        idx = random.choice(indices)
        self.current_hole_index = idx
        cx, cy = self.holes[idx]

        # モグラ描画(楕円+目)
        r = 28
        body = self.canvas.create_oval(cx-r, cy-r-6, cx+r, cy+r-6, fill="#6f4e37", outline="#3e2a1f", width=2, tags=("mole",))
        eye_l = self.canvas.create_oval(cx-10, cy-18, cx-4, cy-12, fill="#ffffff", outline="", tags=("mole",))
        eye_r = self.canvas.create_oval(cx+4, cy-18, cx+10, cy-12, fill="#ffffff", outline="", tags=("mole",))
        nose  = self.canvas.create_oval(cx-4, cy-6, cx+4, cy+2, fill="#111111", outline="", tags=("mole",))
        self.current_mole = (body, eye_l, eye_r, nose)

        # 一定時間で引っ込む
        self.after(self.mole_lifetime_ms, self._despawn_if_alive)

        # 次の出現予約
        self._schedule_spawn()

    def _despawn_if_alive(self):
        # まだ存在していれば消す
        if self.current_mole is not None and self.running:
            self._clear_mole()

    def _clear_mole(self):
        if self.current_mole:
            for item in self.current_mole:
                self.canvas.delete(item)
        self.current_mole = None

    def hit(self, event):
        if not self.running or self.current_mole is None:
            return
        # 当たり判定はタグによるバインドで十分。演出してスコア加算
        self._hit_effect()
        self.score += 1
        self.score_var.set(f"Score: {self.score}")
        # 即座に消す
        self._clear_mole()

    def _hit_effect(self):
        # さっと光るリング演出
        idx = self.current_hole_index
        if idx is None:
            return
        cx, cy = self.holes[idx]
        ring = self.canvas.create_oval(cx-40, cy-40, cx+40, cy+40, outline="#ffd166", width=3)
        self.canvas.after(120, lambda: self.canvas.delete(ring))

    def _game_over(self):
        self.running = False
        self._cancel_after()
        self._clear_mole()
        if self.score > self.best:
            self.best = self.score
            self.best_var.set(f"Best: {self.best}")
        # 終了表示
        self.hint_lbl.config(text=f"終了。スコアは {self.score}")
        self.start_btn.config(state="normal")
        self.reset_btn.config(state="disabled")

        # 画面中央に結果カード
        self._show_result_card()

    def _show_result_card(self):
        w, h = 360, 160
        x0, y0 = (496-w)//2, (496-h)//2
        card = self.canvas.create_rectangle(x0, y0, x0+w, y0+h, fill="#f7f7fb", outline="#ccd1d9", width=2)
        txt1 = self.canvas.create_text(x0+w//2, y0+50, text=f"Score: {self.score}", font=("Yu Gothic UI", 24, "bold"), fill="#222")
        txt2 = self.canvas.create_text(x0+w//2, y0+92, text="スタートで再挑戦", font=("Yu Gothic UI", 14), fill="#444")
        # 数秒で自動で消す
        self.canvas.after(2000, lambda: [self.canvas.delete(card), self.canvas.delete(txt1), self.canvas.delete(txt2)])

    def _cancel_after(self):
        if self.spawn_after_id:
            try:
                self.after_cancel(self.spawn_after_id)
            except Exception:
                pass
            self.spawn_after_id = None
        if self.timer_after_id:
            try:
                self.after_cancel(self.timer_after_id)
            except Exception:
                pass
            self.timer_after_id = None

if __name__ == "__main__":
    app = WhackAMole()
    app.mainloop()

こんなゲームが作れます。

STEP1:ウィンドウと基本レイアウトを用意する

このSTEPでは、アプリの土台となるウィンドウと、上部のスコア表示・中央のキャンバス・下部のボタンといったレイアウト枠だけを作ります。
モグラ出現やカウントダウンなどのゲームの中身はまだ入れません。

import tkinter as tk

class WhackAMole(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("もぐらたたき")
        self.resizable(False, False)
        self.geometry("520x680")

        # 状態(この時点では値の入れ物だけ)
        self.game_time_sec = 30
        self.remaining = self.game_time_sec
        self.score = 0
        self.best = 0
        self.running = False

        # 上部UI
        top = tk.Frame(self, padx=12, pady=8)
        top.pack(fill="x")
        self.score_var = tk.StringVar(value="Score: 0")
        self.time_var = tk.StringVar(value=f"Time: {self.game_time_sec}")
        self.best_var = tk.StringVar(value="Best: 0")
        tk.Label(top, textvariable=self.score_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left")
        tk.Label(top, textvariable=self.time_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left", padx=18)
        tk.Label(top, textvariable=self.best_var, font=("Yu Gothic UI", 14)).pack(side="right")

        # 残り時間バー(見た目だけ用意、のちほど動かします)
        self.bar = tk.Canvas(self, width=496, height=16, bg="#eee", highlightthickness=0)
        self.bar.pack(pady=(0, 10))
        self.bar_rect = self.bar.create_rectangle(0, 0, 496, 16, fill="#4aa3ff", width=0)

        # メインキャンバス(穴やモグラを描く場所)
        self.canvas = tk.Canvas(self, width=496, height=496, bg="#1b1f2a", highlightthickness=0)
        self.canvas.pack(padx=12, pady=4)

        # 下部ボタン
        bottom = tk.Frame(self, pady=8)
        bottom.pack()
        self.start_btn = tk.Button(bottom, text="スタート", font=("Yu Gothic UI", 14, "bold"),
                                   width=12, command=self.start_game)
        self.start_btn.grid(row=0, column=0, padx=6)
        self.reset_btn = tk.Button(bottom, text="リセット", font=("Yu Gothic UI", 12),
                                   width=10, command=self.reset_game, state="disabled")
        self.reset_btn.grid(row=0, column=1, padx=6)

        self.hint_lbl = tk.Label(self, text="このSTEPではレイアウトのみ作成します。",
                                 font=("Yu Gothic UI", 12))
        self.hint_lbl.pack()

    # 以降のSTEPで中身を実装します(このSTEPでは空の関数)
    def start_game(self):
        pass

    def reset_game(self):
        pass

if __name__ == "__main__":
    app = WhackAMole()
    app.mainloop()

手順①:tkinterアプリの土台(クラス継承)を作る

import tkinter as tk

class WhackAMole(tk.Tk):
    def __init__(self):
        super().__init__()

「tkinter」はデスクトップアプリを作るための標準ライブラリです。「tk.Tk」を継承することで、ウィンドウ機能をそのまま使える自分専用のクラス「WhackAMole」を作れます。

なお、「super().__init__()」は親クラス(tk.Tk)の初期化を呼び出すします。
これがないとウィンドウが正しく初期化されません。

手順②:ウィンドウの基本設定

self.title("もぐらたたき")
self.resizable(False, False)
self.geometry("520x680")

「タイトル」「リサイズ不可」「ウィンドウサイズ」を設定しています。
geometryは「幅x高さ」を指定します。
「タイトル」はタスクバーやウィンドウ上部に表示される文字列です。

手順③:スコアやタイマーの入れ物を用意

self.game_time_sec = 30
self.remaining = self.game_time_sec
self.score = 0
self.best = 0
self.running = False

のちほどゲームの進行で使う数値を先に用意します。
ここでは値の「入れ物」を作っているだけで、動きはまだありません。

手順④:上部のスコア・時間表示を作る

top = tk.Frame(self, padx=12, pady=8)
top.pack(fill="x")
self.score_var = tk.StringVar(value="Score: 0")
self.time_var = tk.StringVar(value=f"Time: {self.game_time_sec}")
self.best_var = tk.StringVar(value="Best: 0")
tk.Label(top, textvariable=self.score_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left")
tk.Label(top, textvariable=self.time_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left", padx=18)
tk.Label(top, textvariable=self.best_var, font=("Yu Gothic UI", 14)).pack(side="right")

「Frame」は部品をまとめる箱、「Label」は文字表示の部品です。「StringVar」はラベルに表示する文字を後から更新しやすくするための変数ラッパーです。
のちほどスコアや時間を変更すると、画面の表示も自動で変わります。

手順⑤:残り時間バーの見た目だけ作る

self.bar = tk.Canvas(self, width=496, height=16, bg="#eee", highlightthickness=0)
self.bar.pack(pady=(0, 10))
self.bar_rect = self.bar.create_rectangle(0, 0, 496, 16, fill="#4aa3ff", width=0)

「Canvas」は図形を描くエリアです。
ここでは細長い長方形を描いて、残り時間の棒の見た目を準備しています。
のちほど残り時間に応じて幅を変える処理を追加します。
「highlightthickness=0」で枠線を消してスッキリ見せています。

手順⑥:ゲーム用キャンバスと下部ボタンの作成

self.canvas = tk.Canvas(self, width=496, height=496, bg="#1b1f2a", highlightthickness=0)
self.canvas.pack(padx=12, pady=4)

bottom = tk.Frame(self, pady=8)
bottom.pack()
self.start_btn = tk.Button(bottom, text="スタート", font=(...), width=12, command=self.start_game)
self.start_btn.grid(row=0, column=0, padx=6)
self.reset_btn = tk.Button(bottom, text="リセット", font=(...), width=10, command=self.reset_game, state="disabled")
self.reset_btn.grid(row=0, column=1, padx=6)

中央の大きな「Canvas」が、穴やモグラを描くメインの舞台です。
下部には「スタート」「リセット」のボタンを配置しました。
ここではボタンを押したときに呼ばれる関数を指定していますが、次のSTEPで実装するため中身はまだ空です。

「command=関数名」でクリック時に呼ばれるコールバック関数を登録します。関数名の後ろに()は付けません。

手順⑦:空の関数を用意しておく

def start_game(self):
    pass

def reset_game(self):
    pass

ボタンから呼ばれる関数の「入口」だけ先に作っておきます。
中身は「pass(何もしない)」にしておけば、今はエラーにならず後から中身を足すことができます。

ここまでのSTEP1が完了した時点では以下のアプリができています。

STEP2:3×3の穴を描いて、ステージの見た目を作る

このSTEPでは、中央のCanvasに3×3の穴を描き、もぐらが顔を出す場所を用意します。

import tkinter as tk

class WhackAMole(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("もぐらたたき")
        self.resizable(False, False)
        self.geometry("520x680")

        # 状態(この時点では値の入れ物だけ)
        self.game_time_sec = 30
        self.remaining = self.game_time_sec
        self.score = 0
        self.best = 0
        self.running = False

        # 上部UI
        top = tk.Frame(self, padx=12, pady=8)
        top.pack(fill="x")
        self.score_var = tk.StringVar(value="Score: 0")
        self.time_var = tk.StringVar(value=f"Time: {self.game_time_sec}")
        self.best_var = tk.StringVar(value="Best: 0")
        tk.Label(top, textvariable=self.score_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left")
        tk.Label(top, textvariable=self.time_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left", padx=18)
        tk.Label(top, textvariable=self.best_var, font=("Yu Gothic UI", 14)).pack(side="right")

        # 残り時間バー(見た目だけ用意、のちほど動かします)
        self.bar = tk.Canvas(self, width=496, height=16, bg="#eee", highlightthickness=0)
        self.bar.pack(pady=(0, 10))
        self.bar_rect = self.bar.create_rectangle(0, 0, 496, 16, fill="#4aa3ff", width=0)

        # メインキャンバス(穴やモグラを描く場所)
        self.canvas = tk.Canvas(self, width=496, height=496, bg="#1b1f2a", highlightthickness=0)
        self.canvas.pack(padx=12, pady=4)

        # 下部ボタン
        bottom = tk.Frame(self, pady=8)
        bottom.pack()
        self.start_btn = tk.Button(bottom, text="スタート", font=("Yu Gothic UI", 14, "bold"),width=12, command=self.start_game)
        self.start_btn.grid(row=0, column=0, padx=6)
        self.reset_btn = tk.Button(bottom, text="リセット", font=("Yu Gothic UI", 12),width=10, command=self.reset_game, state="disabled")
        self.reset_btn.grid(row=0, column=1, padx=6)

        self.hint_lbl = tk.Label(self, text="3×3の穴を描画しました。次STEPで中身を作ります。",font=("Yu Gothic UI", 12))
        self.hint_lbl.pack()

        # ★STEP2追加:穴の配置(3×3)
        self.holes = self._build_holes()

        # ★STEP2追加:クリック判定(後でモグラに「mole」タグをつけたとき用)
        self.canvas.tag_bind("mole", "<Button-1>", self.hit)

    # ★STEP2追加:穴を描く関数(中心座標のリストを返す)
    def _build_holes(self):
        holes = []
        padding = 36
        grid = 3
        cell = (496 - padding*2) // grid
        r_outer = int(cell * 0.42)
        r_inner = int(cell * 0.35)

        for gy in range(grid):
            for gx in range(grid):
                cx = padding + cell // 2 + gx * cell
                cy = padding + cell // 2 + gy * cell

                # 穴の縁(外側リング)
                self.canvas.create_oval(cx - r_outer, cy - r_outer, cx + r_outer, cy + r_outer,fill="#16202d", outline="#0d1117", width=2)
                # 穴の中(暗い円)
                self.canvas.create_oval(cx - r_inner, cy - r_inner, cx + r_inner, cy + r_inner,fill="#0f1722", outline="#0b1220", width=2)
                holes.append((cx, cy))
        return holes

    # 以降のSTEPで中身を実装します(このSTEPでは空の関数)
    def start_game(self):
        pass

    def reset_game(self):
        pass

    # ★STEP2追加:クリックイベント処理の枠(まだ中身は未実装)
    def hit(self, event):
        pass

if __name__ == "__main__":
    app = WhackAMole()
    app.mainloop()

手順①:穴の配置メソッドを用意する

def _build_holes(self):
    holes = []
    padding = 36
    grid = 3
    cell = (496 - padding*2) // grid
    r_outer = int(cell*0.42)
    r_inner = int(cell*0.35)
    ...
    holes.append((cx, cy))
    return holes

穴をまとめて描画するための関数を作ります。
戻り値の「holes」は各穴の中心座標を持つリストです。のちほどモグラを出す位置決定に使います。
「padding」は外周の余白、「grid」は縦横の穴数、「cell」は各マスの大きさです。
「return holes」を忘れると、あとで座標が使えずにエラーになるため注意しましょう。

手順②:二重ループで3×3に配置する

for gy in range(grid):
    for gx in range(grid):
        cx = padding + cell//2 + gx*cell
        cy = padding + cell//2 + gy*cell

縦方向「gy」、横方向「gx」をループさせ、各マスの中心「cx, cy」を計算しています。
ここでは「整数の割り算」で中心を求めています。
「//」は小数点を切り捨てる割り算です。
Canvasの座標は整数でも小数でも動きますが、見た目のずれを避けるため整数で揃えています。

手順③「穴の見た目を描く」

self.canvas.create_oval(cx-r_outer, cy-r_outer, cx+r_outer, cy+r_outer,fill="#16202d", outline="#0d1117", width=2)
self.canvas.create_oval(cx-r_inner, cy-r_inner, cx+r_inner, cy+r_inner,fill="#0f1722", outline="#0b1220", width=2)

「Canvas」の「create_oval」で円を描いています。
外側の暗いリングと内側のさらに暗い円を重ねることで、穴の立体感を表現しています。
Canvasの図形は「左上x, 左上y, 右下x, 右下y」の4点指定です。
中心「cx, cy」から半径を引いたり足したりして四角形の座標を作っています。

手順④:穴座標を作成直後に保持する

self.holes = self._build_holes()

「init」の最後で一度だけ穴を描き、中心座標リストを「self.holes」に保存します。
これで次のSTEP以降、どの穴にモグラを出すかを簡単に選べます。

手順⑤:モグラ用のクリック受付の準備

self.canvas.tag_bind("mole", "<Button-1>", self.hit)

Canvasの図形には「タグ」という名前を付けられます。
のちほど、モグラの図形すべてに「mole」というタグを付けます。
「moleタグの図形がクリックされたらhit関数を呼ぶ」という処理を登録しておきます。
現時点ではモグラがいないため、クリックしても何も起きません。

この段階で、アプリを実行すると3×3の穴が表示され、見た目のステージが完成します。
動きはまだありませんが、次のSTEPでモグラの出現と基本の当たり判定へと進みます。

STEP3:モグラがランダムに出現し、クリックで得点できるようにする

このSTEPでは、3×3の穴からモグラが一定間隔で出現し、クリックするとスコアが増える動きを追加します。
タイマーのカウントダウンは最後のSTEPで実装します。

import tkinter as tk
import random  # ★STEP3追加:ランダム出現に使用

class WhackAMole(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("もぐらたたき")
        self.resizable(False, False)
        self.geometry("520x680")

        # 状態(この時点では値の入れ物だけ)
        self.game_time_sec = 30
        self.remaining = self.game_time_sec
        self.score = 0
        self.best = 0
        self.running = False

        # ★STEP3追加:スポーンと現在のモグラ管理に使う状態を追加
        self.current_mole = None
        self.current_hole_index = None
        self.spawn_after_id = None
        self.timer_after_id = None   # STEP4で使用
        self.mole_lifetime_ms = 900
        self.spawn_interval_ms = 950

        # 上部UI
        top = tk.Frame(self, padx=12, pady=8)
        top.pack(fill="x")
        self.score_var = tk.StringVar(value="Score: 0")
        self.time_var = tk.StringVar(value=f"Time: {self.game_time_sec}")
        self.best_var = tk.StringVar(value="Best: 0")
        tk.Label(top, textvariable=self.score_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left")
        tk.Label(top, textvariable=self.time_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left", padx=18)
        tk.Label(top, textvariable=self.best_var, font=("Yu Gothic UI", 14)).pack(side="right")

        # 残り時間バー(見た目のみ/このSTEPでは満タンにリセットを行う)
        self.bar = tk.Canvas(self, width=496, height=16, bg="#eee", highlightthickness=0)
        self.bar.pack(pady=(0, 10))
        self.bar_rect = self.bar.create_rectangle(0, 0, 496, 16, fill="#4aa3ff", width=0)

        # メインキャンバス
        self.canvas = tk.Canvas(self, width=496, height=496, bg="#1b1f2a", highlightthickness=0)
        self.canvas.pack(padx=12, pady=4)

        # 下部ボタン
        bottom = tk.Frame(self, pady=8)
        bottom.pack()
        self.start_btn = tk.Button(bottom, text="スタート", font=("Yu Gothic UI", 14, "bold"),width=12, command=self.start_game)
        self.start_btn.grid(row=0, column=0, padx=6)
        self.reset_btn = tk.Button(bottom, text="リセット", font=("Yu Gothic UI", 12),width=10, command=self.reset_game, state="disabled") 
        self.reset_btn.grid(row=0, column=1, padx=6)

        self.hint_lbl = tk.Label(
            self,
            text="クリックでモグラを叩いてスコアを増やします。",  # ★STEP3変更:STEP内容に合わせて文言変更
            font=("Yu Gothic UI", 12)
        )
        self.hint_lbl.pack()

        # ★STEP2追加:穴の配置(3×3)
        self.holes = self._build_holes()

        # ★STEP2追加:クリック判定(後でモグラに「mole」タグを付ける)
        self.canvas.tag_bind("mole", "<Button-1>", self.hit)

    # ★STEP2追加:穴を描く関数(中心座標のリストを返す)
    def _build_holes(self):
        holes = []
        padding = 36
        grid = 3
        cell = (496 - padding*2) // grid
        r_outer = int(cell * 0.42)
        r_inner = int(cell * 0.35)

        for gy in range(grid):
            for gx in range(grid):
                cx = padding + cell // 2 + gx * cell
                cy = padding + cell // 2 + gy * cell

                # 穴の縁(外側リング)
                self.canvas.create_oval(cx - r_outer, cy - r_outer, cx + r_outer, cy + r_outer,fill="#16202d", outline="#0d1117", width=2)
                # 穴の中(暗い円)
                self.canvas.create_oval(cx - r_inner, cy - r_inner, cx + r_inner, cy + r_inner,fill="#0f1722", outline="#0b1220", width=2)
                holes.append((cx, cy))
        return holes

    # ★STEP3追加:ゲーム開始処理(初期化と最初のスポーン予約)
    def start_game(self):
        if self.running:
            return
        self.running = True
        self.score = 0
        self.remaining = self.game_time_sec
        self.score_var.set(f"Score: {self.score}")
        self.time_var.set(f"Time: {self.remaining}")
        self._update_bar()  # ★STEP3追加:開始直後にバーを満タンへ

        self.start_btn.config(state="disabled")
        self.reset_btn.config(state="normal")
        self.hint_lbl.config(text="狙ってクリック。素早さがカギ。")
        self._schedule_spawn(early=True)

        # タイマーはSTEP4で実装

    # ★STEP3追加:リセット処理(後始末と初期状態へ)
    def reset_game(self):
        self._cancel_after()
        self._clear_mole()
        self.running = False
        self.score = 0
        self.remaining = self.game_time_sec
        self.spawn_interval_ms = 950
        self.mole_lifetime_ms = 900
        self.score_var.set("Score: 0")
        self.time_var.set(f"Time: {self.game_time_sec}")
        self._update_bar()
        self.start_btn.config(state="normal")
        self.reset_btn.config(state="disabled")
        self.hint_lbl.config(text="リセット完了。スタートで再開。")

    # ★STEP3追加:次回出現の予約
    def _schedule_spawn(self, early=False):
        if not self.running:
            return
        delay = 300 if early else self.spawn_interval_ms
        self.spawn_after_id = self.after(delay, self._spawn_mole)

    # ★STEP3追加:モグラ出現(描画と次回予約)
    def _spawn_mole(self):
        if not self.running:
            return

        self._clear_mole()  # 既存があれば見逃し扱いで消す

        # 新しい穴を選ぶ(前回と同じ場所は避ける)
        indices = list(range(len(self.holes)))
        if self.current_hole_index is not None and len(indices) > 1:
            indices.remove(self.current_hole_index)
        idx = random.choice(indices)
        self.current_hole_index = idx
        cx, cy = self.holes[idx]

        # モグラ描画(楕円+目+鼻)にタグ "mole" を付与
        r = 28
        body = self.canvas.create_oval(cx - r, cy - r - 6, cx + r, cy + r - 6,
                                       fill="#6f4e37", outline="#3e2a1f", width=2, tags=("mole",))
        eye_l = self.canvas.create_oval(cx - 10, cy - 18, cx - 4, cy - 12,
                                        fill="#ffffff", outline="", tags=("mole",))
        eye_r = self.canvas.create_oval(cx + 4, cy - 18, cx + 10, cy - 12,
                                        fill="#ffffff", outline="", tags=("mole",))
        nose  = self.canvas.create_oval(cx - 4, cy - 6, cx + 4, cy + 2,
                                        fill="#111111", outline="", tags=("mole",))
        self.current_mole = (body, eye_l, eye_r, nose)

        self.after(self.mole_lifetime_ms, self._despawn_if_alive)  # 表示時間後に引っ込む
        self._schedule_spawn()  # 次の出現を予約

    # ★STEP3追加:表示時間が過ぎてまだいれば消す
    def _despawn_if_alive(self):
        if self.current_mole is not None and self.running:
            self._clear_mole()

    # ★STEP3追加:モグラを消去
    def _clear_mole(self):
        if self.current_mole:
            for item in self.current_mole:
                self.canvas.delete(item)
        self.current_mole = None

    # ★STEP3追加:クリックで得点+簡単な演出
    def hit(self, event):
        if not self.running or self.current_mole is None:
            return
        self._hit_effect()
        self.score += 1
        self.score_var.set(f"Score: {self.score}")
        self._clear_mole()

    # ★STEP3追加:クリック時の演出
    def _hit_effect(self):
        idx = self.current_hole_index
        if idx is None:
            return
        cx, cy = self.holes[idx]
        ring = self.canvas.create_oval(cx - 40, cy - 40, cx + 40, cy + 40, outline="#ffd166", width=3)
        self.canvas.after(120, lambda: self.canvas.delete(ring))

    # ★STEP3追加:時間バーを現在値に合わせて更新(このSTEPでは満タンに戻す用途)
    def _update_bar(self):
        full = 496
        w = int(full * max(0, self.remaining) / self.game_time_sec)
        self.bar.coords(self.bar_rect, 0, 0, w, 16)

    # ★STEP3追加:予約キャンセル(reset時などの後始末)
    def _cancel_after(self):
        if self.spawn_after_id:
            try:
                self.after_cancel(self.spawn_after_id)
            except Exception:
                pass
            self.spawn_after_id = None
        if self.timer_after_id:
            try:
                self.after_cancel(self.timer_after_id)
            except Exception:
                pass
            self.timer_after_id = None

if __name__ == "__main__":
    app = WhackAMole()
    app.mainloop()

手順①:ゲーム開始時の初期化とスポーン開始

def start_game(self):
    if self.running:
        return
    self.running = True
    self.score = 0
    self.remaining = self.game_time_sec
    self.score_var.set(f"Score: {self.score}")
    self.time_var.set(f"Time: {self.remaining}")
    self._update_bar()
    self.start_btn.config(state="disabled")
    self.reset_btn.config(state="normal")
    self.hint_lbl.config(text="狙ってクリック。素早さがカギ。")
    self._schedule_spawn(early=True)

「running」がゲーム中かどうかのフラグです。
開始時にスコアと残り時間の表示を初期化し、最初のモグラ出現を予約します。
「_schedule_spawn」は一定時間後に「_spawn_mole」を呼ぶための予約関数です。

手順②:モグラを一定間隔で出す

def _schedule_spawn(self, early=False):
    if not self.running:
        return
    delay = 300 if early else self.spawn_interval_ms
    self.spawn_after_id = self.after(delay, self._spawn_mole)

「after」は一定時間後に関数を呼ぶタイマー機能です。
ここでは「早めに出す場合は300ms、通常はspawn_interval_ms後」に「_spawn_mole」を呼びます。

手順③:モグラを描く

idx = random.choice(indices)
cx, cy = self.holes[idx]
r = 28
body = self.canvas.create_oval(..., tags=("mole",))
eye_l = self.canvas.create_oval(..., tags=("mole",))
eye_r = self.canvas.create_oval(..., tags=("mole",))
nose  = self.canvas.create_oval(..., tags=("mole",))
self.current_mole = (body, eye_l, eye_r, nose)

「random.choice」で穴の位置をランダムに選び、楕円の組み合わせでモグラの見た目を作っています。
すべてに「tags=(“mole”,)」を付けることで、クリックイベントを一括で受けられます。
前回と同じ穴に出現することを避けるために、直前に使ったインデックスを除外しています。

手順④:一定時間で引っ込む処理

self.after(self.mole_lifetime_ms, self._despawn_if_alive)

モグラは表示時間が過ぎると自動で消えます。
まだ叩かれていなければ「見逃し」扱いで消え、次の出現が続きます。

手順⑤:クリックでスコア加算

def hit(self, event):
    if not self.running or self.current_mole is None:
        return
    self._hit_effect()
    self.score += 1
    self.score_var.set(f"Score: {self.score}")
    self._clear_mole()

「mole」タグに対してバインドしているので、モグラのどのパーツをクリックしてもこの関数が呼ばれます。
スコアを1加算し、表示を更新してから、モグラを消します。
簡単なエフェクトも追加しています。
また、ゲーム停止中にクリックされても何もしないように先頭でチェックしています。

手順⑥:リセットと予約キャンセル

def _cancel_after(self):
    if self.spawn_after_id:
        self.after_cancel(self.spawn_after_id)
    ...

after予約のIDを保持し、リセット時やゲーム終了時にキャンセルできるようにしています。
IDが存在しないときにキャンセルするとエラーになるため、存在確認をしてから実行します。
今はスポーンだけをキャンセルしますが、次のSTEPでタイマーも追加するために「timer_after_id」も用意しています。

この段階で、スタートを押すとモグラが出現し、クリックでスコアが増えます。
残り時間のカウントダウンとバーの連動は次のSTEPで実装します。

STEP4:カウントダウンとゲーム終了、結果表示を実装する

このSTEPでは、残り時間のカウントダウンを動かし、時間切れでゲームを止めて結果カードを表示します。
あわせて難易度を少しずつ上げ、ベストスコアを更新する処理も加えます。

import tkinter as tk
import random

class WhackAMole(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("もぐらたたき")
        self.resizable(False, False)
        self.geometry("520x680")

        # 状態
        self.game_time_sec = 30
        self.remaining = self.game_time_sec
        self.score = 0
        self.best = 0
        self.running = False

        # スポーン・管理用の状態
        self.current_mole = None
        self.current_hole_index = None
        self.spawn_after_id = None
        self.timer_after_id = None   # STEP4で実際に使用
        self.mole_lifetime_ms = 900
        self.spawn_interval_ms = 950

        # 上部UI
        top = tk.Frame(self, padx=12, pady=8)
        top.pack(fill="x")
        self.score_var = tk.StringVar(value="Score: 0")
        self.time_var = tk.StringVar(value=f"Time: {self.game_time_sec}")
        self.best_var = tk.StringVar(value="Best: 0")
        tk.Label(top, textvariable=self.score_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left")
        tk.Label(top, textvariable=self.time_var, font=("Yu Gothic UI", 18, "bold")).pack(side="left", padx=18)
        tk.Label(top, textvariable=self.best_var, font=("Yu Gothic UI", 14)).pack(side="right")

        # 残り時間バー
        self.bar = tk.Canvas(self, width=496, height=16, bg="#eee", highlightthickness=0)
        self.bar.pack(pady=(0, 10))
        self.bar_rect = self.bar.create_rectangle(0, 0, 496, 16, fill="#4aa3ff", width=0)

        # メインキャンバス
        self.canvas = tk.Canvas(self, width=496, height=496, bg="#1b1f2a", highlightthickness=0)
        self.canvas.pack(padx=12, pady=4)

        # 下部ボタン
        bottom = tk.Frame(self, pady=8)
        bottom.pack()
        self.start_btn = tk.Button(bottom, text="スタート", font=("Yu Gothic UI", 14, "bold"),
                                   width=12, command=self.start_game)
        self.start_btn.grid(row=0, column=0, padx=6)
        self.reset_btn = tk.Button(bottom, text="リセット", font=("Yu Gothic UI", 12),
                                   width=10, command=self.reset_game, state="disabled")
        self.reset_btn.grid(row=0, column=1, padx=6)
        self.hint_lbl = tk.Label(self, text="ルール:出てきたモグラをクリック。制限時間は30秒。",
                                 font=("Yu Gothic UI", 12))
        self.hint_lbl.pack()

        # 穴の配置(3×3)
        self.holes = self._build_holes()

        # クリック判定
        self.canvas.tag_bind("mole", "<Button-1>", self.hit)

    # 穴を描く(中心座標のリストを返す)
    def _build_holes(self):
        holes = []
        padding = 36
        grid = 3
        cell = (496 - padding*2) // grid
        r_outer = int(cell * 0.42)
        r_inner = int(cell * 0.35)

        for gy in range(grid):
            for gx in range(grid):
                cx = padding + cell // 2 + gx * cell
                cy = padding + cell // 2 + gy * cell

                # 穴の縁(外側リング)
                self.canvas.create_oval(cx - r_outer, cy - r_outer, cx + r_outer, cy + r_outer,
                                        fill="#16202d", outline="#0d1117", width=2)
                # 穴の中(暗い円)
                self.canvas.create_oval(cx - r_inner, cy - r_inner, cx + r_inner, cy + r_inner,
                                        fill="#0f1722", outline="#0b1220", width=2)
                holes.append((cx, cy))
        return holes

    # ゲーム開始
    def start_game(self):
        if self.running:
            return
        self.running = True
        self.score = 0
        self.remaining = self.game_time_sec
        self.score_var.set(f"Score: {self.score}")
        self.time_var.set(f"Time: {self.remaining}")
        self._update_bar()

        self.start_btn.config(state="disabled")
        self.reset_btn.config(state="normal")
        self.hint_lbl.config(text="狙ってクリック。素早さがカギ。")

        self._schedule_spawn(early=True)
        self._tick_timer()  # ★STEP4追加:1秒ごとのカウントダウン開始

    # リセット
    def reset_game(self):
        self._cancel_after()
        self._clear_mole()
        self.running = False
        self.score = 0
        self.remaining = self.game_time_sec
        self.spawn_interval_ms = 950
        self.mole_lifetime_ms = 900
        self.score_var.set("Score: 0")
        self.time_var.set(f"Time: {self.game_time_sec}")
        self._update_bar()
        self.start_btn.config(state="normal")
        self.reset_btn.config(state="disabled")
        self.hint_lbl.config(text="リセット完了。スタートで再開。")

    # ★STEP4追加:1秒ごとのカウントダウンと難易度上昇
    def _tick_timer(self):
        if not self.running:
            return
        self.time_var.set(f"Time: {self.remaining}")
        self._update_bar()
        if self.remaining <= 0:
            self._game_over()
            return
        # 5秒ごとに少しずつ難しくする
        if self.remaining % 5 == 0:
            self.spawn_interval_ms = max(700, int(self.spawn_interval_ms * 0.93))
            self.mole_lifetime_ms = max(550, int(self.mole_lifetime_ms * 0.94))
        self.remaining -= 1
        self.timer_after_id = self.after(1000, self._tick_timer)

    # ★STEP4記載箇所変更:残り時間バー更新
    def _update_bar(self):
        full = 496
        w = int(full * max(0, self.remaining) / self.game_time_sec)
        self.bar.coords(self.bar_rect, 0, 0, w, 16)

    # 次回出現予約
    def _schedule_spawn(self, early=False):
        if not self.running:
            return
        delay = 300 if early else self.spawn_interval_ms
        self.spawn_after_id = self.after(delay, self._spawn_mole)

    # モグラ出現
    def _spawn_mole(self):
        if not self.running:
            return

        self._clear_mole()  # 見逃し扱いで消す

        # 新しい穴を選ぶ(前回と同じ場所は避ける)
        indices = list(range(len(self.holes)))
        if self.current_hole_index is not None and len(indices) > 1:
            indices.remove(self.current_hole_index)
        idx = random.choice(indices)
        self.current_hole_index = idx
        cx, cy = self.holes[idx]

        # モグラ描画(楕円+目+鼻)
        r = 28
        body = self.canvas.create_oval(cx - r, cy - r - 6, cx + r, cy + r - 6,
                                       fill="#6f4e37", outline="#3e2a1f", width=2, tags=("mole",))
        eye_l = self.canvas.create_oval(cx - 10, cy - 18, cx - 4, cy - 12,
                                        fill="#ffffff", outline="", tags=("mole",))
        eye_r = self.canvas.create_oval(cx + 4, cy - 18, cx + 10, cy - 12,
                                        fill="#ffffff", outline="", tags=("mole",))
        nose  = self.canvas.create_oval(cx - 4, cy - 6, cx + 4, cy + 2,
                                        fill="#111111", outline="", tags=("mole",))
        self.current_mole = (body, eye_l, eye_r, nose)

        self.after(self.mole_lifetime_ms, self._despawn_if_alive)
        self._schedule_spawn()

    # 表示時間が過ぎてまだいれば消す
    def _despawn_if_alive(self):
        if self.current_mole is not None and self.running:
            self._clear_mole()

    # モグラを消去
    def _clear_mole(self):
        if self.current_mole:
            for item in self.current_mole:
                self.canvas.delete(item)
        self.current_mole = None

    # クリックで得点+演出
    def hit(self, event):
        if not self.running or self.current_mole is None:
            return
        self._hit_effect()
        self.score += 1
        self.score_var.set(f"Score: {self.score}")
        self._clear_mole()

    def _hit_effect(self):
        idx = self.current_hole_index
        if idx is None:
            return
        cx, cy = self.holes[idx]
        ring = self.canvas.create_oval(cx - 40, cy - 40, cx + 40, cy + 40, outline="#ffd166", width=3)
        self.canvas.after(120, lambda: self.canvas.delete(ring))

    # ★STEP4追加:ゲーム終了処理(ベスト更新・UI切替・結果カード)
    def _game_over(self):
        self.running = False
        self._cancel_after()
        self._clear_mole()
        if self.score > self.best:
            self.best = self.score
            self.best_var.set(f"Best: {self.best}")
        self.hint_lbl.config(text=f"終了。スコアは {self.score}")
        self.start_btn.config(state="normal")
        self.reset_btn.config(state="disabled")
        self._show_result_card()

    # ★STEP4追加:結果カードの描画
    def _show_result_card(self):
        w, h = 360, 160
        x0, y0 = (496 - w) // 2, (496 - h) // 2
        card = self.canvas.create_rectangle(x0, y0, x0 + w, y0 + h,
                                            fill="#f7f7fb", outline="#ccd1d9", width=2)
        txt1 = self.canvas.create_text(x0 + w // 2, y0 + 50,
                                       text=f"Score: {self.score}",
                                       font=("Yu Gothic UI", 24, "bold"), fill="#222")
        txt2 = self.canvas.create_text(x0 + w // 2, y0 + 92,
                                       text="スタートで再挑戦",
                                       font=("Yu Gothic UI", 14), fill="#444")
        self.canvas.after(2000, lambda: [self.canvas.delete(card),
                                         self.canvas.delete(txt1),
                                         self.canvas.delete(txt2)])

    # 予約キャンセル
    def _cancel_after(self):
        if self.spawn_after_id:
            try:
                self.after_cancel(self.spawn_after_id)
            except Exception:
                pass
            self.spawn_after_id = None
        if self.timer_after_id:
            try:
                self.after_cancel(self.timer_after_id)
            except Exception:
                pass
            self.timer_after_id = None

if __name__ == "__main__":
    app = WhackAMole()
    app.mainloop()

手順①:タイマー処理を開始する

self._schedule_spawn(early=True)
self._tick_timer()

ゲーム開始時に「出現の予約」と「1秒ごとのカウントダウン」を同時に動かします。
「_tick_timer」は1秒後に自分自身をもう一度呼び出す仕組みで、これを繰り返すことでカウントダウンを実現します。

手順②:1秒ごとのカウントダウンを実装する

def _tick_timer(self):
    if not self.running:
        return
    self.time_var.set(f"Time: {self.remaining}")
    self._update_bar()
    if self.remaining <= 0:
        self._game_over()
        return
    if self.remaining % 5 == 0:
        self.spawn_interval_ms = max(700, int(self.spawn_interval_ms * 0.93))
        self.mole_lifetime_ms = max(550, int(self.mole_lifetime_ms * 0.94))
    self.remaining -= 1
    self.timer_after_id = self.after(1000, self._tick_timer)

残り時間をラベルとバーに反映し、0になったらゲームを終了します。
5秒ごとに少しずつ難しくなるように、出現間隔を短くし、表示時間を短縮しています。
「max」で下限を設定し、無理な速度にならないようにしています。

手順③:時間バーの幅を更新する

full = 496
w = int(full * max(0, self.remaining) / self.game_time_sec)
self.bar.coords(self.bar_rect, 0, 0, w, 16)

残り時間に比例してバーの幅を更新します。
視覚的に減っていくことで、残り時間を直感的に理解できます。

手順④:ゲーム終了と結果カードの表示

if self.score > self.best:
    self.best = self.score
    self.best_var.set(f"Best: {self.best}")
...
self._show_result_card()

時間切れでゲームを止め、ベストスコアを更新します。
最後にキャンバス中央へスコアカードを短時間表示して、次の挑戦をうながします。
スタートボタンを再度押すとリトライできます。
結果カードは2秒後に自動で消え、画面に残しっぱなしにしない設計にしています。

手順⑤:予約の後始末

self.after_cancel(self.spawn_after_id)
self.after_cancel(self.timer_after_id)

「after」で登録した処理はIDで取り消します。
リセットやゲーム終了時に必ず後始末を行うことで、二重に動き続けることを防ぎます。

実行すると、スタートでカウントダウンが始まり、時間が減るにつれて難しくなり、時間切れで結果カードが表示されます。
リセットでいつでも最初の状態に戻せます。

完成

以上でPythonで作る「もぐら叩きゲーム」の完成です。

ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。

ABOUT ME
記事URLをコピーしました