简单的客户端/服务器架构,两者通过基于 TCP 的自定义消息格式通信, TCP 是字节流协议,只负责把一个个字节发送到对方主机,字节的含义由我们自己定义
服务器负责维护玩家状态信息,包括:
玩家编号 |
玩家就绪状态 |
玩家筹码状态 |
玩家宝石牌状态 |
玩家贵族牌状态 |
以及当前可获取的卡牌(下称棋盘)信息:
客户端需要负责与服务器通信以获取所有玩家状态,棋盘信息,绘制界面并处理用户交互
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
4 bytes | 8 bytes | 8 bytes | 8 bytes | 不定 |
API ID 是一个 32 位无符号整数
玩家编号是一个 64 位无符号整数
消息长度为 4 + 8 + 8 + 8 + 消息体长度,单位为字节
消息体统一为 json 字符串,可以为空,具体内容见下节
INIT 消息,客户端启动后向服务器发送的第一条消息,表示新客户端上线, 并且向服务器请求获得自己的玩家编号 具体地:
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
1 | 0 | 28 | 0 | None |
INIT RESP 消息是服务器对于 INIT 的回复,回复包含分配给自己的玩家编号, 以及所有其它玩家的玩家编号 具体地
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
2 | 0 | 运行时计算 | 0 | string |
string 是 json 格式字符串,一个示例如下
{
"allocated_player_id": 3,
"other_player_id": [1, 2]
}
这样表明客户端分配到的玩家编号为3,以后的消息就要将 3 填入玩家编号字段, 这盘游戏已经有两个玩家了,他们的编号是 1,2
PLAYER_READY 由客户端在玩家准备就绪时发送给服务器,并由服务器转发给其它玩家, 所以客户端既会发送这条消息又会接受到这条消息 具体地
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
3 | 运行时计算 | 28 | 0 | None |
所有玩家准备就绪之后由服务器广播给所有玩家,消息体包含全局游戏信息
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
4 | 0 | 运行时计算 | 0 | string |
{
"players_number": 3,
"players_sequence": [1, 2, 3],
"nobles_info": [1, 2, 3, 4],
"levelOneCards_info": [5, 6, 7, 8],
"levelTwoCards_info": [9, 10, 11, 12],
"levelThreeCards_info": [13, 14, 15, 16]
}
新的一个回合,由服务器发给所有玩家,指定该回合进行操作的玩家
{
"new_turn_player": 1
}
表示下一个进行操作的玩家为 player_id 为 1 的玩家
表示玩家进行的操作,经服务器检验有效后向所有客户端广播该操作。特别地,服务器以玩家编号为0广播该消息。
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
6 | 运行时计算 | 运行时计算 | 0 | string |
{
"player_id": 1,
"operation_type": "get_gems",
"operation_info": [
{
"gems_type": "sapphire",
"gems_number": 1
},
{
"gems_type": "ruby",
"gems_number": 1
},
{
"gems_type": "diamond",
"gems_number": 1
}
]
}
即玩家1选择拿走一个蓝宝石,一个红宝石,一个钻石
{
"player_id": 1,
"operation_type": "buy_card",
"operation_info": [
{
"card_number": 1
},
{
"gems_type": "sapphire",
"gems_number": 1
},
{
"gems_type": "ruby",
"gems_number": 1
},
{
"gems_type": "diamond",
"gems_number": 1
}
]
}
即玩家1购买卡牌号码为1的卡片
{
"player_id": 1,
"operation_type": "fold_card",
"operation_info": [
{
"card_number": 1
}
]
}
即玩家1选择盖住1号卡牌
以下为服务器广播:
{
"player_id": 1,
"operation_type": "fold_card",
"operation_info": [
{
"card_number": 1
},
{
"golden_number": 1
}
]
}
golden_number 为1则拿走一个黄金,为0则不拿。
示例4:
{
"player_id": 1,
"operation_type": "fold_card_unknown",
"operation_info": [
{
"card_level": 1
"card_number" : 10001
}
]
}
即玩家1选择盖住1级牌库的1张牌
服务器广播会是
{
"player_id": 1,
"operation_type": "fold_card_unknown",
"operation_info": [
{
"card_level": 1,
"card_number" : 10001,
"golden_number": 1
}
]
}
示例5:
{
"player_id": 1,
"operation_type": "discard_gems",
"operation_info": {
"diamond": 1,
"sapphire": 1
}
}
玩家1 丢弃一个钻石筹码,一个蓝宝石筹码.
用以指示客户端上次发送的操作请求不合法
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
7 | 运行时计算 | 28 | 0 | None |
服务器广播告知新玩家加入
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
8 | 运行时计算 | 28 | 0 | None |
用以传输玩家获得贵族牌信息。具体地,若当前仅可能获得一种贵族牌,服务器广播 该消息(示例1);若当前可能获得数种贵族牌,服务器向该客户端发送消息进行确认 (示例2),客户端使用该消息进行回复(如示例1),服务器经校验后进行广播。
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
9 | 运行时计算 | 运行时计算 | 0 | string |
示例1:
{
"player_id": 1,
"noble_number": [1]
}
服务器发送:服务器广播玩家1获得1号贵族牌; 客户端发送:告知服务器玩家1选择1号贵族牌
示例2:
{
"player_id": 1,
"noble_number": [1,2]
}
服务器发送:服务器告知客户端玩家1可选择1号和2号贵族牌并要求回复
服务器广播新卡。
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
10 | 0 | 运行时计算 | 0 | string |
示例:
{
"card_number": 1
}
客户端执行合法的拿筹码操作后,若当前筹码数大于10,服务端将返回此消息
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
11 | 运行时计算 | 运行时计算 | 0 | string |
实例:
{
"number_to_discard": 2
}
代表客户端需要丢弃两个筹码,丢弃的筹码信息通过 API6 通知服务端。
服务器广播胜利玩家并结束该局游戏
API ID | 玩家编号 | 消息长度(单位为 bytes) | 保留 | 消息体 |
---|---|---|---|---|
12 | 运行时计算 | 28 | 0 | None |
@startuml
actor client1 as c1
entity server as s
actor client2 as c2
c1 -> s : INIT
s -> c1 : INIT_RESP
c2 -> s : INIT
s -> c2 : INIT_REP
c1 -> s : PLAYER_READY
s -> c1 : PLAYER_READY
s -> c2 : PLAYER_READY
c2 -> s : PLAYER_READY
s -> c1 : PLAYER_READY
s -> c2 : PLAYER_READY
s -> c1 : GAME_START
s -> c2 : GAME_START
s -> c1 : NEW_TURN(client1 先走)
s -> c2 : NEW_TURN(client1 先走)
@enduml
驼峰命名法,即首字母大写,不使用下划线,如 LevelOneCard
首单词小写,其后驼峰,如 sendInitMsg
全大写,使用下划线连接,如 PLAYER_READY_API_ID
类方法之间空一行,普通函数空两行
python3 startServer.py