基于Pygame的二十一点游戏关键代码解析与实现逻辑

二十一点作为经典扑克牌游戏,其核心在于规则的精准落地与可视化交互的流畅呈现。本文从提供的完整代码中,提取4个最具代表性的关键模块,通过“代码展示+原理剖析”的形式,拆解游戏开发的核心思路,帮助开发者理解卡牌类游戏的设计精髓。

一、Card类:扑克牌的基础数据封装

核心作用:定义单张扑克牌的属性与数值计算规则,是整个游戏的数据基础单元。

class Card:
    """扑克牌类"""
    def __init__(self, suit, rank):
        self.suit = suit  # 花色: hearts(红桃), diamonds(方块), clubs(梅花), spades(黑桃)
        self.rank = rank  # 点数: A, 2-10, J, Q, K
        self.value = self.get_value()  # 点数值(遵循二十一点规则)
        self.image = None  # 预留图片资源位

    def get_value(self):
        """获取牌的点数值,实现二十一点核心计分规则"""
        if self.rank in ['J', 'Q', 'K']:
            return 10  # 花牌统一计10点
        elif self.rank == 'A':
            return 11  # A默认计11点(后续在Hand类中动态调整)
        else:
            return int(self.rank)  # 数字牌按本身点数计算

    def __str__(self):
        return f"{self.rank} of {self.suit}"  # 便于调试时查看牌面信息

代码解析

  1. 属性设计的合理性

    • suit(花色)与rank(点数)分离存储:既满足可视化需求(如红桃/方块显示红色,梅花/黑桃显示黑色),也为后续扩展规则(如某些玩法中花色影响得分)预留空间。
    • value(数值)通过get_value()动态计算:避免硬编码数值,确保规则修改时只需调整该方法,符合“单一职责原则”。
  2. 数值规则的精准落地
    严格遵循二十一点标准:花牌(J/Q/K)统一计10点,A默认计11点(后续可调整为1点),数字牌按面值计算。这种设计让卡牌数据与游戏规则深度绑定,减少外部逻辑依赖。

  3. 调试友好性
    __str__方法重写后,打印Card对象时会显示“点数 of 花色”(如“A of hearts”),便于开发过程中查看手牌内容,降低调试难度。

二、Hand类:手牌的状态管理与规则判断

核心作用:管理玩家/庄家的手牌,实现点数计算、A的动态调整、爆牌/黑杰克判定,是游戏规则的核心执行层。

class Hand:
    """手牌类:处理手牌状态与规则判断"""
    def __init__(self):
        self.cards = []  # 存储手牌列表
        self.value = 0   # 手牌总点数
        self.aces = 0    # 记录A的数量(用于动态调整数值)

    def add_card(self, card):
        """添加牌到手牌,并自动更新状态"""
        self.cards.append(card)
        self.value += card.value
        if card.rank == 'A':
            self.aces += 1  # 新增A时更新计数
        self.adjust_for_ace()  # 即时调整A的数值,避免爆牌误判

    def adjust_for_ace(self):
        """动态调整A的数值:解决“超21点但有A”的核心问题"""
        # 循环调整:若调整一次后仍超21且有剩余A,继续调整(如多A场景)
        while self.value > 21 and self.aces:
            self.value -= 10  # A从11点改为1点,总点数减少10
            self.aces -= 1    # 标记该A已调整,避免重复操作

    def is_bust(self):
        """判断是否爆牌(总点数超过21)"""
        return self.value > 21

    def is_blackjack(self):
        """判断是否为黑杰克(初始2张牌总点数21:A+10/J/Q/K)"""
        return self.value == 21 and len(self.cards) == 2

代码解析

  1. A的动态调整逻辑:游戏规则的核心突破
    二十一点中A的“双值特性”(11点/1点)是规则难点,adjust_for_ace()通过以下设计完美解决:

    • aces计数器记录未调整的A数量,避免遍历手牌判断;
    • 循环调整机制:当总点数超21且有未调整的A时,逐个将A从11点改为1点,直到点数≤21或无A可调整(如手牌为“A+A+10”时,会调整两个A为1点,总点数22→12)。
  2. 状态更新的自动化
    add_card()方法添加牌后,立即执行adjust_for_ace(),确保每次手牌变化后,总点数始终符合规则,避免后续计算时出现“已爆牌但未调整”的错误。

  3. 规则判断的方法化
    将“爆牌”(is_bust())和“黑杰克”(is_blackjack())封装为独立方法,外部调用时无需重复计算(如庄家判定胜负时直接调用player_hand.is_bust()),提升代码复用性。

