案例:卡牌属性对战游戏
学生管理系统把数据、数据操作和交互流程拆成了不同对象。这一节换成卡牌游戏,继续练习多个对象怎样分工协作。
我们先实现一个能完整运行的基础版:随机发牌、决定先手、选择卡牌、比较属性、记录分数,最后判断胜负。如何把这条对战流程拆给不同对象,并让对象之间保持清晰的职责边界?

需求:完成一款两人卡牌属性对战游戏,支持发牌、决定先手、四轮比较、计分和胜负判断。
功能分析:先固定卡牌数据与对战规则,再设计 Card、Dice、Player 和 Game 的职责,最后运行基础版并验证关键状态变化。
游戏需求与规则
游戏中有两位玩家和 8 张牌。每张牌包含牌号、人物名称和四项能力值:
| 牌号 | 人物 | 智力 | 力量 | 速度 | 攻击 |
|---|---|---|---|---|---|
| 1 | 白雪公主 | 5 | 4 | 2 | 2 |
| 2 | 睡美人 | 7 | 5 | 1 | 5 |
| 3 | 灰姑娘 | 9 | 3 | 8 | 7 |
| 4 | 美人鱼 | 6 | 7 | 10 | 1 |
| 5 | 花木兰 | 8 | 10 | 9 | 6 |
| 6 | 阿拉丁 | 4 | 6 | 3 | 9 |
| 7 | 爱莎 | 3 | 8 | 4 | 4 |
| 8 | 勇敢骑士 | 1 | 9 | 5 | 3 |
基础版使用下面的规则:
- 游戏开始时,系统打乱 8 张牌,两位玩家各拿 4 张。
- 第一轮开始前,两位玩家掷骰子,点数大的一方先出牌;点数相同就重新投掷。
- 先手玩家选择一张牌,再选择智力、力量、速度或攻击进行比较。
- 后手玩家选择一张牌,使用先手玩家指定的属性进行比较。
- 属性值更大的一方获得 1 分,并成为下一轮的先手玩家。
- 如果属性值相同,双方都不得分,当前先手玩家下一轮继续先出牌。
- 每张牌只能使用一次。四轮结束后,系统显示总分和获胜者。
先完成基础版
“复活牌”和“上帝之手”需要额外记录弃牌、使用次数和强制出牌状态。先把基础对战流程跑通,再把它们作为进阶功能扩展,避免一次加入太多状态。
对象职责设计
先找出游戏中的主要对象,再决定每个对象保存什么数据、完成什么操作:
| 对象 | 保存的数据 | 负责的操作 |
|---|---|---|
Card | 牌号、人物、四项能力值 | 返回指定能力值、显示卡牌信息 |
Dice | 不需要保存状态 | 随机产生 1 到 6 的点数 |
Player | 玩家名称、手牌、分数 | 显示牌号、选择卡牌、选择属性 |
Game | 两位玩家、当前轮数 | 发牌、决定先手、组织对战、判断胜负 |
这四个类不是各自独立运行的。Game 负责推进流程,在需要时调用 Dice、Player 和 Card 提供的功能。
下面这张图重点观察:Game 负责调度流程,但具体的数据和操作仍然由其他对象负责。
基础版游戏实现
如何把职责表落实为可运行代码?按照 Dice、Card、Player、Game 的顺序编写,所有代码都写在同一个 card_game.py 文件中。
卡牌数据与骰子
先导入 random,再准备 8 张牌的初始数据。能力值要保存为整数,后面才能正确比较大小。
import random
CARD_DATA = [
['1', '白雪公主', 5, 4, 2, 2],
['2', '睡美人', 7, 5, 1, 5],
['3', '灰姑娘', 9, 3, 8, 7],
['4', '美人鱼', 6, 7, 10, 1],
['5', '花木兰', 8, 10, 9, 6],
['6', '阿拉丁', 4, 6, 3, 9],
['7', '爱莎', 3, 8, 4, 4],
['8', '勇敢骑士', 1, 9, 5, 3],
]
class Dice:
@staticmethod
def roll():
return random.randint(1, 6)掷骰子不需要读取实例属性或类属性,所以把 roll() 定义成静态方法。调用 Dice.roll(),就能得到一个 1 到 6 的随机整数。
卡牌对象
一张牌需要保存人物信息和四项能力值。这里用字典保存能力值,属性名称就是字典的键:
class Card:
attribute_names = ('智力', '力量', '速度', '攻击')
def __init__(self, no, name, iq, power, speed, attack):
self.no = no
self.name = name
self.attributes = {
'智力': iq,
'力量': power,
'速度': speed,
'攻击': attack,
}
def get_value(self, attribute):
return self.attributes[attribute]
def __str__(self):
details = ','.join(
f'{name} {value}' for name, value in self.attributes.items()
)
return f'{self.no} 号牌:{self.name}({details})'这一层主要提供两个功能:
get_value()根据属性名称返回能力值。__str__()把卡牌转换成便于阅读的文字,出牌后可以直接打印。
能力值的查找放在 Card 内部,其他对象不需要知道这些数据具体怎样保存。
玩家对象
玩家需要保存自己的手牌和分数,还要完成选牌、选属性两个操作:
class Player:
def __init__(self, name, cards):
self.name = name
self.cards = cards
self.score = 0
def show_card_numbers(self):
numbers = '、'.join(card.no for card in self.cards)
print(f'{self.name}当前拥有的牌:{numbers}')
def select_card(self):
while True:
self.show_card_numbers()
card_no = input(f'{self.name}请选择牌号:')
for card in self.cards:
if card.no == card_no:
self.cards.remove(card)
return card
print('没有这张牌,请重新选择。')
def select_attribute(self):
while True:
options = '、'.join(Card.attribute_names)
attribute = input(f'{self.name}请选择对比属性({options}):')
if attribute in Card.attribute_names:
return attribute
print('没有这个属性,请重新选择。')select_card() 找到对应卡牌后,会先从手牌中删除它,再把卡牌对象返回给 Game。这样同一张牌就不会被重复使用。
两个选择方法都使用 while True。如果输入的牌号或属性不存在,程序会给出提示并重新输入,不会把错误数据交给后面的对战流程。
游戏流程对象
最后编写总控对象 Game,把发牌、掷骰子、每轮对战和结果输出串起来:
class Game:
def __init__(self):
self.round = 0
cards = [Card(*data) for data in CARD_DATA]
random.shuffle(cards)
self.player1 = Player('玩家一', cards[:4])
self.player2 = Player('玩家二', cards[4:])
def decide_first_player(self):
while True:
player1_number = Dice.roll()
player2_number = Dice.roll()
print(f'玩家一掷出 {player1_number},玩家二掷出 {player2_number}')
if player1_number > player2_number:
print('玩家一获得第一轮先手。')
return self.player1
if player2_number > player1_number:
print('玩家二获得第一轮先手。')
return self.player2
print('点数相同,重新掷骰子。')
def get_other_player(self, player):
if player is self.player1:
return self.player2
return self.player1
def play_round(self, first_player, second_player):
self.round += 1
print(f'\n第 {self.round} 轮,{first_player.name}先手')
first_card = first_player.select_card()
attribute = first_player.select_attribute()
second_card = second_player.select_card()
first_value = first_card.get_value(attribute)
second_value = second_card.get_value(attribute)
print(f'{first_player.name}打出:{first_card}')
print(f'{second_player.name}打出:{second_card}')
print(f'对比{attribute}:{first_value} 对 {second_value}')
if first_value > second_value:
first_player.score += 1
print(f'{first_player.name}获得 1 分。')
return first_player
if second_value > first_value:
second_player.score += 1
print(f'{second_player.name}获得 1 分。')
return second_player
print('本轮平局,双方都不得分,先手玩家继续先出牌。')
return first_player
def show_result(self):
print('\n游戏结束')
print(f'玩家一:{self.player1.score} 分')
print(f'玩家二:{self.player2.score} 分')
if self.player1.score > self.player2.score:
print('玩家一获胜!')
elif self.player2.score > self.player1.score:
print('玩家二获胜!')
else:
print('双方平局!')
def run(self):
first_player = self.decide_first_player()
while self.player1.cards and self.player2.cards:
second_player = self.get_other_player(first_player)
first_player = self.play_round(first_player, second_player)
self.show_result()
game = Game()
game.run()这里没有让 Player 继承 Dice。玩家和骰子是两个不同对象,游戏只需要在决定先手时调用 Dice.roll()。继承应该用来表达什么关系,会在下一部分正式学习。
下面这张图重点观察:比较结果不仅决定本轮是否加分,还会决定下一轮由谁先出牌。
运行验证
程序可以运行后,还要确认状态变化和输入校验符合规则。按照提示完成四轮对战,重点观察:
- 玩家选过的牌会从手牌中消失。
- 系统会显示双方打出的完整卡牌信息。
- 系统只比较先手玩家指定的属性。
- 获胜玩家增加
1分,并在下一轮先出牌。 - 四轮结束后,系统输出双方总分和最终结果。
发牌和掷骰子都有随机性,因此每次运行时的牌号、先手顺序和最终分数可能不同。
如果输入不存在的牌号或属性,程序会提示重新选择。先用错误输入测试一次,再输入正确内容,确认程序能够继续运行。
小结
这个案例最核心的不是游戏规则,而是四个对象怎样配合:
Card管理一张牌的数据。Dice提供随机点数。Player管理玩家的手牌、选择和分数。Game负责调度其他对象,推进完整游戏流程。
拆分对象时,先确定每个对象的职责,再让总控对象把流程串起来。
练习
- 每轮结束后,同时输出两位玩家的当前分数。
- 给
Game增加used_cards属性,保存每轮已经打出的两张牌。 - 修改平局规则,让下一轮改由后手玩家先出牌。
完成 used_cards 后,可以继续挑战“复活牌”:每位玩家有一次机会,从已使用的卡牌中随机取回一张。再进一步,可以尝试实现一次“上帝之手”,强制对方打出指定牌号的卡牌。