20254201 2025-2026-2 《Python程序设计》实验4报告

课程:《Python程序设计》
班级: 2542
姓名: 邢艺馨
学号:20254201
实验教师:王志强
实验日期:2026年6月1日
必修/选修: 专选课

1.实验内容

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

2. 实验过程及结果

(1)实验分析

这次实验明显要求我们能够综合应用所学的Python知识,来实现一个具有完整功能的可应用的程序,只交一个以往的单一代码是不行的。对于没有良好编程理工科基础的我来说,爬虫数据处理等综合应用较为抽象难以实现,可以说哪怕是给ai输指令都不知道写什么,所以我选择了较为好上手的游戏类型。又因为我比较喜欢玩uno,于是我决定尝试通过Python综合应用这个实验来制造出属于自己的即点即玩的uno游戏。

需求分析:

UNO是一款风靡全球的休闲卡牌桌游,规则简单、趣味性强,但传统线下游玩需要2-4名玩家参与,单人无法独立游玩,场景局限性较大。基于以上痛点,本次实践依托Python Pygame图形库,开发一款单人可玩、界面可视化、运行流畅、功能丰富的电脑端UNO卡牌游戏。通过设计多AI人机对战模式,解决单人游玩的核心需求,同时优化画面渲染、动画效果、交互逻辑,并新增独家抢铃趣味功能,弥补传统简易UNO游戏的短板。

可行性分析:

本次项目基于Python语言开发,依托Pygame第三方图形库实现可视化界面与游戏交互,结合面向对象编程思想拆分卡牌、玩家、动画、AI、游戏主逻辑等模块,代码结构清晰、耦合度低。所用技术均为Python课程核心知识点,涵盖基础语法、类与对象、函数封装、条件循环、列表字典操作、第三方库调用、动画算法、状态机管理等,技术难度适中,可稳定实现所有预设功能,程序可在Windows系统正常运行,兼容性良好。

(2)实验设计

对于我心中比较理想的uno游戏,应该是能有三个“人性化”的人机陪人一起玩,视觉效果要好,运行要流畅,还要有一些趣味的小玩法,由此做出一些设计:

可视化界面设计

采用深色护眼背景,搭配四种高辨识度卡牌配色,界面分区清晰:底部为玩家手牌区,上、左、右为AI手牌区,中央为出牌区、抽牌堆与颜色指示区。UI采用圆角分层设计,适配中文显示,解决乱码问题。同时通过卡牌背面缓存预渲染技术,减少重复绘制,有效提升游戏帧率。

单人多人机对战设计

设置1V3固定对战布局,3个AI玩家拥有独立思考机制,出牌前随机生成800-1200ms思考时间,出牌后预留展示间隔,高度还原真人游玩节奏,彻底解决单人无玩伴无法游玩的问题。

创新抢铃功能设计

设计抢铃机制,当任意玩家手牌仅剩2张时,全局触发抢铃环节。界面弹出专属提示遮罩,玩家可按空格键抢铃,AI拥有300-1000ms随机反应时间。抢铃成功者免除惩罚,其余玩家统一抽取2张卡牌,大幅增强游戏趣味性与博弈性。

流畅性优化设计

针对画面卡顿、抖动问题进行多重优化:采用坐标缓存机制,仅在手牌变动时重算位置;使用整数坐标渲染杜绝浮点抖动;搭配单帧刷新、缓存渲染、缓动动画等技术,实现游戏60帧稳定运行,全程流畅不卡顿。

(3)实现过程

开发环境搭建

本次实验基于Windows系统,使用PyCharm编辑器、Python3.9解释器,通过pip安装Pygame图形库,配置多兼容中文字体,解决中文乱码问题,搭建可正常编译、运行、调试的完整开发环境。

import pygame
import random
import math
import sys

# 初始化Pygame
pygame.init()
pygame.font.init()

# 窗口参数
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("UNO Game")

# 颜色定义
COLORS = {
    'RED': (220, 50, 50),
    'BLUE': (50, 100, 220),
    'GREEN': (50, 180, 80),
    'YELLOW': (240, 200, 20),
    'BLACK': (40, 40, 40),
    'WHITE': (255, 255, 255),
    'BG': (30, 80, 50),
    'DARK_BG': (20, 60, 40)
}

# 卡牌尺寸与帧率
CARD_WIDTH = 70
CARD_HEIGHT = 100
FPS = 60
clock = pygame.time.Clock()

# 中文字体兼容处理
CHINESE_FONTS = ["SimHei", "Microsoft YaHei", "Arial Unicode MS", "Arial"]
font_small = pygame.font.SysFont(CHINESE_FONTS, 16, True)
font_medium = pygame.font.SysFont(CHINESE_FONTS, 24, True)
font_large = pygame.font.SysFont(CHINESE_FONTS, 36, True)
font_bell = pygame.font.SysFont(CHINESE_FONTS, 72, True)

动画系统模块实现

开发初期测试发现,直接修改卡牌坐标实现移动会出现画面抖动、移动僵硬、帧率不稳定等问题,视觉体验较差。为优化动态效果,项目单独拆分出动画管理模块,通过动画队列统一管理所有动态位移行为,并引入缓动算法优化移动曲线。相较于直接坐标刷新,该方式能够让卡牌移动过渡更加顺滑,同时自动清理失效动画,减少无效渲染带来的性能损耗。

class AnimationManager:
    def __init__(self):
        self.animations = []
        self.is_animating = False

    def add_animation(self, start_pos, end_pos, duration=650, easing='ease_out'):
        anim = {
            'start_x': start_pos[0], 'start_y': start_pos[1],
            'end_x': end_pos[0], 'end_y': end_pos[1],
            'duration': duration, 'elapsed': 0,
            'easing': easing, 'finished': False
        }
        self.animations.append(anim)
        self.is_animating = True
        return anim

    def update(self, dt):
        for anim in self.animations:
            if not anim['finished']:
                anim['elapsed'] += dt
                if anim['elapsed'] >= anim['duration']:
                    anim['finished'] = True
        self.animations = [a for a in self.animations if not a['finished']]
        self.is_animating = len(self.animations) > 0

    def get_position(self, anim):
        t = anim['elapsed'] / anim['duration']
        t = 1 - math.pow(1 - t, 3)
        x = anim['start_x'] + (anim['end_x'] - anim['start_x']) * t
        y = anim['start_y'] + (anim['end_y'] - anim['start_y']) * t
        return (int(x), int(y))

卡牌实体类模块实现

卡牌是游戏的核心基础单元,包含普通数字牌、多种功能牌、万能变色牌三类不同特性,属性与绘制逻辑差异较大。为避免代码冗余、统一管理卡牌状态,项目单独封装Card实体类,集中定义卡牌颜色、数值、碰撞区域等通用属性,同时针对万能牌的动态变色、功能牌的特殊判定单独编写适配逻辑,实现所有卡牌的统一创建、渲染与属性管理。

