初心者向け No.067

スロットマシン

絵文字を使った3リールスロット。当選パターン判定とコイン管理機能付きのカジノゲーム風アプリ。

🎯 難易度: ★★☆ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

絵文字を使った3リールスロット。当選パターン判定とコイン管理機能付きのカジノゲーム風アプリ。

このアプリはgamesカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★★☆ です。

このアプリは「ゲーム」カテゴリです。ゲーム実装は GUI イベント処理・状態管理・描画ループを同時に学べる教材で、画面・入力・ロジックの三層分離の感覚が掴めます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。

コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。

アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。

スロットマシン 実行画面(Windows)
実行画面(Windows)
スロットマシン 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • 「スピン」ボタン(spin())・「コイン補充(+50)」ボタン(add_coins())・「リセット」ボタン(reset())で操作
  • after(60, ...) による60ミリ秒間隔の _animate() 呼び出し(画面の自動更新)
  • messagebox.showwarning() によるダイアログ通知
  • ウィンドウはタイトル「スロットマシン」・サイズ 600x450 で起動

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)

Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。

  • Python 3.10 以上
  • OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)
app067.py
import tkinter as tk
from tkinter import ttk, messagebox
import random


class App067:
    """スロットマシン: 3リール絵文字スロット + コイン管理"""

    SYMBOLS = ["🍒", "🍋", "🍊", "🔔", "⭐", "💎", "7️⃣"]
    # 当選時の獲得倍率(揃った絵文字に対する払い戻し倍率)
    PAYOUT = {
        "7️⃣": 50,
        "💎": 20,
        "⭐": 10,
        "🔔": 5,
        "🍊": 3,
        "🍋": 2,
        "🍒": 2,
    }
    BET = 10  # 1回あたりのベット額

    def __init__(self, root):
        self.root = root
        self.root.title("スロットマシン")
        self.root.geometry("600x450")
        self.root.configure(bg="#f8f9fc")
        self.coins = 100  # 初期所持コイン
        self.spinning = False
        self._build_ui()
        self._update_status()

    def _build_ui(self):
        """UI を構築する"""
        # タイトル
        title_frame = tk.Frame(self.root, bg="#3776ab", pady=12)
        title_frame.pack(fill=tk.X)
        tk.Label(
            title_frame, text="🎰 スロットマシン",
            font=("Noto Sans JP", 16, "bold"),
            bg="#3776ab", fg="white"
        ).pack()

        main = tk.Frame(self.root, bg="#f8f9fc", padx=20, pady=20)
        main.pack(fill=tk.BOTH, expand=True)

        # ステータス(コイン数)
        self.status_var = tk.StringVar()
        tk.Label(
            main, textvariable=self.status_var,
            font=("Noto Sans JP", 12, "bold"), bg="#f8f9fc"
        ).pack(pady=(0, 10))

        # リール表示
        reel_frame = tk.Frame(main, bg="#222", padx=20, pady=20, bd=4, relief=tk.RIDGE)
        reel_frame.pack(pady=10)
        self.reel_vars = [tk.StringVar(value="❓") for _ in range(3)]
        self.reel_labels = []
        for i, var in enumerate(self.reel_vars):
            lbl = tk.Label(
                reel_frame, textvariable=var, font=("Arial", 48),
                width=2, bg="white", fg="#222", bd=2, relief=tk.SUNKEN
            )
            lbl.grid(row=0, column=i, padx=6)
            self.reel_labels.append(lbl)

        # 結果メッセージ
        self.message_var = tk.StringVar(value=f"スピン1回 = {self.BET}コイン消費")
        tk.Label(
            main, textvariable=self.message_var,
            font=("Noto Sans JP", 12), bg="#f8f9fc", fg="#333"
        ).pack(pady=10)

        # 操作ボタン
        btn = tk.Frame(main, bg="#f8f9fc")
        btn.pack(pady=8)
        self.spin_btn = ttk.Button(btn, text="スピン", command=self.spin)
        self.spin_btn.pack(side=tk.LEFT, padx=4)
        ttk.Button(btn, text="コイン補充(+50)", command=self.add_coins).pack(side=tk.LEFT, padx=4)
        ttk.Button(btn, text="リセット", command=self.reset).pack(side=tk.LEFT, padx=4)

    def spin(self):
        """スピン開始"""
        if self.spinning:
            return
        if self.coins < self.BET:
            messagebox.showwarning("コイン不足", "コインが足りません。補充してください。")
            return
        self.coins -= self.BET
        self._update_status()
        self.spinning = True
        self.spin_btn.config(state=tk.DISABLED)
        # アニメーション: 各リールを順に止める
        self._animate(0, frames=15)

    def _animate(self, reel_index, frames):
        """リールを回転表示してから停止"""
        if frames > 0:
            for i in range(reel_index, 3):
                self.reel_vars[i].set(random.choice(self.SYMBOLS))
            self.root.after(60, lambda: self._animate(reel_index, frames - 1))
        else:
            # 現在のリールを確定
            if reel_index < 2:
                self._animate(reel_index + 1, frames=12)
            else:
                self._judge()

    def _judge(self):
        """当選判定"""
        symbols = [v.get() for v in self.reel_vars]
        if symbols[0] == symbols[1] == symbols[2]:
            mult = self.PAYOUT.get(symbols[0], 2)
            payout = self.BET * mult
            self.coins += payout
            self.message_var.set(f"🎉 大当たり! {symbols[0]} x3 → +{payout} コイン")
        elif symbols[0] == symbols[1] or symbols[1] == symbols[2] or symbols[0] == symbols[2]:
            payout = self.BET  # ベット額返却
            self.coins += payout
            self.message_var.set(f"おしい!2つ揃い → +{payout} コイン返還")
        else:
            self.message_var.set("ハズレ… もう一度!")
        self._update_status()
        self.spinning = False
        self.spin_btn.config(state=tk.NORMAL)

    def add_coins(self):
        """コインを補充する"""
        self.coins += 50
        self._update_status()

    def reset(self):
        """初期状態に戻す"""
        self.coins = 100
        for v in self.reel_vars:
            v.set("❓")
        self.message_var.set(f"スピン1回 = {self.BET}コイン消費")
        self._update_status()

    def _update_status(self):
        """所持コイン表示を更新"""
        self.status_var.set(f"💰 所持コイン: {self.coins}")


