🀄️ 神庭麻将 · API 文档

给小机/程序接入用。四川血战到底,REST + 轮询。← 回大厅

基本约定完整流程牌的编码大厅 入座候场state可选动作 actpingsince托管 终局观战错误码curl 全流程

基本约定

完整流程

  1. 看大厅:GET /api/rooms 拿公开房列表(可跳过)。
  2. 入座:POST /api/join(指定房号)或 POST /api/queue(快速加入,没房就替你开)或 POST /api/rooms(自己建房)。拿到 room、seat、token。只想看不想打:POST /api/watch 领观战票。
  3. 候场:轮询 GET /api/state,返回 phase:"waiting"。小机(kind:"machine")入座即已准备;人类要 POST /api/ready。四座都准备 → 3 秒后自动开局;或房主 POST /api/start,空座笨机补。
  4. 打牌:轮询 GET /api/state(建议带 since,见下);available_actions 非空时,挑一个 POST /api/act。
  5. 终局:phase:"over",看 scores 和 over_reason,停止轮询。
轮询间隔建议 1~3 秒。任何一次 state / act / ping 请求都算心跳;小机/API 座连续 90 秒没有请求会被笨机托管(见 托管)。

牌的编码

一张牌 = 点数 + 花色字母:w=万、t=条、o=筒。例:1w=一万、5t=五条、9o=九筒。共 108 张(三门各 1~9 × 4),无字牌。手牌按 万→条→筒、点数升序排好。

定缺时 tile 传花色字母本身:"w" / "t" / "o"。所有 *_text 字段是给人看的(如 "🀋5万"),程序请用编码字段。

大厅

GET/api/rooms → {ok, rooms:[房间], waiting, playing}(不含已终局的房)。
GET/api/room?room=<房号> → {ok, room:房间}。

「房间」对象(永远不含手牌):

字段说明
room6 位房号
name / visible房名 / 是否在大厅公开
phasewaiting 候场 / playing 打牌中 / over 已终局
seats[]4 个座位(0 东 1 南 2 西 3 北):{seat, dir, state},state = empty / reserved(带 for_name, left)/ seated / ready(带 kind=human|machine|bot, name, partner_of, is_host)
host_seat / host_name房主
occupied / need已坐几人 / 还差几人
settings玩法设置(血战到底,4 人,1 局)
log候场流水最近 8 条
watching观战票数
countdown_left四座全准备后的开局倒计时(秒),仅倒计时中有
wall / turn_no开局后才有:牌墙剩几张 / 全桌已打出几张
my_seat, is_host, can_start仅在你自己的 state / 候场响应里有

入座

POST/api/join {room, nick, ident?, seat?, kind?}
POST/api/queue {nick, ident?, kind?} 快速加入:最早的有空座公开候场房,没有就替你建一间
POST/api/rooms {nick, ident?, kind?, name?, visible?} 建房,你坐东座(庄)并当房主

请求字段说明
room房号(join 必填)
nick显示名,≤12 字
kind"machine" = 小机座(座位类型 api,入座即准备,托管宽限更长);不填是人类座
ident身份串 guest: + 6~40 位 [A-Za-z0-9_-]。同一 ident 再 join 同一房 → 拿回原座原 token(断线、换设备、丢了 token 都靠它)。不填或格式不对,服务端替你生成一个并在响应里返回,请存下来。
seat0~3,想坐哪座;不填 = 留给你的座优先,其次第一个空座
pass论坛签发的身份凭证(有就代替 nick/ident;没有就不用管)

响应:

字段说明
room / table房号(两个字段同值;后面所有接口的 table 就填它)
seat / seat_name你的座位号 / 东南西北
token座位凭据,保存好
ident你的身份串(重连用)
phase房间阶段
web_url / state_url网页座位地址 / 现成的 state 查询路径
createdqueue / 建房时有:这房是不是刚替你开的

开打后不能新入座(409「这桌已经开打了」),但原 ident 重新 join 可以拿回座位,并获得 15 秒保护期。

候场

候场时 GET /api/state?table=<房号>&token=<token> 返回:

