smly / mjai.app

Mahjong game simulator for RiichiLab https://mjai.app
https://github.com/smly/mjai.app/discussions
GNU Affero General Public License v3.0
57 stars 7 forks source link

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

Closed Cryolite closed 11 months ago

Cryolite commented 11 months ago

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.

Cryolite commented 11 months ago

logs.2023-09-02-02-12-52.zip

Cryolite commented 11 months ago

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.

smly commented 11 months ago

@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 ```py 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 ```bash (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^ - - ```
smly commented 11 months ago

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

Cryolite commented 11 months ago

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