一个根据时间和剩余奖品数量动态调整抽奖概率的抽奖器

动态调整概率的抽奖器设计
# lottery_gui.py

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  # 活动开始小时 (24小时制)
HALF_HOUR_INTERVAL = 30 * 60  # 30分钟的秒数
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) # 让第一行(包含input_frame)占据所有可用垂直空间
        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() # 用于存储已参与的ID

        # 确保记录文件存在,若不存在则创建并写入表头
        self.ensure_records_file_exists()

        # 启动时读取历史记录以填充 used_ids 集合和奖品状态
        self.load_initial_state()

        # 启动时间更新线程
        self.timer_thread = threading.Thread(target=self._update_probabilities_loop, daemon=True)
        self.timer_thread.start()

        # 构建GUI
        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 内部的网格权重,使内容居中
        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)) # 使用 sticky=tk.E 让标签框架靠右(在列宽的一半处看起来像居中)
        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)) # 使用 sticky=tk.W 让输入框靠左(在列宽的一半处看起来像居中)

        # 学号标签和输入框
        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() # 使用 pack 在 frame 内部居中

        # --- 结果显示区 ---
        # 同样放在一个新的行中
        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() # 使用 pack 在 frame 内部居中

        # --- 概率显示窗口 (初始隐藏) ---
        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) # 每10秒检查一次
            now = datetime.now()
            # 检查是否过了16点
            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) # 每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): # event参数是绑定事件时传入的,可以忽略
        """点击姓名标签时显示概率窗口"""
        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]

        # 添加ID到集合
        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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值