#!/usr/bin/env python3 # -*- coding: utf-8 -*- import tkinter as tk from tkinter import messagebox # 常量定义 SERVE_ICON = "★" # 代表当前发球/接球的图标 class PickleballApp: def __init__(self, master): self.master = master self.master.title("匹克球比赛计分系统") self.master.geometry("1000x750") # 增加窗口大小以适应更多内容 # 背景标签 self.title_label = tk.Label(master, text="南海匹克球协会专用计分系统", font=("Arial", 26, "bold"), fg="#0056b3") self.title_label.pack(pady=15) # 比赛状态变量初始化(大部分在 start_match 时初始化) self.current_game_score_A = 0 self.current_game_score_B = 0 self.set_score_A = 0 self.set_score_B = 0 self.current_serving_team = None # 'A' or 'B' self.current_server_index = 0 # 0 for first server, 1 for second server (doubles specific) self.first_serve_of_match = True # Special rule for the very first serve of the match self.game_points_target = 0 self.is_match_over = False # 参数设置区Frame self.param_frame = tk.Frame(master, bd=2, relief="groove", padx=20, pady=20) self.param_frame.pack(pady=10) self.setup_parameters() # 启动匹克球比赛 self.start_button = tk.Button(master, text="开始比赛", font=("Arial", 18, "bold"), command=self.start_match, bg="#28a745", fg="white") self.start_button.pack(pady=20) self.reset_button = tk.Button(master, text="重置所有", font=("Arial", 14), command=self.reset_all, bg="#dc3545", fg="white") self.reset_button.pack(pady=10) # 初始也显示重置按钮 def setup_parameters(self): # 通用参数设置 # 比赛类型:单打/双打 tk.Label(self.param_frame, text="比赛类型:", font=("Arial", 14)).grid(row=0, column=0, sticky="w", pady=5) self.match_type = tk.StringVar(value="single") tk.Radiobutton(self.param_frame, text="单打", variable=self.match_type, value="single", command=self.check_match_type, font=("Arial", 12)).grid(row=0, column=1, padx=5, sticky="w") tk.Radiobutton(self.param_frame, text="双打", variable=self.match_type, value="double", command=self.check_match_type, font=("Arial", 12)).grid(row=0, column=2, padx=5, sticky="w") # 比赛局数模式 tk.Label(self.param_frame, text="局数模式:", font=("Arial", 14)).grid(row=1, column=0, sticky="w", pady=5) self.set_mode = tk.StringVar(value="best-of-three") # 默认三局两胜 tk.Radiobutton(self.param_frame, text="一局决胜", variable=self.set_mode, value="one-set", font=("Arial", 12)).grid(row=1, column=1, padx=5, sticky="w") tk.Radiobutton(self.param_frame, text="三局两胜", variable=self.set_mode, value="best-of-three", font=("Arial", 12)).grid(row=1, column=2, padx=5, sticky="w") # 局分胜负规则:金球决胜 or 两分决胜 (主要影响局分达到后,是否需要领先两分) tk.Label(self.param_frame, text="局胜负规则:", font=("Arial", 14)).grid(row=2, column=0, sticky="w", pady=5) self.win_rule = tk.StringVar(value="two-point") # 默认两分决胜 tk.Radiobutton(self.param_frame, text="金球决胜", variable=self.win_rule, value="golden", font=("Arial", 12)).grid(row=2, column=1, padx=5, sticky="w") tk.Radiobutton(self.param_frame, text="两分决胜", variable=self.win_rule, value="two-point", font=("Arial", 12)).grid(row=2, column=2, padx=5, sticky="w") # 计分方式:直接得分 or 发球得分 tk.Label(self.param_frame, text="计分方式:", font=("Arial", 14)).grid(row=3, column=0, sticky="w", pady=5) self.score_method = tk.StringVar(value="direct") # 默认直接得分 tk.Radiobutton(self.param_frame, text="直接得分", variable=self.score_method, value="direct", font=("Arial", 12)).grid(row=3, column=1, padx=5, sticky="w") tk.Radiobutton(self.param_frame, text="发球得分", variable=self.score_method, value="serve", font=("Arial", 12)).grid(row=3, column=2, padx=5, sticky="w") # 每局局分设定 tk.Label(self.param_frame, text="每局局分:", font=("Arial", 14)).grid(row=4, column=0, sticky="w", pady=5) self.game_points_entry = tk.Entry(self.param_frame, width=5, font=("Arial", 14)) self.game_points_entry.insert(0, "11") # 默认11分 self.game_points_entry.grid(row=4, column=1, padx=5, sticky="w") # 球员姓名输入区 self.player_names_frame = tk.Frame(self.param_frame) self.player_names_frame.grid(row=5, column=0, columnspan=4, pady=10) # 确保有足够空间 # 单打球员姓名 self.singles_frame = tk.Frame(self.player_names_frame) tk.Label(self.singles_frame, text="A队 (Player A) 姓名:", font=("Arial", 12)).grid(row=0, column=0, sticky="e") self.player_a_name_entry = tk.Entry(self.singles_frame, font=("Arial", 12)) self.player_a_name_entry.insert(0, "A Player") self.player_a_name_entry.grid(row=0, column=1, padx=5) tk.Label(self.singles_frame, text="B队 (Player B) 姓名:", font=("Arial", 12)).grid(row=1, column=0, sticky="e") self.player_b_name_entry = tk.Entry(self.singles_frame, font=("Arial", 12)) self.player_b_name_entry.insert(0, "B Player") self.player_b_name_entry.grid(row=1, column=1, padx=5) # 双打球员姓名 self.doubles_frame = tk.Frame(self.player_names_frame) tk.Label(self.doubles_frame, text="A队 1号球员 (首发球员):", font=("Arial", 12)).grid(row=0, column=0, sticky="e") self.a1_name_entry = tk.Entry(self.doubles_frame, font=("Arial", 12)) self.a1_name_entry.insert(0, "A1") self.a1_name_entry.grid(row=0, column=1, padx=5) tk.Label(self.doubles_frame, text="A队 2号球员 (首接球员):", font=("Arial", 12)).grid(row=1, column=0, sticky="e") self.a2_name_entry = tk.Entry(self.doubles_frame, font=("Arial", 12)) self.a2_name_entry.insert(0, "A2") self.a2_name_entry.grid(row=1, column=1, padx=5) tk.Label(self.doubles_frame, text="B队 1号球员 (首发球员):", font=("Arial", 12)).grid(row=0, column=2, sticky="e") self.b1_name_entry = tk.Entry(self.doubles_frame, font=("Arial", 12)) self.b1_name_entry.insert(0, "B1") self.b1_name_entry.grid(row=0, column=3, padx=5) tk.Label(self.doubles_frame, text="B队 2号球员 (首接球员):", font=("Arial", 12)).grid(row=1, column=2, sticky="e") self.b2_name_entry = tk.Entry(self.doubles_frame, font=("Arial", 12)) self.b2_name_entry.insert(0, "B2") self.b2_name_entry.grid(row=1, column=3, padx=5) self.check_match_type() # 初始根据默认值显示正确的球员输入框 def check_match_type(self): # 切换显示单打或双打的球员输入框 if self.match_type.get() == "double": self.singles_frame.pack_forget() self.doubles_frame.pack(fill="x", expand=True) else: self.doubles_frame.pack_forget() self.singles_frame.pack(fill="x", expand=True) def start_match(self): # 检查局分是否为整数 try: self.game_points_target = int(self.game_points_entry.get()) if self.game_points_target <= 0: raise ValueError except ValueError: messagebox.showerror("输入错误", "请输入合法的局分(正整数)!") return # 检查球员姓名是否填入 if self.match_type.get() == "single": self.player_a_name = self.player_a_name_entry.get().strip() or "A Player" self.player_b_name = self.player_b_name_entry.get().strip() or "B Player" if not (self.player_a_name and self.player_b_name): messagebox.showerror("输入错误", "请输入单打球员姓名!") return self.team_A_players = [self.player_a_name] self.team_B_players = [self.player_b_name] else: # double self.a1_name = self.a1_name_entry.get().strip() self.a2_name = self.a2_name_entry.get().strip() self.b1_name = self.b1_name_entry.get().strip() self.b2_name = self.b2_name_entry.get().strip() if not (self.a1_name and self.a2_name and self.b1_name and self.b2_name): messagebox.showerror("输入错误", "请填写双方首发球员和首接球员姓名!") return # 双打球员列表,用于轮换。第0个元素是默认的第一发球员,第1个是默认的第二发球员 self.team_A_players = [self.a1_name, self.a2_name] self.team_B_players = [self.b1_name, self.b2_name] # 隐藏参数设置界面,进入比赛界面 self.param_frame.pack_forget() self.start_button.pack_forget() self.reset_button.pack_forget() # 比赛进行时,重置按钮显示在比赛界面内 # 创建比赛计分界面 self.match_frame = tk.Frame(self.master) self.match_frame.pack(pady=10) self.setup_match_screen() # 初始化比赛状态 self.reset_current_game_scores() self.set_score_A = 0 self.set_score_B = 0 self.current_serving_team = 'A' # 默认A队开始发球 self.current_server_index = 0 # 默认A队的第一发球员(即team_A_players[0]) self.first_serve_of_match = True # 标记比赛的第一次发球 self.is_match_over = False self.update_display() # 初始显示状态 def setup_match_screen(self): # 比赛分数显示 score_display_frame = tk.Frame(self.match_frame, bd=2, relief="solid", padx=20, pady=10) score_display_frame.pack(pady=10) # 队伍名称或选手名称 self.team_a_label = tk.Label(score_display_frame, text=self.team_A_players[0] if self.match_type.get() == "single" else "A队", font=("Arial", 22, "bold")) self.team_a_label.grid(row=0, column=0, padx=20) self.team_b_label = tk.Label(score_display_frame, text=self.team_B_players[0] if self.match_type.get() == "single" else "B队", font=("Arial", 22, "bold")) self.team_b_label.grid(row=0, column=2, padx=20) # 当前局比分 self.score_label_A = tk.Label(score_display_frame, text="0", font=("Arial", 48, "bold"), fg="blue") self.score_label_A.grid(row=1, column=0, padx=20) self.score_label_B = tk.Label(score_display_frame, text="0", font=("Arial", 48, "bold"), fg="red") self.score_label_B.grid(row=1, column=2, padx=20) tk.Label(score_display_frame, text="-", font=("Arial", 48, "bold")).grid(row=1, column=1) # 总局分 (Sets Won) tk.Label(score_display_frame, text="总局数", font=("Arial", 16)).grid(row=2, column=0, columnspan=3, pady=(10,0)) self.set_score_label_A = tk.Label(score_display_frame, text="0", font=("Arial", 28, "bold")) self.set_score_label_A.grid(row=3, column=0) self.set_score_label_B = tk.Label(score_display_frame, text="0", font=("Arial", 28, "bold")) self.set_score_label_B.grid(row=3, column=2) tk.Label(score_display_frame, text=":", font=("Arial", 28, "bold")).grid(row=3, column=1) # 控制区:得分按钮 btn_frame = tk.Frame(self.match_frame, pady=15) btn_frame.pack(pady=10) self.btn_A_score = tk.Button(btn_frame, text=f"{self.team_A_players[0]} 得分", font=("Arial", 16), command=lambda: self.add_point("A"), bg="#6c757d", fg="white") self.btn_A_score.grid(row=0, column=0, padx=15, pady=5) self.btn_B_score = tk.Button(btn_frame, text=f"{self.team_B_players[0]} 得分", font=("Arial", 16), command=lambda: self.add_point("B"), bg="#6c757d", fg="white") self.btn_B_score.grid(row=0, column=1, padx=15, pady=5) # 发球/接球示意区 self.serve_display_frame = tk.Frame(self.match_frame, bd=2, relief="ridge", padx=15, pady=15) self.serve_display_frame.pack(pady=10) self.setup_serve_display() # 首次调用显示 # 比赛控制按钮 match_control_frame = tk.Frame(self.match_frame, pady=10) match_control_frame.pack(pady=10) # 匹克球一般不需要手动切换发球,得分或失误会自动切换。这里提供一个用于调试或特殊情况的按钮。 self.manual_change_serve_button = tk.Button(match_control_frame, text="手动切换发球方", font=("Arial", 14), command=self.manual_change_serve_team, bg="#ffc107") self.manual_change_serve_button.grid(row=0, column=0, padx=10) # 重置按钮在比赛界面中显示 self.reset_match_button = tk.Button(match_control_frame, text="重置比赛", font=("Arial", 14), command=self.reset_all, bg="#dc3545", fg="white") self.reset_match_button.grid(row=0, column=1, padx=10) def update_display(self): # 更新当前局比分 self.score_label_A.config(text=str(self.current_game_score_A)) self.score_label_B.config(text=str(self.current_game_score_B)) # 更新总局分 self.set_score_label_A.config(text=str(self.set_score_A)) self.set_score_label_B.config(text=str(self.set_score_B)) # 更新得分按钮的文本(双打时显示队员姓名) if self.match_type.get() == "single": self.btn_A_score.config(text=f"{self.team_A_players[0]} 得分") self.btn_B_score.config(text=f"{self.team_B_players[0]} 得分") else: # 按钮文本保持 "A队得分" / "B队得分" 更简洁 self.btn_A_score.config(text="A队 得分") self.btn_B_score.config(text="B队 得分") # 更新发球示意 self.setup_serve_display() # 比赛结束时禁用按钮 if self.is_match_over: self.btn_A_score.config(state=tk.DISABLED) self.btn_B_score.config(state=tk.DISABLED) self.manual_change_serve_button.config(state=tk.DISABLED) else: self.btn_A_score.config(state=tk.NORMAL) self.btn_B_score.config(state=tk.NORMAL) self.manual_change_serve_button.config(state=tk.NORMAL) def add_point(self, scoring_team): if self.is_match_over: messagebox.showinfo("比赛结束", "比赛已结束,请重置比赛。") return if self.score_method.get() == "direct": # 直接得分模式 if scoring_team == "A": self.current_game_score_A += 1 else: self.current_game_score_B += 1 # 任何一方得分,发球权不变 (除非是发球方自己失误,但在直接得分模式下,失误也算对方得分) # 匹克球直接得分模式:谁赢了这分,谁就发下一分 self.current_serving_team = scoring_team # 发球权交给得分方 # 直接得分模式下,发球次序 (server_index) 始终是第一个人(或无所谓) self.current_server_index = 0 self.first_serve_of_match = False # 不再是第一次发球 else: # 发球得分模式 (serve) if scoring_team == self.current_serving_team: # 发球方得分 if scoring_team == "A": self.current_game_score_A += 1 else: self.current_game_score_B += 1 # 发球方得分,发球权和发球人不变 self.first_serve_of_match = False else: # 接球方赢得回合(发球方失误) # 接球方不得分,发球权转换 self.change_serve_opportunity() # 触发匹克球特有的发球权转换逻辑 self.update_display() self.check_game_end() def check_game_end(self): # 检查当前局是否结束 score_A = self.current_game_score_A score_B = self.current_game_score_B target = self.game_points_target game_ended = False winner_team = None if self.win_rule.get() == "golden": # 金球决胜 if score_A >= target and score_A > score_B: game_ended = True winner_team = "A" elif score_B >= target and score_B > score_A: game_ended = True winner_team = "B" else: # 两分决胜 (two-point) if score_A >= target and (score_A - score_B) >= 2: game_ended = True winner_team = "A" elif score_B >= target and (score_B - score_A) >= 2: game_ended = True winner_team = "B" if game_ended: if winner_team == "A": self.set_score_A += 1 messagebox.showinfo("局结束", f"恭喜 {self.team_A_players[0] if self.match_type.get() == 'single' else 'A队'} 赢得本局!") else: self.set_score_B += 1 messagebox.showinfo("局结束", f"恭喜 {self.team_B_players[0] if self.match_type.get() == 'single' else 'B队'} 赢得本局!") self.reset_current_game_scores() # 重置当前局比分 self.update_display() # 更新显示 self.check_match_end() # 检查整场比赛是否结束 # 新局开始,发球权转换给上一局的失利方(一般规则) # 或者简单地切换发球方 self.current_serving_team = 'B' if self.current_serving_team == 'A' else 'A' self.current_server_index = 0 # 新局开始,默认是第一发球手 self.first_serve_of_match = False # 不再是比赛的第一次发球 self.update_display() def check_match_end(self): # 检查整场比赛是否结束 match_ended = False match_winner = None if self.set_mode.get() == "one-set": # 一局决胜 if self.set_score_A >= 1: match_ended = True match_winner = "A" elif self.set_score_B >= 1: match_ended = True match_winner = "B" else: # 三局两胜 if self.set_score_A >= 2: match_ended = True match_winner = "A" elif self.set_score_B >= 2: match_ended = True match_winner = "B" if match_ended: self.is_match_over = True winner_name = self.team_A_players[0] if match_winner == "A" else self.team_B_players[0] if self.match_type.get() == "double": winner_name = "A队" if match_winner == "A" else "B队" messagebox.showinfo("比赛结束", f"恭喜 {winner_name} 赢得整场比赛!") self.update_display() # 禁用按钮 def reset_current_game_scores(self): self.current_game_score_A = 0 self.current_game_score_B = 0 def change_serve_opportunity(self): # 匹克球特有的发球权转换逻辑(仅在发球得分模式下,且接球方赢得回合时调用) if self.match_type.get() == "single": # 单打:直接切换发球方 self.current_serving_team = 'B' if self.current_serving_team == 'A' else 'A' messagebox.showinfo("发球权转换", "对方赢得回合,发球权转换。") else: # 双打 if self.first_serve_of_match: # 比赛的第一次发球:只有一次发球机会,失误后直接转换给对方队伍 self.current_serving_team = 'B' if self.current_serving_team == 'A' else 'A' self.current_server_index = 0 # 对方队伍的第一发球手发球 self.first_serve_of_match = False messagebox.showinfo("发球权转换", "第一次发球失误,发球权转换。") elif self.current_server_index == 0: # 当前是队伍的第一发球手,失误后轮到本队的第二发球手 self.current_server_index = 1 messagebox.showinfo("发球权转换", f"{self.team_A_players[0] if self.current_serving_team == 'A' else self.team_B_players[0]} 发球失误,轮到 {self.team_A_players[1] if self.current_serving_team == 'A' else self.team_B_players[1]} 发球。") else: # self.current_server_index == 1 # 当前是队伍的第二发球手,失误后发球权转换给对方队伍 self.current_serving_team = 'B' if self.current_serving_team == 'A' else 'A' self.current_server_index = 0 # 对方队伍的第一发球手发球 messagebox.showinfo("发球权转换", "本队两次发球均失误,发球权转换。") def manual_change_serve_team(self): # 手动切换发球方(调试或特殊情况用) self.current_serving_team = 'B' if self.current_serving_team == 'A' else 'A' self.current_server_index = 0 # 切换队伍时,默认从对方队伍的第一发球人开始 self.first_serve_of_match = False # 手动切换也意味着不是第一次发球了 self.update_display() messagebox.showinfo("手动切换", f"发球方已手动切换到 {self.current_serving_team} 队。") def setup_serve_display(self): # 清除旧布局 for widget in self.serve_display_frame.winfo_children(): widget.destroy() # 根据单打或双打绘制发球/接球示意图 if self.match_type.get() == "single": current_server_name = self.team_A_players[0] if self.current_serving_team == "A" else self.team_B_players[0] current_receiver_name = self.team_B_players[0] if self.current_serving_team == "A" else self.team_A_players[0] tk.Label(self.serve_display_frame, text=f"发球方: {current_server_name} {SERVE_ICON}", font=("Arial", 18)).pack(pady=5) tk.Label(self.serve_display_frame, text=f"接球方: {current_receiver_name}", font=("Arial", 18)).pack(pady=5) else: # 双打 # 获取当前发球方和接球方的球员姓名 team_A_p1, team_A_p2 = self.team_A_players[0], self.team_A_players[1] team_B_p1, team_B_p2 = self.team_B_players[0], self.team_B_players[1] a_server_display = team_A_p1 a_receiver_display = team_A_p2 b_server_display = team_B_p1 b_receiver_display = team_B_p2 # 根据当前发球方和发球次序,标记发球者 if self.current_serving_team == "A": # A队发球 if self.current_server_index == 0: a_server_display = f"{team_A_p1} {SERVE_ICON}" # A队第一发球手 else: # self.current_server_index == 1 a_receiver_display = f"{team_A_p2} {SERVE_ICON}" # A队第二发球手 (约定队员1是首发,队员2是首接,这里指代发球的队员) else: # B队发球 if self.current_server_index == 0: b_server_display = f"{team_B_p1} {SERVE_ICON}" # B队第一发球手 else: # self.current_server_index == 1 b_receiver_display = f"{team_B_p2} {SERVE_ICON}" # B队第二发球手 tk.Label(self.serve_display_frame, text="队伍 A", font=("Arial", 16, "bold")).grid(row=0, column=0, padx=20) tk.Label(self.serve_display_frame, text="队伍 B", font=("Arial", 16, "bold")).grid(row=0, column=2, padx=20) tk.Label(self.serve_display_frame, text="球员1:", font=("Arial", 14)).grid(row=1, column=0, sticky="e") tk.Label(self.serve_display_frame, text=a_server_display, font=("Arial", 14)).grid(row=1, column=1, sticky="w") tk.Label(self.serve_display_frame, text="球员2:", font=("Arial", 14)).grid(row=2, column=0, sticky="e") tk.Label(self.serve_display_frame, text=a_receiver_display, font=("Arial", 14)).grid(row=2, column=1, sticky="w") tk.Label(self.serve_display_frame, text="球员1:", font=("Arial", 14)).grid(row=1, column=2, sticky="e") tk.Label(self.serve_display_frame, text=b_server_display, font=("Arial", 14)).grid(row=1, column=3, sticky="w") tk.Label(self.serve_display_frame, text="球员2:", font=("Arial", 14)).grid(row=2, column=2, sticky="e") tk.Label(self.serve_display_frame, text=b_receiver_display, font=("Arial", 14)).grid(row=2, column=3, sticky="w") # 额外显示当前的发球计数(比如第一发/第二发) serve_count_text = "" if self.first_serve_of_match: serve_count_text = " (比赛首次发球)" elif self.match_type.get() == "double": serve_count_text = f" ({'第一发' if self.current_server_index == 0 else '第二发'})" tk.Label(self.serve_display_frame, text=f"当前发球方: {self.current_serving_team} 队{serve_count_text}", font=("Arial", 14, "italic")).grid(row=3, column=0, columnspan=4, pady=5) def reset_all(self): # 销毁比赛界面(如果存在) if hasattr(self, 'match_frame') and self.match_frame.winfo_exists(): self.match_frame.destroy() # 重新显示参数设置界面和开始按钮 self.param_frame.pack(pady=10) self.start_button.pack(pady=20) self.reset_button.pack(pady=10) # 重置所有比赛状态变量 self.current_game_score_A = 0 self.current_game_score_B = 0 self.set_score_A = 0 self.set_score_B = 0 self.current_serving_team = None self.current_server_index = 0 self.first_serve_of_match = True self.game_points_target = 0 self.is_match_over = False # 确保球员输入框内容重置或保持默认 # self.player_a_name_entry.delete(0, tk.END) # self.player_a_name_entry.insert(0, "A Player") # ... (对所有Entry进行类似操作) # 实际上,只要重新pack参数设置界面,这些Entry的绑定值就还在,无需额外重置其内容,除非用户手动清空。 messagebox.showinfo("重置", "比赛已重置,请重新设置参数开始新的比赛。") if __name__ == "__main__": root = tk.Tk() app = PickleballApp(root) root.mainloop()