20254119 实验四《Python程序设计》实验报告

20254119 2025-2026-2 《Python程序设计》实验四报告

课程:《Python程序设计》
班级: 2541
姓名: 浦馨宇
学号:20254119
实验教师:王志强
实验日期:2026年5月26日
必修/选修:专选课

一、实验内容

1. 要求

Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等
注:在Windows/Linux系统上使用VIM、PDB、IDLE、Pycharm等工具编程实现。

2. 我的实验内容

编写小游戏:快艇骰子(Yahtzee)是一款经典的骰子策略游戏,规则丰富、计分方式多样。我的目标是使用Python开发一个完整的快艇骰子小游戏,想要具备图形界面、人机/好友对战、账户系统、对局记录等功能。

二、实验过程及结果

1. 实验分析与设计

  • 逻辑分析
    根据游戏规则,每局游戏包含2名玩家,轮流进行13个回合。每回合玩家最多有3次投掷5颗骰子的机会,可选择保留任意骰子重投其余。投掷结束后,玩家必须选择一个尚未使用的计分项填入当前骰子组合的对应得分。计分项分为上半区(1-6点,按点数总和计分,若总分≥63则额外奖励35分)和下半区(三条、四条、葫芦、小顺、大顺、快艇、机会,各有特定计分规则)。
    详细规则如下
    上半区:1-6点分数为对应点数总和,若总和≥63额外奖励35分。
    下半区:
    三条/四条:三/四同点,总骰点,
    葫芦:3+2,25分,
    小顺:连续4个点数,30分,
    大顺:连续5个,40分,
    快艇:5个相同,50分,
    机会:所有骰子总和。
    全部13项填满后,总分高者获胜。
    因此游戏最主要的算法逻辑很简单
    上半区:点数×出现次数。
    三条/四条:只要有三个/四个相同,得分为所有骰子总和。
    葫芦:固定25分,需要统计出现次数满足[0,0,0,0,2,3]或同时有3和2。
    顺子:先去重排序,检查是否有连续4个或完整顺子。
    快艇:五个相同得50分。
    机会:所有骰子总和,即求和。
  • 模块设计
    根据功能的设想,初步设计了五个大的代码模块
    功能1——爬虫获取规则:利用requests和BeautifulSoup库从百度百科抓取游戏规则,若网络请求失败则启用内置备用规则文本。
    功能2——数据处理与游戏:是核心模块,包含骰子随机生成、13项计分项得分计算、总分与奖励计算、电脑AI自动选择策略,以及管理一整局游戏状态的类。
    功能3——构建账户系统:通过DengluCK类创建登录与注册图形界面,实现多用户管理。
    功能4——对局记录存储:利用json库,将用户信息和对局历史读写至本地的yahtzee_data.json文件。用户密码通过hashlib库的SHA256算法加密后存储。因为是想要留存对局数据才设计的账户系统,因此让它初具雏形即可。
    功能5——构建游戏GUI:设立窗口类来搭建。窗口类是图形界面的核心,使用tkinter库构建游戏主窗口,负责展示骰子、计分表、按钮和状态信息,并处理玩家的所有交互操作,比如点击骰子保留、投掷、选择计分项等。
    程序流程
    程序启动后,首先进入登录/注册界面。成功登录后进入主菜单,玩家可选择“人机对战”、“好友对战”、“查看对局记录”或查看“游戏规则”。开始对战后,进入游戏主界面,由游戏逻辑类管理游戏状态,界面类负责呈现和交互。一局结束后,游戏结果自动保存至当前用户的账户历史记录中。

2. 实现过程

小游戏经历了从1.0到2.0的一系列迭代。

  • 1.0版本
    实现了全部五个功能的雏形。
    功能1:爬虫获取规则(一开始没有设计备用规则,但是爬虫总是失败,这里就只能用备用规则补上😭爬虫算是并未成功)
    image
    功能2:数据处理与游戏(定义了骰子、计算得分、总分、AI游戏机制等,用Youxi类搭建一局游戏的基本逻辑,比如玩家的回合轮换、投掷骰子机制、计分表规则等)
    image
    功能3:账户(为功能4的醋而包的饺子,涉及到界面搭建,大部分跟着LLM照猫画虎)
    image
    功能4:游戏数据存储(json也是新接触到的库,但内容比较简洁,一直没有报错过,算是比较成功)
    image
    功能5:可视化(也是照猫画虎)
    image
    缺陷:初始设计中,为了方便查看,每次投掷后骰子会自动按从小到大的顺序排列。这导致选择在重投时,被保留的骰子位置会发生变化,游玩体验不佳。
    改进:在生成骰子的tou_zi函数以及重投骰子的操作中移除sorted(),保证骰子位置固定。

  • 1.1版本
    修复了1.0中好友对战(PVP)模式,原先好友模式会跳转报错,修复后通过点击按钮弹出对话框输入对手名称(或拒绝输入)都可以直接进入对局。
    image
    缺陷:代码逻辑为先销毁主菜单窗口,再调用simpledialog.askstring()弹窗,导致找不到上一级窗口,程序崩溃。
    改进:调整代码执行顺序,先弹出输入框获取对手名称,再销毁主菜单窗口,使运行逻辑正确。

  • 1.2版本
    新增功能:在“选择计分项”的弹出窗口中,通过改变按钮背景色来提示玩家:绿色表示该计分项当前有得分,金色表示所有可选项中当前得分最高的一项(不过在真实对局中,一直选择金色选项并不能达成整体的最优策略,因此这个小设计并没有降低游戏难度,反而增加了思辨性)。这项改进大大降低了玩家尤其是新手玩家,选择零分项的几率,提升了游戏的体验度。
    790628b242d525c8d4a991160b35f09d

  • 2.0 版本
    新增功能:在试玩过程中,有会玩的同学指出隐藏玩法:
    在标准快艇骰子的进阶规则中,当玩家第二次掷出快艇(五个相同点数)时会触发隐藏机制,通常包含:如果第一个快艇已经填在了“快艇”项(哪怕填了0分),第二次快艇可以当作万能牌(Joker)使用。填分规则:如果上半区对应点数项还没填,则必须填上半区该项(并获得该点数的正常分数);如果上半区该项已经填了,则可以任意填一个下半区项,并获得该项满分(比如葫芦直接得25分,大顺直接得40分等)。有些变体还会给每个额外的快艇加 100 分奖励。
    于是将此功能加入。这个过程很繁琐,因为涉及改变计分,一共修改了7个大小板块的代码。最终形成了现在的V2.0。
    新增逻辑:在Youxi类中增加bonus_fen列表,用于记录游戏过程中产生的额外快艇奖励分。为了保证人机和人人对战的公平性,分别在玩家和AI的计分环节插入快艇奖励检测:若当前骰子为快艇,且“快艇”计分项已被使用,则触发奖励(首次+100分,后续+50分),并弹出提示框。
    过程问题处理:最初,我在发放奖励后直接使用了return,导致回合没有正常消耗,玩家点击“计分”会反复触发奖励而无法结束回合。调试后发现,需要移除return语句,让程序在弹出奖励提示后,强制玩家继续选择一个有效的计分项填入,以此正常结束回合。
    屏幕录制 2026-06-16 014708

