import tkinter as tk
from tkinter import ttk, messagebox
import csv
import random
from datetime import datetime, timedelta
import threading
import time
import os
PRIZES = {
'地球钥匙扣': {'initial_count': 5, 'current_count': 5},
'鼠标垫': {'initial_count': 5, 'current_count': 5},
'徽章': {'initial_count': 5, 'current_count': 5}
}
RECORDS_FILE = 'lottery_records.csv'
START_HOUR = 16
HALF_HOUR_INTERVAL = 30 * 60
PROBABILITY_INCREASE_PER_INTERVAL = 0.001
INITIAL_BASE_PROB = 0.045
class LotteryGUI:
def __init__(self, root):
self.root = root
self.root.title("抽奖程序")
self.root.geometry("700x500")
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.base_probability = INITIAL_BASE_PROB
self.last_update_time = datetime.now()
self.lock = threading.Lock()
self.used_ids = set()
self.ensure_records_file_exists()
self.load_initial_state()
self.timer_thread = threading.Thread(target=self._update_probabilities_loop, daemon=True)
self.timer_thread.start()
self.build_gui()
self.prob_display_thread = threading.Thread(target=self._update_probability_display_loop, daemon=True)
self.prob_display_thread.start()
def ensure_records_file_exists(self):
"""确保记录文件存在,不存在则创建并写入表头"""
if not os.path.exists(RECORDS_FILE):
print(f"记录文件 {RECORDS_FILE} 不存在,正在创建新文件。")
with open(RECORDS_FILE, mode='w', newline='', encoding='utf-8') as file:
fieldnames = ['Timestamp', 'Name', 'StudentID', 'Prize']
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
print(f"已创建 {RECORDS_FILE} 并写入表头。")
def load_initial_state(self):
"""程序启动时读取文件,构建初始的 used_ids 集合和奖品状态"""
try:
with open(RECORDS_FILE, mode='r', newline='', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
student_id = row['StudentID']
prize = row['Prize']
self.used_ids.add(student_id)
if prize in PRIZES:
PRIZES[prize]['current_count'] -= 1
if PRIZES[prize]['current_count'] < 0:
PRIZES[prize]['current_count'] = 0
print(f"已从 {RECORDS_FILE} 加载 {len(self.used_ids)} 条历史记录。")
except FileNotFoundError:
print(f"记录文件 {RECORDS_FILE} 不存在,将从空状态开始。")
except Exception as e:
print(f"加载初始状态时出错: {e}")
self.used_ids = set()
def build_gui(self):
input_frame = ttk.Frame(self.root, padding="20")
input_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
input_frame.grid_rowconfigure(0, weight=1)
input_frame.grid_rowconfigure(1, weight=1)
input_frame.grid_columnconfigure(0, weight=1)
input_frame.grid_columnconfigure(1, weight=1)
name_label_frame = ttk.Frame(input_frame)
name_label_frame.grid(row=0, column=0, sticky=tk.E, padx=(0, 10), pady=(0, 10))
self.name_label = ttk.Label(name_label_frame, text="姓名:", font=("Arial", 12))
self.name_label.grid(row=0, column=0)
self.name_label.bind("<Button-1>", self.show_probabilities_window)
name_label_frame.bind("<Button-1>", self.show_probabilities_window)
self.name_entry = ttk.Entry(input_frame, width=30, font=("Arial", 12))
self.name_entry.grid(row=0, column=1, sticky=tk.W, pady=(0, 10))
id_label_frame = ttk.Frame(input_frame)
id_label_frame.grid(row=1, column=0, sticky=tk.E, padx=(0, 10), pady=(0, 20))
ttk.Label(id_label_frame, text="学号:", font=("Arial", 12)).grid(row=0, column=0)
self.id_entry = ttk.Entry(input_frame, width=30, font=("Arial", 12))
self.id_entry.grid(row=1, column=1, sticky=tk.W, pady=(0, 20))
button_frame = ttk.Frame(self.root)
button_frame.grid(row=1, column=0, pady=10)
self.draw_button = ttk.Button(button_frame, text="抽奖", command=self.draw, width=20, padding=10, style='Large.TButton')
self.draw_button.pack()
result_frame = ttk.Frame(self.root)
result_frame.grid(row=2, column=0, pady=10)
self.result_label = ttk.Label(result_frame, text="请输入您的姓名和学号参与抽奖!", foreground="blue", font=("Arial", 12))
self.result_label.pack()
self.prob_window = None
self.prob_text_widget = None
style = ttk.Style()
style.configure('Large.TButton', font=('Arial', 14), padding=10)
def _update_probabilities_loop(self):
"""在后台线程中定期更新基础概率"""
while True:
time.sleep(10)
now = datetime.now()
if now.hour >= START_HOUR or (now.hour == START_HOUR - 1 and now.minute == 59):
elapsed_seconds = (now - self.last_update_time).total_seconds()
intervals_passed = int(elapsed_seconds // HALF_HOUR_INTERVAL)
if intervals_passed > 0:
with self.lock:
self.base_probability += intervals_passed * PROBABILITY_INCREASE_PER_INTERVAL
self.last_update_time = now + timedelta(seconds=(elapsed_seconds % HALF_HOUR_INTERVAL))
print(f"[DEBUG] 时间更新: 基础概率提升至 {self.base_probability:.4f}")
def _update_probability_display_loop(self):
"""在后台线程中定期更新概率显示文本框(如果窗口存在)"""
while True:
time.sleep(5)
self.update_probability_display()
def update_probability_display(self):
"""计算并更新显示各奖品的当前概率(如果概率窗口存在)"""
if self.prob_window and self.prob_window.winfo_exists():
with self.lock:
current_base_prob = self.base_probability
current_prizes = {k: v.copy() for k, v in PRIZES.items()}
prob_text = "当前奖品中奖概率:\n"
total_prob = 0
for prize_name, details in current_prizes.items():
if details['initial_count'] > 0:
current_prob = current_base_prob * (details['current_count'] / details['initial_count'])
total_prob += current_prob
prob_text += f"- {prize_name}: {current_prob * 100:.4f}% (剩余: {details['current_count']}/{details['initial_count']})\n"
else:
prob_text += f"- {prize_name}: 0.0000% (剩余: {details['current_count']}/{details['initial_count']})\n"
prob_text += f"\n未中奖概率: {max(0.0, 1.0 - total_prob) * 100:.4f}%"
prob_text += f"\n基础概率: {current_base_prob * 100:.4f}%"
self.prob_text_widget.config(state='normal')
self.prob_text_widget.delete(1.0, tk.END)
self.prob_text_widget.insert(tk.END, prob_text)
self.prob_text_widget.config(state='disabled')
def show_probabilities_window(self, event=None):
"""点击姓名标签时显示概率窗口"""
if self.prob_window is None or not self.prob_window.winfo_exists():
self.prob_window = tk.Toplevel(self.root)
self.prob_window.title("当前中奖概率")
self.prob_window.geometry("500x300")
self.prob_window.resizable(False, False)
text_frame = ttk.Frame(self.prob_window)
text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.prob_text_widget = tk.Text(text_frame, height=15, width=60, font=("Arial", 10), state='disabled')
self.prob_text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.prob_text_widget.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.prob_text_widget.configure(yscrollcommand=scrollbar.set)
self.update_probability_display()
else:
self.prob_window.lift()
self.prob_window.focus_force()
def draw(self):
name = self.name_entry.get().strip()
student_id = self.id_entry.get().strip()
if not name or not student_id:
messagebox.showwarning("输入错误", "请填写姓名和学号!")
return
with self.lock:
if student_id in self.used_ids:
self.result_label.config(text=f"{name} (学号: {student_id}),您已经抽过奖了!", foreground="red")
return
active_prizes = {name: details for name, details in PRIZES.items() if details['current_count'] > 0}
if not active_prizes:
self.result_label.config(text="所有奖品已抽完!", foreground="red")
self.draw_button.config(state='disabled')
return
prize_names = []
weights = []
total_weight = 0
with self.lock:
for prize_name, details in active_prizes.items():
current_prob = self.base_probability * (details['current_count'] / details['initial_count'])
prize_names.append(prize_name)
weights.append(current_prob)
total_weight += current_prob
no_prize_prob = max(0.0, 1.0 - total_weight)
prize_names.append("未中奖")
weights.append(no_prize_prob)
chosen_prize = random.choices(prize_names, weights=weights, k=1)[0]
with self.lock:
self.used_ids.add(student_id)
if chosen_prize == "未中奖":
result_text = f"{name} (学号: {student_id}),很遗憾,没有中奖!"
result_color = "orange"
prize_to_save = "未中奖"
else:
with self.lock:
if PRIZES[chosen_prize]['current_count'] > 0:
PRIZES[chosen_prize]['current_count'] -= 1
result_text = f"恭喜 {name} (学号: {student_id}) 抽中了 {chosen_prize}!"
result_color = "green"
prize_to_save = chosen_prize
if all(details['current_count'] <= 0 for details in PRIZES.values()):
self.draw_button.config(state='disabled')
print("所有奖品已抽完,抽奖结束。")
else:
self.used_ids.discard(student_id)
result_text = f"{name} (学号: {student_id}),很遗憾,没有中奖!"
result_color = "orange"
prize_to_save = "未中奖"
save_success = self.save_record(name, student_id, prize_to_save)
if not save_success:
with self.lock:
self.used_ids.discard(student_id)
result_text = f"抽奖记录保存失败,请联系管理员。"
result_color = "red"
messagebox.showerror("错误", "抽奖记录保存失败!")
self.result_label.config(text=result_text, foreground=result_color)
self.name_entry.delete(0, tk.END)
self.id_entry.delete(0, tk.END)
def save_record(self, name, student_id, prize):
"""安全地将抽奖记录追加到文件末尾"""
try:
with open(RECORDS_FILE, mode='a', newline='', encoding='utf-8') as file:
fieldnames = ['Timestamp', 'Name', 'StudentID', 'Prize']
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writerow({
'Timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'Name': name,
'StudentID': student_id,
'Prize': prize
})
return True
except Exception as e:
print(f"保存记录时出错: {e}")
return False
if __name__ == "__main__":
root = tk.Tk()
app = LotteryGUI(root)
root.mainloop()