見出し画像

AIのための雀荘mjai.appにMjxのAgentを投稿する

前回の記事で、そこそこ戦えそうな麻雀AIのAgentを作ることができました。このAgentをAI雀荘に投稿してみようと思います。

実装

AI雀荘(mjai.app)はMjxのAgentに対応しており、以下のリポジトリにMjxのサンプルが公開されています。

https://github.com/smly/mjai.app/tree/main/examples/shantenbot

このbot.pyを参考に、MjxのShantenAgentを使っているところを書き換えればよさそうです。
以前の記事で実装していたMLPAgentを使い、↓のように編集しました。

import json
import sys
import random
import torch
from torch import nn, Tensor
import mjx
from mjx import Agent, Observation, Action
from mjx.agents import ShantenAgent
from gateway import MjxGateway, to_mjai_tile


class MLP(nn.Module):
    def __init__(self, obs_size=544, n_actions=181, hidden_size=544):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, n_actions),
        )

    def forward(self, x):
        return self.net(x.float())
    

class MLPAgent(Agent):

    def __init__(self, model) -> None:
        super().__init__()
        self.model = model

    def act(self, observation: Observation) -> Action:
        legal_actions = observation.legal_actions()
        try:
            if len(legal_actions) == 1:
                return legal_actions[0]
            
            # 予測
            feature = observation.to_features(feature_name="mjx-small-v0")
            with torch.no_grad():
                action_logit = self.model(Tensor(feature.ravel()))
            action_proba = torch.sigmoid(action_logit).numpy()
            
            # アクション決定
            mask = observation.action_mask()
            action_idx = (mask * action_proba).argmax()
            return mjx.Action.select_from(action_idx, legal_actions)
        except:
            return random.choice(legal_actions)


def main():
    
    model = MLP()
    model.load_state_dict(torch.load('./model.pth'))
    agent = MLPAgent(model)

    player_id = int(sys.argv[1])
    assert player_id in range(4)
    bot = MjxGateway(player_id, agent)

    while True:
        line = sys.stdin.readline().strip()
        resp = bot.react(line)
        sys.stdout.write(resp + "\n")
        sys.stdout.flush()


if __name__ == "__main__":
    main()

アップロード

編集したbot.pyと、モデルファイル、gateway.py(サンプルそのまま)を以下のように同じフォルダにおいてzipファイルを作ります。
親フォルダを圧縮するのではなく、解凍したら直下にこの3ファイルが見えている状態にする必要があります。Windowsの場合、3ファイルを同時に選択して右クリック→送る→圧縮(zip形式)フォルダーでOKです。

zipファイルを作成したら、https://mjai.app/でtwitter連携してサインアップし、アップロードを行います。
提出後、4つのテストケースが走り、これらに合格するとAccepted状態となりました。

https://mjai.app/upload

【余談】エラー時のローカルでの動作確認方法

zipアップロード時にテストが通らないとき、ローカルで動作確認をする方法を書いておきます。

提出が通らない場合、このような表示になります。

https://mjai.app/upload zip提出時のエラー表示

test case1(対局開始時のケース)でエラーになったことがわかります。
(原因はzip作成時にフォルダごと圧縮してしまっていて、解凍するとフォルダができてしまうことだったのですが、備忘的に手順を残しておきます)

動作確認には、まずmjxの0.1.0を動かすためのLinux環境と、gateway.pyで使われている構文をサポートするpython3.10以上が必要です。
colabではpythonのバージョンが低く動きそうになかったので、Windows上にdocker環境を構築して動作確認しました。

まず Docker Desktop for Windows をインストールします。

次に、ここからDockerfileとrequrements.txtを取得します。
https://github.com/smly/mjai.app/tree/main/docker

この2ファイルをフォルダに配置し、そのフォルダでdocker buildコマンドを使いイメージを作成します。

docker build -t docker-whale .

このコマンドの中で、Dockerfileに書かれているPython-3.10.5のインストールやmjxのインストールが行われるため、1時間程度かかりました。

イメージができたらコンテナを起動し、提出したファイルを置きます。
Windows端末のPowerShellを使い、docker psでコンテナIDを調べ、docker cpでローカルのファイルをコンテナ上にコピーしました。

> docker ps
CONTAINER ID   IMAGE                 COMMAND   CREATED          STATUS          PORTS     NAMES
a32da4ddc41d   docker-whale:latest   "bash"    34 minutes ago   Up 34 minutes             cool_feistel
> docker cp bot.py a32da4ddc41d:/workspace/
> docker cp gateway.py a32da4ddc41d:/workspace/
> docker cp model.pth a32da4ddc41d:/workspace/

次に、Docker DesktopからコンテナのCLIを起動し、環境の状態を確認します。

# bash
root@a32da4ddc41d:~# python -V
Python 3.10.5
root@a32da4ddc41d:~# ls /opt
mjx

pythonとmjxが入っていることが確認できました。
bot.pyを実行すると標準入力を待つ状態になるので、
https://github.com/smly/mjai.app/blob/main/verify_submission.py
に書いてある動作確認用の入力を入れてみます。

> python bot.py 0
[{"type":"start_game","id":0}] 
{"type":"none"}

bot.pyに、プレイヤーIDとして0を与えて起動しています
標準入力に[{"type":"start_game","id":0}]
を与えると{"type":"none"}が出力されています(期待値通り)

さらにdo_test_2に書かれている文字列を入力すると、DEBUGログが出力された後に打牌の文字列が出力されています。

[{"type":"start_kyoku","bakaze":"E","kyoku":1,"honba":0,"kyotaku":0,"oya":0,"dora_marker":"7s","scores":[2500,2500,2500,2500],"tehais":[["3m","4m","3p","5pr","7p","9p","4s","4s","5sr","7s","7s","W","N"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]]},{"type":"tsumo","actor":0,"pai":"3m"}]
2022-10-09 16:40:27.278 | DEBUG    | gateway:_get_mjx_obs:428 - [DEBUG] start_kyoku: [8, 12, 44, 52, 60, 68, 84, 85, 88, 97, 98, 116, 120]
2022-10-09 16:40:27.279 | DEBUG    | gateway:_get_mjx_obs:461 -  - apply tsumo event {'type': 'tsumo', 'actor': 0, 'pai': '3m'} (9)
2022-10-09 16:40:28.278 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":8}
2022-10-09 16:40:28.279 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"type":"ACTION_TYPE_TSUMOGIRI","tile":9}
2022-10-09 16:40:28.279 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":12}
2022-10-09 16:40:28.279 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":44}
2022-10-09 16:40:28.280 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":52}
2022-10-09 16:40:28.280 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":60}
2022-10-09 16:40:28.280 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":68}
2022-10-09 16:40:28.281 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":84}
2022-10-09 16:40:28.281 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":88}
2022-10-09 16:40:28.281 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":97}
2022-10-09 16:40:28.282 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":116}
2022-10-09 16:40:28.282 | DEBUG    | gateway:_get_mjx_obs:637 -  - action: {"tile":120}

{"type":"dahai","actor":0,"pai":"N","tsumogiri":false}

このようにして、作成したbot.pyの動作確認を行うことができます。

この記事が気に入ったらサポートをしてみませんか?