三国杀结算模型
考虑在游戏过程中, 某君使用万箭齐发, 对司马懿造成一点伤害, 司马懿发动反馈, 此时游戏暂停, 等待司马懿选择卡牌区域 (手牌或装备). 而在司马懿选择了反馈的卡牌区域后, 后续的玩家需要继续响应万箭齐发. 在万箭齐发结算完毕后, 回到某君的出牌阶段继续出牌或选择弃牌.好吧, 仔细看一下, 这就是个栈.
题外话, 三国杀对延迟锦囊的判定顺序也是个栈, 后来的先判定.
现在的问题就是怎么来表示这个游戏状态栈.
栈帧
天下的栈大抵都一个样, 关键还是在于其中的帧是个什么样子的. 首先帧必须能够接受玩家的输入, 以推进游戏状态; 其次, 每个帧只能接受特定玩家的输入, 比如司马懿发动反馈时, 其他玩家是不能决定反馈区域的. 那么, 帧的声明可能会像这个样子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
剩余的部分, 需要理清下面的问题- 怎么获得之前栈顶的结果
- 结果怎么被传递给当前栈顶
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
火攻帧大抵如此, 它所依赖的两个帧
ArsonTargetShowCard
与 ArsonUserDiscardSameSuit
应该很容易想象如何实现其中的 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
函数看起来就可以被重构一下然后合并起来了. 虽然两者一个是展示卡牌, 一个是弃置卡牌, 但对于客户端而言, 并不需要知道这两者之间的差异. 客户端只需要老老实实选择一张合适的卡牌传给服务器就行了, 至于这张卡牌到底是展示还是弃置, 到时候服务器会传回一个对应的事件的 (客户端在传出用户的操作后, 不要擅自乱丢卡牌就行了).