Curosor(AI)でGalton BoardシミュレーションをPythonで作ってみた
Youtube動画でダルトン板のショートが回ってきたので、お?と思いPythonで作ってみた(作らせた)。僅か15分。いい時代ですね。どなたかの酒のつまみにどうぞw
(手抜き記事で済みません…)
# -------------------------------------------------------
# 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