3. 实验结果

2.0代码如下

点击查看代码
# -*- coding: utf-8 -*-
"""
作者:20254119
名称:快艇骰子2.0
日期:2026年6月15日

功能1:爬虫获取游戏规则
功能2:数据处理与游戏
功能3:构建账户系统
功能4:对局记录存储(JSON)
功能5:构建游戏GUI 图形界面
"""

import os
import json
import random
import hashlib
import tkinter as tk
from tkinter import messagebox, ttk, simpledialog

JFX = [
    "1点", "2点", "3点", "4点", "5点", "6点",
    "三条", "四条", "葫芦", "小顺", "大顺", "快艇", "机会"] # 所有计分项目的名称列表
SJWJ = "yahtzee_data.json" # 保存用户数据的文件名
BYGZ = """
快艇骰子(Yahtzee):
每名玩家有13回合,每回合最多投3次骰子。
可保留任意骰子重投其余。最后选择一个计分项填分。
上半区:1-6点分数为对应点数总和,若总和≥63额外奖励35分。
下半区:三条/四条(三/四同点,总骰点),
葫芦(3+2,25分),
小顺(连续4个点数,30分),
大顺(连续5个,40分),
快艇(5个相同,50分),
机会(总和)。
总分最高者胜。""" # 如果爬虫失败,就用这段文字作为游戏规则

# ===================== 功能1:爬虫获取规则 =====================
def pa_gui_ze():
    try:
        import requests
        from bs4 import BeautifulSoup
        url = "https://baike.baidu.com/item/%E5%BF%AB%E8%89%87%E9%AA%B0%E5%AD%90" # 目标网址
        headers = {'User-Agent': 'Mozilla/5.0'} # 伪装成浏览器
        resp = requests.get(url, headers=headers, timeout=5) # 发送请求,最长等5秒
        resp.encoding = 'utf-8'
        soup = BeautifulSoup(resp.text, 'html.parser') # 用 BeautifulSoup 解析网页
        para = soup.find('div', class_='lemma-summary') # 查找摘要部分(通常是 class='lemma-summary' 的 div)
        if para:
            return para.get_text(strip=True)   # 返回纯文本规则
        else:
            raise ValueError("未找到规则摘要")
    except Exception:
        return BYGZ   # 任何错误都用备用规则

# 获取规则文本,存到变量 gz 中,方便其他窗口显示
gz = pa_gui_ze()

# ===================== 功能2:数据处理与游戏 =====================
# ***** 计分规则 *****
def tou_zi(num=5):
    return [random.randint(1, 6) for _ in range(num)]
    #随机生成5个骰子

def ji_suan_df(shaizi):
    js = [shaizi.count(i) for i in range(1, 7)]
    df = {}
    # 计算当前骰子在各计分项中的得分,返回一个字典
    # 分别对项目计算得分
    for i in range(1, 7):
        df[f"{i}点"] = i * js[i-1] # 上半区:1点到6点,得分 = 点数 × 出现次数
    # 下半区:
    df["三条"] = sum(shaizi) if any(c >= 3 for c in js) else 0 # 三条(至少三个相同,得分是所有骰子总和)
    df["四条"] = sum(shaizi) if any(c >= 4 for c in js) else 0 # 四条(至少四个相同)
    df["葫芦"] = 25 if (sorted(js) == [0,0,0,0,2,3]) or (3 in js and 2 in js) else 0 # 葫芦(三个相同 + 两个相同),固定25分
        # sorted(js) 若为 [0,0,0,0,2,3] 表示一个点数出现2次,另一个出现3次
    wy = sorted(set(shaizi))
        # 创建集合“唯一”,重复的数字不影响判断顺子,但会产生个数干扰,因此用集合去重并排序
    is_xs = any(len(set(range(u, u+4)) & set(wy)) >= 4 for u in range(1, 4))
        # 检查是否有连续4个的情况:从1、2、3三个起点分别尝试
    df["小顺"] = 30 if is_xs else 0 # 小顺(连续4个不同的点数),固定30分
    df["大顺"] = 40 if wy in [[1,2,3,4,5], [2,3,4,5,6]] else 0 # 大顺(连续5个),固定40分
    df["快艇"] = 50 if any(c == 5 for c in js) else 0 # 快艇(五个完全相同),固定50分
    df["机会"] = sum(shaizi) # 机会(所有骰子总和)
    return df