if __name__ == "__main__":
    root = tk.Tk()
    app = App067(root)
    root.mainloop()

5. コード解説

スロットマシン: 3リール絵文字スロット + コイン管理のコードを、実際に書かれている実装に沿って解説します。

クラス設計とコンストラクタ

App067 クラスにアプリの全機能をまとめています(メソッド8個・全147行)。__init__ ではタイトル「スロットマシン」とウィンドウサイズ 600x450 を設定します。あわせて状態を保持する変数(self.coinsself.spinning)を初期化し、そのあと _build_ui()_update_status() の順に呼び出して初期状態を作ります。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×4・tk.Label×3(ほかにループでも生成)・ttk.Button×3 で構成しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

「スピン」ボタン(spin())・「コイン補充(+50)」ボタン(add_coins())・「リセット」ボタン(reset())を command= で接続しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

after() による定期処理

_animate() の末尾で after(60, ...) を呼び、60ミリ秒後に自分自身の再実行を予約しています。tkinter では while ループで待つと画面が固まるため、この「予約の連鎖」で定期処理を作るのが定石です。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

クラス定数によるデータ定義

クラスの先頭でデータを定数として定義しています:SYMBOLS = ['🍒', '🍋', '🍊', '🔔', '⭐', '💎', '7️⃣']PAYOUT = {'7️⃣': 50, '💎': 20, '⭐': 10, '🔔': 5, '🍊': 3, '🍋': 2, '🍒': 2}。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

例外処理を書いていない理由

このコードには try-except による例外処理がありません。テキスト入力欄がなくボタン操作だけで完結し、ファイルやネットワークへのアクセスも無い設計のため、例外を投げうる処理(型変換・ファイル/通信)がそもそも無いからです。入力欄やファイル保存を追加する改造をするときは、例外処理もセットで検討しましょう。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app067.py と保存します。使うのは randomtkinter だけなので、追加インストールは不要です。

  2. 2
    クラスの骨格を作る

    App067 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("スロットマシン")geometry("600x450")configure(bg="#f8f9fc") をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×4・tk.Label×3(ほかにループでも生成)・ttk.Button×3 を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(spin()add_coins()reset())につなぎます。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である _animate()(12行)・spin()(13行)・reset()(7行)・add_coins()(4行) を実装します。定期処理は after(60, ...) で自分自身を予約し続ける形にします。

  7. 7
    動作確認する

    python app067.py で起動し、各ボタンが反応すること・表示が自動で更新されることを確認します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 更新間隔を変えてみる

