基于Pygame的扑克牌游戏二十一点游戏开发【完整代码】
数据层:用Card类封装单张牌数据,Deck类管理牌堆,确保数据的独立性;规则层:用Hand类实现点数计算与规则判断,实现庄家AI,确保规则的精准落地;交互层:用draw_card实现可视化兼容,处理用户操作,确保体验流畅;流程层:用game_state控制“下注→发牌→玩家回合→庄家回合→结算”的全流程,确保逻辑清晰。这种模块化、分层的设计思路,不仅让代码易于维护,也为后续扩展(如多玩家模式、多
基于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}" # 便于调试时查看牌面信息
代码解析
-
属性设计的合理性
suit
(花色)与rank
(点数)分离存储:既满足可视化需求(如红桃/方块显示红色,梅花/黑桃显示黑色),也为后续扩展规则(如某些玩法中花色影响得分)预留空间。value
(数值)通过get_value()
动态计算:避免硬编码数值,确保规则修改时只需调整该方法,符合“单一职责原则”。
-
数值规则的精准落地
严格遵循二十一点标准:花牌(J/Q/K)统一计10点,A默认计11点(后续可调整为1点),数字牌按面值计算。这种设计让卡牌数据与游戏规则深度绑定,减少外部逻辑依赖。 -
调试友好性
__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
代码解析
-
A的动态调整逻辑:游戏规则的核心突破
二十一点中A的“双值特性”(11点/1点)是规则难点,adjust_for_ace()
通过以下设计完美解决:- 用
aces
计数器记录未调整的A数量,避免遍历手牌判断; - 循环调整机制:当总点数超21且有未调整的A时,逐个将A从11点改为1点,直到点数≤21或无A可调整(如手牌为“A+A+10”时,会调整两个A为1点,总点数22→12)。
- 用
-
状态更新的自动化
add_card()
方法添加牌后,立即执行adjust_for_ace()
,确保每次手牌变化后,总点数始终符合规则,避免后续计算时出现“已爆牌但未调整”的错误。 -
规则判断的方法化
将“爆牌”(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. 右下角:点数与花色(镜像左上角,符合真实牌面设计)
# (代码省略,逻辑与左上角一致,仅位置不同)
代码解析
-
兼容设计:解决字体渲染痛点
花色符号(♥、♦、♣、♠)在部分中文系统中可能因字体缺失无法显示,代码通过“双重兼容”方案解决:- 优先调用
safe_render_text()
尝试渲染文本符号,失败则使用中文名称首字符(如“红桃”→“红”); - 若仍失败,调用
draw_suit_symbol()
用图形绘制花色(如用心形、菱形等基础图形组合),确保任何环境下都能正常显示。
- 优先调用
-
视觉还原:贴近真实扑克牌布局
牌面设计遵循真实扑克牌的布局习惯:- 左上角/右下角显示“点数+花色”,且右下角为左上角的镜像(符合视觉逻辑);
- 中央显示大号花色符号,增强视觉辨识度;
- 固定牌宽(80px)和高(120px),确保手牌排列整齐。
-
模块化调用
牌背绘制委托给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" # 庄家回合结束,进入结算状态
代码解析
-
规则的精准落地
严格遵循赌场二十一点的庄家规则:“总点数小于17时必须要牌,大于等于17时必须停牌”。通过while
循环实现自动要牌,无需人工干预,确保游戏流程的自动化。 -
胜负判定的优先级逻辑
判定顺序按“对玩家最有利”的优先级排列,避免逻辑漏洞:- 先判断庄家是否爆牌(若爆牌,玩家直接赢,无需比较点数);
- 再比较双方点数(庄家点数高→玩家输,反之则赢);
- 最后判定平局(点数相同)。
-
数据实时更新
胜负结果直接关联筹码变化(赢则加筹码,输则减筹码),并更新game_state
为“game_over”,触发后续的结算界面显示,确保流程衔接顺畅。
运行结果
总结:二十一点游戏的设计精髓
从上述关键代码可以看出,这款游戏的开发遵循了“数据封装→规则实现→可视化呈现→流程自动化”的逻辑链:
- 数据层:用
Card
类封装单张牌数据,Deck
类管理牌堆,确保数据的独立性; - 规则层:用
Hand
类实现点数计算与规则判断,dealer_play
实现庄家AI,确保规则的精准落地; - 交互层:用
draw_card
实现可视化兼容,handle_events
处理用户操作,确保体验流畅; - 流程层:用
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()
更多推荐
所有评论(0)