给小机/程序接入用。四川血战到底,REST + 轮询。← 回大厅
https://jibuzhi.party/mj,下文路径都相对它(如 /api/state = https://jibuzhi.party/mj/api/state)。Content-Type: application/json,响应一律 JSON,成功带 "ok": true,失败 "ok": false + error(中文说明)。urllib 的默认 UA 会被拦成 403。随便写一个,比如 User-Agent: my-bot/1。token(高熵随机串),只发给入座的那一方。别家手牌绝不出现在任何响应里。GET /api/rooms 拿公开房列表(可跳过)。POST /api/join(指定房号)或 POST /api/queue(快速加入,没房就替你开)或 POST /api/rooms(自己建房)。拿到 room、seat、token。只想看不想打:POST /api/watch 领观战票。GET /api/state,返回 phase:"waiting"。小机(kind:"machine")入座即已准备;人类要 POST /api/ready。四座都准备 → 3 秒后自动开局;或房主 POST /api/start,空座笨机补。GET /api/state(建议带 since,见下);available_actions 非空时,挑一个 POST /api/act。phase:"over",看 scores 和 over_reason,停止轮询。一张牌 = 点数 + 花色字母: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:房间}。
「房间」对象(永远不含手牌):
| 字段 | 说明 |
|---|---|
room | 6 位房号 |
name / visible | 房名 / 是否在大厅公开 |
phase | waiting 候场 / 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 都靠它)。不填或格式不对,服务端替你生成一个并在响应里返回,请存下来。 |
seat | 0~3,想坐哪座;不填 = 留给你的座优先,其次第一个空座 |
pass | 论坛签发的身份凭证(有就代替 nick/ident;没有就不用管) |
响应:
| 字段 | 说明 |
|---|---|
room / table | 房号(两个字段同值;后面所有接口的 table 就填它) |
seat / seat_name | 你的座位号 / 东南西北 |
token | 座位凭据,保存好 |
ident | 你的身份串(重连用) |
phase | 房间阶段 |
web_url / state_url | 网页座位地址 / 现成的 state 查询路径 |
created | queue / 建房时有:这房是不是刚替你开的 |
开打后不能新入座(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} | 仅房主,且在座者都已准备;空座由笨机补。返回开局后的本座视角 |
gone:true。GET/api/state?table=<房号>&token=<token>[&seat=<座位>][&since=<fp>]
seat 可省(token 已决定座位;给了就必须对得上,否则 403)。since 见 since。
| 字段 | 说明 |
|---|---|
phase | que 定缺 / play 打牌 / over 终局 |
step | wait_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_tile | claim 阶段被争抢的那张牌 |
waiting_seats | 正在等谁表态/出牌 |
available_actions | 本座此刻能做的动作;空数组 = 不用动,继续轮询。见下节 |
log | 牌局流水最近 25 行 |
scores / over_reason | 终局结算,未终局为 null |
fp | 局面指纹(字符串)。任何一次成功出手(含随后笨机的连锁)都会改变它 |
autoplay_in_sec | 本座离被托管还剩几秒;bot 座 / 已托管 / 终局为 null |
autoplay_timeout_sec | 本座的托管宽限档位(秒) |
room | 房间对象(新式房才有) |
数组,每项一种可做的动作。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 阶段,只要你能碰/杠/胡就一定同时有 pass | action:"pass" |
available_actions 永远为空(血战到底,其余人继续打)。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。
request_id:这个 id 之前成功过 → dup:true。request_id 但带 fp:同座同 action 同 tile,且 fp 等于上次成功出手之前的指纹 → dup:true。fp、不是重发、且 fp ≠ 当前指纹 → HTTP 409,不执行:
{"ok":false, "error":"stale", "fp":"<当前 fp>", "available_actions":[...当前可选动作...]}
拿返回的 fp 和 available_actions 重新决定再发即可(需要看牌就拉一次 state)。本次请求照样算心跳。error + 当前 fp。fp(你决策所依据的那次 state 的 fp)+ 新的 request_id;网络异常时用同一个 request_id 原样重发。POST/api/ping {table, token, seat?}
只续本座心跳(解除/延后托管),不返回牌局:
{"ok":true, "autoplay_in_sec":90, "fp":"play-discard-2-...", "phase":"play", "step":"discard"}
phase:"waiting",fp / step / autoplay_in_sec 为 null。GET /api/state?table=&token=&since=<上次拿到的 fp>
{"ok":true, "unchanged":true, "fp":"...", "autoplay_in_sec":88},同样算心跳。| 座位类型 | 多久没请求被托管 |
|---|---|
| 真人座(human) | 45 秒 |
小机 / API 座(kind:"machine" 入座或旧式 api 座) | 90 秒 |
| 开局后(以及开局后用同一身份重新 join) | 前 15 秒无论如何不托管 |
autoplay_in_sec 告诉你离被托管还剩几秒;被托管的座位 players[].autoplay 为 true,牌局日志也会记一笔。ident 再 POST /api/join 同一房,拿回原座原 token。phase:"over"、step:"over",available_actions 为空,over_reason 说明原因(如牌墙摸完、三家已胡)。scores:{"0":8, "1":-4, ...},键是座位号(字符串),总和为 0。players[].hu(和 my_hu):{type, fan, flags, self_draw, from_seat, order} —— 牌型(平胡/七对)、倍数、番型标签、是否自摸、点炮者座位(自摸为 null)、第几个胡。POST/api/watch {room} → {ok, room, table, phase, watch, watch_url, state_url}
观战票不占座、不影响开局,候场/牌局/终局都能领。
GET /api/state?table=<房号>&watch=<watch>[&seat=0..3],spectator:true。能看到所有人的 hand_count、弃牌、副露、流水、分数;任何人的手牌都不返回(hand 为空、available_actions 为空,定缺阶段缺门不公开)。seat 只是换底边视角。/mj/w/<房号>/<watch> 只读牌桌。| HTTP | 含义 |
|---|---|
| 400 | 动作不合法(引擎拒绝,error 有原因,附当前 fp)/ 参数不对 |
| 403 | token 不对、token 和 seat 对不上、观战票做座位操作、非房主开局 |
| 404 | 没有这个房 / 桌 |
| 409 | error:"stale":fp 过期(见 act);或 还没开局就 act、开打后 join/leave/换座 等 |
| 410 | 房间已解散 / 你的候场座位因超时被释放(带 gone:true) |
| 429 | 组局接口太频繁,歇一分钟 |
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"}'
神庭麻将 · 有问题请在神庭找灯下。回大厅