乱数サイコロ
1〜6の目を複数個同時に振れるアプリ。Canvas上にサイコロの目をグラフィックで描画する実践課題。
1. アプリ概要
1〜6の目を複数個同時に振れるアプリ。Canvas上にサイコロの目をグラフィックで描画する実践課題。
このアプリはgamesカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★☆☆ です。
このアプリは「ゲーム」カテゴリです。ゲーム実装は GUI イベント処理・状態管理・描画ループを同時に学べる教材で、画面・入力・ロジックの三層分離の感覚が掴めます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
動かしながら読むことが理解の最短経路です。まずはコードをコピーして実行し、想定どおりに動くことを確認したうえで解説と照らし合わせてください。
カスタマイズでは「機能追加」「UI 改善」「エラー耐性」の三方向で考えると視野が広がります。練習問題にもそれぞれの方向の具体例を用意しています。
2. 機能一覧
- 「🎲 振る」ボタン(
roll())で操作 messagebox.showerror()によるダイアログ通知- ウィンドウはタイトル「乱数サイコロ」・サイズ 680x441 で起動
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. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import random
import tkinter as tk
from tkinter import messagebox, ttk
# 各目のドット位置(セル中心の相対座標 -1〜+1 グリッド)
PIPS = {
1: [(0, 0)],
2: [(-1, -1), (1, 1)],
3: [(-1, -1), (0, 0), (1, 1)],
4: [(-1, -1), (-1, 1), (1, -1), (1, 1)],
5: [(-1, -1), (-1, 1), (1, -1), (1, 1), (0, 0)],
6: [(-1, -1), (-1, 0), (-1, 1), (1, -1), (1, 0), (1, 1)],
}
class App056:
"""乱数サイコロ"""
def __init__(self, root):
self.root = root
self.root.title("乱数サイコロ")
self.root.geometry("680x441")
self.root.minsize(680, 441)
self.root.configure(bg="#f8f9fc")
self.values = [1]
self._build_ui()
self.draw()
def _build_ui(self):
"""UI を構築する"""
title = tk.Frame(self.root, bg="#3776ab", pady=12)
title.pack(fill=tk.X)
tk.Label(title, text="乱数サイコロ",
font=("Noto Sans JP", 16, "bold"),
bg="#3776ab", fg="white").pack()
ctrl = ttk.LabelFrame(self.root, text="操作", padding=10)
ctrl.pack(fill=tk.X, padx=15, pady=8)
ttk.Label(ctrl, text="個数:").pack(side=tk.LEFT, padx=4)
self.num_var = tk.IntVar(value=2)
ttk.Spinbox(ctrl, from_=1, to=6, textvariable=self.num_var,
width=4).pack(side=tk.LEFT)
ttk.Button(ctrl, text="🎲 振る", command=self.roll).pack(side=tk.LEFT, padx=10)
self.canvas = tk.Canvas(self.root, width=640, height=200, bg="white",
highlightthickness=1, highlightbackground="#ccc")
self.canvas.pack(padx=15, pady=10)
self.sum_label = tk.Label(self.root, text="合計: 1",
font=("Noto Sans JP", 14, "bold"),
bg="#f8f9fc", fg="#3776ab")
self.sum_label.pack(pady=4)
# 履歴
ttk.Label(self.root, text="履歴 (最新5件):").pack()
self.history_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.history_var, bg="#f8f9fc",
fg="#555").pack()
self.history = []
def roll(self):
"""サイコロを振る"""
try:
n = int(self.num_var.get())
except (ValueError, tk.TclError):
messagebox.showerror("エラー", "個数は整数で指定してください")
return
n = max(1, min(6, n))
self.values = [random.randint(1, 6) for _ in range(n)]
self.draw()
s = sum(self.values)
self.sum_label.config(text=f"出目: {self.values} 合計: {s}")
self.history.insert(0, f"{self.values} (計{s})")
self.history = self.history[:5]
self.history_var.set("\n".join(self.history))
def draw(self):
"""Canvas にサイコロを描画する"""
self.canvas.delete("all")
n = len(self.values)
size = min(160, 600 // max(1, n))
gap = 20
total_w = n * size + (n - 1) * gap
start_x = (640 - total_w) // 2
cy = 100
for i, v in enumerate(self.values):
x0 = start_x + i * (size + gap)
y0 = cy - size // 2
self._draw_die(x0, y0, size, v)
def _draw_die(self, x, y, size, value):
"""1個のサイコロを描画する"""
r = size // 12
# 本体
self.canvas.create_rectangle(x, y, x + size, y + size,
fill="white", outline="#333", width=2)
# ドット
cx, cy = x + size // 2, y + size // 2
offset = size // 3
for ox, oy in PIPS[value]:
dx, dy = cx + ox * offset, cy + oy * offset
self.canvas.create_oval(dx - r, dy - r, dx + r, dy + r,
fill="#1f2933", outline="")
if __name__ == "__main__":
root = tk.Tk()
app = App056(root)
root.mainloop()
5. コード解説
乱数サイコロのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App056 クラスにアプリの全機能をまとめています(メソッド5個・全111行)。__init__ ではタイトル「乱数サイコロ」とウィンドウサイズ 680x441 を設定します。そのあと _build_ui() → draw() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame・tk.Label×3・ttk.LabelFrame・ttk.Label×2・ttk.Spinbox・ttk.Button・tk.Canvas で構成しています。見出し付きの枠(LabelFrame)は「操作」のラベルでエリアを区切っています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「🎲 振る」ボタン(roll())を command= で接続しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で ValueError・tk.TclError を捕捉しています。あわせて messagebox.showerror() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app056.py と保存します。使うのは
random・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App056クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("乱数サイコロ")・geometry("680x441")・configure(bg="#f8f9fc")・minsize(680, 441)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame・tk.Label×3・ttk.LabelFrame・ttk.Label×2・ttk.Spinbox・ttk.Button・tk.Canvasを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(roll())につなぎます。 -
6中心になるメソッドを実装する
アプリの本体である
roll()(15行)・draw()(13行)・_draw_die()(13行) を実装します。 -
7動作確認する
python app056.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 結果をファイルに保存する
このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("680x441") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(680, 441) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。どちらの課題も新しいライブラリは要りません(起動して「🎲 振る」を押すだけで確かめられます)。
-
課題1:ゾロ目のときだけ履歴に印を付ける
履歴は出目と合計だけが並びます。すべて同じ目が出た回をあとから見つけられるよう、履歴に印を足しましょう。
期待結果:個数2で振って
[4, 4]が出たとき、履歴の1行目が[4, 4] (計8) ★ゾロ目になる。[2, 5]のときは[2, 5] (計7)のまま。合格条件
- 2個以上で全部同じ目のときだけ ★ゾロ目 が付く
- 個数1のときは付かない
- 履歴は今までどおり最新5件までしか残らない
ヒント①(どこを触るか): 触るのは1か所です。
roll()のself.history.insert(0, f"{self.values} (計{s})")です。
ヒント②(使うもの):set()は重複を取り除くので、全部同じ目ならlen(set(self.values))は1になります。mark = " ★ゾロ目" if len(set(self.values)) == 1 and len(self.values) >= 2 else ""を作り、f 文字列の末尾に{mark}を足しましょう。
つまずきやすい点:len(set(self.values)) == 1だけで判定すると、個数1のときは要素が必ず1個なので毎回ゾロ目になります。合計欄はself.sum_label.config(...)で別に作っているため、履歴だけ直しても合計欄には印が付きません。 -
課題2:4個以上でサイコロが画面からはみ出すのを直す
個数を増やすとサイコロの大きさは自動で小さくなりますが、あいだの隙間が計算に入っていません。隙間の分を差し引いて、6個でも全部が収まるようにしましょう。
期待結果:個数6で「🎲 振る」を押すと、6個すべての外枠が見える。いまは1辺100ピクセル・隙間20ピクセルで全体が700ピクセルになり、幅640ピクセルの Canvas から左右に30ピクセルずつはみ出して枠が切れる。
合格条件
- 個数6でも、左端と右端のサイコロの枠線が切れずに見える
- 個数1〜3のときの大きさは今までどおり(1辺160ピクセル)
- 合計欄と履歴の表示は変わらない
ヒント①(どこを触るか): 触るのは2か所です。
draw()のsize = min(160, 600 // max(1, n))と、その次の行のgap = 20です。この2行は順番と式の両方を変えます。
ヒント②(使うもの): 隙間の合計は(n - 1) * gapです。gap = 20を先に書いてからsize = min(160, (600 - (n - 1) * gap) // max(1, n))にすると、6個で1辺83ピクセル・全体598ピクセルとなり Canvas に収まります。
つまずきやすい点:gap = 20の行を上に移さずにsizeの式で使うとUnboundLocalErrorが出ます(Python 3.12 のメッセージはcannot access local variable 'gap' where it is not associated with a value)。min(160, ...)を外すのも禁物で、個数1のとき1辺600ピクセルになり、高さ200ピクセルの Canvas に収まりません。 -
課題3:保存機能の追加
処理結果をファイルに保存する機能を追加しましょう。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。