Giter Site home page Giter Site logo

mjai.app's Introduction

mjai-simulator

AI Jansou is a platform for mahjong AI competition. This repository contains the implementation of mahjong game simulator for evaluating submission files on AI Jansou.

Usage

You can simulate a mahjong game by specifying submission files as shown in the code below. The Simulator runs Docker internally. The docker command must be installed and be available to run with user privileges.

import mjai

submissions = [
    "examples/shanten.zip",
    "examples/tsumogiri.zip",
    "examples/tsumogiri.zip",
    "examples/invalidbot2.zip",
]
mjai.Simulator(submissions, logs_dir="./logs").run()

Docker Image

Submission files are deployed in a Docker container and run as a Docker container.

This repository contains Dockerfile and other resources that will be used to create the Docker container. The docker image is pushed to Docker Hub (docker.io/smly/mjai-client:v3).

Submission format

Please prepare a program that outputs the appropriate mjai protocol message to the standard output when given input in the mjai protocol format from standard input. Name this program "bot.py" and pack it into a zip file. The zip file should contain bot.py directly under the root directory.

bot.py must be a Python script, but it is also possible to include precompiled libraries if they are executable. The program will be executed in a linux/amd64 environment. The submission file must be 1000 MB or less. bot.py takes a player ID as its first argument. Player ID must be 0, 1, 2, or 3. Player ID 0 represents the chicha 起家, and subsequent numbers represent the shimocha 下家, toimen 対面 or kamicha 上家 of the chicha 起家. See example code for details.

Timeout

When the mjai.Simulator instance creates an environment to run the submission file in docker, it specifies the --platform linux/amd64 option. If you want to run on a different architecture, you will have to emulate and run the container, which will be much slower. If you are debugging on an architecture other than linux/amd64, you can avoid timeout errors by relaxing the timeout limit. Specify the timeout argument as follows. The timeout is set to 2.0 by default.

Simulator(submissions, logs_dir="./logs", timeout=10.0).run()

If the simulation is interrupted, the docker container may not terminate successfully. If past docker containers remain, the new docker container may fail to launch due to duplicate HTTP ports. You can remove all docker containers that have the smly/mjai-client image as an ancestor in a batch as follows:

% for cid in `docker ps -a --filter ancestor=smly/mjai-client:v3 --format "{{.ID}}"`; do docker rm -f $cid; done

Protocol

The protocol is basically based on the Mjai Protocol, but customized based on Mortal's Mjai Engine implementation. The following points are customized:

  • Messages are sent and received by standard input and standard output.
  • The game server sends messages collectively up to the event that the player can act on.
  • When the player does not send the appropriate event message, it is treated as a chombo and a mangan-sized penalty is paid.
  • If the player does not send a message within 2 seconds, it is treated as a chombo and a mangan-sized penalty is paid.
  • The first kyoku is not necessarily East 1.

The first kyoku is not necessarily East 1

エラーが発生した局は流局扱いとなり、すべてのプレイヤーのプログラムは終了させられる。 すべてのプレイヤーのプログラムは再起動され、エラーが発生した次の局からゲームが再開される。 そのためプレイヤーのプログラムが初めて受け取る局は東 1 局とは限らない。

The following are messages sent and received as seen from player 0. <- denotes events for the player. -> denotes events from the player.

# Game resumed from S1-1
0 <- [{"type":"start_game","names":["0","1","2","3"],"id":0}]
0 -> {"type":"none"}
# S1-1 流局による終局イベント (first actionable event が来る前に局が終わる)
# NOTE: No ryukyoku events are sent from the game server.
0 <- [{"type":"end_kyoku"}]
0 -> {"type":"none"}
# S2-2 first tsumo tile
0 <- [{"type":"start_kyoku","bakaze":"S","dora_marker":"1p","kyoku":2,"honba":2,"kyotaku":0,"oya":1,"scores":[800,61100,11300,26800],"tehais":[["4p","4s","P","3p","1p","5s","2m","F","1m","7s","9m","6m","9s"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]]},{"type":"tsumo","actor":1,"pai":"?"},{"type":"dahai","actor":1,"pai":"F","tsumogiri":false},{"type":"tsumo","actor":2,"pai":"?"},{"type":"dahai","actor":2,"pai":"3m","tsumogiri":true},{"type":"tsumo","actor":3,"pai":"?"},{"type":"dahai","actor":3,"pai":"1m","tsumogiri":true},{"type":"tsumo","actor":0,"pai":"3s"}]
0 -> {"type":"dahai","pai":"3s","actor":0,"tsumogiri":true}

