Skip to content

Stage 0.1: Action stack and frames

neuront edited this page Dec 19, 2011 · 6 revisions

Code review: https://github.com/neuront/sgs/commit/55a5e4167809060cf7da6dde5742e367c76b00fa

游戏状态控制

动作栈 core.src.action_stack.ActionStack 与动作帧 core.src.action_frames.*

当游戏正在进行时, 轮到某个玩家响应一个特定的动作, 比如出牌阶段, 使用一张牌或者放弃 (进入弃牌阶段), 或者在被火攻阶段被要求展示一张手牌, 此时内核会暂停下来, 等待玩家响应. 那么, 需要引入栈来保持内核此时的游戏结算状态, 为此, 内核中设计了动作栈和动作帧.

当玩家交互信息被发送到内核中 (通过调用 core.src.game_control.GameControl.player_act) 时, 内核会将此消息直接发送给动作栈栈顶的帧, 由该帧的 react 方法处理该消息. 消息以及 react 函数的返回值都是数值, 字符串, 或这些基本数据类型构成的列表及字典, 而不应改包含复杂数据类型. 具体的消息格式由帧定义, 帧也会定义返回的消息格式.

虽然帧的返回可能各不相同, 但在当前, react 函数返回只可能包括

  • 如果消息被正确处理, 返回 { 'code': core.src.ret_code.OK }
  • 否则, 返回 { 'code': core.src.ret_code.BAD_REQUEST, 'reason': 原因 }

详细错误原因可以查看 core.src.ret_code.BR_*.

帧返回与结果传递

当一个动作帧结束响应 (如: 出牌阶段结束后, 响应出牌的帧应该退栈, 然后响应弃牌的帧入栈) 后, 它在出栈后将向外传递消息. 该消息的结构仍然是简单数据类型或简单数据类型的复合. 接收此消息的函数称之为 on_result 函数 (core.src.action_frames.FrameBase.on_result), 它由动作帧构造时传入.

要结束动作帧的响应, 需要调用该帧的 done 函数, 并传入帧结果. 帧结果的消息格式也由帧来定义.

例子

现在, 以已经实现的锦囊火攻作为一个例子, 描述一个玩家的一个完整回合 (目前没有设计体力和手牌上限机制, 在回合结束阶段, 玩家被要求弃掉两张手牌). 这个例子的流程在单元测试 ext/test/test_fire_attack.py 中. 火攻的实现目前在 ext/src/player_using_cards.py 中. 而上一个版本中, 玩家 core.src.Player 的代码已经被转移到 ext.src.Player 中了.

摸牌阶段

玩家的摸牌阶段非常简单, 仅从牌堆中摸两张牌即可, 在 ext.src.Player.round 中这个过程被直接执行, 接下来就调用 ext.src.Player.using_cards_stage, 在这个函数向动作栈中压入出牌阶段帧 (core.src.action_frames.UseCards), 于是玩家进入出牌阶段.

出牌阶段

出牌阶段帧在构造时, 会要求传入一个字典, 表示该玩家出牌阶段能进行的动作. 目前, 该字典的构造在 ext.src.player_using_cards.get_using_cards_interface_map 中, 且只能使用火攻. 在出牌阶段帧构造函数内, 这个字典会被追加一项 { 'give up': core.src.action_frames.FrameBase.done }, 此项表示玩家终止出牌阶段, 进入弃牌阶段. 出牌阶段帧的传入参数要求玩家传入自己的 token 标识身份, 然后是出牌动作 action, 以及使用的牌, 还有指定的目标. 其中, token 的检查会在帧内检查, 同时还会检查使用的牌是否为该玩家持有, 但如果动作本身无需牌, 则可以不传递 cards, 或者传递空的序列. 然后, 帧会将传入的参数保持原样传入 interface_map 中 action 对应的函数中去.

在 ext/test/test_fire_attack.py 中, 通过如下调用发起火攻

gc.player_act({
    'token': players[0].token,
    'action': 'fire attack',
    'targets': [players[1].player_id],
    'cards': [1],
})

其中, cards 对应的列表项为牌的 id.

火攻

在构造时传入的字典中, 火攻 ("fire attack") 对应的函数是 ext.src.player_using_cards.fire_attack, 此函数中继续进行一些检查, 包括目标是否恰好一人, 以及牌是否恰好是一张火攻 (但是没有检查目标是否持有手牌, 这个问题将在以后的版本中修正). 在检查通过后, 首先向内核中传递一个玩家使用牌的消息 (调用 core.src.game_control.GameControl.use_cards_for_player), 它将在事件列表中加入一个使用牌的事件; 接下来, 它将压入一个展示手牌帧 (core.src.action_frames.ShowCards), 传入的 on_result 是包装过的 ext.src.player_using_cards.fire_attack_discard_same_suit, 这个参数在需要弃牌的时候再谈.

展示手牌

构造展示手牌帧时, 传入的函数参数除了 on_result, 还有牌筛选函数, 此函数返回 True 表示通过筛选, 否则返回 False.

同样, 检查手牌是否属于当前玩家, 也在帧内进行, 不需要牌筛选函数进行. 不过, 即使无需展示手牌, 也必须传入 'show' 参数, 映射到一个空列表.

展示手牌帧会调用 core.src.game_control.GameControl.show_cards 让内核加入一个展示手牌事件.

在展示手牌帧结束后, 会将传入的消息传递给 on_result.

在 ext/test/test_fire_attack.py 中, 通过如下调用展示了一张手牌

gc.player_act({
    'token': players[1].token,
    'show': [6],
})

其中, show 对应的列表项亦为牌的 id.

火攻方弃牌

回顾之前构造展示手牌帧传入的 on_result, 它包装的是 fire_attack_discard_same_suit 会向动作栈内压入一个弃牌帧 core.src.action_frames.DiscardCards, 然后结算进入火攻方弃牌步骤. 弃牌帧结束后, 调用的 on_result 则是 ext.src.player_using_cards.fire_attack_done 包装过的函数, 在函数中将检查弃牌数量, 如果没有弃牌, 则表示火攻失败, 否则将藉由 core.src.game_control.GameControl.damage 增加一个伤害事件.

在 ext/test/test_fire_attack.py 中, 通过如下调用弃置了一张手牌

gc.player_act({
    'token': players[0].token,
    'discard': [2],
})

同样, 其中 discard 对应的列表项为牌的 id.

终止出牌

当玩家决定终止出牌阶段时, 可进行如下调用

gc.player_act({
    'token': players[0].token,
    'action': 'give up',
})

此时进入弃牌阶段. 在弃牌阶段通过如下调用弃置两张手牌

gc.player_act({
    'token': players[0].token,
    'discard': [0, 8],
})

此参数的格式与火攻时弃牌的参数相同.

动作帧细则: https://github.com/neuront/sgs/wiki/Action-Frames-Specification

主要设计修正

PlayerPlayersControl 被移出内核模块, 因为玩家的行为以及控制是特定的游戏规则, 而不应该由内核确定; 在不同游戏规则中 (如 3v3, 虎牢关等) 会有所不同. 现在它们分别是 ext.src.player.Playerext.src.players_control.PlayersControl.

Player 将不再持有牌 core.src.card.Card 对象, 转而由 Card 对象指定其持有人, 或当无人持有时该字段为空 (core.src.card.Card.owner_or_nil).

Clone this wiki locally