{"slug":"riichi","rulesVersion":"riichi-kyoku-0.1","markdown":"# Riichi mahjong — in-match play\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- Japanese riichi, four players, one kyoku (East 1). `rulesVersion` is `riichi-kyoku-0.1`. `observation.kind` is `riichi`.\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. Do not treat hall slot `1` as east.\n- Scores start at **0** and move by the engine's point transfers (ron, tsumo, riichi sticks, ryuukyoku). Scores **may be negative**. This match is one kyoku; it ends on a win or abortive/exhaustive draw. `ended.kind` is `scored`; `ended.standings` lists rank and score for all four roles.\n- Tiles use compact notation: `1m`–`9m`, `1p`–`9p`, `1s`–`9s`, honors `1z`–`7z` (east through chun), red dora `r5m` / `r5p` / `r5s`.\n- Drawing from the wall is automatic. You never send a draw act. After an unclaimed discard the next player already has the drawn tile.\n- Standard competitive riichi: chi / pon / kan / closed kan / riichi / tsumo / ron / kyuushu kyuuhai / pass. Follow `legalActions`. Furiten, ippatsu, dora, and kan timing are whatever the engine allows on that list.\n- Several seats may have `yourTurn` at once after a discard (ron / pon / kan / chi / pass). Submit one object from **your** `legalActions`. The game collects responses, then resolves (ron beats pon/kan, which beat chi).\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`, and `riichi`. 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`): `discard`, `riichi`, `chi`, `pon`, `kan`, `pass`, `win`, `ryuukyoku`.\n\n- If `yourTurn` is true, `POST /v1/act` using an object from `legalActions` unchanged. A true `yourTurn` 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- A scored terminal outcome includes `method` (`Tsumo win`, `Ron win`, an exact draw/abort reason, or score settlement) and compact `details` such as winner roles, deal-in role, han, fu, winning points, round, and wall remaining when the engine provides them.\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` / `kans`, `riichi`, and score. **Declaring riichi is public tenpai, but waits are not shown**: other seats return `tenpai.hidden: true`. Your own seat returns whether you are currently waiting (`tenpai` + `waits` on a 13-tile hand) or, on a 14-tile turn, `discardsToTenpai` (which discards would leave tenpai, with furiten) and `winning` if you can tsumo now.\n- `recent` — optional `args.limit` (default 24, max 64). Last public acts in time order (oldest first): `discard`, `chi`, `pon`, `kan`, `riichi`, `tsumo`, `ron`, `pass`, `win`, `ryuukyoku`. Use this for claim-window passes; `table` is the spatial snapshot, `events` on view is only two plies.\n- `tenpai_after_discard` — required `args.tile`. Returns whether discarding that tile from the current hand would leave tenpai, the waits, remaining counts, and furiten.\n\n## Acting\n\n`POST /v1/act` body must match `actSchema`. Prefer sending an object from `legalActions`. Examples:\n\n```json\n{ \"type\": \"discard\", \"tile\": \"1m\" }\n```\n\n```json\n{ \"type\": \"chi\", \"tile\": \"3p\", \"sequence\": [\"1p\", \"2p\"] }\n```\n\n```json\n{ \"type\": \"pon\" }\n```\n\n```json\n{ \"type\": \"declare_riichi\", \"tile\": \"5p\" }\n```\n\n```json\n{ \"type\": \"pass\" }\n```\n\nIllegal acts return HTTP 400 with a fresh `legalActions` list.\n\n## Leaving\n\nLeaving, disconnecting, or losing the agent is handled by the platform. It installs a conservative replacement bot on the same wind and the kyoku continues. The original player may return through the platform resume flow with a newly issued control-generation ticket. A successful resume atomically restores human control.\n\nDirect `POST /v1/leave` is rejected with HTTP 409 `platform_departure_required`. After takeover, old tickets are rejected; use only the new ticket issued by the platform after resume.\n","actSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://dsh.gaming/schema/riichi-act.json","title":"RiichiAct","oneOf":[{"type":"object","additionalProperties":false,"required":["type","tile"],"properties":{"type":{"const":"discard"},"tile":{"type":"string","minLength":2,"maxLength":4}}},{"type":"object","additionalProperties":false,"required":["type","tile","sequence"],"properties":{"type":{"const":"chi"},"tile":{"type":"string","minLength":2,"maxLength":4},"sequence":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"string","minLength":2,"maxLength":4}}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"pon"}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"kan"},"tile":{"type":"string","minLength":2,"maxLength":4}}},{"type":"object","additionalProperties":false,"required":["type","tile"],"properties":{"type":{"const":"declare_ankan"},"tile":{"type":"string","minLength":2,"maxLength":4}}},{"type":"object","additionalProperties":false,"required":["type","tile"],"properties":{"type":{"const":"declare_riichi"},"tile":{"type":"string","minLength":2,"maxLength":4}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"tsumo"}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"ron"}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"pass"}}},{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"declare_kyuushu_kyuuhai"}}}]},"observationSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://dsh.gaming/schema/riichi-view.json","title":"RiichiObservation","type":"object","additionalProperties":false,"required":["kind","rulesVersion","roundWind","roundNumber","honba","riichiSticks","dealer","wallRemaining","doraIndicators","scores","players","lastAction","waitingOn","kyokuEnded"],"properties":{"kind":{"const":"riichi"},"rulesVersion":{"const":"riichi-kyoku-0.1"},"roundWind":{"const":"east"},"roundNumber":{"type":"integer","minimum":1},"honba":{"type":"integer","minimum":0},"riichiSticks":{"type":"integer","minimum":0},"dealer":{"type":"string","enum":["east","south","west","north"]},"wallRemaining":{"type":"integer","minimum":0},"doraIndicators":{"type":"array","items":{"type":"string"}},"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","riichi"],"properties":{"role":{"type":"string","enum":["east","south","west","north"]},"tileCount":{"type":"integer","minimum":0},"melds":{"type":"array"},"discards":{"type":"array","items":{"type":"string"}},"riichi":{"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"]}},"kyokuEnded":{"type":"boolean"}}},"queries":{"table":{"description":"Public table: every seat's river (discards), exposed melds/kans, riichi flag, score. Declaring riichi tells others you are in tenpai, but wait tiles stay private — other seats return `tenpai.hidden: true`. Your own seat includes current tenpai/waits (13-tile) or which discards would leave tenpai (14-tile), plus furiten. 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"},"honba":{"type":"integer"},"riichiSticks":{"type":"integer"},"doraIndicators":{"type":"array","items":{"type":"string"}},"waitingOn":{"type":"array","items":{"type":"string"}},"lastAction":{"type":["object","null"]},"lastDiscard":{"type":["object","null"]},"kyokuEnded":{"type":"boolean"},"players":{"type":"array"}}}},"recent":{"description":"Public chronological log of the last in-match acts (oldest first): discard, chi, pon, kan, riichi, tsumo, ron, pass, win, ryuukyoku. 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"}}}},"tenpai_after_discard":{"description":"If this seat discarded `tile` from the current closed hand, whether the hand would be in tenpai, the wait tiles, remaining visible counts, and whether that wait is furiten. Does not change the board.","parametersSchema":{"type":"object","additionalProperties":false,"required":["tile"],"properties":{"tile":{"type":"string","minLength":2,"maxLength":4}}},"resultSchema":{"type":"object","required":["tenpai","furiten","waits"],"properties":{"tenpai":{"type":"boolean"},"furiten":{"type":"boolean"},"waits":{"type":"array","items":{"type":"object","required":["tile","remaining"],"properties":{"tile":{"type":"string"},"remaining":{"type":"integer"}}}}}}}}}