after(60, ...) の 60 が更新間隔(ミリ秒)です。大きくすると更新頻度が下がって CPU 負荷が軽くなり、小さくすると更新が細かくなります(表示の刻み幅より細かくしても見た目は変わりません)。数値を変えて違いを確かめてみましょう。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

8. よくある問題と解決法

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。

❌ 改造したら表示の更新が二重になった・速くなった

原因:_animate()after(60, ...) で自分自身を予約し続ける構造です。予約を止めずにもう一度開始経路を通ると、予約の連鎖が2本になり、更新が倍速になります。

解決法:開始処理の前に動作中かどうかをフラグで判定してください。

❌ ウィンドウの大きさが画面に合わない

原因:geometry("600x450") の固定サイズで起動するためです。

解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。画面のコインは得点として数えるだけの数字で、ファイルへの保存も外部への通信もありません。どちらの課題も新しいライブラリは要らず、「スピン」と「コイン補充(+50)」を押すだけで確かめられます。

  1. 課題1:所持コインの最高記録を並べて表示する

    いまのステータス欄には、そのときの所持コインしか出ていません。これまでで一番多かった数を横に並べ、増えたときだけ更新されるようにしましょう。

    期待結果:起動直後は「💰 所持コイン: 100 最高: 100」と出る。「コイン補充(+50)」を押すと「💰 所持コイン: 150 最高: 150」になる。そこからスピンを1回してそろわなければ「💰 所持コイン: 140 最高: 150」になる

    合格条件

    • コインが減っても「最高」の数字は下がらない
    • 「リセット」を押すと所持コインは 100 に戻る
    • 「最高」は起動した直後から数字が入っている(空欄にならない)

    ヒント①(どこを触るか): 触るのは2か所です。__init__()self.coins = 100 の下と、_update_status() の中です。
    ヒント②(使うもの): __init__()self.best = self.coins を1行足し、_update_status() の先頭で self.best = max(self.best, self.coins) としてから表示を f"💰 所持コイン: {self.coins} 最高: {self.best}" に伸ばします。_update_status()spin()_judge()add_coins()reset() から呼ばれるので、ここだけで全部の場面をカバーできます。
    つまずきやすい点: __init__()self.best を用意せず _update_status() の中だけで比べると、起動のときに AttributeError: 'App067' object has no attribute 'best' が出てウィンドウが開きません。__init__()_build_ui() のあとで _update_status() を呼んでおり、その1回目にはまだ self.best が無いためです。

  2. 課題2:回転中はリセットを受け付けないようにする

    3つのリールが回っている最中でも「リセット」は効いてしまい、所持コインだけが 100 に戻ります。回転が止まるまでは受け付けないようにしましょう。

    期待結果:所持コイン 100 の状態で「スピン」を押し、リールが回っている間に「リセット」を押す。すると「回転が止まってからリセットしてください」と出て、所持コインは 90 のまま変わらない。いまは同じ操作でその場で 100 に戻る

    合格条件

    • 回転が止まっているときは今までどおりリセットできる(コイン 100・リールが ❓ に戻る)
    • 回転中に押しても所持コインの数字は変わらない
    • 回転が終わったあとにもう一度押せばリセットできる

    ヒント①(どこを触るか): 触るのは1か所です。reset() の中の self.coins = 100 の上に足します。
    ヒント②(使うもの): 回転中かどうかは self.spinning が持っています(spin()True になり、_judge() の最後で False に戻ります)。if self.spinning: の中で messagebox.showinfo("情報", "回転が止まってからリセットしてください") を出して return します。
    つまずきやすい点: return を書き忘れて知らせだけを出すと、ダイアログを閉じたあとにそのままリセットが走り、コインは 100 に戻ります。また reset() はリールの絵柄も ❓ に戻しますが、_animate() が書き直すのは range(reel_index, 3) =まだ止まっていないリールだけです。1本目が止まっただけの段階ではまだ2本が回転中なので ❓ は1個しか残りませんが、2本目まで止まって残り1本になってからリセットを押すと、止まった2本とも ❓ のまま最後まで残り、_judge() がその ❓ 同士を絵柄一致として数えて「おしい!2つ揃い」の返還が必ず起きます。

  3. 課題3:保存機能の追加

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリに挑戦しましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
独学を1冊で体系化するなら

写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。