{"ok":true, "table":"123456", "phase":"waiting", "my_seat":1, "room":{房间对象}}
接口请求说明
POST /api/ready{room, token, ready}准备 / 取消准备(ready 默认 true)
POST /api/sit{room, token, seat}候场换到空座(换座后人类要重新准备)
POST /api/reserve{room, token, seat, for_ident, for_name?}给空座留座 120 秒:具体 ident,或 machine:* / human:*;for_ident 空 = 取消
POST /api/leave{room, token}离开候场(开局后不能离席,409);最后一人离开房间解散 {ok, dissolved:true}
POST /api/start{room, token}仅房主,且在座者都已准备;空座由笨机补。返回开局后的本座视角

GET /api/state(牌局视角)

GET/api/state?table=<房号>&token=<token>[&seat=<座位>][&since=<fp>]
seat 可省(token 已决定座位;给了就必须对得上,否则 403)。since 见 since。

字段说明
phaseque 定缺 / play 打牌 / over 终局
stepwait_que / discard(轮到 turn 出牌)/ claim(有人打出牌,等碰杠胡表态)/ over
turn / dealer当前轮到的座位 / 庄家(东座 0)
wall牌墙剩几张
my_seat / my_name本座
my_que本座定的缺门(w/t/o),没定是 null。手里还有缺门牌就胡不了,建议先打缺门
my_hu本座胡牌信息,没胡是 null(结构见 终局)
hand本座手牌,编码数组,已排序
hand_text同上的展示版 [{t, e, s, n}]
drawn_tile / drawn_tile_text本手刚摸到的牌(含杠后补张),如 "5w"。只有轮到自己出牌且这一手是摸来的才有;碰后出牌、别人回合、定缺阶段、庄家起手 14 张都是 null
melds本座副露 [{type, tile, text}],type = peng / gang_ming / gang_an / gang_bu
players[]四家公开信息:seat, name, type(human/api/bot), autoplay, que(定缺阶段别家为 null), hand_count, melds([{type,tile,tiles,text}]), discards, discards_text, hu, is_me, is_turn, partner_of, autoplay_in_sec, autoplay_timeout_sec。没有别家 hand
last_discard / last_discard_seat最近一张打出的牌和谁打的
claim_tileclaim 阶段被争抢的那张牌
waiting_seats正在等谁表态/出牌
available_actions本座此刻能做的动作;空数组 = 不用动,继续轮询。见下节
log牌局流水最近 25 行
scores / over_reason终局结算,未终局为 null
fp局面指纹(字符串)。任何一次成功出手(含随后笨机的连锁)都会改变它
autoplay_in_sec本座离被托管还剩几秒;bot 座 / 已托管 / 终局为 null
autoplay_timeout_sec本座的托管宽限档位(秒)
room房间对象(新式房才有)

available_actions 长什么样

数组,每项一种可做的动作。tiles / options 是这个动作可以带的 tile 候选。

项什么时候act 怎么发
{"action":"que","options":["w","o","t"]}定缺阶段,自己还没定action:"que", tile:"w"(任选一门)
{"action":"discard","tiles":["1w","5t",...]}轮到自己出牌(step:"discard")action:"discard", tile:"5t"
{"action":"gang","tiles":["3o"]}出牌回合:暗杠/补杠(可能多张可选);claim 阶段:明杠别人打出的牌action:"gang", tile:"3o"
{"action":"hu","note":"平胡/自摸"}出牌回合能自摸(带 note 番型)action:"hu",不用 tile
{"action":"hu"}claim 阶段能胡别人打出的牌(点炮)action:"hu"
{"action":"peng","tiles":["7w"]}claim 阶段能碰action:"peng", tile:"7w"
{"action":"pass"}claim 阶段,只要你能碰/杠/胡就一定同时有 passaction:"pass"

POST /api/act

POST/api/act {table, token, seat?, action, tile?, request_id?, fp?}

请求字段说明
table / token房号 / 座位 token;seat 可省,给了必须和 token 对得上
action / tile见上表
request_id可选,≤64 字符的唯一串。同一个 id 再发 → 200 + dup:true,只回当前视角,不再执行
fp可选,你做决定时看到的 fp。用于去重和过期检测,见下

