Skip to content

案例:卡牌属性对战游戏 ​

学生管理系统把数据、数据操作和交互流程拆成了不同对象。这一节换成卡牌游戏,继续练习多个对象怎样分工协作。

我们先实现一个能完整运行的基础版:随机发牌、决定先手、选择卡牌、比较属性、记录分数,最后判断胜负。如何把这条对战流程拆给不同对象,并让对象之间保持清晰的职责边界?

两位玩家正在进行卡牌属性对战

需求:完成一款两人卡牌属性对战游戏,支持发牌、决定先手、四轮比较、计分和胜负判断。

功能分析:先固定卡牌数据与对战规则,再设计 Card、Dice、Player 和 Game 的职责,最后运行基础版并验证关键状态变化。

游戏需求与规则 ​

游戏中有两位玩家和 8 张牌。每张牌包含牌号、人物名称和四项能力值:

牌号人物智力力量速度攻击
1白雪公主5422
2睡美人7515
3灰姑娘9387
4美人鱼67101
5花木兰81096
6阿拉丁4639
7爱莎3844
8勇敢骑士1953

基础版使用下面的规则:

  1. 游戏开始时,系统打乱 8 张牌,两位玩家各拿 4 张。
  2. 第一轮开始前,两位玩家掷骰子,点数大的一方先出牌;点数相同就重新投掷。
  3. 先手玩家选择一张牌,再选择智力、力量、速度或攻击进行比较。
  4. 后手玩家选择一张牌,使用先手玩家指定的属性进行比较。
  5. 属性值更大的一方获得 1 分,并成为下一轮的先手玩家。
  6. 如果属性值相同,双方都不得分,当前先手玩家下一轮继续先出牌。
  7. 每张牌只能使用一次。四轮结束后,系统显示总分和获胜者。

先完成基础版

“复活牌”和“上帝之手”需要额外记录弃牌、使用次数和强制出牌状态。先把基础对战流程跑通,再把它们作为进阶功能扩展,避免一次加入太多状态。

对象职责设计 ​

先找出游戏中的主要对象,再决定每个对象保存什么数据、完成什么操作:

对象保存的数据负责的操作
Card牌号、人物、四项能力值返回指定能力值、显示卡牌信息
Dice不需要保存状态随机产生 1 到 6 的点数
Player玩家名称、手牌、分数显示牌号、选择卡牌、选择属性
Game两位玩家、当前轮数发牌、决定先手、组织对战、判断胜负

这四个类不是各自独立运行的。Game 负责推进流程,在需要时调用 Dice、Player 和 Card 提供的功能。

下面这张图重点观察:Game 负责调度流程,但具体的数据和操作仍然由其他对象负责。

Card、Dice、Player 和 Game 的协作关系

基础版游戏实现 ​

如何把职责表落实为可运行代码?按照 Dice、Card、Player、Game 的顺序编写,所有代码都写在同一个 card_game.py 文件中。

卡牌数据与骰子 ​

先导入 random,再准备 8 张牌的初始数据。能力值要保存为整数,后面才能正确比较大小。

python
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 的随机整数。

卡牌对象 ​

一张牌需要保存人物信息和四项能力值。这里用字典保存能力值,属性名称就是字典的键:

python
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})'

这一层主要提供两个功能:

  1. get_value() 根据属性名称返回能力值。
  2. __str__() 把卡牌转换成便于阅读的文字,出牌后可以直接打印。

能力值的查找放在 Card 内部,其他对象不需要知道这些数据具体怎样保存。

玩家对象 ​

玩家需要保存自己的手牌和分数,还要完成选牌、选属性两个操作:

python
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,把发牌、掷骰子、每轮对战和结果输出串起来:

python
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. 玩家选过的牌会从手牌中消失。
  2. 系统会显示双方打出的完整卡牌信息。
  3. 系统只比较先手玩家指定的属性。
  4. 获胜玩家增加 1 分,并在下一轮先出牌。
  5. 四轮结束后,系统输出双方总分和最终结果。

发牌和掷骰子都有随机性,因此每次运行时的牌号、先手顺序和最终分数可能不同。

如果输入不存在的牌号或属性,程序会提示重新选择。先用错误输入测试一次,再输入正确内容,确认程序能够继续运行。

小结 ​

这个案例最核心的不是游戏规则,而是四个对象怎样配合:

  1. Card 管理一张牌的数据。
  2. Dice 提供随机点数。
  3. Player 管理玩家的手牌、选择和分数。
  4. Game 负责调度其他对象,推进完整游戏流程。

拆分对象时,先确定每个对象的职责,再让总控对象把流程串起来。

练习 ​

  1. 每轮结束后,同时输出两位玩家的当前分数。
  2. 给 Game 增加 used_cards 属性,保存每轮已经打出的两张牌。
  3. 修改平局规则,让下一轮改由后手玩家先出牌。

完成 used_cards 后,可以继续挑战“复活牌”:每位玩家有一次机会,从已使用的卡牌中随机取回一张。再进一步,可以尝试实现一次“上帝之手”,强制对方打出指定牌号的卡牌。