初心者向け No.067

スロットマシン

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

🎯 難易度: ★★☆ 📦 ライブラリ: tkinter(標準ライブラリ) 🧩 作るもの: 倍率表で払い戻す絵文字3リールのスロット(全147行)

1. アプリ概要

乱数で絵柄を選び、そろい方を判定してコインを増減するスロットは、if 文の分岐と画面の書き換えを一緒に練習できる題材です。作るのは、絵文字を使った3リールスロットに当選パターンの判定とコイン管理を付けたカジノゲーム風アプリです。

所持コインは100から始まり、スピン1回ごとに10を払います。3つそろえば絵柄ごとの倍率(7️⃣ は50倍)で払い戻し、2つそろえば払った10コインが戻ります。

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

2. 機能一覧

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

3. 事前準備・環境

ℹ️
動作確認環境

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

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

  • Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認)
  • 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.coins・self.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 と保存します。使うのは random・tkinter だけなので、追加インストールは不要です。

  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 が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:Noto Sans JP はタイトル・所持コイン・結果メッセージの3か所、Arial(48ポイント)は3つのリールの指定です。🍒 や 💎 などの絵文字は Arial に字形がないため、どう描かれるかは OS 側のフォントしだいです。当たり外れは文字どうしを == で比べるので、見た目が違っても3つそろいの払い戻しは PAYOUT の倍率どおりです。直すときは Noto Sans JP の3か所を手元のフォント名へ差し替えるか font 引数ごと外し、リールの font=("Arial", 48) は48を残して名前だけ替えます。リールの引数まで消すと、絵文字が既定サイズまで縮みます。

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

原因:_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エラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
この記事の書き方を本で確かめるなら

このアプリで中心になるのは絵柄のリストと配当の辞書、random.choice() と after() で回すリール。下の表は『スッキリ』と『独習』について、出版社や著者が公開している目次から引いた該当章です(各章の解説内容は当サイトでは未確認。「—」は目次の見出しにその語が無いもので、扱っていないとは限りません)。

この記事で使う書き方スッキリわかるPython入門 第2版独習Python 第2版
リストと辞書chapter 2(コンテナ・2.2 リスト/2.3 ディクショナリ)第6章(標準ライブラリ[コレクション])
random(乱数)—第7章(7.5.2 randomモジュール)
内包表記—第4章(4.2.4 リスト内包表記)/第6章(6.3.5 辞書内包表記)
class(クラス)—第10章(オブジェクト指向構文・10.1 クラスの定義)
文字列と f-stringchapter 1(1.3.4 f-string)第5章(5.2 文字列の操作)

本が無くても進めます。tkinter の部品はtkinter ウィジェット一覧、文法は基本文法一覧(どちらも当サイト)。2冊の違いと向く人は書籍紹介で比較しています。