0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Curosor(AI)でGalton BoardシミュレーションをPythonで作ってみた

Posted at

Curosor(AI)でGalton BoardシミュレーションをPythonで作ってみた

Youtube動画でダルトン板のショートが回ってきたので、お?と思いPythonで作ってみた(作らせた)。僅か15分。いい時代ですね。どなたかの酒のつまみにどうぞw
(手抜き記事で済みません…)
rapture_20250505142124.png

# -------------------------------------------------------
# Galton Board Simulator(EMYH)
# Generated by Cursor
# -------------------------------------------------------
import tkinter as tk
import random
import time
import math
from tkinter import simpledialog

class GaltonBoard:
    def __init__(self, root, rows=8, balls=100):
        self.root = root
        self.rows = rows
        self.balls = balls
        self.canvas_width = 400
        self.canvas_height = 500
        self.pins = []
        self.bins = [0] * (rows + 1)
        self.ball_speed = 5  # ボールの落下速度

        self.canvas = tk.Canvas(root, width=self.canvas_width, height=self.canvas_height, bg="white")
        self.canvas.pack()

        self.create_pins()
        self.draw_bins()
        self.animate_balls()

    def create_pins(self):
        """ ピンを配置 """
        for row in range(self.rows):
            for col in range(row + 1):
                x = self.canvas_width // 2 + (col - row / 2) * 40
                y = 50 + row * 40
                self.pins.append((x, y))
                self.canvas.create_oval(x-5, y-5, x+5, y+5, fill="black")

    def get_pin_xs_for_row(self, row):
        # 指定した段のピンのx座標リストを返す
        return [self.canvas_width // 2 + (col - row / 2) * 40 for col in range(row + 1)]

    def animate_balls(self):
        """ ボールの落下をアニメーション化(ピンの位置で跳ねる, 加速度付き, 全体で約15秒, ピンの真下でのみ跳ねる, 時間制御, ボール数表示) """
        import time
        total_time = 15.0  # 全体の目標時間(秒)
        start_time = time.perf_counter()
        accel = max(0.5, 20 / self.rows)
        for ball_idx in range(self.balls):
            row = 0
            col = 0  # 最上段のピンは1つだけ
            x = self.get_pin_xs_for_row(0)[0]
            y = 50
            ball = self.canvas.create_oval(x-5, y-5, x+5, y+5, fill="red")

            for row in range(self.rows):
                pin_xs = self.get_pin_xs_for_row(row)
                x = pin_xs[col]
                target_y = 50 + row * 40
                velocity = 1
                while y < target_y:
                    elapsed = time.perf_counter() - start_time
                    remaining_balls = self.balls - ball_idx
                    remaining_time = max(total_time - elapsed, 0.001)
                    remaining_steps = remaining_balls * (self.rows * 2)
                    base_sleep = max(remaining_time / remaining_steps, 0.0005)

                    self.canvas.move(ball, 0, velocity)
                    y += velocity
                    velocity = min(velocity + accel, 20)
                    self.root.update()
                    time.sleep(base_sleep)
                if y != target_y:
                    self.canvas.move(ball, 0, target_y - y)
                    y = target_y
                    self.root.update()
                direction = random.choice([0, 1])  # 0:左, 1:右
                if direction == 1:
                    col += 1
                if row < self.rows - 1:
                    next_pin_xs = self.get_pin_xs_for_row(row + 1)
                    next_x = next_pin_xs[col]
                    dx = (next_x - x)
                    step = 1 if dx > 0 else -1
                    moved = 0
                    velocity = 1
                    while abs(moved) < abs(dx):
                        elapsed = time.perf_counter() - start_time
                        remaining_balls = self.balls - ball_idx
                        remaining_time = max(total_time - elapsed, 0.001)
                        remaining_steps = remaining_balls * (self.rows * 2)
                        base_sleep = max(remaining_time / remaining_steps, 0.0005)

                        self.canvas.move(ball, step * velocity, 0)
                        x += step * velocity
                        moved += abs(step * velocity)
                        velocity = min(velocity + accel, 20)
                        self.root.update()
                        time.sleep(base_sleep)
                    if x != next_x:
                        self.canvas.move(ball, next_x - x, 0)
                        x = next_x
                        self.root.update()
            bin_index = col
            self.bins[bin_index] += 1
            self.canvas.delete(ball)
            self.update_bins()
            self.update_info(ball_idx + 1)
        # シミュレーション完了後にメッセージ表示(画面中央、フォントサイズ12)
        self.canvas.create_text(self.canvas_width // 2, self.canvas_height // 2, text="Q:終了、R:再実行、P:パラメータ変更", font=("Arial", 12, "bold"), fill="black", tags="endmsg")

    def get_bottom_pin_xs(self):
        # 最下段のピンのx座標リストを返す
        bottom_row = self.rows - 1
        xs = []
        for col in range(self.rows + 1):
            x = self.canvas_width // 2 + (col - self.rows / 2) * 40
            xs.append(x)
        return xs

    def draw_normal_curve(self):
        # binの範囲を細かく分割して滑らかな正規分布曲線を描画
        xs = self.get_bottom_pin_xs()
        n_points = 100
        n = self.rows
        mu = n / 2
        sigma = (n / 4) ** 0.5
        min_x = xs[0]
        max_x = xs[-1]
        # x軸上で細かくサンプリング
        points = []
        for i in range(n_points + 1):
            frac = i / n_points
            # bin indexに対応する理論値
            bin_pos = frac * n
            # x座標を線形補間
            cx = min_x + frac * (max_x - min_x)
            # 正規分布値
            p = (1 / (sigma * (2 * math.pi) ** 0.5)) * math.exp(-((bin_pos - mu) ** 2) / (2 * sigma ** 2))
            points.append((cx, self.canvas_height - (p * 200 / max(1e-8, p))))
        # 正規化して最大値を200ピクセルに合わせる
        max_p = max((1 / (sigma * (2 * math.pi) ** 0.5)) * math.exp(-((i - mu) ** 2) / (2 * sigma ** 2)) for i in range(n+1))
        for i in range(len(points)):
            frac = i / n_points
            bin_pos = frac * n
            p = (1 / (sigma * (2 * math.pi) ** 0.5)) * math.exp(-((bin_pos - mu) ** 2) / (2 * sigma ** 2))
            points[i] = (points[i][0], self.canvas_height - (p / max_p) * 200)
        # 曲線を描画
        for i in range(len(points) - 1):
            self.canvas.create_line(points[i][0], points[i][1], points[i+1][0], points[i+1][1], fill="red", width=2, tags="normal_curve")

    def update_bins(self):
        self.canvas.delete("bin")
        self.canvas.delete("normal_curve")
        xs = self.get_bottom_pin_xs()
        max_count = max(self.bins) if max(self.bins) > 0 else 1
        for i, count in enumerate(self.bins):
            x1 = xs[i] - 15
            x2 = xs[i] + 15
            y1 = self.canvas_height
            y2 = self.canvas_height - (count / max_count) * 200
            self.canvas.create_rectangle(x1, y1, x2, y2, fill="blue", outline="black", tags="bin")
        self.draw_normal_curve()

    def update_info(self, dropped_balls=None):
        # 画面上部に情報を表示
        self.canvas.delete("info")
        if dropped_balls is None:
            dropped_balls = 0
        info_text1 = f"段数: {self.rows}    ボール数: {self.balls}    落としたボール: {dropped_balls}"
        info_text2 = f"bin: {self.bins}"
        self.canvas.create_text(self.canvas_width // 2, 20, text=info_text1, font=("Arial", 12), fill="black", tags="info")
        self.canvas.create_text(self.canvas_width // 2, 40, text=info_text2, font=("Arial", 12), fill="black", tags="info")

    def draw_bins(self):
        xs = self.get_bottom_pin_xs()
        for i in range(len(xs)):
            x1 = xs[i] - 15
            x2 = xs[i] + 15
            y1 = self.canvas_height
            y2 = self.canvas_height
            self.canvas.create_rectangle(x1, y1, x2, y2, fill="blue", outline="black", tags="bin")
        self.draw_normal_curve()
        self.update_info(0)

if __name__ == "__main__":
    import sys
    def restart_board(rows, balls):
        for widget in root.winfo_children():
            widget.destroy()
        board = GaltonBoard(root, rows=rows, balls=balls)
        root.board = board

    root = tk.Tk()
    root.title("ダルトン板シミュレーション")
    default_rows = 5
    default_balls = 100
    restart_board(default_rows, default_balls)

    def on_key(event):
        if event.char == 'q':
            root.destroy()
        elif event.char == 'r':
            restart_board(root.board.rows, root.board.balls)
        elif event.char == 'p':
            rows = simpledialog.askinteger("段数の入力", "段数を入力してください (2~12):", initialvalue=root.board.rows, minvalue=2, maxvalue=20)
            balls = simpledialog.askinteger("ボール数の入力", "ボール数を入力してください (1~2000):", initialvalue=root.board.balls, minvalue=1, maxvalue=2000)
            if rows and balls:
                restart_board(rows, balls)

    root.bind('<Key>', on_key)
    root.mainloop()

以下はrequirements.txtです。

ansicon==1.89.0
appdirs==1.4.4
blessed==1.21.0
flyingcircus==0.1.4.1
jinxed==1.3.0
packaging==25.0
pytk==0.0.2.1
setuptools==80.3.1
setuptools-scm==8.3.1
wcwidth==0.2.13
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?