三、draw_card方法:牌面可视化的兼容实现

核心作用:绘制扑克牌的正面与背面,解决字体兼容性问题(如中文系统中花色符号显示异常),是连接数据与视觉的关键。

def draw_card(self, x, y, card, show_back=False):
    """绘制一张牌,支持正面/背面显示,兼容花色符号渲染"""
    card_width = 80
    card_height = 120
    card_rect = pygame.Rect(x, y, card_width, card_height)

    if show_back:
        # 绘制牌背:蓝色底色+装饰图案(菱形+圆点)
        self.draw_card_back(x, y, card_width, card_height)
    else:
        # 绘制牌面:白色底色+黑色边框
        pygame.draw.rect(self.screen, WHITE, card_rect)
        pygame.draw.rect(self.screen, BLACK, card_rect, 2)

        if card:
            # 获取花色显示信息(符号、中文名称、颜色)
            symbol, name, color = self.get_suit_display(card.suit)

            # 1. 左上角:点数
            rank_surf = self.safe_render_text(card.rank, self.small_font, BLACK)
            if rank_surf:
                self.screen.blit(rank_surf, (x + 5, y + 5))
            else:
                # 兼容方案:仅显示点数首字符(如"10"→"1")
                rank_text = self.small_font.render(card.rank[0], True, BLACK)
                self.screen.blit(rank_text, (x + 5, y + 5))

            # 2. 左上角:花色符号(优先文本符号,失败则绘制图形)
            suit_surf = self.safe_render_text(symbol, self.small_font, color, name[:1])
            if suit_surf:
                self.screen.blit(suit_surf, (x + 5, y + 25))
            else:
                self.draw_suit_symbol(x + 5, y + 25, card.suit, 16)  # 绘制图形花色

            # 3. 牌面中央:大号花色符号
            large_suit_surf = self.safe_render_text(symbol, self.large_font, color, name)
            if large_suit_surf:
                large_suit_rect = large_suit_surf.get_rect(center=(x + card_width//2, y + card_height//2))
                self.screen.blit(large_suit_surf, large_suit_rect)
            else:
                self.draw_suit_symbol(x + card_width//2 -10, y + card_height//2 -10, card.suit, 20)

            # 4. 右下角:点数与花色(镜像左上角,符合真实牌面设计)
            # (代码省略,逻辑与左上角一致,仅位置不同)

代码解析

  1. 兼容设计:解决字体渲染痛点
    花色符号(♥、♦、♣、♠)在部分中文系统中可能因字体缺失无法显示,代码通过“双重兼容”方案解决:

    • 优先调用safe_render_text()尝试渲染文本符号,失败则使用中文名称首字符(如“红桃”→“红”);
    • 若仍失败,调用draw_suit_symbol()用图形绘制花色(如用心形、菱形等基础图形组合),确保任何环境下都能正常显示。
  2. 视觉还原:贴近真实扑克牌布局
    牌面设计遵循真实扑克牌的布局习惯:

    • 左上角/右下角显示“点数+花色”,且右下角为左上角的镜像(符合视觉逻辑);
    • 中央显示大号花色符号,增强视觉辨识度;
    • 固定牌宽(80px)和高(120px),确保手牌排列整齐。
  3. 模块化调用
    牌背绘制委托给draw_card_back()方法,花色图形绘制委托给draw_suit_symbol()方法,避免draw_card()方法过于臃肿,符合“模块化设计”原则。

四、dealer_play方法:庄家AI的自动化逻辑

核心作用:实现庄家的自动出牌规则(17点以上停牌),并完成胜负判定,是游戏流程自动化的关键。

def dealer_play(self):
    """庄家自动出牌逻辑,遵循二十一点标准规则"""
    # 庄家核心规则:总点数<17时必须持续要牌
    while self.dealer_hand.get_value() < 17:
        self.dealer_hand.add_card(self.deck.deal_card())

    # 胜负判定:按优先级依次判断
    if self.dealer_hand.is_bust():
        self.game_result = "庄家爆牌! 你赢了!"
        self.player_chips += self.current_bet  # 玩家赢,筹码增加
    elif self.dealer_hand.get_value() > self.player_hand.get_value():
        self.game_result = "庄家点数更高! 你输了!"
        self.player_chips -= self.current_bet  # 玩家输,筹码减少
    elif self.dealer_hand.get_value() < self.player_hand.get_value():
        self.game_result = "你的点数更高! 你赢了!"
        self.player_chips += self.current_bet
    else:
        self.game_result = "平局!"  # 点数相同,无筹码变化

    self.game_state = "game_over"  # 庄家回合结束,进入结算状态

代码解析

  1. 规则的精准落地
    严格遵循赌场二十一点的庄家规则:“总点数小于17时必须要牌,大于等于17时必须停牌”。通过while循环实现自动要牌,无需人工干预,确保游戏流程的自动化。

  2. 胜负判定的优先级逻辑
    判定顺序按“对玩家最有利”的优先级排列,避免逻辑漏洞:

    1. 先判断庄家是否爆牌(若爆牌,玩家直接赢,无需比较点数);
    2. 再比较双方点数(庄家点数高→玩家输,反之则赢);
    3. 最后判定平局(点数相同)。
  3. 数据实时更新
    胜负结果直接关联筹码变化(赢则加筹码,输则减筹码),并更新game_state为“game_over”,触发后续的结算界面显示,确保流程衔接顺畅。

运行结果

在这里插入图片描述

总结:二十一点游戏的设计精髓

从上述关键代码可以看出,这款游戏的开发遵循了“数据封装→规则实现→可视化呈现→流程自动化”的逻辑链:

  1. 数据层:用Card类封装单张牌数据,Deck类管理牌堆,确保数据的独立性;
  2. 规则层:用Hand类实现点数计算与规则判断,dealer_play实现庄家AI,确保规则的精准落地;
  3. 交互层:用draw_card实现可视化兼容,handle_events处理用户操作,确保体验流畅;
  4. 流程层:用game_state控制“下注→发牌→玩家回合→庄家回合→结算”的全流程,确保逻辑清晰。

这种模块化、分层的设计思路,不仅让代码易于维护,也为后续扩展(如多玩家模式、多副牌规则)预留了充足的空间,是卡牌类游戏开发的经典参考范式。

完整代码

import pygame
import random
import sys

# 初始化pygame
pygame.init()

# 游戏常量
SCREEN_WIDTH = 1000
SCREEN_HEIGHT = 700
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (220, 20, 60)
GREEN = (34, 139, 34)
BLUE = (30, 144, 255)
YELLOW = (255, 215, 0)
GRAY = (128, 128, 128)
LIGHT_GRAY = (211, 211, 211)
DARK_GREEN = (0, 100, 0)
GOLD = (255, 215, 0)

class Card:
    """扑克牌类"""
    def __init__(self, suit, rank):
        self.suit = suit  # 花色: hearts, diamonds, clubs, spades
        self.rank = rank  # 点数: A, 2-10, J, Q, K
        self.value = self.get_value()  # 点数值
        self.image = None  # 可以添加图片资源

    def get_value(self):
        """获取牌的点数值"""
        if self.rank in ['J', 'Q', 'K']:
            return 10
        elif self.rank == 'A':
            return 11  # A默认为11,后续可以调整为1
        else:
            return int(self.rank)

    def __str__(self):
        return f"{self.rank} of {self.suit}"

class Deck:
    """牌堆类"""
    def __init__(self):
        self.cards = []
        self.create_deck()
        self.shuffle()

    def create_deck(self):
        """创建一副牌"""
        suits = ['hearts', 'diamonds', 'clubs', 'spades']
        ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
        self.cards = [Card(suit, rank) for suit in suits for rank in ranks]

    def shuffle(self):
        """洗牌"""
        random.shuffle(self.cards)

    def deal_card(self):
        """发牌"""
        if len(self.cards) > 0:
            return self.cards.pop()
        return None

class Hand:
    """手牌类"""
    def __init__(self):
        self.cards = []
        self.value = 0
        self.aces = 0  # A的个数

    def add_card(self, card):
        """添加牌到手牌"""
        self.cards.append(card)
        self.value += card.value
        if card.rank == 'A':
            self.aces += 1
        self.adjust_for_ace()

    def adjust_for_ace(self):
        """调整A的值(11或1)"""
        while self.value > 21 and self.aces:
            self.value -= 10
            self.aces -= 1

    def get_value(self):
        """获取手牌总点数"""
        return self.value

    def is_bust(self):
        """判断是否爆牌"""
        return self.value > 21

    def is_blackjack(self):
        """判断是否为黑杰克(21点)"""
        return self.value == 21 and len(self.cards) == 2

class BlackjackGame:
    """二十一点游戏主类"""
    def __init__(self):
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("扑克牌游戏 - 二十一点")

        # 字体 - 确保有默认字体,然后尝试加载支持中文和符号的字体
        self.large_font = pygame.font.Font(None, 36)
        self.medium_font = pygame.font.Font(None, 28)
        self.small_font = pygame.font.Font(None, 20)
        self.tiny_font = pygame.font.Font(None, 16)

        # 尝试加载支持中文和Unicode符号的字体
        font_names = [
            'simhei', 'simsun', 'msyh', 'msyhui', 'mingliu', 'pmingliu',
            'Arial Unicode MS', 'arialuni', 'arialunicode',
            'dejavusans', 'dejavu sans',
            'freesans', 'free sans',
            'noto sans', 'noto',
            'segoe ui emoji', 'seguiemj'
        ]

        # 先尝试系统中文字体
        for font_name in font_names:
            try:
                self.large_font = pygame.font.SysFont(font_name, 36)
                self.medium_font = pygame.font.SysFont(font_name, 28)
                self.small_font = pygame.font.SysFont(font_name, 20)
                self.tiny_font = pygame.font.SysFont(font_name, 16)
                break
            except:
                continue

        # 游戏状态
        self.deck = Deck()
        self.player_hand = Hand()
        self.dealer_hand = Hand()
        self.game_state = "betting"  # betting, playing, player_turn, dealer_turn, game_over
        self.game_result = ""
        self.player_chips = 1000
        self.current_bet = 100

        # 按钮
        self.hit_button = pygame.Rect(SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT - 100, 140, 50)
        self.stand_button = pygame.Rect(SCREEN_WIDTH // 2 + 10, SCREEN_HEIGHT - 100, 140, 50)
        self.deal_button = pygame.Rect(SCREEN_WIDTH // 2 - 70, SCREEN_HEIGHT - 100, 140, 50)
        self.bet_buttons = [
            pygame.Rect(50, SCREEN_HEIGHT - 150, 100, 45),   # 10
            pygame.Rect(160, SCREEN_HEIGHT - 150, 100, 45),  # 50
            pygame.Rect(270, SCREEN_HEIGHT - 150, 100, 45),  # 100
            pygame.Rect(380, SCREEN_HEIGHT - 150, 100, 45),  # 500
        ]

        # 开始新游戏
        self.deal_initial_cards()

    def deal_initial_cards(self):
        """发初始牌"""
        # 清空手牌
        self.player_hand = Hand()
        self.dealer_hand = Hand()

        # 发两张牌给玩家和庄家
        for _ in range(2):
            self.player_hand.add_card(self.deck.deal_card())
            self.dealer_hand.add_card(self.deck.deal_card())

        # 检查是否黑杰克
        if self.player_hand.is_blackjack() and self.dealer_hand.is_blackjack():
            self.game_result = "平局! 双方都是黑杰克!"
            self.game_state = "game_over"
        elif self.player_hand.is_blackjack():
            self.game_result = "黑杰克! 你赢了!"
            self.player_chips += int(self.current_bet * 1.5)
            self.game_state = "game_over"
        elif self.dealer_hand.is_blackjack():
            self.game_result = "庄家黑杰克! 你输了!"
            self.player_chips -= self.current_bet
            self.game_state = "game_over"
        else:
            self.game_state = "player_turn"

    def player_hit(self):
        """玩家要牌"""
        if self.game_state == "player_turn":
            self.player_hand.add_card(self.deck.deal_card())
            if self.player_hand.is_bust():
                self.game_result = "爆牌! 你输了!"
                self.player_chips -= self.current_bet
                self.game_state = "game_over"

    def player_stand(self):
        """玩家停牌"""
        if self.game_state == "player_turn":
            self.game_state = "dealer_turn"
            self.dealer_play()

    def dealer_play(self):
        """庄家游戏"""
        # 庄家必须在17点以下要牌
        while self.dealer_hand.get_value() < 17:
            self.dealer_hand.add_card(self.deck.deal_card())

        # 判断结果
        if self.dealer_hand.is_bust():
            self.game_result = "庄家爆牌! 你赢了!"
            self.player_chips += self.current_bet
        elif self.dealer_hand.get_value() > self.player_hand.get_value():
            self.game_result = "庄家点数更高! 你输了!"
            self.player_chips -= self.current_bet
        elif self.dealer_hand.get_value() < self.player_hand.get_value():
            self.game_result = "你的点数更高! 你赢了!"
            self.player_chips += self.current_bet
        else:
            self.game_result = "平局!"

        self.game_state = "game_over"

    def new_game(self):
        """开始新游戏"""
        # 清除游戏结果文本
        self.game_result = ""

        if self.player_chips <= 0:
            self.player_chips = 1000  # 重新开始

        # 检查是否需要重新洗牌
        if len(self.deck.cards) < 15:
            self.deck = Deck()

        self.deal_initial_cards()

    def place_bet(self, amount):
        """下注"""
        if self.game_state == "betting" and amount <= self.player_chips:
            self.current_bet = amount
            self.game_state = "playing"
            self.deal_initial_cards()

    def draw_card_back(self, x, y, width, height):
        """绘制优化的扑克牌背面花纹"""
        card_rect = pygame.Rect(x, y, width, height)

        # 绘制牌背底色
        pygame.draw.rect(self.screen, BLUE, card_rect)
        pygame.draw.rect(self.screen, WHITE, card_rect, 2)

        # 绘制装饰边框
        inner_rect = pygame.Rect(x + 5, y + 5, width - 10, height - 10)
        pygame.draw.rect(self.screen, (100, 180, 255), inner_rect, 2)

        # 绘制中心图案
        center_x = x + width // 2
        center_y = y + height // 2

        # 绘制多个菱形图案
        for i in range(4):
            angle = i * 90
            points = []
            for j in range(4):
                rad = pygame.math.Vector2(1, 0).rotate(angle + j * 90)
                px = center_x + rad.x * 15
                py = center_y + rad.y * 15
                points.append((px, py))
            pygame.draw.polygon(self.screen, WHITE, points)

        # 绘制中心圆点
        pygame.draw.circle(self.screen, WHITE, (center_x, center_y), 8)
        pygame.draw.circle(self.screen, BLUE, (center_x, center_y), 5)

    def get_suit_display(self, suit):
        """
        获取花色显示信息
        返回: (symbol, name, color) 元组
        """
        if suit == 'hearts':
            return "♥", "红桃", RED
        elif suit == 'diamonds':
            return "♦", "方块", RED
        elif suit == 'clubs':
            return "♣", "梅花", BLACK
        else:  # spades
            return "♠", "黑桃", BLACK

    def draw_suit_symbol(self, x, y, suit, size=20):
        """
        绘制花色符号(当文本符号无法显示时的替代方案)
        """
        center_x = x + size // 2
        center_y = y + size // 2
        half_size = size // 2

        if suit == 'hearts':
            # 绘制心形
            # 绘制两个圆形组成心形上半部分
            pygame.draw.circle(self.screen, RED, (center_x - half_size//3, center_y - half_size//6), half_size//2)
            pygame.draw.circle(self.screen, RED, (center_x + half_size//3, center_y - half_size//6), half_size//2)

            # 绘制心形下半部分三角形
            points = [
                (center_x - half_size//1.5, center_y - half_size//6),
                (center_x + half_size//1.5, center_y - half_size//6),
                (center_x, center_y + half_size//1.5)
            ]
            pygame.draw.polygon(self.screen, RED, points)

        elif suit == 'diamonds':
            # 绘制菱形
            points = [
                (center_x, center_y - half_size//1.5),
                (center_x + half_size//1.5, center_y),
                (center_x, center_y + half_size//1.5),
                (center_x - half_size//1.5, center_y)
            ]
            pygame.draw.polygon(self.screen, RED, points)

        elif suit == 'clubs':
            # 绘制梅花
            # 绘制三个圆形
            pygame.draw.circle(self.screen, BLACK, (center_x, center_y - half_size//3), half_size//3)
            pygame.draw.circle(self.screen, BLACK, (center_x - half_size//3, center_y + half_size//6), half_size//3)
            pygame.draw.circle(self.screen, BLACK, (center_x + half_size//3, center_y + half_size//6), half_size//3)

            # 绘制下方菱形
            diamond_points = [
                (center_x, center_y + half_size//6),
                (center_x + half_size//4, center_y + half_size//3),
                (center_x, center_y + half_size//2),
                (center_x - half_size//4, center_y + half_size//3)
            ]
            pygame.draw.polygon(self.screen, BLACK, diamond_points)

        else:  # spades
            # 绘制黑桃
            # 绘制两个圆形
            pygame.draw.circle(self.screen, BLACK, (center_x - half_size//4, center_y - half_size//6), half_size//3)
            pygame.draw.circle(self.screen, BLACK, (center_x + half_size//4, center_y - half_size//6), half_size//3)

            # 绘制下方心形翻转
            heart_points = [
                (center_x - half_size//2, center_y - half_size//6),
                (center_x + half_size//2, center_y - half_size//6),
                (center_x, center_y + half_size//2)
            ]
            pygame.draw.polygon(self.screen, BLACK, heart_points)

    def safe_render_text(self, text, font, color, fallback_text=""):
        """
        安全渲染文本,如果失败则尝试备用方案
        """
        # 首先尝试渲染原始文本
        try:
            return font.render(text, True, color)
        except:
            pass

        # 如果原始文本渲染失败,尝试备用文本
        if fallback_text:
            try:
                return font.render(fallback_text, True, color)
            except:
                pass

        # 如果都失败了,返回None
        return None

    def draw_card(self, x, y, card, show_back=False):
        """绘制一张牌"""
        card_width = 80
        card_height = 120
        card_rect = pygame.Rect(x, y, card_width, card_height)

        if show_back:
            # 绘制优化的牌背
            self.draw_card_back(x, y, card_width, card_height)
        else:
            # 绘制牌面
            pygame.draw.rect(self.screen, WHITE, card_rect)
            pygame.draw.rect(self.screen, BLACK, card_rect, 2)

            # 绘制点数和花色
            if card:
                # 获取花色显示信息
                symbol, name, color = self.get_suit_display(card.suit)

                # 点数(左上角)
                rank_surf = self.safe_render_text(card.rank, self.small_font, BLACK)
                if rank_surf:
                    self.screen.blit(rank_surf, (x + 5, y + 5))
                else:
                    # 如果文本渲染失败,使用简单绘制
                    rank_text = self.small_font.render(card.rank[0], True, BLACK)  # 只取第一个字符
                    self.screen.blit(rank_text, (x + 5, y + 5))

                # 花色符号(左上角)- 优先使用符号,失败则绘制图形符号
                suit_surf = self.safe_render_text(symbol, self.small_font, color, name[:1])
                if suit_surf:
                    self.screen.blit(suit_surf, (x + 5, y + 25))
                else:
                    # 绘制图形符号作为替代
                    self.draw_suit_symbol(x + 5, y + 25, card.suit, 16)

                # 大号花色符号(中央)- 优先使用符号,失败则绘制图形符号
                large_suit_surf = self.safe_render_text(symbol, self.large_font, color, name)
                if large_suit_surf:
                    large_suit_rect = large_suit_surf.get_rect(center=(x + card_width // 2, y + card_height // 2))
                    self.screen.blit(large_suit_surf, large_suit_rect)
                else:
                    # 绘制图形符号作为替代
                    self.draw_suit_symbol(x + card_width // 2 - 10, y + card_height // 2 - 10, card.suit, 20)

                # 点数(右下角)
                rank_flip_surf = self.safe_render_text(card.rank, self.small_font, BLACK)
                if rank_flip_surf:
                    rank_flip_rect = rank_flip_surf.get_rect()
                    rank_flip_rect.bottomright = (x + card_width - 5, y + card_height - 5)
                    self.screen.blit(rank_flip_surf, rank_flip_rect)
                else:
                    # 如果文本渲染失败,使用简单绘制
                    rank_text = self.small_font.render(card.rank[0], True, BLACK)  # 只取第一个字符
                    rank_flip_rect = rank_text.get_rect()
                    rank_flip_rect.bottomright = (x + card_width - 5, y + card_height - 5)
                    self.screen.blit(rank_text, rank_flip_rect)

                # 花色符号(右下角)- 优先使用符号,失败则绘制图形符号
                suit_flip_surf = self.safe_render_text(symbol, self.small_font, color, name[:1])
                if suit_flip_surf:
                    suit_flip_rect = suit_flip_surf.get_rect()
                    suit_flip_rect.bottomright = (x + card_width - 5, y + card_height - 25)
                    self.screen.blit(suit_flip_surf, suit_flip_rect)
                else:
                    # 绘制图形符号作为替代
                    self.draw_suit_symbol(x + card_width - 20, y + card_height - 25, card.suit, 16)

    def draw_hand(self, x, y, hand, show_all=True):
        """绘制手牌"""
        for i, card in enumerate(hand.cards):
            show_back = not show_all and i == 1  # 庄家的第二张牌背面朝上
            self.draw_card(x + i * 90, y, card, show_back)

    def draw_button(self, rect, text, font, bg_color, text_color, hover_color=None):
        """绘制按钮"""
        mouse_pos = pygame.mouse.get_pos()
        if rect.collidepoint(mouse_pos) and hover_color:
            bg_color = hover_color

        pygame.draw.rect(self.screen, bg_color, rect, border_radius=10)
        pygame.draw.rect(self.screen, BLACK, rect, 2, border_radius=10)

        # 安全渲染按钮文本
        text_surf = self.safe_render_text(text, font, text_color)
        if text_surf:
            text_rect = text_surf.get_rect(center=rect.center)
            self.screen.blit(text_surf, text_rect)

    def draw(self):
        """绘制游戏界面"""
        self.screen.fill(GREEN)

        # 绘制桌面
        pygame.draw.rect(self.screen, DARK_GREEN,
                        (50, 50, SCREEN_WIDTH - 100, SCREEN_HEIGHT - 150),
                        border_radius=15)
        pygame.draw.rect(self.screen, GOLD,
                        (50, 50, SCREEN_WIDTH - 100, SCREEN_HEIGHT - 150),
                        3, border_radius=15)

        # 绘制标题
        title_surf = self.safe_render_text("二十一点 Blackjack", self.large_font, WHITE)
        if title_surf:
            title_rect = title_surf.get_rect(center=(SCREEN_WIDTH // 2, 30))
            self.screen.blit(title_surf, title_rect)

        # 绘制庄家区域
        dealer_area = pygame.Rect(80, 80, SCREEN_WIDTH - 160, 200)
        pygame.draw.rect(self.screen, (0, 80, 0), dealer_area, 2, border_radius=10)

        # 绘制庄家标签
        dealer_text_surf = self.safe_render_text("庄家 Dealer", self.medium_font, WHITE)
        if dealer_text_surf:
            self.screen.blit(dealer_text_surf, (100, 90))

        # 绘制庄家手牌
        self.draw_hand(100, 140, self.dealer_hand, self.game_state == "game_over")

        # 绘制玩家区域
        player_area = pygame.Rect(80, 300, SCREEN_WIDTH - 160, 200)
        pygame.draw.rect(self.screen, (0, 80, 0), player_area, 2, border_radius=10)

        # 绘制玩家标签
        player_text_surf = self.safe_render_text("玩家 Player", self.medium_font, WHITE)
        if player_text_surf:
            self.screen.blit(player_text_surf, (100, 310))

        # 绘制玩家手牌
        self.draw_hand(100, 360, self.player_hand, True)

        # 绘制点数
        dealer_value = self.dealer_hand.get_value() if self.game_state == "game_over" else "?"
        if self.game_state != "game_over" and len(self.dealer_hand.cards) > 0:
            dealer_value = self.dealer_hand.cards[0].value  # 只显示第一张牌的点数

        dealer_score_surf = self.safe_render_text(f"点数: {dealer_value}", self.medium_font, WHITE)
        if dealer_score_surf:
            self.screen.blit(dealer_score_surf, (SCREEN_WIDTH - 250, 90))

        player_score_surf = self.safe_render_text(f"点数: {self.player_hand.get_value()}", self.medium_font, WHITE)
        if player_score_surf:
            self.screen.blit(player_score_surf, (SCREEN_WIDTH - 250, 310))

        # 绘制筹码信息
        chips_text_surf = self.safe_render_text(f"筹码: ${self.player_chips}", self.medium_font, YELLOW)
        if chips_text_surf:
            self.screen.blit(chips_text_surf, (50, SCREEN_HEIGHT - 200))

        bet_text_surf = self.safe_render_text(f"当前下注: ${self.current_bet}", self.medium_font, YELLOW)
        if bet_text_surf:
            self.screen.blit(bet_text_surf, (300, SCREEN_HEIGHT - 200))

        # 绘制游戏结果(只在游戏结束时显示)
        if self.game_state == "game_over" and self.game_result:
            result_color = WHITE
            if "赢" in self.game_result:
                result_color = YELLOW
            elif "输" in self.game_result:
                result_color = RED

            result_surf = self.safe_render_text(self.game_result, self.large_font, result_color)
            if result_surf:
                result_rect = result_surf.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
                # 绘制结果背景
                bg_rect = pygame.Rect(result_rect.x - 20, result_rect.y - 10,
                                    result_rect.width + 40, result_rect.height + 20)
                pygame.draw.rect(self.screen, (0, 0, 0, 180), bg_rect, border_radius=10)
                pygame.draw.rect(self.screen, GOLD, bg_rect, 2, border_radius=10)
                self.screen.blit(result_surf, result_rect)

        # 绘制按钮
        if self.game_state == "player_turn":
            self.draw_button(self.hit_button, "要牌 Hit", self.small_font,
                           BLUE, WHITE, (70, 130, 180))
            self.draw_button(self.stand_button, "停牌 Stand", self.small_font,
                           RED, WHITE, (200, 0, 0))
        elif self.game_state == "game_over":
            self.draw_button(self.deal_button, "新游戏 New Game", self.small_font,
                           GREEN, WHITE, (0, 200, 0))

        # 绘制下注按钮
        if self.game_state == "betting":
            bet_label_surf = self.safe_render_text("请选择下注金额:", self.medium_font, WHITE)
            if bet_label_surf:
                self.screen.blit(bet_label_surf, (50, SCREEN_HEIGHT - 250))

            bet_amounts = [10, 50, 100, 500]
            bet_labels = ["$10", "$50", "$100", "$500"]
            for i, (button, label) in enumerate(zip(self.bet_buttons, bet_labels)):
                if bet_amounts[i] <= self.player_chips:
                    self.draw_button(button, label, self.small_font,
                                   LIGHT_GRAY, BLACK, GRAY)

        # 绘制游戏规则
        rules_title_surf = self.safe_render_text("游戏规则:", self.medium_font, YELLOW)
        if rules_title_surf:
            self.screen.blit(rules_title_surf, (SCREEN_WIDTH - 350, SCREEN_HEIGHT - 250))

        rules = [
            "• 目标是让手牌点数接近21点但不超过",
            "• A可算作1点或11点,J/Q/K都算作10点",
            "• 玩家先行动,庄家后行动",
            "• 庄家必须17点以上才能停牌"
        ]

        for i, rule in enumerate(rules):
            rule_surf = self.safe_render_text(rule, self.tiny_font, WHITE)
            if rule_surf:
                self.screen.blit(rule_surf, (SCREEN_WIDTH - 340, SCREEN_HEIGHT - 200 + i * 20))

        pygame.display.flip()

    def handle_events(self):
        """处理事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False

            if event.type == pygame.MOUSEBUTTONDOWN:
                if self.game_state == "player_turn":
                    if self.hit_button.collidepoint(event.pos):
                        self.player_hit()
                    elif self.stand_button.collidepoint(event.pos):
                        self.player_stand()
                elif self.game_state == "game_over":
                    if self.deal_button.collidepoint(event.pos):
                        self.new_game()
                elif self.game_state == "betting":
                    bet_amounts = [10, 50, 100, 500]
                    for button, amount in zip(self.bet_buttons, bet_amounts):
                        if button.collidepoint(event.pos) and amount <= self.player_chips:
                            self.place_bet(amount)

        return True

    def run(self):
        """运行游戏主循环"""
        clock = pygame.time.Clock()
        running = True

        while running:
            running = self.handle_events()
            self.draw()
            clock.tick(60)

        pygame.quit()
        sys.exit()

# 运行游戏
if __name__ == "__main__":
    game = BlackjackGame()
    game.run()

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