def zong_fen(scores_dict, bonus_fen=0):
    total = sum(scores_dict.values())
    upper = sum(scores_dict.get(f"{i}点", 0) for i in range(1, 7))
    if upper >= 63:
        total += 35
    total += bonus_fen   # V2.0新增,额外快艇奖励分直接累加
    return total
    # 根据已经填好的计分表,计算最终总分,并加上上半区奖励

def ai_xuanze(available, shaizi):
    all_s = ji_suan_df(shaizi)
    best = None
    best_val = -1
    # 只考虑还没用过的计分项
    for cat in available:
        if all_s[cat] > best_val:
            best_val = all_s[cat]
            best = cat
    return best
    # AI对局设置:电脑自动选择在可用的计分项里,得分最高的那个

# ***** 游戏逻辑 *****
class Youxi:
    """一局游戏的逻辑:记录玩家、计分表、回合、投掷次数、填分等"""
    def __init__(self, moshi, p1_name, p2_name):
        self.moshi = moshi # 'pve' 人机, 'pvp' 好友
        self.wj = [p1_name, p2_name] # 玩家名字列表,0号是当前用户,1号是对手
        self.scores = [{}, {}] # 两个玩家的计分表(字典,键是计分项,值是分数)
        self.used = [[], []] # 两个玩家已经用过的计分项列表
        self.huihe = [1, 1] # 当前进行到的回合数(1~13),用于显示
        self.dq = 0 # 当前玩家索引(0或1)
        self.shaizi = [] # 当前玩家的骰子点数列表
        self.baoliu = [False]*5 # 五个骰子是否保留(True表示保留)
        self.bonus_fen = [0, 0]  # 额外快艇奖励分(直接加入总分)
        self.touci = 0 # 当前玩家已经投掷的次数(0~3)
        self.jieshu = False # 游戏是否结束

    def keyong(self, idx):
        """返回索引为 idx 的玩家还可以选的计分项列表"""
        # 列表推导式:如果计分项不在已用列表中,就选出来
        return [c for c in JFX if c not in self.used[idx]]

    def huan_ren(self):
        """轮到下一位玩家,初始化骰子和投掷次数"""
        self.dq = 1 - self.dq # 0变1,1变0
        self.shaizi = []
        self.baoliu = [False]*5
        self.touci = 0

    def jiancha_jieshu(self):
        """检查两个玩家是否都完成了13个回合"""
        return self.huihe[0] > 13 and self.huihe[1] > 13

    def tian_fen(self, category):
        """把当前骰子的成绩填到指定计分项"""
        idx = self.dq
        score = ji_suan_df(self.shaizi)[category]
        self.used[idx].append(category)
        self.scores[idx][category] = score
        self.huihe[idx] += 1
        if self.jiancha_jieshu():
            self.jieshu = True

# ===================== 功能3:构建登录与账户系统 =====================
class DengluCK:
    """登录和注册的图形界面"""
    def __init__(self, master):
        self.master = master
        self.master.title("快艇骰子 - 登录")
        self.users = du_yonghu() # 加载已有的用户数据
        self.dq_yonghu = None # 当前登录的用户名

        # 创建用户名输入框
        tk.Label(master, text="用户名:").grid(row=0, column=0, padx=10, pady=10)
        self.sr_user = tk.Entry(master)
        self.sr_user.grid(row=0, column=1)

        # 创建密码输入框(show="*" 让输入内容显示为星号)
        tk.Label(master, text="密码:").grid(row=1, column=0, padx=10, pady=10)
        self.sr_pass = tk.Entry(master, show="*")
        self.sr_pass.grid(row=1, column=1)

        # 登录和注册按钮
        tk.Button(master, text="登录", command=self.denglu).grid(row=2, column=0, pady=10)
        tk.Button(master, text="注册", command=self.zhuce).grid(row=2, column=1, pady=10)

    def denglu(self):
        """点击登录按钮后要做的事情"""
        username = self.sr_user.get()
        password = self.sr_pass.get()
        # 用户名或密码为空就提示
        if not username or not password:
            messagebox.showerror("错误", "请输入用户名和密码")
            return
        # 检查用户是否存在,并且密码密文是否匹配
        if username in self.users and self.users[username]['password'] == jiami_mima(password):
            self.dq_yonghu = username   # 登录成功,记录用户名
            self.master.destroy()       # 关闭登录窗口
        else:
            messagebox.showerror("错误", "用户名或密码错误")

    def zhuce(self):
        """点击注册按钮后要做的事情"""
        username = self.sr_user.get()
        password = self.sr_pass.get()
        if not username or not password:
            messagebox.showerror("错误", "请输入用户名和密码")
            return
        if username in self.users:
            messagebox.showerror("错误", "用户名已存在")
            return
        # 将新用户信息存入字典,密码只存密文
        self.users[username] = {
            'password': jiami_mima(password),
            'history': []            # 对局历史记录初始为空列表
        }
        cun_yonghu(self.users)      # 保存到文件
        messagebox.showinfo("成功", "注册成功,请登录")

