About
RSS

Bit Focus


基于 B/S 的桌游设计 - 游戏状态控制

Posted at 2012-03-08 08:08:42 | Updated at 2024-12-24 03:47:36

三国杀结算模型

    考虑在游戏过程中, 某君使用万箭齐发, 对司马懿造成一点伤害, 司马懿发动反馈, 此时游戏暂停, 等待司马懿选择卡牌区域 (手牌或装备). 而在司马懿选择了反馈的卡牌区域后, 后续的玩家需要继续响应万箭齐发. 在万箭齐发结算完毕后, 回到某君的出牌阶段继续出牌或选择弃牌.
    好吧, 仔细看一下, 这就是个栈.
    题外话, 三国杀对延迟锦囊的判定顺序也是个栈, 后来的先判定.
    现在的问题就是怎么来表示这个游戏状态栈.

栈帧

    天下的栈大抵都一个样, 关键还是在于其中的帧是个什么样子的. 首先帧必须能够接受玩家的输入, 以推进游戏状态; 其次, 每个帧只能接受特定玩家的输入, 比如司马懿发动反馈时, 其他玩家是不能决定反馈区域的. 那么, 帧的声明可能会像这个样子
class FrameBase:
    def react(self, args):
        pass # response to player's action

    def allowed_players(self):
        return [] # which players are allowed
    其中, react 函数的参数 args 就是在之前提到的, 从浏览器端传来的 JSON 解析后的字典数据.
    另外, 当浏览器, 也就是客户端程序向服务器请求 hint 时, 服务器应该给出当前栈顶帧所对应的 hint. 因此, 每个帧都必须还能获得 hint 数据
class FrameBase:
    # other functions

    def hint(self, token):
        if token in map(lambda player: player.token, self.allowed_players()):
            return {} # detail info
        return {} # just who are active players

    虽然说栈大抵都一样, 不过一些必须的功能还是得手动引进, 很关键的就是帧退栈时的动作, 以及如何使帧和帧之间可以传递信息. 下面是结算栈的架子
class ActionStack:
    def __init__(self):
        self.frames = []

    def call(self, args):
        return self.frames[-1].react(args)

    def allowed_players(self):
        return self.frames[-1].allowed_players()

    def hint(self, token):
        return self.frames[-1].hint(token)

    def push(self, frame):
        self.frames.append(frame)

    def pop(self):
        stack_top = self.frames.pop()
        # pass something from stack_top to current stack top
    实现这个 pop 剩余的部分, 需要理清下面的问题
    最后一个问题可以效仿函数调用栈, 退栈工作在一般程序设计语言或汇编中, 都是由一句 return 语句或者相应的 ret 指令完成的. 因此这里同理, 由帧本身在 react 过程中, 满足某个特定的用户输入便调用栈的 pop 方法, 同时, 顺便再把结果传递过去. 而对应的, 每个帧应该添加一个接口, 以便接受前一栈帧传递过来的参数; 这一接口另一作用是, 告知当前栈帧它已经恢复为栈顶, 可以视情况调整自身的状态.
class FrameBase:
    def resume(self, result): # take result from upper frame
        pass

class ActionStack:
    def pop(self, result):
        stack_top = self.frames.pop()
        self.frames[-1].resume(result)
    不过这样一来, 如果栈顶最后一帧被弹出, 那么 self.frames[-1].resume(result) 会导致索引越界, 所以在构造栈时需要压入一个垫底的帧
class ActionStack:
    def __init__(self):
        class DummyFrame:
            def resume(self, result):
                pass
        self.frames = [DummyFrame()]

例子: 火攻

    以火攻 (姑且译为 arson attack) 作为例子, 在一次火攻的过程中, 可能用到如下这些帧 (有些伪代码的味道)
class ArsonAttack(FrameBase):
    def __init__(self): # might have other args
        # check arguments
        # stack is the action stack
        stack.push(ArsonTargetShowCard()) # constructor args are left out
        self.resume = self.after_target_show_a_card

    def after_target_show_a_card(self, result):
        # check arguments
        # assume client pass data in this format
        # { 'card': card_id }
        # and the card object can be mapped from card id via a function
        #    get_card_by_id
        suit = get_card_by_id(result['card']).suit()
        self.resume = self.user_discard_card
        stack.push(ArsonUserDiscardSameSuit(suit)) # may have other args

    def user_discard_card(self, result):
        if result['action'] != 'abort':
            # check the card is of the same suit
            # damage the arson target
            # NOTE
        stack.pop(None) # don't have to pass result to using cards frame
Note 也许这里并不应该立即弹出火攻帧, 因为目标玩家可能被火攻跪了, 那么就需要插入濒死求桃帧. 这个情况以后会说如何解决, 此处暂时保持为火攻结束后立即弹出火攻帧.
    火攻帧大抵如此, 它所依赖的两个帧 ArsonTargetShowCardArsonUserDiscardSameSuit 应该很容易想象如何实现其中的 react 函数, 所以这里就不详述 react 了, 而是讨论一下这两类帧的 hint.
    虽然其中一个是展示手牌, 另一个是弃置手牌, 但总的来说有共同点, 就是两者只需要指定卡牌, 无需指定目标. 所以可以用同一种固定的格式来生成 hint
class ArsonTargetShowCard(FrameBase):
    def hint(self, token):
        hint = {
            'code': 200,
            'players': [0],
        }
        if token == self.player.token:
            hint['action'] = 'discard'
            hint['cards'] = [] # which cards can be shown
            hint['abort'] = 'disallow'
        return hint

class ArsonUserDiscardSameSuit(FrameBase):
    def hint(self, token):
        hint = {
            'code': 200,
            'players': [0],
        }
        if token == self.player.token:
            hint['action'] = 'discard'
            hint['cards'] = [] # which cards can be shown
            hint['abort'] = 'allow'
        return hint
    于是这两个 hint 函数看起来就可以被重构一下然后合并起来了. 虽然两者一个是展示卡牌, 一个是弃置卡牌, 但对于客户端而言, 并不需要知道这两者之间的差异. 客户端只需要老老实实选择一张合适的卡牌传给服务器就行了, 至于这张卡牌到底是展示还是弃置, 到时候服务器会传回一个对应的事件的 (客户端在传出用户的操作后, 不要擅自乱丢卡牌就行了).

Post tags:   Sanguosha  Boarder game  Game development  Python

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site