Case Study: Furiten (振聴)

When the player has already made a tenpai but the hand is furiten. Since the player cannot Ron, even if the waiting tile is discarded by the opponent, no action is possible. For example, let's say you have 2333678m 678s 678p. The waiting tile is 14m and 1m has already been discarded. Since the hand is a Furiten, even if the other player discards 1m, the player cannot Ron.

3 <- [{"type":"dahai","actor":3,"pai":"P","tsumogiri":true},{"type":"tsumo","actor":0,"pai":"?"},{"type":"dahai","actor":0,"pai":"2p","tsumogiri":true},{"type":"tsumo","actor":1,"pai":"?"},{"type":"dahai","actor":1,"pai":"4m","tsumogiri":true},{"type":"tsumo","actor":2,"pai":"?"},{"type":"dahai","actor":2,"pai":"6m","tsumogiri":true}]

In this case, immediately after actor 2 discards 6m, input is given to actor 3. actor 3 needs to decide whether to call chi on 6m.

Case Study: Ankan (暗槓)

In the case of an ankan, the dora event comes first, followed by the tsumo event.

3 -> {"type": "ankan", "actor": 3, "consumed": ["6s", "6s", "6s", "6s"]}
3 <- [{"type":"ankan","actor":3,"consumed":["6s","6s","6s","6s"]},{"type":"dora","dora_marker":"6p"},{"type":"tsumo","actor":3,"pai":"7p"}]
3 -> {"type":dahai","actor":3,"pai":"7p","tsumogiri":true}

For Developers

Debug with interactive shell

The procedures executed by Simulator can be checked and debugged one by one as follows:

# pull latest docker image
% docker pull docker.io/smly/mjai-client:v3

# launch
% CONTAINER_ID=`docker run -d --rm -p 28080:3000 --mount "type=bind,src=/Users/smly/gitws/mjai.app/examples/rulebase.zip,dst=/bot.zip,readonly" smly/mjai-client:v3 sleep infinity`

# install
% docker exec ${CONTAINER_ID} unzip -q /bot.zip
% docker cp python/mjai/http_server/server.py ${CONTAINER_ID}:/workspace/00__server__.py

# debug
% docker exec -it ${CONTAINER_ID} /workspace/.pyenv/shims/python -u bot.py 0
[{"type":"start_game","id":0}]  <-- Input
{"type":"none"}  <-- Output
[{"type":"start_kyoku","bakaze":"E","dora_marker":"2s","kyoku":1,"honba":0,"kyotaku":0,"oya":0,"scores":[25000,25000,25000,25000],"tehais":[["E","6p","9m","8m","C","2s","7m","S","6m","1m","S","3s","8m"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]]},{"type":"tsumo","actor":0,"pai":"1m"}]  <-- Input
{"type": "dahai", "actor": 0, "pai": "C", "tsumogiri": false}  <-- Output
[{"type":"dahai","actor":0,"pai":"C","tsumogiri":false},{"type":"tsumo","actor":1,"pai":"?"},{"type":"dahai","actor":1,"pai":"3m","tsumogiri":false},{"type":"tsumo","actor":2,"pai":"?"},{"type":"dahai","actor":2,"pai":"1m","tsumogiri":false}]  <-- Input
{"type": "none"}  <-- Output
[{"type":"tsumo","actor":3,"pai":"?"},{"type":"dahai","actor":3,"pai":"1m","tsumogiri":false}]  <-- Input
{"type": "none"}  <-- Output
[{"type":"tsumo","actor":0,"pai":"C"}]  <-- Input
{"type": "dahai", "actor": 0, "pai": "C", "tsumogiri": true}  <-- Output

Debug with http sever

# http server mode. `0` is the player index.
% docker exec -it ${CONTAINER_ID} /workspace/.pyenv/shims/python 00__server__.py 0
% curl http://localhost:28080/
OK

% curl -X POST -d '[{"type":"start_game","id":0}]' http://localhost:28080/
{"type":"none"}

% cat > request.json
[{"type":"start_kyoku","bakaze":"E","dora_marker":"2s","kyoku":1,"honba":0,"kyotaku":0,"oya":0,"scores":[25000,25000,25000,25000],"tehais":[["E","6p","9m","8m","C","2s","7m","S","6m","1m","S","3s","8m"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]]},{"type":"tsumo","actor":0,"pai":"1m"}]

% curl -X POST -d '@request.json' http://localhost:28080/
{"type":"dahai","actor":0,"pai":"6p","tsumogiri":false}

Development

Confirmed working with rustc 1.70.0 (90c541806 2023-05-31).

Special Thanks

The code in ./src directory is Mortal's libriichi with minor updates. Mortal is distributed under the AGPL-3.0 and is copyrighted by Equim.

mjai.app's People

Contributors

renovate[bot] avatar smly avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

mjai.app's Issues

`tehai_mjai()` gets incorrect output when there is aka dora

Describe the bug

@property
def tehai_mjai(self) -> list[str]:
"""
Player's hand as a list of tile strings in mjai format.
Example:
>>> bot.tehai_mjai
["1m", "2m", "6m", "9m", "1p", "3p", "4p", "3s", "4s", "5s",
"7s", "9s", "5z", "6z"]
"""
zi_map = ["E", "S", "W", "N", "P", "F", "C"]
ms, ps, ss, zis, akas = [], [], [], [], []
tiles = []
for tile_idx, tile_count in enumerate(self.player_state.tehai):
if tile_count and tile_idx == 4 and self.akas_in_hand[0]:
akas.append("5mr")
elif tile_count and tile_idx == 4 + 9 and self.akas_in_hand[1]:
akas.append("5pr")
elif tile_count and tile_idx == 4 + 18 and self.akas_in_hand[2]:
akas.append("5sr")
elif tile_count and tile_idx < 9:
ms += [f"{tile_idx + 1}m"] * tile_count
elif tile_count and tile_idx < 18:
ps += [f"{tile_idx - 9 + 1}p"] * tile_count
elif tile_count and tile_idx < 27:
ss += [f"{tile_idx - 18 + 1}s"] * tile_count
else:
for _ in range(tile_count):
zis.append(zi_map[tile_idx - 27])
tiles = ms + ps + ss + zis + akas
return tiles

When both 5mr and 5m in hand, output only gives '5mr'.

To Reproduce
Example contidion:

self.player_state.tehai = [
    2,1,1,1,4,1,1,1,1,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0
]
self.akas_in_hand = [True, False, False]

Output:

['1m', '1m', '2m', '3m', '4m', '6m', '7m', '8m', '9m', '5mr']

Expected behavior
Should output:

['1m', '1m', '2m', '3m', '4m', '5m', '5m', '5m', '6m', '7m', '8m', '9m', '5mr']

Additional context
A simple fix:

     @property 
     def tehai_mjai(self) -> list[str]: 
         """ 
         Player's hand as a list of tile strings in mjai format. 
  
         Example: 
             >>> bot.tehai_mjai 
             ["1m", "2m", "6m", "9m", "1p", "3p", "4p", "3s", "4s", "5s", 
              "7s", "9s", "5z", "6z"] 
         """ 
         zi_map = ["E", "S", "W", "N", "P", "F", "C"] 
         ms, ps, ss, zis, akas = [], [], [], [], [] 
         tiles = [] 
         for tile_idx, tile_count in enumerate(self.player_state.tehai): 
             if tile_count and tile_idx == 4 and self.akas_in_hand[0]: 
                 ms += [f"{tile_idx + 1}m"] * (tile_count-1)
                 akas.append("5mr") 
             elif tile_count and tile_idx == 4 + 9 and self.akas_in_hand[1]: 
                 ms += [f"{tile_idx + 1}p"] * (tile_count-1)
                 akas.append("5pr") 
             elif tile_count and tile_idx == 4 + 18 and self.akas_in_hand[2]: 
                 ms += [f"{tile_idx + 1}s"] * (tile_count-1)
                 akas.append("5sr") 
             elif tile_count and tile_idx < 9: 
                 ms += [f"{tile_idx + 1}m"] * tile_count 
             elif tile_count and tile_idx < 18: 
                 ps += [f"{tile_idx - 9 + 1}p"] * tile_count 
             elif tile_count and tile_idx < 27: 
                 ss += [f"{tile_idx - 18 + 1}s"] * tile_count 
             else: 
                 for _ in range(tile_count): 
                     zis.append(zi_map[tile_idx - 27]) 
  
         tiles = ms + ps + ss + zis + akas 
         return tiles 

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

cargo
Cargo.toml
  • anyhow 1
  • log 0.4
  • pyo3-log 0.8
  • once_cell 1
  • serde_json 1
  • boomphf 0.6
  • byteorder 1
  • rayon 1
  • ndarray 0.15
  • numpy 0.19
  • serde_with 3
  • rand 0.8
  • rand_chacha 0.3
  • flate2 1
  • sha3 0.10
  • glob 0.3
  • derivative 2
  • ahash 0.8
  • tinyvec 1
  • serde 1
  • mimalloc 0.1
  • pyo3 0.19
github-actions
.github/workflows/CI.yml
  • actions/checkout v4
  • actions/setup-python v4
  • PyO3/maturin-action v1
  • actions/upload-artifact v3
  • actions/checkout v4
  • actions/setup-python v4
  • PyO3/maturin-action v1
  • actions/upload-artifact v3
  • actions/checkout v4
  • actions/setup-python v4
  • PyO3/maturin-action v1
  • actions/upload-artifact v3
  • actions/checkout v4
  • PyO3/maturin-action v1
  • actions/upload-artifact v3
  • actions/download-artifact v3
.github/workflows/cargo_test.yml
  • actions/checkout v4
  • actions-rs/toolchain v1
  • actions-rs/cargo v1
  • actions-rs/cargo v1
  • actions-rs/cargo v1
.github/workflows/lint.yml
  • actions/checkout v4
  • technote-space/get-diff-action v6
  • actions/setup-python v4
  • PyO3/maturin-action v1
.github/workflows/pytest.yml
  • actions/checkout v4
  • technote-space/get-diff-action v6
  • actions/setup-python v4
  • PyO3/maturin-action v1
pep621
pyproject.toml
  • mahjong ~= 1.2.0
  • test/pytest ~=7.4.0
  • maturin >=1.1,<2.0
pip_requirements
docker/requirements.txt
  • torch ==2.0.1
  • numpy ==1.25.2

  • Check this box to trigger a request for Renovate to run again on this repository

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

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.

Comment of `Bot.last_self_tsumo()`

Describe the bug

@property
def last_self_tsumo(self) -> str:
"""
Last tile that the player drew by itself.
Tile format is mjai-style like '5mr' or 'P'.
Return a empty string when the player's first action is not tsumo.
"""
return self.player_state.last_self_tsumo() or ""

Return a empty string when the player's first last action is not tsumo.

Bot.action_kakan() uses wrong pai

Describe the bug

def action_kakan(self, consumed: list[str]) -> str:
return json.dumps(
{
"type": "kakan",
"actor": self.player_id,
"pai": self.last_kawa_tile,
"consumed": consumed, # 3 tiles to be consumed
},
separators=(",", ":"),
)

Here at line 434, pai should be the tile that add to the pon meld but not last_kawa_tile

Additional context
A fix:

    def action_kakan(self, consumed: list[str]) -> str:
        if consumed[0][0] == '5' and consumed[0][1] != 'z':
            if any(red in consumed for red in ['5mr', '5pr', '5sr']):
                pai = consumed[0][:2]
            else:
                pai = consumed[0]+'r'
        else:
            pai = consumed[0]
        return json.dumps(
            {
                "type": "kakan",
                "actor": self.player_id,
                "pai": pai,
                "consumed": consumed,  # 3 tiles to be consumed
            },
            separators=(",", ":"),
        )

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.