# ===================== 功能4:账户数据存储 =====================
def du_yonghu(): # 读取用户数据
    if os.path.exists(SJWJ):   # 如果文件存在
        with open(SJWJ, 'r', encoding='utf-8') as f:
            return json.load(f)   # json.load 把文件内容变成 Python 字典
    return {}   # 文件不存在就返回空字典

def cun_yonghu(users): # 存储用户数据
    with open(SJWJ, 'w', encoding='utf-8') as f:
        # json.dump 把字典转成 JSON 字符串写入文件,indent=2 让文件好看
        json.dump(users, f, indent=2, ensure_ascii=False)

def jiami_mima(password): # 密码加密
    # hashlib.sha256(字符串编码).hexdigest() 得到十六进制密文字符串
    return hashlib.sha256(password.encode()).hexdigest()

# ===================== 功能5:游戏图形界面 =====================
class YouxiCK:
    """游戏主窗口,负责显示骰子、按钮、计分表和交互"""
    def __init__(self, master, youxi, yonghuming):
        self.master = master
        self.youxi = youxi
        self.yhm = yonghuming # 当前登录的用户名(用于保存记录)
        self.master.title("快艇骰子")
        self.master.protocol("WM_DELETE_WINDOW", self.guanbi) # 关闭窗口时触发 on_close 方法

        # ---------- 总分显示区(新增) ----------
        zf_kuang = tk.Frame(master)
        zf_kuang.pack(pady=5)
        tk.Label(zf_kuang, text="总分:", font=("微软雅黑", 12)).pack(side=tk.LEFT, padx=5)
        self.zf_p0 = tk.Label(zf_kuang, text="", font=("微软雅黑", 12), fg="blue")
        self.zf_p0.pack(side=tk.LEFT, padx=10)
        self.zf_p1 = tk.Label(zf_kuang, text="", font=("微软雅黑", 12), fg="red")
        self.zf_p1.pack(side=tk.LEFT, padx=10)

        # ---------- 状态显示行 ----------
        self.zt_var = tk.StringVar() # 用于动态更新文字
        tk.Label(master, textvariable=self.zt_var, font=("微软雅黑", 12)).pack(pady=5)

        # ---------- 骰子展示区 ----------
        self.sz_kuang = tk.Frame(master)
        self.sz_kuang.pack(pady=10)
        self.sz_bq = [] # 存放5个 Label 组件
        for i in range(5):
            # 每个骰子是一个 Label,点击可以切换保留状态
            lbl = tk.Label(self.sz_kuang, text="?", font=("Arial", 24), width=4, relief="ridge",
                          bg="lightgray", cursor="hand2")
            # 绑定鼠标点击事件,lambda 传递当前索引
            lbl.bind("<Button-1>", lambda e, idx=i: self.qiehuan(idx))
            lbl.pack(side=tk.LEFT, padx=5)
            self.sz_bq.append(lbl)

        # ---------- 按钮区 ----------
        btn_frame = tk.Frame(master)
        btn_frame.pack(pady=5)
        self.tou_btn = tk.Button(btn_frame, text="投掷", command=self.touzhi, font=("微软雅黑", 12))
        self.tou_btn.pack(side=tk.LEFT, padx=5)
        self.jf_btn = tk.Button(btn_frame, text="计分", command=self.xuan_jfx, font=("微软雅黑", 12),
                                state=tk.DISABLED) # 开始时不能按计分
        self.jf_btn.pack(side=tk.LEFT, padx=5)

        # ---------- 计分表格 ----------
        biao_kuang = tk.Frame(master)
        biao_kuang.pack(pady=10, fill=tk.BOTH, expand=True)
        columns = ("计分项", youxi.wj[0], youxi.wj[1])
        self.tree = ttk.Treeview(biao_kuang, columns=columns, show="headings", height=15)
        for col in columns:
            self.tree.heading(col, text=col)
            self.tree.column(col, width=100, anchor=tk.CENTER)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        # 滚动条
        gundong = ttk.Scrollbar(biao_kuang, orient=tk.VERTICAL, command=self.tree.yview)
        gundong.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.configure(yscrollcommand=gundong.set)

        # 初始化界面:刷新表格、更新状态
        self.gengxin_biao()
        self.gengxin_zt()

        # 如果是人机模式并且当前是电脑(索引1),让电脑自动开始
        if self.youxi.moshi == 'pve' and self.youxi.dq == 1:
            self.master.after(500, self.ai_huihe)

    # ---------- 骰子交互 ----------
    def qiehuan(self, idx):
        """点击骰子切换保留状态(仅在投掷1~2次后有效)"""
        # 还没投掷过,或者已经投了3次,都不能切换
        if self.youxi.touci == 0 or self.youxi.touci == 3:
            return
        # 翻转保留标记
        self.youxi.baoliu[idx] = not self.youxi.baoliu[idx]
        self.xianshi_sz()

    def xianshi_sz(self):
        """更新5个骰子标签的显示内容与颜色"""
        for i in range(5):
            # 如果骰子点数列表长度不够5(比如还没投),显示 ?
            if i < len(self.youxi.shaizi):
                self.sz_bq[i].config(text=str(self.youxi.shaizi[i]))
            else:
                self.sz_bq[i].config(text="?")
            # 保留的骰子用金色背景突出显示
            if i < len(self.youxi.shaizi) and self.youxi.baoliu[i]:
                self.sz_bq[i].config(bg="gold")
            else:
                self.sz_bq[i].config(bg="lightgray")

    # ---------- 状态与按钮控制 ----------
    def gengxin_zt(self):
        """更新顶部状态文字和按钮可用性"""
        wj = self.youxi.wj[self.youxi.dq] # 当前玩家名字
        r = self.youxi.huihe[self.youxi.dq] # 当前回合
        tou_info = f"第{r}回合,第{self.youxi.touci}/3次投掷"
        self.zt_var.set(f"当前玩家:{wj}   {tou_info}")

        # 根据投掷次数控制投掷按钮和计分按钮
        if self.youxi.jieshu:
            # 游戏结束,两个按钮都禁用
            self.tou_btn.config(state=tk.DISABLED)
            self.jf_btn.config(state=tk.DISABLED)
        elif self.youxi.touci == 0:
            # 还没投过,可以投掷,但不能计分
            self.tou_btn.config(text="投掷", state=tk.NORMAL)
            self.jf_btn.config(state=tk.DISABLED)
        elif self.youxi.touci < 3:
            # 投了1或2次,可以继续投(重投未保留的),也可以计分
            self.tou_btn.config(text="重投剩余", state=tk.NORMAL)
            self.jf_btn.config(state=tk.NORMAL)
        else:
            # 投了3次,不能再投,只能计分
            self.tou_btn.config(state=tk.DISABLED)
            self.jf_btn.config(state=tk.NORMAL)

    def gengxin_biao(self):
        """刷新计分表格,同时更新底部总分"""
        for row in self.tree.get_children():
            self.tree.delete(row)
        for cat in JFX:
            p0 = self.youxi.scores[0].get(cat, "-")
            p1 = self.youxi.scores[1].get(cat, "-")
            self.tree.insert("", tk.END, values=(cat, p0, p1))
        # V2.0修改,更新总分
        total0 = zong_fen(self.youxi.scores[0], self.youxi.bonus_fen[0])
        total1 = zong_fen(self.youxi.scores[1], self.youxi.bonus_fen[1])
        self.zf_p0.config(text=f"{self.youxi.wj[0]}:{total0}分")
        self.zf_p1.config(text=f"{self.youxi.wj[1]}:{total1}分")

    # ---------- 投掷与计分操作 ----------
    def touzhi(self):
        """点击投掷按钮(或重投)"""

        if self.youxi.touci == 0:
            # 第一次投:随机生成5个新骰子,所有都不保留
            self.youxi.shaizi = tou_zi(5)
            self.youxi.baoliu = [False]*5
        else:
            # 重投:根据保留标记重新生成
            new = []
            for i in range(5):
                if self.youxi.baoliu[i]:
                    new.append(self.youxi.shaizi[i]) # 保留的骰子直接加入
                else:
                    new.append(random.randint(1, 6)) # 不保留的重新随机
            self.youxi.shaizi = new
        self.youxi.touci += 1 # 投掷次数+1
        self.xianshi_sz()
        self.gengxin_zt()
        # 如果已经投满3次,投掷按钮自动变灰
        if self.youxi.touci == 3:
            self.tou_btn.config(state=tk.DISABLED)

    def xuan_jfx(self):
        """点击计分按钮,弹出窗口让玩家选择一个计分项"""

        # ----------  V2.0修改:额外快艇奖励 ----------
        idx = self.youxi.dq
        if self.youxi.touci > 0:
            df = ji_suan_df(self.youxi.shaizi)
            # 当前是快艇,且“快艇”项已经被填过(无论得分)
            if df["快艇"] == 50 and "快艇" in self.youxi.used[idx]:
                # 第一次额外快艇奖100,以后奖50
                bonus = 100 if self.youxi.bonus_fen[idx] == 0 else 50
                self.youxi.bonus_fen[idx] += bonus
                messagebox.showinfo("🎲 Joker!额外快艇!",
                                    f"奖励 +{bonus} 分!\n(不消耗计分项,请再次点击“计分”填入本回合分数)")
                self.gengxin_biao()

        if self.youxi.touci == 0:
            return # 没投过骰子不能计分
        # 获取当前玩家可选的计分项
        keyong = self.youxi.keyong(self.youxi.dq)
        if not keyong:
            messagebox.showinfo("提示", "所有计分项已填完")
            return
        # 计算当前骰子在各计分项上的可能得分
        df_kao = ji_suan_df(self.youxi.shaizi)
        # 创建新窗口
        win = tk.Toplevel(self.master)
        win.title("选择计分项")
        win.grab_set() # 锁定焦点,必须先关掉这个窗口才能操作主窗口
        tk.Label(win, text="请选择计分项:", font=("微软雅黑", 12)).pack(pady=5)
        # 选择计分项窗口
        max_pts = max(df_kao[cat] for cat in keyong)
        for cat in keyong:
            pts = df_kao[cat]
            if pts == max_pts and pts > 0:
                bg_color = "gold" # 最高分项用金色(V1.2新增,用颜色标出醒目项目)
            elif pts > 0:
                bg_color = "lightgreen" # 其他有分项用浅绿
            else:
                bg_color = "SystemButtonFace"
            btn = tk.Button(win, text=f"{cat}(+{pts}分)", width=20,
                            bg=bg_color,
                            command=lambda c=cat: self.queren_jf(c, win))
            btn.pack(pady=2)
        win.wait_window() # 等待窗口关闭

    def queren_jf(self, category, window):
        """确认计分,关闭选择窗口,完成填分并切换玩家"""
        window.destroy()
        self.youxi.tian_fen(category) # 将分数填入计分表
        self.gengxin_biao()
        if self.youxi.jieshu:
            self.jieshu_youxi() # 游戏结束处理
            return
        # 轮到下一个玩家
        self.youxi.huan_ren()
        self.xianshi_sz()
        self.gengxin_zt()
        self.gengxin_biao()
        # 如果是人机模式且现在轮到电脑,启动电脑自动回合
        if self.youxi.moshi == 'pve' and self.youxi.dq == 1:
            self.master.after(500, self.ai_huihe)

       # ---------- 电脑自动操作 ----------
    def ai_huihe(self):
        """电脑回合入口,分步模拟投掷和计分"""
        if self.youxi.jieshu or self.youxi.dq != 1:
            return
        self.zt_var.set("电脑思考中...")
        self.master.update()
        # 第一次投掷
        shaizi = tou_zi(5)
        self.youxi.shaizi = shaizi
        self.youxi.baoliu = [False]*5
        self.youxi.touci = 1
        self.xianshi_sz()
        self.gengxin_zt()
        # 延时后继续第2次投掷
        self.master.after(400, lambda: self.ai_tou2(shaizi))

    def ai_tou2(self, old_shaizi):
        """电脑第二次投掷:保留点数 >= 4 的骰子"""
        # 保留规则:点数大于等于4的保留
        keeps = [d >= 4 for d in old_shaizi]
        new = []
        for i in range(5):
            if keeps[i]:
                new.append(old_shaizi[i])
            else:
                new.append(random.randint(1, 6))
        new_shaizi = new
        self.youxi.shaizi = new_shaizi
        self.youxi.baoliu = keeps
        self.youxi.touci = 2
        self.xianshi_sz()
        self.gengxin_zt()
        # 延时后第3次投掷
        self.master.after(400, lambda: self.ai_tou3(new_shaizi))

    def ai_tou3(self, old_shaizi):
        """电脑第三次投掷:保留点数 >= 3 的骰子"""
        keeps = [d >= 3 for d in old_shaizi]
        new = []
        for i in range(5):
            if keeps[i]:
                new.append(old_shaizi[i])
            else:
                new.append(random.randint(1, 6))
        new_shaizi = new
        self.youxi.shaizi = new_shaizi
        self.youxi.baoliu = keeps
        self.youxi.touci = 3
        self.xianshi_sz()
        self.gengxin_zt()
        # 延时后自动计分
        self.master.after(500, self.ai_jifen)

    def ai_jifen(self):
        """电脑自动选择计分项并填分"""
        # 先计算当前骰子的得分
        df = ji_suan_df(self.youxi.shaizi)
        # V2.0新增,额外快艇奖励检测
        if df["快艇"] == 50 and "快艇" in self.youxi.used[1]:
            bonus = 100 if self.youxi.bonus_fen[1] == 0 else 50
            self.youxi.bonus_fen[1] += bonus
            self.gengxin_biao()
        keyong = self.youxi.keyong(1)
        if not keyong:
            self.youxi.jieshu = True
            self.jieshu_youxi()
            return
        cat = ai_xuanze(keyong, self.youxi.shaizi)
        self.youxi.tian_fen(cat)
        self.gengxin_biao()
        if self.youxi.jieshu:
            self.jieshu_youxi()
            return
        self.youxi.huan_ren()
        self.xianshi_sz()
        self.gengxin_zt()
        self.gengxin_biao()

    # ---------- 游戏结束 ----------
    def jieshu_youxi(self):
        """游戏结束,显示总分和胜者,保存对局记录"""
        z0 = zong_fen(self.youxi.scores[0], self.youxi.bonus_fen[0])
        z1 = zong_fen(self.youxi.scores[1], self.youxi.bonus_fen[1])
        p0, p1 = self.youxi.wj
        # 判定胜者
        if z0 > z1:
            winner = p0
        elif z1 > z0:
            winner = p1
        else:
            winner = "平局"
        msg = f"{p0}:{z0}分  vs  {p1}:{z1}分\n{winner}获胜!"
        messagebox.showinfo("游戏结束", msg)

        # 将对局记录存入用户文件
        users = du_yonghu()
        if self.yhm in users:
            record = {
                "mode": self.youxi.moshi,
                "players": self.youxi.wj,
                "scores": [z0, z1],
                "winner": winner
            }
            users[self.yhm]['history'].append(record)
            cun_yonghu(users)

        # 游戏结束禁用按钮
        self.tou_btn.config(state=tk.DISABLED)
        self.jf_btn.config(state=tk.DISABLED)

    def guanbi(self):
        """关闭窗口时的确认"""
        if not self.youxi.jieshu:
            # 游戏没结束,询问是否真的要退出
            if messagebox.askokcancel("退出", "游戏尚未结束,确定退出吗?"):
                self.master.destroy()
        else:
            self.master.destroy()