class Card:
    def __init__(self, color, value):
        self.color = color
        self.value = value
        self.x = 0
        self.y = 0
        self.rect = pygame.Rect(0, 0, CARD_WIDTH, CARD_HEIGHT)
        self.wild_color = None

    def get_display_color(self):
        if self.color == 'BLACK' and self.wild_color:
            return COLORS[self.wild_color]
        return COLORS[self.color]

    def is_special(self):
        return self.value in ['SKIP', 'REVERSE', '+2', 'WILD', '+4']

    def draw(self, surface, x=None, y=None):
        draw_x = int(x) if x is not None else int(self.x)
        draw_y = int(y) if y is not None else int(self.y)
        self.rect.x = draw_x
        self.rect.y = draw_y

        pygame.draw.rect(surface, self.get_display_color(), (draw_x, draw_y, CARD_WIDTH, CARD_HEIGHT), border_radius=8)
        pygame.draw.rect(surface, COLORS['WHITE'], (draw_x+3, draw_y+3, CARD_WIDTH-6, CARD_HEIGHT-6), border_radius=6, width=2)

        text_color = COLORS['WHITE'] if self.color != 'YELLOW' else COLORS['BLACK']
        text = font_medium.render(str(self.value), True, text_color)
        text_rect = text.get_rect(center=(draw_x + CARD_WIDTH//2, draw_y + CARD_HEIGHT//2))
        surface.blit(text, text_rect)

玩家实体模块实现

本游戏采用1v3人机对战模式,真人玩家与AI玩家的展示位置、手牌样式、交互逻辑完全不同。为区分两类玩家身份,同时适配上下左右多方位的界面布局,项目设计独立Player玩家类。通过缓存手牌坐标位置,避免每帧重复计算坐标数据,有效降低性能消耗,同时区分真人可见手牌与AI背面手牌的渲染样式,贴合实际桌游的视觉效果。

class Player:
    def __init__(self, name, is_ai=False, position='bottom'):
        self.name = name
        self.hand = []
        self.is_ai = is_ai
        self.position = position
        self._hand_positions = []
        self._hand_dirty = True

    def add_card(self, card):
        self.hand.append(card)
        self._hand_dirty = True

    def remove_card(self, card):
        if card in self.hand:
            self.hand.remove(card)
            self._hand_dirty = True

    def _calculate_hand_positions(self):
        if not self._hand_dirty:
            return
        self._hand_positions = []
        count = len(self.hand)
        if self.position == 'bottom':
            total_width = min(count * 45, SCREEN_WIDTH - 200)
            start_x = (SCREEN_WIDTH - total_width) // 2
            y = SCREEN_HEIGHT - CARD_HEIGHT - 30
            for i in range(count):
                self._hand_positions.append((int(start_x + i*45), y))
        elif self.position == 'top':
            total_width = min(count * 45, SCREEN_WIDTH - 200)
            start_x = (SCREEN_WIDTH - total_width) // 2
            y = 30
            for i in range(count):
                self._hand_positions.append((int(start_x + i*45), y))
        elif self.position == 'left':
            x = 30
            start_y = 150
            for i in range(count):
                self._hand_positions.append((x, int(start_y + i*30)))
        elif self.position == 'right':
            x = SCREEN_WIDTH - 30 - CARD_WIDTH
            start_y = 150
            for i in range(count):
                self._hand_positions.append((x, int(start_y + i*30)))
        self._hand_dirty = False

    def draw_hand(self, surface):
        self._calculate_hand_positions()
        for idx, card in enumerate(self.hand):
            pos = self._hand_positions[idx]
            if self.is_ai:
                pygame.draw.rect(surface, COLORS['DARK_BG'], (pos[0], pos[1], CARD_WIDTH, CARD_HEIGHT), border_radius=8)
                pygame.draw.rect(surface, COLORS['WHITE'], (pos[0]+2, pos[1]+2, CARD_WIDTH-4, CARD_HEIGHT-4), border_radius=6, width=1)
            else:
                card.draw(surface, pos[0], pos[1])

AI智能决策模块实现

为提升人机对战的趣味性,避免AI机械无脑出牌,项目配套开发了完整的AI决策逻辑。通过编写卡牌合规性判定条件,让AI能够精准识别可出牌型,同时设置功能牌优先的出牌策略,模拟真人玩家优先打出干扰牌的对战思路。针对万能牌场景,AI可根据自身手牌色系自动选择最优变色方案,让人机对抗更加智能、真实。

class AILogic:
    @staticmethod
    def is_playable(card, top_card, current_color):
        if card.color == "BLACK":
            return True
        if card.color == current_color:
            return True
        if card.value == top_card.value and top_card.color != "BLACK":
            return True
        return False

    @staticmethod
    def choose_card(player, top_card, current_color):
        playable = [c for c in player.hand if AILogic.is_playable(c, top_card, current_color)]
        if not playable:
            return None
        special = [c for c in playable if c.is_special()]
        if special:
            return random.choice(special)
        return random.choice(playable)

    @staticmethod
    def choose_wild_color(player):
        cnt = {"RED":0,"BLUE":0,"GREEN":0,"YELLOW":0}
        for c in player.hand:
            if c.color in cnt:
                cnt[c.color] += 1
        return max(cnt, key=cnt.get)

游戏主规则模块实现

游戏主类是整个项目的逻辑核心,承担统筹全局游戏流程的作用。模块内部完成牌组创建、随机洗牌、初始发牌等基础工作,同时封装回合跳转、出牌方向切换、各类功能卡牌的特效触发逻辑。整合所有游戏规则与全局状态变量,统一管控游戏运行流程,保证每一轮出牌、抽牌、回合切换都符合标准UNO桌游规则,形成闭环的游戏运行逻辑。

class UNOGame:
    def __init__(self):
        self.animation_manager = AnimationManager()
        self.reset_game()

    def reset_game(self):
        self.deck = []
        self.discard_pile = []
        self.players = [
            Player("玩家", False, "bottom"),
            Player("AI1", True, "top"),
            Player("AI2", True, "left"),
            Player("AI3", True, "right")
        ]
        self.current_idx = 0
        self.direction = 1
        self.current_color = None
        self.game_state = "playing"
        self.tip_text = ""

        # 抢铃变量
        self.bell_ringing = False
        self.bell_round_counter = 0
        self.bell_triggered_round = -1
        self.bell_winner = None
        self.bell_timers = {}

        self.create_deck()
        self.shuffle_deck()
        self.deal_cards()
        self.init_top_card()

    def create_deck(self):
        colors = ["RED","BLUE","GREEN","YELLOW"]
        # 数字牌
        for c in colors:
            self.deck.append(Card(c, "0"))
            for _ in range(2):
                for num in range(1,10):
                    self.deck.append(Card(c, str(num)))
        # 功能牌
        for c in colors:
            for _ in range(2):
                self.deck.append(Card(c, "SKIP"))
                self.deck.append(Card(c, "REVERSE"))
                self.deck.append(Card(c, "+2"))
        # 万能牌
        for _ in range(4):
            self.deck.append(Card("BLACK","WILD"))
            self.deck.append(Card("BLACK","+4"))

    def shuffle_deck(self):
        random.shuffle(self.deck)

    def deal_cards(self):
        for p in self.players:
            for _ in range(7):
                self.deck and p.add_card(self.deck.pop())

    def init_top_card(self):
        while True:
            card = self.deck.pop()
            if not card.is_special():
                self.discard_pile.append(card)
                self.current_color = card.color
                break
            self.deck.insert(0, card)

    @property
    def current_player(self):
        return self.players[self.current_idx]

    @property
    def top_card(self):
        return self.discard_pile[-1]

    def next_player(self):
        self.current_idx = (self.current_idx + self.direction) % 4

    def process_card_effect(self, card):
        self.discard_pile.append(card)
        if card.color == "BLACK":
            new_color = AILogic.choose_wild_color(self.current_player)
            card.wild_color = new_color
            self.current_color = new_color
            self.tip_text = f"万能牌变色:{new_color}"
        else:
            self.current_color = card.color

        if card.value == "SKIP":
            self.tip_text = "跳过下一玩家回合!"
            self.next_player()
        elif card.value == "REVERSE":
            self.tip_text = "反转出牌方向!"
            self.direction *= -1
        elif card.value == "+2":
            self.tip_text = "+2惩罚!"
            self.next_player()
            for _ in range(2):
                self.deck and self.current_player.add_card(self.deck.pop())
        elif card.value == "+4":
            self.tip_text = "+4惩罚!"
            self.next_player()
            for _ in range(4):
                self.deck and self.current_player.add_card(self.deck.pop())

    def draw(self, surface):
        surface.fill(COLORS['BG'])
        for p in self.players:
            p.draw_hand(surface)
        # 绘制顶牌
        if self.discard_pile:
            cx = SCREEN_WIDTH//2 - CARD_WIDTH//2
            cy = SCREEN_HEIGHT//2 - CARD_HEIGHT//2
            self.top_card.draw(surface, cx, cy)
        # 提示文字
        tip = font_medium.render(self.tip_text, True, COLORS['WHITE'])
        surface.blit(tip, (20,20))
        # 抢铃提示
        if self.bell_ringing:
            bell_text = font_bell.render("!! UNO抢铃 按空格抢铃 !!", True, (255,220,0))
            rect = bell_text.get_rect(center=(SCREEN_WIDTH//2, 100))
            surface.blit(bell_text, rect)

抢铃特色功能实现

在标准UNO规则之外,项目新增抢铃博弈玩法,作为核心特色拓展功能。常规UNO游戏仅存在出牌对抗,玩法较为单一,新增抢铃机制后,系统会实时监测所有玩家手牌数量,当任意玩家手牌仅剩两张时自动触发抢铃竞赛。支持玩家键盘手动抢铃与AI随机计时抢铃,抢铃成功者规避惩罚,其余玩家统一抽牌,大幅提升游戏的博弈性与趣味性。

    def handle_keydown(self, key):
        """处理键盘按键"""
        if key == pygame.K_SPACE and self.bell_ringing:
            # 玩家按空格抢铃
            if self.players[0] not in self.bell_timers:
                self.bell_timers[self.players[0]] = 0  # 立即抢到

    # ========== 抢铃功能 - 空格键处理 END ==========

    # ========== 抢铃功能 - 触发检测 START ==========
    def check_bell_trigger(self):
        """检测是否触发抢铃"""
        # 同一轮只触发一次
        if self.bell_triggered_round == self.bell_round_counter:
            return False

        # 检查是否有玩家手牌为2张
        for player in self.players:
            if len(player.hand) == 2:
                return True
        return False

    def start_bell_ringing(self):
        """开始抢铃环节"""
        self.bell_ringing = True
        self.bell_winner = None
        self.bell_timers = {}
        self.bell_triggered_round = self.bell_round_counter

        # AI设置随机反应时间 (300-1000ms)
        for player in self.players:
            if player.is_ai:
                self.bell_timers[player] = random.randint(300, 1000)

    # ========== 抢铃功能 - 触发检测 END ==========

    def update(self, dt):
        """更新游戏状态"""
        if self.winner:
            return
        self.check_empty_deck_win()  # 新增

        # ========== 抢铃功能 - 抢铃状态逻辑 START ==========
        if self.bell_ringing:
            # 更新AI计时器
            for player in list(self.bell_timers.keys()):
                if player.is_ai:
                    self.bell_timers[player] -= dt
                    if self.bell_timers[player] <= 0:
                        # AI抢到铃
                        self.bell_winner = player
                        self.end_bell_ringing()
                        return

            # 检查人类玩家是否抢到
            if self.players[0] in self.bell_timers and self.bell_winner is None:
                self.bell_winner = self.players[0]
                self.end_bell_ringing()
                return
            return
        # ========== 抢铃功能 - 抢铃状态逻辑 END ==========

        # 正常游戏逻辑
        # 更新动画
        self.animation_manager.update(dt)

        # 更新消息计时器
        if self.message_timer > 0:
            self.message_timer -= dt
            if self.message_timer <= 0:
                self.message = ""

        # 等待计时器
        if self.wait_timer < self.wait_duration:
            self.wait_timer += dt

            if self.wait_timer >= self.wait_duration:
                # 等待结束
                if self.game_state == 'ai_thinking':
                    self.play_ai_card()
                elif self.game_state == 'ai_showing':
                    # ========== 抢铃功能 - AI出牌后检测 START ==========
                    if self.check_bell_trigger():
                        self.start_bell_ringing()
                        return
                    # ========== 抢铃功能 - AI出牌后检测 END ==========
                    self.next_player()
                    if self.current_player.is_ai:
                        self.start_ai_turn()
                    else:
                        self.game_state = 'playing'
                elif self.game_state == 'animating':
                    # ========== 抢铃功能 - 动画结束后检测 START ==========
                    if self.check_bell_trigger():
                        self.start_bell_ringing()
                        return
                    # ========== 抢铃功能 - 动画结束后检测 END ==========
                    self.next_player()
                    if self.current_player.is_ai:
                        self.start_ai_turn()
                    else:
                        self.game_state = 'playing'

        # 动画结束检查
        if not self.animation_manager.is_animating and self.game_state == 'animating':
            if self.wait_timer >= self.wait_duration:
                # ========== 抢铃功能 - 玩家出牌后检测 START ==========
                if self.check_bell_trigger():
                    self.start_bell_ringing()
                    return
                # ========== 抢铃功能 - 玩家出牌后检测 END ==========
                self.next_player()
                if self.current_player.is_ai:
                    self.start_ai_turn()
                else:
                    self.game_state = 'playing'

    # ========== 抢铃功能 - 结束抢铃 START ==========
    def end_bell_ringing(self):
        """结束抢铃环节"""
        self.bell_ringing = False

        # 惩罚:除了获胜者,其他玩家各抽2张
        for player in self.players:
            if player != self.bell_winner:
                for _ in range(2):
                    if self.deck:
                        player.add_card(self.deck.pop())

        # 显示结果消息
        self.message = f"{self.bell_winner.name} 抢到铃!其他玩家各抽2张"
        self.message_timer = 1500

        # 恢复正常游戏状态
        self.bell_winner = None
        self.bell_timers = {}
        self.next_player()
        if self.current_player.is_ai:
            self.start_ai_turn()
        else:
            self.game_state = 'playing'

交互逻辑与游戏主循环实现

为实现完整的人机交互体验,项目搭建游戏主循环与事件监听体系。通过持续刷新游戏界面、更新动画与游戏状态,保证游戏持续运行;同时监听鼠标点击、键盘按下等操作,适配玩家出牌、手动抢铃、一键重置游戏等交互行为。所有交互操作均增加状态锁判定,避免动画、抢铃过程中出现误操作,保障游戏逻辑稳定不冲突。

def handle_click(self, pos):
    if self.game_state != "playing" or self.current_player.is_ai or self.bell_ringing:
        return
    for card in reversed(self.current_player.hand):
        if card.rect.collidepoint(pos):
            if AILogic.is_playable(card, self.top_card, self.current_color):
                self.current_player.remove_card(card)
                self.process_card_effect(card)
                self.next_player()
                self.bell_round_counter += 1
                if self.check_bell_trigger():
                    self.start_bell_ringing()

def ai_turn(self):
    if not self.current_player.is_ai or self.bell_ringing:
        return
    card = AILogic.choose_card(self.current_player, self.top_card, self.current_color)
    if card:
        self.current_player.remove_card(card)
        self.process_card_effect(card)
    else:
        self.deck and self.current_player.add_card(self.deck.pop())
    self.next_player()
    self.bell_round_counter += 1
    if self.check_bell_trigger():
        self.start_bell_ringing()

def update(self, dt):
    self.animation_manager.update(dt)
    self.update_bell(dt)
    if self.current_player.is_ai and not self.bell_ringing:
        self.ai_turn()

def handle_key(self, key):
    if key == pygame.K_r:
        self.reset_game()
    self.handle_bell_key(key)

# 主函数
def main():
    game = UNOGame()
    running = True
    while running:
        dt = clock.tick(FPS)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.MOUSEBUTTONDOWN:
                game.handle_click(event.pos)
            if event.type == pygame.KEYDOWN:
                game.handle_key(event.key)
        game.update(dt)
        game.draw(screen)
        pygame.display.flip()
    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

(4)实验结果

视频链接:https://www.bilibili.com/video/BV1pK5F6gEw6/?spm_id_from=333.1387.homepage.video_card.click&vd_source=3c8d52923bc1f6b76965d53f5da957f3

B站视频

完整代码(宇宙超级无敌刷屏臃肿长代码预警⚠️老师对不起🤦‍♀️一不会就豆包启动最后就成这样了):

import pygame
import random
import math
import sys

# ==================== 初始化 ====================
pygame.init()
pygame.font.init()

# 屏幕设置
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("UNO Game - 最终修复版")

# 颜色定义
COLORS = {
    'RED': (220, 50, 50),
    'BLUE': (50, 100, 220),
    'GREEN': (50, 180, 80),
    'YELLOW': (240, 200, 20),
    'BLACK': (40, 40, 40),
    'WHITE': (255, 255, 255),
    'BG': (30, 80, 50),
    'DARK_BG': (20, 60, 40)
}

CARD_WIDTH = 70
CARD_HEIGHT = 100
FPS = 60
clock = pygame.time.Clock()

# ========== 中文支持修复 - 字体替换 START ==========
# 优先使用支持中文的字体,回退到系统默认
CHINESE_FONTS = ["SimHei", "Microsoft YaHei", "Arial Unicode MS", "Arial"]
font_small = pygame.font.SysFont(CHINESE_FONTS, 16, True)
font_medium = pygame.font.SysFont(CHINESE_FONTS, 24, True)
font_large = pygame.font.SysFont(CHINESE_FONTS, 36, True)
font_bell = pygame.font.SysFont(CHINESE_FONTS, 72, True)  # 抢铃大字
font_win = pygame.font.SysFont(CHINESE_FONTS, 80, True)


# ========== 中文支持修复 - 字体替换 END ==========


# ==================== 动画系统 ====================
class AnimationManager:
    """动画管理器 - 统一管理所有动画,确保节奏可控"""

    def __init__(self):
        self.animations = []
        self.is_animating = False

    def add_animation(self, start_pos, end_pos, duration=650, easing='ease_out'):
        """添加卡牌动画
        duration: 650ms (符合500-800ms要求)
        """
        anim = {
            'start_x': start_pos[0],
            'start_y': start_pos[1],
            'end_x': end_pos[0],
            'end_y': end_pos[1],
            'duration': duration,
            'elapsed': 0,
            'easing': easing,
            'finished': False
        }
        self.animations.append(anim)
        self.is_animating = True
        return anim

    def update(self, dt):
        """更新所有动画"""
        for anim in self.animations:
            if not anim['finished']:
                anim['elapsed'] += dt
                if anim['elapsed'] >= anim['duration']:
                    anim['elapsed'] = anim['duration']
                    anim['finished'] = True

        # 清理完成的动画
        self.animations = [a for a in self.animations if not a['finished']]
        self.is_animating = len(self.animations) > 0

    def get_position(self, anim):
        """获取当前动画位置"""
        t = anim['elapsed'] / anim['duration']

        # Ease-out 缓动函数
        if anim['easing'] == 'ease_out':
            t = 1 - math.pow(1 - t, 3)
        elif anim['easing'] == 'ease_in_out':
            if t < 0.5:
                t = 4 * t * t * t
            else:
                t = 1 - math.pow(-2 * t + 2, 3) / 2

        x = anim['start_x'] + (anim['end_x'] - anim['start_x']) * t
        y = anim['start_y'] + (anim['end_y'] - anim['start_y']) * t
        return (int(x), int(y))  # 使用整数坐标避免抖动


# ==================== 卡牌类 ====================
class Card:
    def __init__(self, color, value):
        self.color = color
        self.value = value
        self.x = 0
        self.y = 0
        self.rect = pygame.Rect(0, 0, CARD_WIDTH, CARD_HEIGHT)
        self.wild_color = None

    def get_display_color(self):
        if self.color == 'BLACK' and self.wild_color:
            return COLORS[self.wild_color]
        return COLORS[self.color]

    def is_special(self):
        return self.value in ['SKIP', 'REVERSE', '+2', 'WILD', '+4']

    def draw(self, surface, x=None, y=None):
        """绘制卡牌 - 使用整数坐标避免浮点抖动"""
        draw_x = int(x) if x is not None else int(self.x)
        draw_y = int(y) if y is not None else int(self.y)

        # 卡牌背景
        pygame.draw.rect(surface, self.get_display_color(),
                         (draw_x, draw_y, CARD_WIDTH, CARD_HEIGHT), border_radius=8)
        pygame.draw.rect(surface, COLORS['WHITE'],
                         (draw_x + 3, draw_y + 3, CARD_WIDTH - 6, CARD_HEIGHT - 6),
                         border_radius=6, width=2)

        # 卡牌文字
        text_color = COLORS['WHITE'] if self.color != 'YELLOW' else COLORS['BLACK']
        text = font_medium.render(str(self.value), True, text_color)
        text_rect = text.get_rect(center=(draw_x + CARD_WIDTH // 2, draw_y + CARD_HEIGHT // 2))
        surface.blit(text, text_rect)

        # 角落小数字
        corner_text = font_small.render(str(self.value), True, text_color)
        surface.blit(corner_text, (draw_x + 6, draw_y + 6))
        surface.blit(corner_text, (draw_x + CARD_WIDTH - 20, draw_y + CARD_HEIGHT - 24))


# ==================== 卡牌背面缓存 ====================
class CardBackCache:
    """卡牌背面预渲染缓存 - 避免重复创建相同图形元素"""

    _instance = None
    _back_surface = None

    @classmethod
    def get_back_surface(cls):
        """获取单例的卡牌背面surface"""
        if cls._back_surface is None:
            cls._back_surface = pygame.Surface((CARD_WIDTH, CARD_HEIGHT), pygame.SRCALPHA)
            cls._render_back(cls._back_surface)
        return cls._back_surface

    @staticmethod
    def _render_back(surface):
        """渲染统一的卡牌背面图案"""
        # 背景
        pygame.draw.rect(surface, COLORS['BLACK'], (0, 0, CARD_WIDTH, CARD_HEIGHT), border_radius=8)
        # 内边框
        pygame.draw.rect(surface, (100, 100, 100),
                         (3, 3, CARD_WIDTH - 6, CARD_HEIGHT - 6), border_radius=6, width=2)
        # UNO标志
        uno_text = font_large.render("UNO", True, COLORS['WHITE'])
        text_rect = uno_text.get_rect(center=(CARD_WIDTH // 2, CARD_HEIGHT // 2))
        surface.blit(uno_text, text_rect)
        # 装饰圆圈
        pygame.draw.circle(surface, COLORS['RED'], (15, 15), 8)
        pygame.draw.circle(surface, COLORS['BLUE'], (CARD_WIDTH - 15, 15), 8)
        pygame.draw.circle(surface, COLORS['GREEN'], (15, CARD_HEIGHT - 15), 8)
        pygame.draw.circle(surface, COLORS['YELLOW'], (CARD_WIDTH - 15, CARD_HEIGHT - 15), 8)


# ==================== 玩家类 ====================
class Player:
    def __init__(self, name, is_ai=False, position='bottom'):
        self.name = name
        self.hand = []
        self.is_ai = is_ai
        self.position = position

        # 位置缓存 - 避免每一帧重新计算
        self._hand_positions = []  # 缓存手牌位置
        self._hand_dirty = True  # 脏标记:手牌变化时才重新计算

        # AI区域固定位置
        self.ai_area_rect = None
        self._init_ai_area()

    def _init_ai_area(self):
        """初始化AI区域固定位置"""
        if self.position == 'top':
            self.ai_area_rect = pygame.Rect(SCREEN_WIDTH // 2 - 300, 20, 600, 130)
        elif self.position == 'left':
            self.ai_area_rect = pygame.Rect(20, SCREEN_HEIGHT // 2 - 150, 130, 300)
        elif self.position == 'right':
            self.ai_area_rect = pygame.Rect(SCREEN_WIDTH - 150, SCREEN_HEIGHT // 2 - 150, 130, 300)

    def add_card(self, card):
        """添加卡牌到手牌"""
        self.hand.append(card)
        self._hand_dirty = True  # 标记需要重新计算位置

    def remove_card(self, card):
        """从手牌移除卡牌"""
        if card in self.hand:
            self.hand.remove(card)
            self._hand_dirty = True  # 标记需要重新计算位置

    def _calculate_hand_positions(self):
        """手牌位置只在手牌变化时计算一次并缓存
        不在draw()函数中动态计算,避免抖动
        """
        if not self._hand_dirty:
            return

        self._hand_positions = []
        card_count = len(self.hand)

        if self.position == 'bottom':
            # 玩家手牌 - 底部扇形展开
            total_width = min(card_count * 45, SCREEN_WIDTH - 200)
            start_x = (SCREEN_WIDTH - total_width) // 2
            y = SCREEN_HEIGHT - CARD_HEIGHT - 30

            for i in range(card_count):
                # 使用整数坐标,避免浮点精度导致的抖动
                x = int(start_x + i * 45)
                self._hand_positions.append((x, y))

        elif self.position == 'top':
            # AI手牌 - 顶部整齐排列
            max_visible = 10
            display_count = min(card_count, max_visible)
            spacing = 35 if card_count <= 8 else 28
            total_width = display_count * spacing
            start_x = SCREEN_WIDTH // 2 - total_width // 2
            y = 40

            for i in range(display_count):
                x = int(start_x + i * spacing)
                self._hand_positions.append((x, y))

        elif self.position == 'left':
            # AI手牌 - 左侧垂直排列
            max_visible = 8
            display_count = min(card_count, max_visible)
            spacing = 32
            x = 40
            start_y = SCREEN_HEIGHT // 2 - (display_count * spacing) // 2

            for i in range(display_count):
                y = int(start_y + i * spacing)
                self._hand_positions.append((x, y))

        elif self.position == 'right':
            # AI手牌 - 右侧垂直排列
            max_visible = 8
            display_count = min(card_count, max_visible)
            spacing = 32
            x = SCREEN_WIDTH - CARD_WIDTH - 40
            start_y = SCREEN_HEIGHT // 2 - (display_count * spacing) // 2

            for i in range(display_count):
                y = int(start_y + i * spacing)
                self._hand_positions.append((x, y))

        self._hand_dirty = False  # 清除脏标记

    def get_card_position(self, index):
        """获取卡牌位置 - 从缓存读取"""
        self._calculate_hand_positions()
        if index < len(self._hand_positions):
            return self._hand_positions[index]
        return (0, 0)

    def draw(self, surface):
        """绘制玩家手牌"""
        self._calculate_hand_positions()

        if self.is_ai:
            # AI区域背景固定
            if self.ai_area_rect:
                pygame.draw.rect(surface, COLORS['DARK_BG'], self.ai_area_rect, border_radius=10)
                pygame.draw.rect(surface, (80, 80, 80), self.ai_area_rect, border_radius=10, width=2)

            # AI名称标签 - 位置绝对固定
            name_text = font_medium.render(self.name, True, COLORS['WHITE'])
            if self.position == 'top':
                name_pos = (SCREEN_WIDTH // 2 - name_text.get_width() // 2, 10)
            elif self.position == 'left':
                name_pos = (25, SCREEN_HEIGHT // 2 - 165)
            elif self.position == 'right':
                name_pos = (SCREEN_WIDTH - 145, SCREEN_HEIGHT // 2 - 165)
            surface.blit(name_text, name_pos)

            # AI手牌背面统一渲染,位置固定
            back_surface = CardBackCache.get_back_surface()

            for i, pos in enumerate(self._hand_positions):
                x, y = pos
                # 轻微偏移制造堆叠效果
                stack_offset = i * 2
                surface.blit(back_surface, (x + stack_offset, y))

            # 手牌数量文本 - 固定位置,抗锯齿渲染
            count_text = font_medium.render(f"×{len(self.hand)}", True, COLORS['WHITE'])
            if self.position == 'top':
                count_pos = (SCREEN_WIDTH // 2 + 250, 10)
            elif self.position == 'left':
                count_pos = (95, SCREEN_HEIGHT // 2 - 165)
            elif self.position == 'right':
                count_pos = (SCREEN_WIDTH - 75, SCREEN_HEIGHT // 2 - 165)
            surface.blit(count_text, count_pos)

        else:
            # 玩家手牌 - 正面显示
            for i, card in enumerate(self.hand):
                if i < len(self._hand_positions):
                    x, y = self._hand_positions[i]
                    card.x, card.y = x, y
                    card.rect.topleft = (x, y)
                    card.draw(surface)

            # 玩家名称
            name_text = font_medium.render(self.name, True, COLORS['WHITE'])
            surface.blit(name_text, (SCREEN_WIDTH // 2 - name_text.get_width() // 2,
                                     SCREEN_HEIGHT - CARD_HEIGHT - 60))


# ==================== AI 逻辑 ====================
class AILogic:
    """AI出牌逻辑"""

    @staticmethod
    def choose_card(player, top_card, current_color):
        """AI选择要出的牌"""
        playable_cards = []

        for card in player.hand:
            if AILogic.is_playable(card, top_card, current_color):
                playable_cards.append(card)

        if not playable_cards:
            return None

        # 简单AI策略:优先出功能牌,然后出同色,然后出同数字
        special_cards = [c for c in playable_cards if c.is_special()]
        if special_cards:
            return random.choice(special_cards)

        color_cards = [c for c in playable_cards if c.color == current_color and c.color != 'BLACK']
        if color_cards:
            return random.choice(color_cards)

        return random.choice(playable_cards)

    @staticmethod
    def is_playable(card, top_card, current_color):
        """检查牌是否可以出"""
        if card.color == 'BLACK':
            return True
        if card.color == current_color:
            return True
        if card.value == top_card.value and not top_card.color == 'BLACK':
            return True
        return False

    @staticmethod
    def choose_wild_color(player):
        """AI选择万能牌颜色"""
        color_counts = {'RED': 0, 'BLUE': 0, 'GREEN': 0, 'YELLOW': 0}
        for card in player.hand:
            if card.color in color_counts:
                color_counts[card.color] += 1
        return max(color_counts, key=color_counts.get)


# ==================== 游戏主类 ====================
class UNOGame:
    def __init__(self):
        self.animation_manager = AnimationManager()
        self.reset_game()

    def reset_game(self):
        """重置游戏"""
        self.deck = self.create_deck()
        self.discard_pile = []
        self.players = [
            Player("玩家", is_ai=False, position='bottom'),
            Player("AI-上", is_ai=True, position='top'),
            Player("AI-左", is_ai=True, position='left'),
            Player("AI-右", is_ai=True, position='right')
        ]

        # 发牌
        for _ in range(7):
            for player in self.players:
                if self.deck:
                    player.add_card(self.deck.pop())

        # 确保第一张不是功能牌
        while self.deck and self.deck[-1].is_special():
            random.shuffle(self.deck)
        self.discard_pile.append(self.deck.pop())

        self.current_player_index = 0
        self.current_color = self.discard_pile[-1].color
        self.direction = 1
        self.game_state = 'playing'  # playing, ai_thinking, animating, bell_ringing
        self.wait_timer = 0
        self.wait_duration = 0
        self.message = ""
        self.message_timer = 0
        self.winner = None

        # ========== 抢铃功能 - 新增状态变量 START ==========
        self.bell_ringing = False  # 是否在抢铃状态
        self.bell_winner = None  # 抢铃获胜者
        self.bell_timers = {}  # 各玩家抢铃计时器
        self.bell_triggered_round = -1  # 标记哪一轮触发了抢铃,避免重复
        self.bell_round_counter = 0  # 轮次计数器
        # ========== 抢铃功能 - 新增状态变量 END ==========

    def create_deck(self):
        """创建一副UNO牌"""
        deck = []
        colors = ['RED', 'BLUE', 'GREEN', 'YELLOW']

        # 数字牌
        for color in colors:
            deck.append(Card(color, '0'))
            for _ in range(2):
                for num in range(1, 10):
                    deck.append(Card(color, str(num)))

        # 功能牌
        for color in colors:
            for _ in range(2):
                deck.append(Card(color, 'SKIP'))
                deck.append(Card(color, 'REVERSE'))
                deck.append(Card(color, '+2'))

        # 万能牌
        for _ in range(4):
            deck.append(Card('BLACK', 'WILD'))
            deck.append(Card('BLACK', '+4'))

        random.shuffle(deck)
        return deck

    @property
    def current_player(self):
        return self.players[self.current_player_index]

    @property
    def top_card(self):
        return self.discard_pile[-1]

    def next_player(self):
        """切换到下一个玩家"""
        self.current_player_index = (self.current_player_index + self.direction) % len(self.players)
        self.bell_round_counter += 1  # 轮次+1

    def check_winner(self):
        for p in self.players:
            if len(p.hand) == 0:
                self.winner = p
                self.game_state = "game_over"
                return True
        return False

    # ====================== 【你要的新功能】牌堆为空时,牌最少的人胜利 ======================
    def check_empty_deck_win(self):
        if len(self.deck) == 0 and not self.winner:
            # 找手牌最少的玩家
            min_count = min(len(p.hand) for p in self.players)
            for p in self.players:
                if len(p.hand) == min_count:
                    self.winner = p
                    self.message = f"牌堆已空!{self.winner.name} 手牌最少,获得胜利!"
                    self.message_timer = 3000
                    return True
        return False

    def start_ai_turn(self):
        """AI回合开始前添加思考停顿
        800-1200ms 思考时间
        """
        if self.check_empty_deck_win(): return  # 新增
        self.game_state = 'ai_thinking'
        self.wait_timer = 0
        self.wait_duration = random.randint(800, 1200)
        self.message = f"{self.current_player.name} 思考中..."
        self.message_timer = self.wait_duration

    def play_ai_card(self):
        """AI出牌"""
        if self.check_empty_deck_win(): return  # 新增
        card = AILogic.choose_card(self.current_player, self.top_card, self.current_color)

        if card:
            # 播放出牌动画
            card_index = self.current_player.hand.index(card)
            start_pos = self.current_player.get_card_position(card_index)
            end_pos = (SCREEN_WIDTH // 2 - CARD_WIDTH // 2, SCREEN_HEIGHT // 2 - CARD_HEIGHT // 2)

            self.animation_manager.add_animation(start_pos, end_pos, duration=650)
            self.animating_card = card

            # 从手牌移除
            self.current_player.remove_card(card)

            # 处理卡牌效果
            self.process_card_effect(card)
            self.check_winner()

            # AI出牌后添加展示停顿
            # 600-1000ms 展示时间
            self.game_state = 'ai_showing'
            self.wait_timer = 0
            self.wait_duration = random.randint(600, 1000)

        else:
            # 抽牌
            if self.deck:
                drawn_card = self.deck.pop()
                self.current_player.add_card(drawn_card)
                self.message = f"{self.current_player.name} 抽了一张牌"
                self.message_timer = 800

                # 检查抽到的牌是否可以出
                if AILogic.is_playable(drawn_card, self.top_card, self.current_color):
                    # 可以出,再给一次机会
                    self.wait_timer = 0
                    self.wait_duration = 500
                    self.game_state = 'ai_showing'
                else:
                    self.next_player()
                    if self.current_player.is_ai:
                        self.start_ai_turn()
                    else:
                        self.game_state = 'playing'
            else:
                self.next_player()
                if self.current_player.is_ai:
                    self.start_ai_turn()
                else:
                    self.game_state = 'playing'

    def process_card_effect(self, card):
        """处理卡牌效果"""
        self.discard_pile.append(card)

        if card.color == 'BLACK':
            chosen_color = AILogic.choose_wild_color(self.current_player)
            card.wild_color = chosen_color
            self.current_color = chosen_color
            self.message = f"{self.current_player.name} 选择了 {chosen_color}"
            self.message_timer = 1000
        else:
            self.current_color = card.color

        # 功能牌效果
        if card.value == 'SKIP':
            self.next_player()
            self.message = "跳过下一位!"
            self.message_timer = 500
        elif card.value == 'REVERSE':
            self.direction *= -1
            self.message = "方向反转!"
            self.message_timer = 500
        elif card.value == '+2':
            self.next_player()
            for _ in range(2):
                if self.deck:
                    self.current_player.add_card(self.deck.pop())
            self.message = "+2 抽牌!"
            self.message_timer = 500
        elif card.value == '+4':
            self.next_player()
            for _ in range(4):
                if self.deck:
                    self.current_player.add_card(self.deck.pop())
            self.message = "+4 抽牌!"
            self.message_timer = 500

    def handle_click(self, pos):
        """处理玩家点击"""
        if self.winner or self.bell_ringing:
            return
        if self.check_empty_deck_win(): return  # 新增

        if self.game_state != 'playing' or self.current_player.is_ai:
            return

        # 检查是否点击了手牌
        for card in reversed(self.current_player.hand):
            if card.rect.collidepoint(pos):
                if AILogic.is_playable(card, self.top_card, self.current_color):
                    # 可以出牌
                    start_pos = (card.x, card.y)
                    end_pos = (SCREEN_WIDTH // 2 - CARD_WIDTH // 2, SCREEN_HEIGHT // 2 - CARD_HEIGHT // 2)
                    self.animation_manager.add_animation(start_pos, end_pos, duration=650)
                    self.animating_card = card

                    self.current_player.remove_card(card)

                    # 处理万能牌颜色选择
                    if card.color == 'BLACK':
                        card.wild_color = 'RED'  # 默认红色

                    self.process_card_effect(card)
                    self.check_winner()
                    self.game_state = 'animating'
                else:
                    self.message = "这张牌不能出!"
                    self.message_timer = 600
                return

        # 点击抽牌堆
        deck_rect = pygame.Rect(SCREEN_WIDTH // 2 - 120, SCREEN_HEIGHT // 2 - CARD_HEIGHT // 2,
                                CARD_WIDTH, CARD_HEIGHT)
        if deck_rect.collidepoint(pos):
            if self.deck:
                drawn_card = self.deck.pop()
                self.current_player.add_card(drawn_card)
                self.message = "你抽了一张牌"
                self.message_timer = 600

                # 检查抽到的牌是否可以出
                if not AILogic.is_playable(drawn_card, self.top_card, self.current_color):
                    self.next_player()
                    if self.current_player.is_ai:
                        self.start_ai_turn()
            else:
                self.check_empty_deck_win()  # 新增

    # ========== 抢铃功能 - 空格键处理 START ==========
    def handle_keydown(self, key):
        """处理键盘按键"""
        if key == pygame.K_SPACE and self.bell_ringing:
            # 玩家按空格抢铃
            if self.players[0] not in self.bell_timers:
                self.bell_timers[self.players[0]] = 0  # 立即抢到

    # ========== 抢铃功能 - 空格键处理 END ==========

    # ========== 抢铃功能 - 触发检测 START ==========
    def check_bell_trigger(self):
        """检测是否触发抢铃"""
        # 同一轮只触发一次
        if self.bell_triggered_round == self.bell_round_counter:
            return False

        # 检查是否有玩家手牌为2张
        for player in self.players:
            if len(player.hand) == 2:
                return True
        return False

    def start_bell_ringing(self):
        """开始抢铃环节"""
        self.bell_ringing = True
        self.bell_winner = None
        self.bell_timers = {}
        self.bell_triggered_round = self.bell_round_counter

        # AI设置随机反应时间 (300-1000ms)
        for player in self.players:
            if player.is_ai:
                self.bell_timers[player] = random.randint(300, 1000)

    # ========== 抢铃功能 - 触发检测 END ==========

    def update(self, dt):
        """更新游戏状态"""
        if self.winner:
            return
        self.check_empty_deck_win()  # 新增

        # ========== 抢铃功能 - 抢铃状态逻辑 START ==========
        if self.bell_ringing:
            # 更新AI计时器
            for player in list(self.bell_timers.keys()):
                if player.is_ai:
                    self.bell_timers[player] -= dt
                    if self.bell_timers[player] <= 0:
                        # AI抢到铃
                        self.bell_winner = player
                        self.end_bell_ringing()
                        return

            # 检查人类玩家是否抢到
            if self.players[0] in self.bell_timers and self.bell_winner is None:
                self.bell_winner = self.players[0]
                self.end_bell_ringing()
                return
            return
        # ========== 抢铃功能 - 抢铃状态逻辑 END ==========

        # 正常游戏逻辑
        # 更新动画
        self.animation_manager.update(dt)

        # 更新消息计时器
        if self.message_timer > 0:
            self.message_timer -= dt
            if self.message_timer <= 0:
                self.message = ""

        # 等待计时器
        if self.wait_timer < self.wait_duration:
            self.wait_timer += dt

            if self.wait_timer >= self.wait_duration:
                # 等待结束
                if self.game_state == 'ai_thinking':
                    self.play_ai_card()
                elif self.game_state == 'ai_showing':
                    # ========== 抢铃功能 - AI出牌后检测 START ==========
                    if self.check_bell_trigger():
                        self.start_bell_ringing()
                        return
                    # ========== 抢铃功能 - AI出牌后检测 END ==========
                    self.next_player()
                    if self.current_player.is_ai:
                        self.start_ai_turn()
                    else:
                        self.game_state = 'playing'
                elif self.game_state == 'animating':
                    # ========== 抢铃功能 - 动画结束后检测 START ==========
                    if self.check_bell_trigger():
                        self.start_bell_ringing()
                        return
                    # ========== 抢铃功能 - 动画结束后检测 END ==========
                    self.next_player()
                    if self.current_player.is_ai:
                        self.start_ai_turn()
                    else:
                        self.game_state = 'playing'

        # 动画结束检查
        if not self.animation_manager.is_animating and self.game_state == 'animating':
            if self.wait_timer >= self.wait_duration:
                # ========== 抢铃功能 - 玩家出牌后检测 START ==========
                if self.check_bell_trigger():
                    self.start_bell_ringing()
                    return
                # ========== 抢铃功能 - 玩家出牌后检测 END ==========
                self.next_player()
                if self.current_player.is_ai:
                    self.start_ai_turn()
                else:
                    self.game_state = 'playing'

    # ========== 抢铃功能 - 结束抢铃 START ==========
    def end_bell_ringing(self):
        """结束抢铃环节"""
        self.bell_ringing = False

        # 惩罚:除了获胜者,其他玩家各抽2张
        for player in self.players:
            if player != self.bell_winner:
                for _ in range(2):
                    if self.deck:
                        player.add_card(self.deck.pop())

        # 显示结果消息
        self.message = f"{self.bell_winner.name} 抢到铃!其他玩家各抽2张"
        self.message_timer = 1500

        # 恢复正常游戏状态
        self.bell_winner = None
        self.bell_timers = {}
        self.next_player()
        if self.current_player.is_ai:
            self.start_ai_turn()
        else:
            self.game_state = 'playing'

    # ========== 抢铃功能 - 结束抢铃 END ==========

    def draw(self, surface):
        """彻底重构渲染机制
        - 只调用一次 flip()
        - 位置全部使用缓存
        - 消除所有动态计算
        """
        # 背景
        surface.fill(COLORS['BG'])

        # 绘制装饰
        pygame.draw.circle(surface, COLORS['DARK_BG'], (100, 100), 80)
        pygame.draw.circle(surface, COLORS['DARK_BG'], (SCREEN_WIDTH - 100, 100), 80)
        pygame.draw.circle(surface, COLORS['DARK_BG'], (100, SCREEN_HEIGHT - 100), 80)
        pygame.draw.circle(surface, COLORS['DARK_BG'], (SCREEN_WIDTH - 100, SCREEN_HEIGHT - 100), 80)

        # 绘制抽牌堆
        deck_x = SCREEN_WIDTH // 2 - 120
        deck_y = SCREEN_HEIGHT // 2 - CARD_HEIGHT // 2
        back_surface = CardBackCache.get_back_surface()
        surface.blit(back_surface, (deck_x, deck_y))

        deck_text = font_small.render(f"剩余:{len(self.deck)}", True, COLORS['WHITE'])
        surface.blit(deck_text, (deck_x, deck_y - 25))

        # 绘制弃牌堆顶部
        discard_x = SCREEN_WIDTH // 2 + 50
        discard_y = SCREEN_HEIGHT // 2 - CARD_HEIGHT // 2
        self.top_card.draw(surface, discard_x, discard_y)

        # 当前颜色指示
        color_indicator = pygame.Rect(SCREEN_WIDTH // 2 - 20, SCREEN_HEIGHT // 2 + 70, 40, 20)
        pygame.draw.rect(surface, COLORS[self.current_color], color_indicator, border_radius=5)
        color_text = font_small.render("当前颜色", True, COLORS['WHITE'])
        surface.blit(color_text, (SCREEN_WIDTH // 2 - color_text.get_width() // 2,
                                  SCREEN_HEIGHT // 2 + 95))

        # 绘制所有玩家
        for player in self.players:
            player.draw(surface)

        # 绘制动画中的卡牌
        if self.animation_manager.is_animating and hasattr(self, 'animating_card'):
            for anim in self.animation_manager.animations:
                pos = self.animation_manager.get_position(anim)
                self.animating_card.draw(surface, pos[0], pos[1])

        # 绘制消息
        if self.message:
            msg_surface = pygame.Surface((400, 50), pygame.SRCALPHA)
            msg_surface.fill((0, 0, 0, 180))
            surface.blit(msg_surface, (SCREEN_WIDTH // 2 - 200, SCREEN_HEIGHT // 2 - 100))

            msg_text = font_medium.render(self.message, True, COLORS['WHITE'])
            surface.blit(msg_text, (SCREEN_WIDTH // 2 - msg_text.get_width() // 2,
                                    SCREEN_HEIGHT // 2 - 90))

        # 回合指示
        if not self.current_player.is_ai and self.game_state == 'playing' and not self.bell_ringing and not self.winner:
            turn_text = font_medium.render("你的回合!点击牌出牌或点击牌堆抽牌", True, (255, 255, 100))
            surface.blit(turn_text, (SCREEN_WIDTH // 2 - turn_text.get_width() // 2,
                                     SCREEN_HEIGHT - CARD_HEIGHT - 90))

        # ========== 抢铃功能 - UI渲染 START ==========
        if self.bell_ringing:
            # 半透明遮罩
            overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 150))
            surface.blit(overlay, (0, 0))

            # 抢铃大字提示
            bell_text = font_bell.render("抢铃!", True, (255, 215, 0))
            bell_rect = bell_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
            surface.blit(bell_text, bell_rect)

            # 提示按空格
            hint_text = font_medium.render("按空格键抢铃!", True, COLORS['WHITE'])
            hint_rect = hint_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 80))
            surface.blit(hint_text, hint_rect)

        # 胜利画面
        if self.winner:
            overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 180))
            surface.blit(overlay, (0, 0))

            win_text = font_win.render(f"{self.winner.name} 胜利!", True, (255, 215, 0))
            surface.blit(win_text, (SCREEN_WIDTH // 2 - win_text.get_width() // 2, SCREEN_HEIGHT // 2 - 50))

            tip = font_medium.render("按 R 重新开始", True, COLORS['WHITE'])
            surface.blit(tip, (SCREEN_WIDTH // 2 - tip.get_width() // 2, SCREEN_HEIGHT // 2 + 50))


# ==================== 主游戏循环 ====================
def main():
    game = UNOGame()
    running = True

    while running:
        dt = clock.tick(FPS)  # 控制帧率

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:
                    game.handle_click(event.pos)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    game.reset_game()
                # ========== 抢铃功能 - 键盘事件处理 START ==========
                elif event.key == pygame.K_SPACE:
                    game.handle_keydown(event.key)
                # ========== 抢铃功能 - 键盘事件处理 END ==========

        # 更新游戏
        game.update(dt)

        # 绘制
        game.draw(screen)

        # 双缓冲优化 - 确保只调用一次 flip()
        pygame.display.flip()

    pygame.quit()
    sys.exit()


if __name__ == "__main__":
    main()

3. 实验过程中遇到的问题和解决过程

  • 问题1:游戏卡牌移动画面卡顿、抖动明显,把人都要看晕了
  • 问题1解决方案:初期直接通过刷新坐标实现卡牌移动,画面过渡生硬,频繁出现轻微抖动和掉帧情况。查阅相关渲染逻辑后发现,每帧重复计算坐标会造成渲染不稳定。后续新增独立动画管理模块,采用缓动算法控制移动节奏,统一管理动画生命周期,不再频繁刷新原始坐标,最终画面过渡变得流畅自然,彻底解决抖动问题。
  • 问题2:中文文字渲染乱码、显示异常,全是空格分不清谁在出牌
  • 问题2解决方案:Pygame默认无法正常识别中文字符,运行后界面提示文字全部变成乱码。最开始仅单独更换字体,问题依旧。后续通过预设多类主流中文字体做兼容适配,让程序自动匹配可用字体,成功解决中文乱码问题,界面文字能够正常展示。
  • 问题3:AI出牌逻辑机械、偶尔出现卡死情况
  • 问题3解决方案:初始AI仅能简单匹配颜色和数字,出牌逻辑十分死板,且偶尔出现无牌可出时程序卡顿停滞。排查后发现缺少合规牌型判断与抽牌兜底逻辑。通过新增卡牌合法性检测,优化AI选牌优先级,同时补充无牌可出时的自动抽牌机制,AI运行稳定,对战逻辑也更加智能。
  • 问题4:牌全出完不显示获胜、抽牌堆空时没有结算
  • 问题4解决方案:新增check_winner方法,玩家 / AI 出完牌后自动检测手牌为空的玩家,判定胜利并弹出结算画面,终止游戏流程;新增check_empty_deck_win检测函数,全局多处调用:当抽牌堆数量为 0 且无人出完手牌时,自动对比所有玩家手牌数,手牌最少者直接获胜,并提示对应文案。

其他(感悟、思考等)

嗯大概就是这本来应该是一个理想很丰满现实很骨感的故事,但奈何科技进步实在太强大,也是硬生生让我实现了我本不能的愿望了()
在此特别鸣谢我们伟大的豆包老师!一点一点教会我编写一个相对完善的程序到底该怎么做。其实从第一步我就在卡壳,刚开始做的是一个股票代码的分析系统,但一直在报错,后面问了豆老师才知道还要下载相应的拓展库,火速下载了7个发现还要注册账号获取信息就放弃了换成了现在这个pygame的才终于起码能让诗山代码运行起来了。后面就一有问题就开始问东问西,历经我三个多小时的调试完善以及红温😡一个我梦想中的uno游戏就这样像天使一样降临在了我眼前😇努力获得结果的感觉竟是如此的美妙。
二编:其实在一点点像愚公移山一样不断解决完好像无穷无尽的问题,尤其是在本以为已经做好的情况下时,真的已经要崩溃了,前面确定uno这个最终方向就用了一个多小时,我放弃了贪吃蛇这些比较简单的程序,因为我想给自己的Python课一个完美的交代,也像给自己一个证明我也能行的机会。完成uno现在基本的界面运行又用了两个多小时,后面在基本写完报告,录制运行视频而完整玩完时,又发现了n多问题,我就又磨啊磨,磨到差点就想直接把贪吃蛇交上去的时候,我又咬了咬牙,终于,那个理想中的uno来了。现在打字的此刻心力交瘁的我真的有点想哭,好幸福。我真的可以做到,我可以仅凭自己和llm工具和网络就做到理科生也要耗时很久才能完成的程序。我想,我在最后一次实验,用心交出了自己满意的答卷。真心期盼老师可以给我一份建议❤️

全课总结

本学期我们系统学习了 Python 相关知识,内容由浅入深。首先掌握了基础语法,包括变量、各类数据类型、运算符以及输入输出语句,搭建起了编程的基础框架。
随后学习了条件判断、循环等流程控制语句,配合字符串、列表、字典、集合等数据结构,实现了各类基础逻辑运算与数据处理。接着学习函数相关内容,掌握了函数定义、参数使用、变量作用域等知识,学会对代码进行封装复用。
课程中期接触面向对象编程,理解了类与对象,以及封装、继承等核心特性。同时学习了文件读写、异常捕获与程序调试,掌握了数据持久化和处理程序报错的方法。
后期我们还了解了模块与第三方库的使用,学习了数据库操作、图形界面开发、Pygame、简易网络编程等拓展内容。最后的综合项目,将全学期知识点融合运用,进一步巩固了所学内容。
经过一学期的学习,我初步掌握了 Python 核心语法与常用功能模块,也熟悉了不同场景下的代码编写思路。

课程感想及建议

感想:一开始得知我们明明是文科生但还得上Python课真的感觉很崩溃很无力,其实最初抱着的是浑浑噩噩混过去的想法的(惭愧)也确实有时候完全没有注意老师讲的一些细节重点内容,还有一次作业因为没仔细听讲还不好意思问老师闹了大乌龙(滑跪)但是老师人真的感觉特别平易近人,还很风趣幽默,跟我们完全没代沟,还特别喜欢缅怀劳大(确信)都说好的老师非常重要,或许我的知识掌握的不是很牢固,但是因为王老师也对Python编程之类的抵触情绪逐渐消减掉了,也能渐渐找到乐趣了,会自己生成能运行的程序的文科生超酷啊喂!虽然结课了,但我想我应该会在暑假这种空闲时间再自己琢磨琢磨,争取能不问豆老师独立写出一个程序!重生之我要当嘉豪😋
建议:老师在讲解知识点的时候可以慢一点,希望能尽量用傻子都能听懂的白话说一下含义用途,这样理解了再用感觉会很得心应手(不过课程时间紧还是超大课也能理解老师),再有就是socket编程这个超超超抽象的部分希望老师能先讲解一下一些常见的踩坑点和解决方法!
也没有什么啦,祝王老师事业成功、弟子顺心,一直能有这么积极幽默的灵魂!🎉

参考资料

[1] 黄文凯.Python程序设计基础[M].北京:人民邮电出版社,2021.
[2] 黑马程序员.Python游戏开发入门与实战[M].北京:电子工业出版社,2022.
[3] Pygame官方文档.Pygame 2.0 开发手册[EB/OL]. https://www.pygame.org/docs/ ,2023.
[4] 李刚.面向对象编程思想与Python实战案例解析[M].北京:机械工业出版社,2022.
[5] 知乎专栏.基于Pygame的卡牌游戏开发教程[EB/OL].2024.
[6] 王硕.小游戏开发中的动画优化与人机交互设计[J].电脑知识与技术,2023,19(12):78-80.
4387E6F1

posted on 2026-06-01 22:36  小元宵呱  阅读(36)  评论(0)    收藏  举报