smly/mjai.app

A legitimate `hora` is not accepted and results in an error

Closed this issue · 5 comments

Describe the bug
As stated in the title.

Log
The log folder is attached as a .zip file to this bug report.

Expected behavior
The following is an excerpt from mjai_log.json related to the round where the error occurred. Note that after the log of each dahai by Player 1, I provide the Player 1's hand and the meldings (副露) immediately following the dahai.

{"type":"start_kyoku","bakaze":"E","dora_marker":"6s","kyoku":4,"honba":0,"kyotaku":0,"oya":3,"scores":[12800,52600,22500,12100],"tehais":[["7s","7m","9m","7p","P","8s","4s","8s","F","2m","7s","5pr","7p"],["9m","8p","4p","N","3m","4p","E","8s","2p","S","6m","C","8m"],["5s","5p","W","1m","3s","9s","1p","1s","C","2m"$
{"type":"tsumo","actor":3,"pai":"1s"}
{"type":"dahai","actor":3,"pai":"W","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"5s"}
{"type":"dahai","actor":0,"pai":"F","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"4m"}
{"type":"dahai","actor":1,"pai":"N","tsumogiri":false},"3m","4m","6m","8m","9m","2p","4p","4p","8p","8s","E","S","C"
{"type":"tsumo","actor":2,"pai":"W"}
{"type":"dahai","actor":2,"pai":"9p","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"4m"}
{"type":"dahai","actor":3,"pai":"S","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"F"}
{"type":"dahai","actor":0,"pai":"F","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"3p"}
{"type":"dahai","actor":1,"pai":"S","tsumogiri":false},"3m","4m","6m","8m","9m","2p","3p","4p","4p","8p","8s","E","C"
{"type":"tsumo","actor":2,"pai":"3p"}
{"type":"dahai","actor":2,"pai":"C","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"1p"}
{"type":"dahai","actor":3,"pai":"1m","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"1p"}
{"type":"dahai","actor":0,"pai":"1p","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"9m"}
{"type":"dahai","actor":1,"pai":"C","tsumogiri":false},"3m","4m","6m","8m","9m","9m","2p","3p","4p","4p","8p","8s","E"
{"type":"tsumo","actor":2,"pai":"P"}
{"type":"dahai","actor":2,"pai":"P","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"F"}
{"type":"dahai","actor":3,"pai":"F","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"3p"}
{"type":"dahai","actor":0,"pai":"P","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"E"}
{"type":"dahai","actor":1,"pai":"8p","tsumogiri":false},"3m","4m","6m","8m","9m","9m","2p","3p","4p","4p","8s","E","E"
{"type":"tsumo","actor":2,"pai":"5m"}
{"type":"dahai","actor":2,"pai":"9s","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"N"}
{"type":"dahai","actor":3,"pai":"N","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"7s"}
{"type":"dahai",
[logs.2023-09-02-02-12-52.zip](https://github.com/smly/mjai.app/files/12613400/logs.2023-09-02-02-12-52.zip)
"actor":0,"pai":"2m","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"7p"}
{"type":"dahai","actor":1,"pai":"7p","tsumogiri":true},"3m","4m","6m","8m","9m","9m","2p","3p","4p","4p","8s","E","E"
{"type":"pon","actor":0,"target":1,"pai":"7p","consumed":["7p","7p"]}
{"type":"dahai","actor":0,"pai":"9m","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"4s"}
{"type":"dahai","actor":1,"pai":"8s","tsumogiri":false},"3m","4m","6m","8m","9m","9m","2p","3p","4p","4p","4s","E","E"
{"type":"tsumo","actor":2,"pai":"5mr"}
{"type":"dahai","actor":2,"pai":"5s","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"9p"}
{"type":"dahai","actor":3,"pai":"9p","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"3m"}
{"type":"dahai","actor":0,"pai":"3m","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"6s"}
{"type":"dahai","actor":1,"pai":"4p","tsumogiri":false},"3m","4m","6m","8m","9m","9m","2p","3p","4p","4s","6s","E","E"
{"type":"tsumo","actor":2,"pai":"8p"}
{"type":"dahai","actor":2,"pai":"8p","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"2s"}
{"type":"dahai","actor":3,"pai":"1p","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"1m"}
{"type":"dahai","actor":0,"pai":"7m","tsumogiri":false}
{"type":"chi","actor":1,"target":0,"pai":"7m","consumed":["6m","8m"]}
{"type":"dahai","actor":1,"pai":"6s","tsumogiri":false},"3m","4m","9m","9m","2p","3p","4p","4s","E","E",("7m",("6m","8m"))
{"type":"tsumo","actor":2,"pai":"C"}
{"type":"dahai","actor":2,"pai":"C","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"9s"}
{"type":"dahai","actor":3,"pai":"9s","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"7m"}
{"type":"dahai","actor":0,"pai":"7m","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"4m"}
{"type":"dahai","actor":1,"pai":"4s","tsumogiri":false},"3m","4m","4m","9m","9m","2p","3p","4p","E","E",("7m",("6m","8m"))
{"type":"tsumo","actor":2,"pai":"6p"}
{"type":"dahai","actor":2,"pai":"6p","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"P"}
{"type":"dahai","actor":3,"pai":"P","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"8m"}
{"type":"dahai","actor":0,"pai":"8m","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"6p"}
{"type":"dahai","actor":1,"pai":"6p","tsumogiri":true},"3m","4m","4m","9m","9m","2p","3p","4p","E","E",("7m",("6m","8m"))
{"type":"tsumo","actor":2,"pai":"7m"}
{"type":"dahai","actor":2,"pai":"7m","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"9s"}
{"type":"dahai","actor":3,"pai":"9s","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"S"}
{"type":"dahai","actor":0,"pai":"1m","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"6p"}
{"type":"dahai","actor":1,"pai":"6p","tsumogiri":true},"3m","4m","4m","9m","9m","2p","3p","4p","E","E",("7m",("6m","8m"))
{"type":"tsumo","actor":2,"pai":"P"}
{"type":"dahai","actor":2,"pai":"P","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"5p"}
{"type":"dahai","actor":3,"pai":"1s","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"5m"}
{"type":"dahai","actor":0,"pai":"5m","tsumogiri":true}
{"type":"chi","actor":1,"target":0,"pai":"5m","consumed":["3m","4m"]}
{"type":"dahai","actor":1,"pai":"4m","tsumogiri":false},"9m","9m","2p","3p","4p","E","E",("7m",("6m","8m")),("5m",("3m","4m"))
{"type":"tsumo","actor":2,"pai":"9m"}
{"type":"dahai","actor":2,"pai":"9m","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"E"}
{"type":"dahai","actor":3,"pai":"E","tsumogiri":true} ===> player 1 returns {"type": "hora", "actor": 1, "target": 3, "pai": "E"}
{"type":"ryukyoku","deltas":[2000,-8000,2000,4000],"reason":"error"}
{"type":"end_kyoku"}

As described above, Player 1 declares hora at the end of this round. However, this is flagged as an error. Since 'E' is the round wind, there is a valid hand and 1 han (飜), and it's a legitimate declaration of a win. This win declaration should be correctly accepted.

Here is another example of an unjustified error.

Log
logs.2023-09-02-13-11-44.zip

Expected behavior

{"type":"start_kyoku","bakaze":"S","dora_marker":"9m","kyoku":1,"honba":1,"kyotaku":0,"oya":0,"scores":[34900,32100,22400,10600],"tehais":[["S","7m","5m","2s","F","1m","8p","9m","8m","3p","2m","1m","7s"],["8p","4m","6m","8p","2p","S","N","8m","8s","7p","S","5s","8p"],["4m","2m","5m","6s","3m","5s","2p","7s","7m","9$
{"type":"tsumo","actor":0,"pai":"1s"}
{"type":"dahai","actor":0,"pai":"S","tsumogiri":false},"1m","1m","2m","5m","7m","8m","9m","3p","8p","1s","2s","7s","F"
{"type":"pon","actor":1,"target":0,"pai":"S","consumed":["S","S"]}
{"type":"dahai","actor":1,"pai":"N","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"E"}
{"type":"dahai","actor":2,"pai":"E","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"9s"}
{"type":"dahai","actor":3,"pai":"E","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"2s"}
{"type":"dahai","actor":0,"pai":"F","tsumogiri":false},"1m","1m","2m","5m","7m","8m","9m","3p","8p","1s","2s","2s","7s"
{"type":"tsumo","actor":1,"pai":"C"}
{"type":"dahai","actor":1,"pai":"C","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"3m"}
{"type":"dahai","actor":2,"pai":"C","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"C"}
{"type":"dahai","actor":3,"pai":"C","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"3s"}
{"type":"dahai","actor":0,"pai":"2s","tsumogiri":false},"1m","1m","2m","5m","7m","8m","9m","3p","8p","1s","2s","3s","7s"
{"type":"tsumo","actor":1,"pai":"3p"}
{"type":"dahai","actor":1,"pai":"8s","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"C"}
{"type":"dahai","actor":2,"pai":"C","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"6p"}
{"type":"dahai","actor":3,"pai":"F","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"1m"}
{"type":"dahai","actor":0,"pai":"8p","tsumogiri":false},"1m","1m","1m","2m","5m","7m","8m","9m","3p","1s","2s","3s","7s"
{"type":"tsumo","actor":1,"pai":"7s"}
{"type":"dahai","actor":1,"pai":"7s","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"E"}
{"type":"dahai","actor":2,"pai":"E","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"N"}
{"type":"dahai","actor":3,"pai":"N","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"1p"}
{"type":"dahai","actor":0,"pai":"7s","tsumogiri":false},"1m","1m","1m","2m","5m","7m","8m","9m","1p","3p","1s","2s","3s"
{"type":"tsumo","actor":1,"pai":"2p"}
{"type":"dahai","actor":1,"pai":"5s","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"5m"}
{"type":"dahai","actor":2,"pai":"9s","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"6s"}
{"type":"dahai","actor":3,"pai":"9s","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"N"}
{"type":"dahai","actor":0,"pai":"5m","tsumogiri":false},"1m","1m","1m","2m","N","7m","8m","9m","1p","3p","1s","2s","3s"
{"type":"chi","actor":1,"target":0,"pai":"5m","consumed":["4m","6m"]}
{"type":"dahai","actor":1,"pai":"8m","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"8m"}
{"type":"dahai","actor":2,"pai":"2p","tsumogiri":false}
{"type":"pon","actor":1,"target":2,"pai":"2p","consumed":["2p","2p"]}
{"type":"dahai","actor":1,"pai":"3p","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"S"}
{"type":"dahai","actor":2,"pai":"S","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"5p"}
{"type":"dahai","actor":3,"pai":"W","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"4m"}
{"type":"dahai","actor":0,"pai":"4m","tsumogiri":true},"1m","1m","1m","2m","7m","8m","9m","1p","3p","1s","2s","3s","N"
{"type":"tsumo","actor":1,"pai":"P"}
{"type":"dahai","actor":1,"pai":"P","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"W"}
{"type":"dahai","actor":2,"pai":"W","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"4p"}
{"type":"dahai","actor":3,"pai":"4p","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"2p"}
{"type":"dahai","actor":0,"pai":"N","tsumogiri":false},"1m","1m","1m","2m","7m","8m","9m","1p","2p","3p","1s","2s","3s"
{"type":"tsumo","actor":1,"pai":"2m"}
{"type":"dahai","actor":1,"pai":"2m","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"3m"}
{"type":"dahai","actor":2,"pai":"3m","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"W"}
{"type":"dahai","actor":3,"pai":"W","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"P"} ===> Player 0 returns {"type": "hora", "actor": 0, "target": 2, "pai": "3m"}
{"type":"ryukyoku","deltas":[-12000,4000,4000,4000],"reason":"error"}
{"type":"end_kyoku"}

In the above example, despite Player 0 meeting the conditions to declare hora at the point of {"type":"dahai","actor":2,"pai":"3m","tsumogiri":true}, the system does not pause nor wait for Player 0's output but proceeds to Player 0's tsumo. As a result, this leads to an inconsistency.

@Cryolite Thanks for the report! I checked the first game log with the following code. Seems like it is incorrectly treated as a furiten. I will use a debugger later to find out the detail.

code
import json
from pathlib import Path

from mjai.mlibriichi.state import PlayerState  # type: ignore


def main():
    player = PlayerState(1)

    lines = Path("../../ws/logs.2023-09-02-02-12-52/mjai_log.json").open("r")
    bakaze = "E"
    kyoku = 0
    honba = 0
    for json_line in lines:
        json_line = json_line.strip()
        json_data = json.loads(json_line)
        if json_data["type"] == "start_kyoku":
            bakaze = json_data["bakaze"]
            kyoku = json_data["kyoku"]
            honba = json_data["honba"]

        if bakaze == "E" and kyoku == 4 and honba == 0:
            target_event = (
                '{"type":"dahai","actor":3,"pai":"E","tsumogiri":true}'
            )
            print(f">> {json_line}")
            if json_line.startswith(target_event):
                print(player.brief_info())
                break

        player.update(json_line)


if __name__ == "__main__":
    main()
result
(snip)
>> {"type":"dahai","actor":2,"pai":"9m","tsumogiri":true}
>> {"type":"tsumo","actor":3,"pai":"E"}
>> {"type":"dahai","actor":3,"pai":"E","tsumogiri":true}
player (abs): 1
oya (rel): 2
kyoku: E4-0
turn: 10
jikaze: W
score (rel): [52600, 22500, 12100, 12800]
tehai: 99m 234p 11z
fuuro: [[6m, 8m, 7m], [3m, 4m, 5m]]
ankan: []
tehai len: 2
shanten: 0
furiten: true
waits: [9m, E]
dora indicators: [6s]
doras owned: [0, 0, 0, 0]
doras seen: 0
action candidates: ActionCandidate {
    can_discard: false,
    can_chi_low: false,
    can_chi_mid: false,
    can_chi_high: false,
    can_pon: false,
    can_daiminkan: false,
    can_kakan: false,
    can_ankan: false,
    can_riichi: false,
    can_tsumo_agari: false,
    can_ron_agari: false,
    can_ryukyoku: false,
    target_actor: 3,
}
last self tsumo: None
last kawa tile: Some(9m)
tiles left: 26
kawa:
 0. -   -       W       F
 1. N   9p      S       F^
 2. S   C       1m      1p^
 3. C   P^      F^      P
 4. 8p  9s      N^      2m
 5. 7p^ -       -       (7p7p+7p)9m
 6. 8s  5s      9p^     3m^
 7. 4p  8p^     1p      7m
 8. (6m8m+7m)6s C^      9s^     7m^
 9. 4s  6p^     P^      8m^
10. 6p^ 7m^     9s^     1m
11. 6p^ P^      1s      5m^
12. (3m4m+5m)4m 9m^     -       -

It's 同巡内フリテン. Player1 is waiting for 9m and E. So he cannot win with E in the same cycle as when he ignored 9m.

Oops, I'm sorry for taking up your time. It's completely my fault.