# ===================== 对局历史查看 =====================
def xianshi_lishi(username):
    """弹窗显示指定用户的历史对局记录"""
    users = du_yonghu()
    if username not in users or not users[username]['history']:
        messagebox.showinfo("历史记录", "暂无对局记录")
        return
    history = users[username]['history']
    win = tk.Toplevel()
    win.title(f"{username} 的对局记录")
    tree = ttk.Treeview(win, columns=("模式", "玩家", "对手", "你的分数", "对手分数", "结果"),
                        show="headings")
    for col in ["模式", "玩家", "对手", "你的分数", "对手分数", "结果"]:
        tree.heading(col, text=col)
        tree.column(col, width=80, anchor=tk.CENTER)
    tree.pack(fill=tk.BOTH, expand=True)

    for rec in history:
        moshi = "人机" if rec['mode'] == 'pve' else "好友"
        p0, p1 = rec['players']
        s0, s1 = rec['scores']
        # 判断哪一方是当前用户
        if p0 == username:
            my_score, opp_score, opp_name = s0, s1, p1
        else:
            my_score, opp_score, opp_name = s1, s0, p0
        # 胜负平判断
        if rec['winner'] == username:
            result = "胜"
        elif rec['winner'] == "平局":
            result = "平"
        else:
            result = "负"
        tree.insert("", tk.END, values=(moshi, username, opp_name, my_score, opp_score, result))
    win.transient() # 让窗口置于父窗口之上
    win.grab_set()

