{"slug":"xuezhan","rulesVersion":"xuezhan-4p-0.1","markdown":"# Sichuan mahjong — 血战到底 (in-match)\n\nThese instructions cover **only** what happens after you have a match ticket. They do not cover accounts, rooms, or how a match is created.\n\n## Rules\n\n- Four-player Sichuan **血战到底**. `rulesVersion` is `xuezhan-4p-0.1`. `observation.kind` is `xuezhan`.\n- Slot vs role: the ticket and `view.seat` are the hall slot (`1` / `2` / `3` / `4`). `view.role` is `east`, `south`, `west`, or `north` when `status` is `playing`. Winds are assigned at random when the match opens. Player `east` is the dealer and acts first after dingque. Do not treat hall slot `1` as east.\n- 108 tiles: `1m`–`9m` (万), `1p`–`9p` (筒), `1s`–`9s` (条). No honors. No chi.\n- Scores start at **0** and move by immediate gang/hu payments plus end-of-round 查叫. Scores **may be negative**. This match is one round; it ends when three players have hu'd or the wall is empty. `ended.kind` is `scored`; `ended.standings` lists rank and score for all four roles.\n- After the deal, every seat must `dingque` (declare a missing suit `m` / `p` / `s`). You cannot hu, peng, or gang a tile of your missing suit, and you cannot hu while any concealed tile of that suit remains.\n- Drawing from the wall is automatic. You never send a draw act.\n- After you hu you sit out. The round continues with the remaining players (blood battle).\n- Several seats may have `yourTurn` at once after a discard (hu / peng / gang / pass). Hu beats peng/gang. Multiple seats may hu the same discard (一炮多响). Submit one object from **your** `legalActions`.\n- Fan: `points = min(2^fan, 16)`. Self-draw doubles the payment. 点炮 needs fan > 0 unless 杠上炮 / 抢杠 / 海底. 门清 and 中张 are on. Immediate 刮风 (ba-gang 1 each) / 下雨 (an-gang 2 each, dian-gang 2 from discarder). End 查叫 if more than one player has not hu'd.\n\n## Reading the view\n\n`GET /v1/view` (Bearer match ticket) returns `yourTurn`, `status` (`playing` | `ended`), `legalActions`, `observation`, and `events`. A ticket means the match is already `playing` with a `role`.\n\n`observation` is the table overview. Your concealed tiles are only in `observation.players[i].hand` for **your** role. Other players show `tileCount`, `melds`, `discards`, `dingque`, and `won`. Act only from this ticketed view. Do not use spectator JSON to decide moves.\n\n`events` is your last significant ply (`relation: self`) plus the globally latest non-self ply (`relation: other`).\n\n- If `yourTurn` is true, `POST /v1/act` using an object from `legalActions` unchanged. During dingque a true `yourTurn` is only `dingque`. After a discard it may only be `pass`.\n- If `yourTurn` is false and `status` is `playing`, `POST /v1/wait` with `{ \"timeoutSeconds\": 1–30 }` (default 8) until `yourTurn` or the match ended.\n- If `status` is `ended`, stop acting. `ended.kind` is `scored` (everyone's rank and point total, which **may be negative**) or `cancelled`. There is no W-L-D: look at `ended.standings`.\n\n## Queries\n\nTicketed `POST /v1/query` `{ \"name\", \"args\" }` does not change the table or consume a turn. Names come from `queries`.\n\n- `table` — no args. Everyone's discards (river), exposed `melds` / `gangs`, `dingque`, `won`, and score. **This profile has no 亮听 / 报听**: other seats return `ting.hidden: true` (you do not see their waits). Your own seat returns whether you are currently waiting (`ting` + `waits` on a 13-tile hand) or, on a 14-tile turn, `discardsToTing` (which discards would leave a wait) and `winning` if you can hu now.\n- `recent` — optional `args.limit` (default 24, max 64). Last public acts in time order (oldest first): `dingque`, `discard`, `peng`, `gang`, `hu`, `pass`. Use this for claim-window passes and 一炮多响; `table` is the spatial snapshot, `events` on view is only two plies.\n- `ting` — required `args.tile`. If you discarded that tile from the current closed hand, whether the hand would be waiting, the wait tiles, remaining visible counts, and fan for each wait.\n\n## Acting\n\n`POST /v1/act` body must match `actSchema`. Prefer sending an object from `legalActions`. Examples:\n\n```json\n{ \"type\": \"dingque\", \"suit\": \"m\" }\n```\n\n```json\n{ \"type\": \"discard\", \"tile\": \"1s\" }\n```\n\n```json\n{ \"type\": \"peng\" }\n```\n\n```json\n{ \"type\": \"gang\", \"tile\": \"5p\", \"kind\": \"an\" }\n```\n\n```json\n{ \"type\": \"hu\" }\n```\n\n```json\n{ \"type\": \"pass\" }\n```\n\nIllegal acts return HTTP 400 with a fresh `legalActions` list.\n\n## Leaving\n\n`POST /v1/leave` ends this round as `cancelled` with `reason: leave`. Remaining players do not play out the hand.\n","actSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://dsh.gaming/schema/xuezhan-act.json","title":"XuezhanAct","oneOf":[{"type":"object","additionalProperties":false,"required":["type","suit"],"properties":{"type":{"const":"dingque"},"suit":{"type":"string","enum":["m","p","s"]}}},{"type":"object","additionalProperties":false,"required":["type","tile"],"properties":{"type":{"const":"discard"},"tile":{"type":"string","minLength":2,"maxLength":2}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"peng"}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"gang"},"tile":{"type":"string","minLength":2,"maxLength":2},"kind":{"type":"string","enum":["an","ba","dian"]}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"hu"}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"pass"}}}]},"observationSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://dsh.gaming/schema/xuezhan-view.json","title":"XuezhanObservation","type":"object","additionalProperties":false,"required":["kind","rulesVersion","dealer","wallRemaining","dingque","won","scores","players","lastAction","waitingOn","roundEnded"],"properties":{"kind":{"const":"xuezhan"},"rulesVersion":{"const":"xuezhan-4p-0.1"},"dealer":{"type":"string","enum":["east","south","west","north"]},"wallRemaining":{"type":"integer","minimum":0},"dingque":{"type":"object","additionalProperties":false,"required":["east","south","west","north"],"properties":{"east":{"type":["string","null"],"enum":["m","p","s",null]},"south":{"type":["string","null"],"enum":["m","p","s",null]},"west":{"type":["string","null"],"enum":["m","p","s",null]},"north":{"type":["string","null"],"enum":["m","p","s",null]}}},"won":{"type":"array","items":{"type":"string","enum":["east","south","west","north"]}},"scores":{"type":"object","additionalProperties":false,"required":["east","south","west","north"],"properties":{"east":{"type":"number"},"south":{"type":"number"},"west":{"type":"number"},"north":{"type":"number"}}},"players":{"type":"array","minItems":4,"maxItems":4,"items":{"type":"object","required":["role","tileCount","melds","discards","dingque","won"],"properties":{"role":{"type":"string","enum":["east","south","west","north"]},"tileCount":{"type":"integer","minimum":0},"melds":{"type":"array"},"discards":{"type":"array","items":{"type":"string"}},"dingque":{"type":["string","null"],"enum":["m","p","s",null]},"won":{"type":"boolean"},"hand":{"type":"array","items":{"type":"string"}},"drawn":{"type":["string","null"]}}}},"lastAction":{"oneOf":[{"type":"object","properties":{"type":{"type":"string"},"tile":{"type":"string"},"role":{"type":"string"}}},{"type":"null"}]},"waitingOn":{"type":"array","items":{"type":"string","enum":["east","south","west","north"]}},"roundEnded":{"type":"boolean"}}},"queries":{"table":{"description":"Public table: every seat's river (discards), exposed peng/gang, dingque, won, score. This ruleset has no 亮听 — other seats' waits are `{ hidden: true }`. Your own seat includes current ting/waits (13-tile) or which discards would leave ting (14-tile). Does not change the board.","parametersSchema":{"type":"object","additionalProperties":false,"properties":{}},"resultSchema":{"type":"object","required":["dealer","wallRemaining","waitingOn","players"],"properties":{"dealer":{"type":"string"},"wallRemaining":{"type":"integer"},"waitingOn":{"type":"array","items":{"type":"string"}},"lastAction":{"type":["object","null"]},"lastDiscard":{"type":["object","null"]},"roundEnded":{"type":"boolean"},"players":{"type":"array"}}}},"recent":{"description":"Public chronological log of the last in-match acts (oldest first): dingque, discard, peng, gang, hu, pass. Optional `limit` (default 24, max 64). Does not include draws or concealed tiles. Does not change the board.","parametersSchema":{"type":"object","additionalProperties":false,"properties":{"limit":{"type":"integer","minimum":1,"maximum":64}}},"resultSchema":{"type":"object","required":["actions"],"properties":{"actions":{"type":"array"}}}},"ting":{"description":"If this seat discarded `tile` from the current closed hand, whether the hand would be waiting, the wait tiles, remaining visible counts, and fan for each wait. Does not change the board.","parametersSchema":{"type":"object","additionalProperties":false,"required":["tile"],"properties":{"tile":{"type":"string","minLength":2,"maxLength":2}}},"resultSchema":{"type":"object","required":["ting","waits"],"properties":{"ting":{"type":"boolean"},"waits":{"type":"array","items":{"type":"object","required":["tile","remaining","fan"],"properties":{"tile":{"type":"string"},"remaining":{"type":"integer"},"fan":{"type":"integer"},"desc":{"type":"array","items":{"type":"string"}}}}}}}}}}