Python实战案例 : 21点 小游戏
文章目录
案例背景
在掌握函数的基础语法(定义、参数、返回值)、函数复用、作用域等核心概念后,
我们需要通过一个综合案例来理解如何将复杂问题拆解为模块化的函数设计思想。
本文以21点扑克牌游戏为例,通过分析代码中函数的设计逻辑与协作方式,深入理解函数在实际项目中的应用场景。
一、案例源码
# 定义扑克牌花色和数字列表
list1 = ['黑桃', '红桃', '梅花', '方块'] # 四种花色
list2 = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] # 13种牌面
# 初始化牌堆列表
list3 = [i + j for i in list1 for j in list2]
def get_card(list3):
"""从牌堆中随机抽取一张牌并移除,如果牌堆为空则重新生成
参数: list3 - 当前牌堆列表
返回: 抽取的牌字符串"""
import random
if not list3: # 检查牌堆是否为空
print("牌堆已空")
# 重新生成完整牌堆
list3.clear()
list3.extend([i + j for i in list1 for j in list2]) # 生成所有可能的牌组合
print("重新洗牌")
card = random.choice(list3) # 随机选择一张牌
list3.remove(card) # 从牌堆中移除该牌
print('当前牌堆剩余纸牌数目: {}'.format(len(list3))) # 显示剩余牌数
return card # 返回抽取的牌
def get_user_cards(num, list3):
"""获取指定数量的手牌
参数: num - 需要获取的牌数,list3 - 牌堆列表
返回: 包含指定数量牌的列表"""
user_cards = []
for _ in range(num):
user_cards.append(get_card(list3)) # 循环抽取指定数量的牌
return user_cards
def get_score(cards):
"""计算手牌的21点分数
参数: cards - 手牌列表
返回: 计算后的分数"""
score = 0
aces = 0 # 记录A的数量
for card in cards:
# 提取牌面值
rank = card[2:] # 例如"红桃10"取"10","黑桃A"取"A"
if rank in ['J', 'Q', 'K']: # 人头牌计10分
score += 10
elif rank == 'A': # A先计11分
aces += 1
score += 11
else: # 数字牌按面值计分
score += int(rank)
# 处理A的灵活计分(当总分超过21时,将A视为1分)
while score > 21 and aces > 0:
score -= 10 # 将A从11分转为1分,相当于总分减10
aces -= 1
return score
def user_card_gen(user_cards, list3):
"""玩家要牌生成器
参数: user_cards - 当前手牌,list3 - 牌堆列表
生成: 更新后的手牌列表"""
while True:
new_card = get_card(list3) # 抽取新牌
user_cards.append(new_card)
print(f'抽到新牌: {new_card}')
yield user_cards # 使用生成器保持状态,每次yield返回当前手牌
def user_play(user_cards, list3):
"""玩家回合处理
参数: user_cards - 初始手牌,list3 - 牌堆列表
返回: 最终手牌列表"""
print('玩家回合'.center(30, '-'))
user_gen = user_card_gen(user_cards, list3) # 初始化生成器
while True:
current_score = get_score(user_cards)
print('当前手牌:', user_cards, '当前分数:', current_score)
if current_score > 21: # 先检查是否已爆牌
print('爆牌!')
return user_cards
if current_score == 21: # 检查是否已达到21点
print('恭喜你,已是最优点数:21点!')
print('已自动为你停止要牌')
return user_cards
choice = input('是否要牌?(y/n): ').strip().lower()
if choice == 'y':
print('~~玩家选择继续要牌~~')
user_cards = next(user_gen) # 通过生成器获取新牌
if get_score(user_cards) > 21: # 抽牌后检查分数
return user_cards
elif choice == 'n':
print('~~玩家停止要牌~~')
return user_cards
else:
print('输入无效,请重新输入')
def print_pc_cards(computer_cards):
"""打印庄家包含隐藏牌面的手牌
参数: computer_cards - 庄家手牌列表
返回: 隐藏第二张牌的手牌列表"""
# 复制当前手牌用于显示
pc_cards = computer_cards.copy()
pc_cards[1] = '暗牌' # 隐藏第二张牌
# 返回庄家手牌(隐藏第二张牌)
return pc_cards
def computer_play(computer_cards, list3):
"""庄家回合处理(根据规则自动要牌)
参数: computer_cards - 初始手牌,list3 - 牌堆列表
返回: 最终手牌列表"""
print('庄家回合'.center(30, '-'))
print('庄家手牌:', print_pc_cards(computer_cards))
while get_score(computer_cards) < 17: # 分数小于17必须要牌
new_card = get_card(list3)
computer_cards.append(new_card)
print(f'庄家要牌: {new_card}')
# 这里使用了一个lambda函数来打印庄家的手牌
pc_cards = (lambda x: print_pc_cards(x))(computer_cards)
print('庄家当前手牌:', pc_cards)
return computer_cards
# 游戏记录装饰器
def game_logger(func):
"""记录游戏结果的装饰器,将结果写入日志文件"""
import time
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
# 构建日志内容
log_text = (
f'时间: {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}\n'
f"游戏结果: {result.get('winner', '无')}\n"
f"玩家分数: {result.get('player_score', 0)}\n"
f"庄家分数: {result.get('computer_score', 0)}\n"
f"玩家手牌: {', '.join(result.get('player_cards', []))}\n"
f"庄家手牌: {', '.join(result.get('computer_cards', []))}\n"
"-----------------------------\n"
)
# 追加写入日志文件
with open("game_logs.txt", "a", encoding="utf-8") as f:
f.write(log_text)
return result
return wrapper
@game_logger
def play_game():
"""主游戏函数,控制游戏流程"""
import time
print("欢迎来到21点游戏!\n")
print("游戏规则".center(30, '='))
print('''
1. A可算作1或11
2. J、Q、K算10点
3. 超过21点直接判负
4. 庄家(电脑)分数小于17必须继续要牌
5. 玩家可以选择要牌或停牌
''')
print("游戏开始".center(30, '='))
# 初始化游戏结果字典
game_result = {
'winner': None,
'player_score': 0,
'computer_score': 0,
'player_cards': [],
'computer_cards': []
}
# 初始化牌堆
print('洗牌中...')
# get_card(list3)
time.sleep(2) # 模拟洗牌过程
print('洗牌完成')
print('当前牌堆剩余纸牌数目: {}'.format(len(list3)))
# 发牌阶段
print('玩家发牌中...')
user = get_user_cards(2, list3) # 玩家初始两张
time.sleep(1)
print('庄家发牌中...')
computer = get_user_cards(2, list3) # 庄家初始两张
computer_score = get_score(computer)
game_result.update({ # 记录庄家初始卡牌信息
'computer_score': computer_score,
'computer_cards': computer.copy()
})
time.sleep(1)
# 显示初始牌局
print('初始手牌'.center(30, '='))
print('玩家手牌:', user, '当前分数:', get_score(user))
print('庄家手牌: ', print_pc_cards(computer)) # 隐藏庄家第二张牌
# 玩家回合
user = user_play(user, list3)
user_score = get_score(user)
game_result.update({ # 记录玩家信息
'player_score': user_score,
'player_cards': user.copy()
})
print('\n玩家最终手牌:', user, '最终分数:', user_score)
if user_score > 21: # 玩家爆牌直接结束
game_result['winner'] = '玩家爆牌,庄家获胜!'
print('玩家爆牌,庄家获胜!')
return game_result
# 庄家回合
computer = computer_play(computer, list3)
computer_score = get_score(computer)
game_result.update({ # 记录庄家信息
'computer_score': computer_score,
'computer_cards': computer.copy()
})
print('\n庄家最终手牌:', computer, '最终分数:', computer_score)
# 胜负判定
if computer_score > 21:
game_result['winner'] = '庄家爆牌,玩家获胜!'
print('庄家爆牌,玩家获胜!')
elif user_score > computer_score:
game_result['winner'] = '玩家获胜!'
print('玩家获胜!')
elif user_score < computer_score:
game_result['winner'] = '庄家获胜!'
print('庄家获胜!')
else:
game_result['winner'] = '平局!'
print('平局!')
return game_result
if __name__ == "__main__":
"""主程序入口"""
while True:
play_game() # 开始游戏
# 询问是否继续
if input('\n是否再玩一局?(Y 继续/其他任意键退出): ').lower() != 'y':
print('游戏结束')
break
二、函数设计
1. 基础工具函数:牌堆操作与手牌生成
-
get_card(list3)
:核心抽牌逻辑
功能:从牌堆中随机抽牌并移除,牌堆为空时自动重新生成(洗牌)。
边界条件处理:通过if not list3
检查牌堆是否为空,体现防御性编程思想。
列表操作:使用random.choice()
随机选牌,list.remove()
移除已抽牌,list.extend()
重置牌堆。
复用性:被get_user_cards
、user_card_gen
等函数调用,实现“抽牌”功能的单一职责封装。 -
get_user_cards(num, list3)
:批量获取手牌
功能:循环调用get_card
获取指定数量的手牌。
设计思想:通过参数num
灵活控制手牌数量,体现函数的通用性。例如:
user = get_user_cards(2, list3)
实现发两张初始手牌。
2. 计分逻辑:get_score(cards)
-
核心算法:处理A的灵活计分
J/Q/K计10分,A初始计11分,数字牌按面值计分。
若总分超过21且存在A,将A从11分转为1分(每次减10分,直到总分≤21或无A)。aces = 0 # 记录A的数量 for card in cards: rank = card[2:] # 提取牌面值(如“红桃A”→“A”) if rank == 'A': aces += 1 score += 11 # ...其他计分逻辑 while score > 21 and aces > 0: # 调整A的计分 score -= 10 aces -= 1
-
函数价值:将计分规则封装为独立函数,便于后续玩家/庄家回合调用,避免代码重复。
3. 玩家交互:生成器与回合控制
-
user_card_gen(user_cards, list3)
:要牌生成器
生成器优势:通过yield
保持状态,每次调用next()
时抽取新牌并返回当前手牌,实现“按需抽牌”的惰性加载。
使用场景:配合user_play
函数的循环逻辑,玩家每选择“要牌”时,通过生成器动态更新手牌,避免频繁创建临时变量。 -
user_play(user_cards, list3)
:玩家回合主逻辑
交互流程:
1. 显示当前手牌与分数,判断是否爆牌(>21)或 blackjack(=21)。
2. 接收用户输入(要牌/停牌),通过生成器获取新牌或结束回合。
4. 庄家(电脑)逻辑:自动化决策
computer_play(computer_cards, list3)
:庄家自动要牌
规则实现:庄家分数<17时必须要牌,≥17时停牌。
隐藏牌设计:通过print_pc_cards
函数隐藏庄家第二张牌(显示“暗牌”),模拟真实游戏体验。
代码细节:使用lambda
函数简化手牌打印逻辑,体现函数式编程的简洁性:pc_cards = (lambda x: print_pc_cards(x))(computer_cards) # 匿名函数调用
5. 日志系统:装饰器的应用
@game_logger
装饰器
功能:记录游戏结果到日志文件,包括时间、胜负、分数、手牌等信息。
闭包与函数包装:通过嵌套函数wrapper
包裹被装饰函数(如play_game
),在不修改原函数的前提下添加日志功能。
文件操作:使用with open
追加写入日志,确保程序崩溃时数据不丢失。
三、主函数流程
-
play_game()
:核心流程调度
1. 初始化:生成牌堆、显示游戏规则、模拟洗牌过程。
2. 发牌阶段:调用get_user_cards
为玩家和庄家各发2张牌。
3. 玩家回合:调用user_play
处理交互逻辑,若玩家爆牌直接结束游戏。
4. 庄家回合:调用computer_play
执行自动化要牌规则。
5. 胜负判定:比较双方分数,处理平局、爆牌等边界情况。
6. 结果返回:通过字典game_result
存储游戏数据,供装饰器记录日志。 -
函数调用关系图
play_game() ├─ get_user_cards() → get_card() ├─ user_play() → user_card_gen() → get_card() │ └─ get_score() ├─ computer_play() → get_card() │ └─ get_score() └─ game_logger装饰器 → 日志写入
四、关键编程思想总结
1. 单一职责原则
每个函数专注于一个独立功能:
get_card
负责抽牌,get_score
负责计分,user_play
负责玩家交互,避免“大而全”的函数设计。
2. 代码复用与模块化
通过函数调用实现逻辑复用,例如get_card
被玩家、庄家、生成器共同使用,减少冗余代码。
3. 状态管理
- 可变对象(如
list3
牌堆)作为参数传递时,函数可直接修改其状态(如抽牌后移除元素) - 生成器通过
yield
保存状态,避免使用全局变量维护玩家手牌。
4. 进阶特性应用
- 生成器:适用于需要逐步生成结果的场景(如玩家按需抽牌)。
- 装饰器:实现功能扩展(日志记录等),提升代码可维护性。
五、案例总结
本案例展示了函数在实际项目中的完整应用:
从基础工具函数到复杂业务逻辑,从单一功能封装到多函数协作,再到进阶特性(生成器、装饰器)的灵活运用。
通过这种模块化设计,代码结构清晰、易于调试,且方便后续扩展。