# ===================== 主菜单窗口 =====================
def zhu_caidan(username):
    """登录成功后显示的主菜单"""
    menu = tk.Tk()
    menu.title(f"快艇骰子 - 欢迎 {username}")
    menu.geometry("300x300")
    tk.Label(menu, text=f"你好,{username}", font=("微软雅黑", 14)).pack(pady=20)

    def kaishi(moshi):
        """根据模式启动游戏"""
        # *** V1.2修复:先获取对手名字(如果需要),再销毁菜单 ***
        if moshi == 'pve':
            p1 = username
            p2 = "电脑"
            menu.destroy() # 人机模式直接关闭菜单
        else:
            # 好友模式:先弹窗,后销毁菜单,防止 parent 被销毁导致报错
            p2 = simpledialog.askstring("好友对局", "请输入对手名称:")
            menu.destroy() # 弹窗完成后再销毁
            if not p2:
                p2 = "对手"
            p1 = username
        # 创建游戏逻辑对象
        yx = Youxi(moshi, p1, p2)
        # 创建游戏窗口并进入主循环
        root = tk.Tk()
        YouxiCK(root, yx, username)
        root.mainloop()

    # 按钮布局
    tk.Button(menu, text="人机对战", width=20, height=2,
              command=lambda: kaishi('pve')).pack(pady=10)
    tk.Button(menu, text="好友对局", width=20, height=2,
              command=lambda: kaishi('pvp')).pack(pady=10)
    tk.Button(menu, text="查看对局记录", width=20, height=2,
              command=lambda: xianshi_lishi(username)).pack(pady=10)
    tk.Button(menu, text="游戏规则", width=20, height=2,
              command=lambda: messagebox.showinfo("规则", gz)).pack(pady=10)
    tk.Button(menu, text="退出", width=20, height=2, command=menu.destroy).pack(pady=10)
    menu.mainloop()