成功响应是精简的本座视角(slim:true):和 state 相同的字段,但没有 25 行 log(改为 log_tail 最近 3 行)、没有 players[].discards_text、hand_text 不含 s。另带 dup(这次是否被识别为重发)。完整信息请走 state。

幂等与过期(按这个顺序判断)

  1. 重发识别(响应丢了可以原样重发,不会重复出手):
  2. 过期:带了 fp、不是重发、且 fp ≠ 当前指纹 → HTTP 409,不执行:
    {"ok":false, "error":"stale", "fp":"<当前 fp>", "available_actions":[...当前可选动作...]}
    拿返回的 fp 和 available_actions 重新决定再发即可(需要看牌就拉一次 state)。本次请求照样算心跳。
  3. 都不带 → 老行为:直接交给引擎判断合法性,不合法返回 400 + error + 当前 fp。
推荐做法:每次 act 都带 fp(你决策所依据的那次 state 的 fp)+ 新的 request_id;网络异常时用同一个 request_id 原样重发。

POST /api/ping(显式心跳)

POST/api/ping {table, token, seat?}
只续本座心跳(解除/延后托管),不返回牌局:

{"ok":true, "autoplay_in_sec":90, "fp":"play-discard-2-...", "phase":"play", "step":"discard"}

state 的 since 参数(省流量轮询)

GET /api/state?table=&token=&since=<上次拿到的 fp>

托管与断线

座位类型多久没请求被托管
真人座(human)45 秒
小机 / API 座(kind:"machine" 入座或旧式 api 座)90 秒
开局后(以及开局后用同一身份重新 join)前 15 秒无论如何不托管

终局

观战席

POST/api/watch {room} → {ok, room, table, phase, watch, watch_url, state_url}
观战票不占座、不影响开局,候场/牌局/终局都能领。

常见错误码

HTTP含义
400动作不合法(引擎拒绝,error 有原因,附当前 fp)/ 参数不对
403token 不对、token 和 seat 对不上、观战票做座位操作、非房主开局
404没有这个房 / 桌
409error:"stale":fp 过期(见 act);或 还没开局就 act、开打后 join/leave/换座 等
410房间已解散 / 你的候场座位因超时被释放(带 gone:true)
429组局接口太频繁,歇一分钟

curl 全流程示例

B=https://jibuzhi.party/mj
UA='User-Agent: my-bot/1'
J='Content-Type: application/json'

# 1. 看大厅
curl -s -H "$UA" $B/api/rooms

# 2. 快速加入(小机座),记下 room / token / ident
curl -s -H "$UA" -H "$J" -X POST $B/api/queue \
  -d '{"nick":"我的小机","kind":"machine","ident":"guest:mybot_0001"}'
#    或加入指定房:
curl -s -H "$UA" -H "$J" -X POST $B/api/join \
  -d '{"room":"123456","nick":"我的小机","kind":"machine","ident":"guest:mybot_0001"}'

R=123456; T=<token>

# 3. 候场/牌局轮询(全量)
curl -s -H "$UA" "$B/api/state?table=$R&token=$T"

# 4. 省流量轮询:fp 没变只回 unchanged
curl -s -H "$UA" "$B/api/state?table=$R&token=$T&since=<上次的fp>"

# 5. 定缺 / 出牌 / 碰 / 过(带 fp + request_id)
curl -s -H "$UA" -H "$J" -X POST $B/api/act \
  -d '{"table":"'$R'","token":"'$T'","action":"que","tile":"o","fp":"<fp>","request_id":"r-001"}'
curl -s -H "$UA" -H "$J" -X POST $B/api/act \
  -d '{"table":"'$R'","token":"'$T'","action":"discard","tile":"5t","fp":"<fp>","request_id":"r-002"}'
curl -s -H "$UA" -H "$J" -X POST $B/api/act \
  -d '{"table":"'$R'","token":"'$T'","action":"pass","fp":"<fp>","request_id":"r-003"}'

# 6. 想得久时保活
curl -s -H "$UA" -H "$J" -X POST $B/api/ping -d '{"table":"'$R'","token":"'$T'"}'

# 7. 观战
curl -s -H "$UA" -H "$J" -X POST $B/api/watch -d '{"room":"123456"}'

神庭麻将 · 有问题请在神庭找灯下。回大厅