About
RSS

Bit Focus


基于 B/S 的桌游设计 - 协议

Posted at 2012-03-02 13:34:47 | Updated at 2024-12-24 03:46:55

前言

    虽然我觉得国内桌游的始祖可能国粹麻将什么的, 不过貌似这个词在三国杀风靡之后才被正式使用. 这系列文章中, 我也打算使用三国杀来做样例项目, 分析 B/S 结构的桌游设计方式.
    按惯例, 首节内容应该是纯吹水而无码的. 这里就先拔一拔目前比较靠谱的三国杀实现.
    其一是我很敬佩的一个民间 MOD 版本太阳神三国杀, 武将比官网的全, 还有自制武将, 卡牌, 以及各种趣味度爆表的模式. 不过太阳神三国杀是 C/S 一体的, 与我想要设计的并不很搭边.
    另一就是官方的三国杀, 使用 Adobe Flash 制成 (桌面版似乎用的 Adobe Air), 所以也并不能严格算是 B/S 结构. 并且官方版本也不是开源或者开放 API 的, 很难一窥其究竟, 没什么好借鉴的.
    移动终端的三国杀就没怎么实测过了, 我本身也不是智能或智障手机爱好者, 对这方面也不是太有兴趣.
    读者最好知道三国杀的基本规则, 这样一些具体的例子理解起来会更容易.

B/S 架构的游戏

    继续吹一下水. 对于一般的网络应用, 比如 Google Doc 那样的字处理应用, 大部分的逻辑都在浏览器脚本中, 而即使用户去改了浏览器脚本, 导致保存的文档格式有误, 最终受害者也是用户自身. 而多人连线游戏服务器最起码的一点就是要防止用户作弊, 以免破坏游戏状态影响其他玩家, 因此至少服务器端有一份游戏逻辑是必需的.
    现在的问题是, 客户端 (也就是 Browser 端) 是否也复制一分游戏逻辑呢?
    当然可以, 不过假如要重新开发除了浏览器之外, 能够以相同协议与服务器通信的客户端, 或者开放 API, 使得第三方可以自行开发客户端, 那样必然陷入不断重复开发客户端游戏逻辑的无底洞里.
    因此, 除了尽可能将全部游戏逻辑堆在服务器端, 服务器端还应该尽可能指导客户端, 例如告知当前游戏状态, 以及客户端该如何响应.

服务器职责

    好, 吹水结束, 现在理一下, 服务器端应该
注 a: 向客户端提供状态有方式有直接提供当前游戏的完整状态, 或提供从游戏某个时机开始的一系列状态增量. 前一种方式每次传输的信息量多, 负担较大, 所以这里采用后者.

协议格式

    B/S 模式下传递信息可以考虑传递 JSON. 而 Python 跟 JS 一样, 是不需要指定变量类型的, 如果发现转出来的值有任何问题就直接认为客户端发送了错误数据就行了, 所以服务器端用 Python 可以省不少事.
    通信内容的具体格式可设计为一个字典, 例如向服务端发送如下信息
{
    'token': '10178a67c867b82392f41c9723cf2daae8fd6ab0',
    'previous event id': 4,
}
    此时服务器返回的可能是如下格式的列表
{
    'code': 200,
    'events': [
        {
            'type': 'DiscardCards',
            'player': 0,
            'discard': [
                {
                    'id': 0,
                    'name': 'slash',
                    'rank': 1,
                    'suit': 1,
                },
                {
                    'id': 1,
                    'name': 'dodge',
                    'rank': 2,
                    'suit': 2,
                },
                {
                    'id': 2,
                    'name': 'slash',
                    'rank': 3,
                    'suit': 1,
                },
            ],
        },
        {
            'type': 'DrawCards',
            'player': 1,
            'draw': 2,
        },
    ],
}
    其中的 'code': 200 沿袭 HTTP 返回码, 表示 OK.

游戏状态增量与游戏事件

    游戏状态的增量就是游戏中的事件, 比如三国杀中的摸牌, 弃牌, 出牌, 发动技能.
    上面的请求和返回示例的是对服务器请求一次从 event id 为 4 开始, 直到当前时刻的游戏事件列表. 其中的 token 是玩家标识, 在游戏开始分别传给各个玩家, 玩家在请求事件, 提示以及执行动作时需要带上 token. 在客户端需要根据对应的 type 解释这些事件, 然后更新客户端状态, 那么呈现在玩家眼前的就是当前的局面. 事件类型就那么确定的几个, 而客户端就像一台虚拟机一样把这些事件当作指令来执行就好了.
    另外, 同一个事件在不同玩家看来是不一样的, 典型是摸牌时, 自己可以看到牌是什么, 而其它玩家只能看到牌的数量. 因此, 获取事件必须带上 token.

服务器提示

    服务器提示则是另一番光景. 服务器首先要提示现在是谁在做什么, 接着, 对于那些要做事的客户端, 它们的请求将返回更具体的信息, 该怎么做. 如下面是一次请求提示的返回
{
    'code': 200,
    'players': [0],
    'action': 'transfer',
    'transfer': [10, 11],
    'candidates': [1],
    'abort': 'allow',
}
    对于任何玩家, 'players': [0]'action': 'transfer' 都是可见的, 表示当前活动玩家的列表 (仅含有玩家 0) 和这些玩家正在做的事情 (转移卡牌). 而后面的 'transfer''abort' 等项目, 只有活动玩家本身才能知道. 这里它们表示
    如果对三国杀很熟悉的话, 就能看出来, 这不就是郭嘉单挑时被搞了一下, 遗计等着分牌么? 没错, 只不过客户端并不知道什么武将技能, 也不知道场上发生了什么, 它只知道现在要做什么, 怎么去做, 然后让玩家来选择 (而玩家看到这个局面应该能明白该怎么做), 并把这个选择序列化并发给服务器. 用一句名言来说, 客户端要 stay foolish, 尽可能简单而不掺杂游戏逻辑, 另外 stay hungry, 随时向服务器要求当前的游戏进度.

推进游戏进程

    当玩家决定了做什么事情时, 客户端会将玩家的动作序列化为如下的东西扔给服务器
{
    'token': '10178a67c867b82392f41c9723cf2daae8fd6ab0',
    'action': 'transfer',
    'transfer': [11],
    'target': 1,
}
    服务器此时来解释这个信息
    (这看来是郭嘉把一张屎牌交给了对方.)
    至于服务器为什么知道要利用字典里面这些键值, 以后谈到服务器如何保存游戏状态再讨论, 现在服务器根据游戏状态抓取了这些信息之后开始验证
    之前说过了, 客户端并不知道当前它选择了这张牌再选择了一名目标并点击确定的意义是什么, 它只是这样响应了玩家的操作, 并把它传给了服务器. 在这时, 客户端甚至仍然不知道这张卡牌已经离手了, 而是仍然等待服务器响应, 同时询问服务器是否有新的事件产生. 直到服务器确实将卡牌转移事件传回了客户端, 客户端解释了这个事件, 这才从手牌中去掉这张牌.

感谢双木成林对本文的审阅和指导, 他是一位热衷于 Python 的上进青年 & geek.

Post tags:   Game development  Sanguosha  Boarder game  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