# ===================== 程序入口 =====================
if __name__ == '__main__':
    # 第一步:打开登录窗口
    root = tk.Tk()
    dl = DengluCK(root)
    root.mainloop()   # 等待登录窗口关闭
    # 如果成功登录(dq_yonghu 不为 None),就进入主菜单
    if dl.dq_yonghu:
        zhu_caidan(dl.dq_yonghu)

程序最终运行稳定,基本实现了我最初的全部预期功能
账户系统:支持多用户注册与登录,密码加密存储。
规则获取:失败时使用内置规则。
计分系统:完整实现全部13个计分项的逻辑,包括上半区63分奖励和下半区特殊牌型判定,甚至在后期加入并完美实现了“快艇加倍”机制的隐藏玩法,大大提高了游戏的吸引程度。
骰子交互:玩家可通过点击骰子来切换保留/重投状态,保留的骰子以金色背景高亮显示。
智能提示:选择计分项时,有得分的项目会以绿色/金色高亮,辅助玩家决策。
对战模式:实现了人机对战(电脑AI会基于最高分策略自动行动)和好友同机对战。
对局记录:每局游戏的总分和胜负结果会被自动保存,并可在主菜单中随时查看历史战绩。

运行结果视频如下
https://www.bilibili.com/video/BV1fnjT6fEgy/

三、实验过程中遇到的问题和解决过程

  • 问题1:有想法但不知道如何着手实现。

  • 问题1解决方案:面对复杂的游戏规则,我选择化整为零,从解构规则开始。我将大问题拆解成“生成骰子”、“计算单个项目得分”、“判断牌型(如葫芦、顺子)”、“切换玩家”等一系列独立的小问题,然后再一个个用函数实现,最后再组合起来。

  • 问题2:PyCharm中安装第三方库(requests, BeautifulSoup)失败。

  • 问题2解决方案:在IDE内部多次安装均报错,怀疑是环境依赖混乱。最终,我决定彻底卸载Python和PyCharm,重新安装,并直接打开系统终端使用pip install命令安装所需库。这个“推倒重来”的方法虽然耗时,但最终一劳永逸地解决了环境问题。

  • 问题3:学习应用新库(如Tkinter)时,代码频繁报错,对GUI编程不熟练。

  • 问题3解决方案:作为小白,我很难一次性写出完全正确的代码。我选择先理解大模型和网络文档提供的示例代码,弄懂每个参数(如grid的row、column;pack的side)的含义,然后在示例基础上进行模仿和修改。通过不断的试错和调试,逐渐掌握了tkinter的布局和事件绑定机制。

  • 问题4:最初尝试将游戏做成网页版,但程序运行后网页无法加载,由于对这部分领域很不熟悉,也无法排错。

  • 问题4解决方案:Web开发对我而言是一个完全陌生的领域,排查错误非常困难。联想到在之前的实验三中,LLM曾使用Tkinter构建GUI,我果断将整个可视化方案从Web全面转向Tkinter桌面应用。

  • 问题5:好友对战(PVP)模式下,一点击弹窗就导致程序崩溃。

  • 问题5解决方案:经过排错发现,崩溃的根本原因是“父窗口在子窗口弹出前已被销毁”。解决方法是将“弹出输入对话框”的代码调整到“销毁主菜单”的代码之前执行。

  • 问题6:2.0版本加入隐藏玩法的功能后,游戏流程陷入死循环。

  • 问题6解决方案:调试发现,在检测到额外快艇并发放奖励后,我使用了return语句返回,导致系统认为该回合的“计分”操作已完成,但实际并没有消耗任何计分项,因此玩家再次点击“计分”时又会触发奖励,形成死循环。解决方法是将触发奖励后的return移除,这样程序在弹出奖励提示后,可以继续执行正常的“选择计分项”流程,强制玩家使用一个计分项来结束这个回合。

四、其他(感悟、思考等)

1. 课程总结

从零基础学Python,我学到了什么?
(1)首先认识了Python。我知道了Python是Guido创造的,名字来自一部喜剧,含义是蟒蛇。它是一种跨平台、解释型、面向对象、动态数据类型的高级程序设计语言,它语法简单、容易读懂,目前主流用Python 3.x。我学会了怎么装Python、用IDLE或PyCharm写第一个“人生苦短,我用python”,还了解了本课程的出勤、实验、综合实践这些考核方式。
(2)学会了基础语法和流程控制。Python用#来单行注释,用'''来多行注释,用缩进表示代码块,不需要花括号。它的标识符是用来标识变量、函数、类、模块和其他对象的名称,保留字则是像print、if等在python语言里有特殊用法的词,不可以用来当标识符。我掌握了变量、数字、字符串、布尔值这些基本数据类型,还有加减乘除、比较、逻辑等运算符。接着学了if-elif-else做判断,用while和for循环重复执行代码,还知道了pass占位,和条件表达式,比如a if a>b else b。
(3)了解和使用了序列。它包含列表[]、元组()、字典{里面是键值对}、集合{}。列表像购物车,可以增删改查、排序、推导式。元组像密封罐,一旦创建不能修改。字典是电话本,可以通过键(名字)来找值(号码)。集合是无重复元素的一种序列,可以做交集、并集。在此基础上我学会了切片、索引、序列相加等操作。
(4)学习了函数、面向对象、模块和异常处理。我学会了定义自己的函数,用lambda写简短的匿名函数。理解了“类”是图纸,“对象”是房子,有封装、继承、多态。还把代码拆成多个.py模块,用import导入。遇到程序出错时,用try-except抓住异常,程序就不会轻易崩溃而能丝滑运行。
(5)接触了文件、数据库和爬虫入门。我们在课程后期简单接触了SQLite和MySQL,体验了网络爬虫,知道网络爬虫可以爬取网页信息,但也会承担过线使用的入狱风险。不过感觉这些内容我还没入门,还得学诶。

2. 感想体会

这门课下来,我最大的收获是慢慢养成了编程的思维习惯。我刚开始看到大段大段的代码总有点发怵,后来在综合实践的时候发现怕也没用,急也没用,只好静下心来,把复杂问题拆成一步步、写成一个个代码模块。再后来就发现编程和写代码也没那么可怕。Python本身比较灵活,写错了改起来也快,所以对于新手的我来说,不停出错不停试也没关系。出错就改,改完再跑,跑不通就再改,或者向外寻求帮助,慢慢我也体会到了一种创造的快乐。
另外我也明白了,编程不是背语法,而是把自己的想法翻译成计算机能懂的逻辑。这个过程中,我一直用AI工具辅助学习,比如让它帮忙解释报错、给出示例代码。但我发现,AI生成的代码虽然好看而且高效,可如果自己不理解的话,就根本不知道从何下手去用、去改。所以工具归工具,写代码的逻辑还是得自己来掌握。这门课还让我接触了Git、爬虫、数据库这些实用东西,让我认识到用python写代码能做的事,比去竞赛解题什么的多得多。虽然现阶段我离高手还差得远,但至少我不怕动手写代码了,也敢犯错、敢调试。就起点而言,我很开心自己收获得那么多。

3. 课程建议

整体的课程体验对我来说十分难忘!就是对我们纯小白来说,学习爬虫、web和pygame之类的知识会有些慢,最后的综合实践想要真正用起来也会有些难,所以还是希望王老师后期也能带我们文科生亲手写写综合实践会用到的这些功能的代码~
最后,十分感谢王老师的引领!

参考资料

ZetCode 中文网

Python官方文档——Tkinter图形界面

百度百科(快艇骰子规则参考)

英文维基百科(快艇骰子进阶规则参考)

posted @ 2026-06-16 02:58  浦馨宇  阅读(14)  评论(0)    收藏  举报