生成AIを使ってリアルな案件対応をやってみる〜麻雀の点数計算アプリ失敗編〜

こんにちは！逆瀬川 ( https://x.com/gyakuse ) です！

このアドベントカレンダーでは生成AIのアプリケーションを実際に作り、どのように作ればいいのか、ということをわかりやすく書いていければと思います。アプリケーションだけではなく、プロダクト開発に必要なモデルの調査方法、training方法、基礎知識等にも触れていければと思います。

## 0. 今回の記事について

今日は生成AI系でよくある依頼をもとに、それを解決するまでのリアルな流れについて教えられたらと思います。架空の相談を対象に、実際にどうやって動いているのかみていきましょう。

### 想定する読者

- 生成AI系で働いているみんな

## 1. 相談

働いているとSlackでメッセージが来ました。

>【社長】
逆瀬川ﾁｬﾝおはよ😄社長だヨ😘
忙しいなかすまない😅
今日の20時にオンライン麻雀🀄するんだけど、他の社長にウチの技術を見せつけたくてさ😉
麻雀の点数計算器をAIでさくっと作ってヨ👍

こういう相談が来たときどうするべきでしょうか？

無論、一番いいのは退職届を書いたり殺意を顕にすることですが、それだと話が終わってしまうので、一旦考えてみることにします。

### 要件の抽出と確認

まず何をしたいのか汲み取ります。

>オンライン麻雀
>麻雀の点数計算器

ここが曖昧なポイントです。
一般的なオンライン麻雀ゲームでは、以下のように点数が表示されます。

![雀魂_-じゃんたま-__麻雀を無料で気軽に.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/3368d611-879e-62da-11a8-9b8d0f09f6b0.png)

詳しく聞いてみることにします。

>【逆瀬川】
お疲れ様です🖕👎
オンラインゲームであれば点数計算がそもそも付いていると思います
お疲れ様でした。

するとこんな返信が来ました。

>【社長】
やだな〜逆瀬川ﾁｬﾝ😉
雀魂のリーチ後の状況からすぐ点数確認できるやつ作ってってって話だよ📸
もっとおじさんのこと理解してね🙏✨

![雀魂_-じゃんたま-__麻雀を無料で気軽に.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/d7d5aa8b-c9d4-0d7e-ce8b-d6262b32748b.png)


## 2. リーチ後の状況から点数を推定する

ということで問題がある程度理解できました。今回のタスクは麻雀においてリーチ時の状況から点数を予測するシステムを作る、ということになります。しかし他の部分が不明です。どのようなプラットフォームにするべきか、どのように利用したいか。社長が出かけてしまったのでレスポンスが返ってくる状況にありません。とりあえず時間もないので作業を進めます。

### タスクの難しさについて

麻雀において **和了前時点** での点数計算をする、というタスクはわりと難しい問題に属します。なぜならば、条件によって点数は変わるためです。点数を決定するパラメータはざっくり考えると以下のようになります

- 待ちの形
- 役
- ツモ上がり or ロン上がり
- 親 or 子
- 自風 or 場風
- 供託
- 本場
- 表ドラ、赤ドラ、裏ドラ

裏ドラ情報について考慮するかは一番大きい問題になります。裏ドラが何であったかは結果画面でしか見ることができないため、裏ドラが何であるかを仮定したパターンを導出する必要があります。カンがある場合、ありうる点数のパターンがさらに増大します。よって、裏ドラについて考慮はしないほうがよいでしょう。

また、点数のパターンは多面待ちかつロン上がりやツモ上がりで点数が変わる場合、10パターン以上になるケースも容易に考えられます。そうした多量のパターンをどう表示するかを検討する必要があります。**発生しやすい確率を計算し、その確率順に並べ、かつ期待値も導出して** と言われないことを祈るしかありません。

### アプローチの検討

さて、AIを使いたい、というモチベーションの場合、多くは他のアプローチで解決することができる場合が多いと思います。これはAIに限らずすべての仕事で言えますが、より楽なやり方で解決することができるのであればそうすべきです。

では今回のタスクはどのように解けるでしょうか。
基本的には上記で検討した**点数を決定するパラメータを抽出するタスク**と**各パラメータから点数を予測するタスク**に分割できます。
雑に検討すると以下のようなオプションが考えられます

- 人間が行う
    - 麻雀に詳しい人間が回答するパターンです。ファジーな問題にも対処しやすいものとなります
- 状況を入力させる
    - 人間がパラメータを入力し、点数計算だけライブラリを使うパターンです。人間の入力にミスを帰着させることができ、嬉しさがあります
- gpt-4oやgeminiなどのLMMでいい感じにやる
- 画像からテンプレートマッチングする
    - 本場などの情報は任意の座標にあり、また麻雀牌の種類も限定されることからテンプレートマッチングが可能なタスクです。これで各種情報を抽出し、点数計算を行うパターンです

なお、一般的にこういうタスクでは物体検出器が用いられがちですが、今回は現実空間ではなく、レイアウトが固定的な画面上に表示される牌やその他情報を抽出するタスクであるため、もっと単純に解くことが可能です。
今回は以下のように問題を簡単にし解きたいと思います。

- 状況をすべて手入力させる
- **和了形も入力させる** (25p待ちのリーチだったら、『5p』を『ツモ』ったらという条件まで入力させ、一意にする)

一般的なお仕事の場合、難しいものを難しく解くこともたまに要求されますが、簡単な問題に変えて実行するのも重要です。

### 麻雀点数計算をさせてみる

次に、こうしたパラメータを入れて計算できるライブラリを探索します。
麻雀ライブラリ探しで考慮しなければならないのは、日本風のリーチ麻雀に対応しているかです。
以下のライブラリが出てきました。

- https://github.com/MahjongRepository/mahjong
- https://github.com/yusuke4869/mahjong

どちらも計算してくれそうです。
UIでは本場等を入力させ、手牌も入力させ、ツモ・ロン牌を選択させるUIが検討できます。

とりあえず https://github.com/MahjongRepository/mahjong をインストールし、画像から得られるパラメータを入力します。

![雀魂_-じゃんたま-__麻雀を無料で気軽に.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/d7d5aa8b-c9d4-0d7e-ce8b-d6262b32748b.png)

```bash
pip install mahjong
```

```python
from mahjong.tile import TilesConverter
from mahjong.hand_calculating.hand import HandCalculator
from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules
from mahjong.constants import EAST, SOUTH, WEST, NORTH
from mahjong.meld import Meld

# 手牌
tiles = TilesConverter.string_to_136_array(
    man='444',
    pin='111789',
    sou='345',
    honors='44' # 北北
)

# 和了牌
win_tile = TilesConverter.string_to_136_array(pin='1')[0]
# ドラ牌
dora_indicators = [
    TilesConverter.string_to_136_array(honors='2')[0], # 表ドラ
    TilesConverter.string_to_136_array(sou='2')[0], # 裏ドラ
]
# 副露
melds = None

options = OptionalRules(
    has_aka_dora=True,  # 赤ドラルール
)

config = HandConfig(
    is_riichi=True,     # 立直しているかどうか
    player_wind=WEST,   # プレイヤーの風
    round_wind=EAST,    # 場風
    kyoutaku_number=0,  # 供託
    tsumi_number=1,     # 本場
)

calculator = HandCalculator()

result = calculator.estimate_hand_value(
    tiles=tiles,
    win_tile=win_tile,
    melds=melds,
    dora_indicators=dora_indicators,
    config=config
)

print("Han:", result.han, "Fu:", result.fu)
print("Yaku:", result.yaku)
print("Cost:", result.cost)
for fu_detail in result.fu_details:
    print(fu_detail)

```

すると以下のように点数が表示されます。

```
Han: 2 Fu: 40
Yaku: [Riichi, Dora 1]
Cost: {'main': 2600, 'main_bonus': 300, 'additional': 0, 'additional_bonus': 0, 'kyoutaku_bonus': 0, 'total': 2900, 'yaku_level': ''}
{'fu': 30, 'reason': 'base'}
{'fu': 4, 'reason': 'closed_pon'}
{'fu': 4, 'reason': 'open_terminal_pon'}
```

子の2翻40符のロンアガリとなり、2600点で1本場なので300点加算で2900点となります。
雀魂では本場加算が表示されませんが、同じ計算であることがわかります

![雀魂_-じゃんたま-__麻雀を無料で気軽に_🔊.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/a35d4b22-bc0c-be1d-b87f-3bb49864083d.png)

あとはUIを作り込めば良いわけです。
AI (という名のLLMやMLテクニック等) は別に使う必要はありません。

## 3. 仕様の追加情報

>【社長】
お疲れ〜〜！😎✨会社に戻ってきたぞ〜💼💨
あ、そうそう！📸点数計算は画像から自動的にやってね〜🙏😉

ということで画像から情報抽出するタスクをやらなければならなくなりました。
退職届も並行して作成します。
テンプレートマッチングの実装をする時間もないので、gpt-4oに託します。

## 4. 画像をもとに自動判定する

今回のタスクを「画像から情報抽出を行い、点数計算をする」というタスクにします。
また、別途UIについて検討します。

### 画像から情報抽出する

今回のように限られた時間で実装するならgpt-4o等を利用するに限ります。できると信じ、まずChatGPT上で実験を行います。

まず、領域に注釈をつけ、各領域について理解してもらいます。

![ChatGPT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/1636855b-9414-8e4e-d78b-d76077b9bcd0.png)

```
画像には、麻雀のゲーム画面が表示されており、ピンク色の注釈が付けられています。それに基づき、各情報の位置を以下のように整理します：

- ドラ表示牌: 左上に表示されています。現在のドラが何かを示しています。
- 供託: ドラ表示牌のすぐ下にあります。現在の供託点棒の数を示しています。
- 本場：供託の隣にあります。現在の場の連荘回数を示しています。
- 場風: 中央にある場風表示エリアの上部に書かれています。例えば「東」が場風を示しています。
- 自風: 自分の座席の位置に合わせて中央の場風エリアに書かれています。例えば「西」が自風です。
- 手牌: 画面下部に表示され、自分の持ち牌が並んでいます。
- ツモ牌: 自分の手牌の右隣に、最新のツモ牌が表示されています。
```

正しく理解できているようです。
続けて、牌を理解してもらいます。理想的には1枚ずつ渡すのが良いですが、ちょっと雑に渡してみます。

![Cursor_と_ChatGPT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/edf61855-95b6-0fe9-4181-38377bc900a4.png)


```
画像の情報から、麻雀の各牌種の見た目と構成を整理しました：

1. **萬子（マンズ）**
    - 赤い漢数字で「一萬」から「九萬」まで描かれています。
    - 各牌に「萬」という文字が付いており、順序正しく並べられています。
2. **筒子（ピンズ）**
    - 円形のデザインで「一筒」から「九筒」まで描かれています。
    - 数が増えるごとに円の数が増加し、単純明快に数を表現しています。
3. **索子（ソーズ）**
    - 緑色の竹のデザインで「一索」から「九索」まで描かれています。
    - 「一索」は鳥のようなデザイン、それ以降は竹が連なって描かれています。
4. **風牌**
    - 「東」「南」「西」「北」の文字が描かれた牌です。
    - 風牌はこの順番で並べられており、各風を象徴しています。
5. **三元牌**
    - 「白」「發」「中」の牌で構成されています。
    - 「白」は白地の無地、「發」は緑色の文字、「中」は赤い文字で描かれています。
```

これも正しく理解できているようです。
それではこのコンテキストを継続させ、最後に情報抽出してみます。これでできたら、あとはうまくプロンプトを組み、APIで利用できる形にするだけです。

![ChatGPT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958603/b44f6b90-99a2-894e-f448-3085d39e1298.png)

```
### 1. **ドラ表示牌**

- 左上に表示されています。
- ドラ表示牌は「南」です。

### 2. **供託**

- ドラ表示牌の下に表示されています。
- 供託点は「1」です。

### 3. **本場**

- 供託の右に表示されています。
- 本場の数は「1」です。

### 4. **場風**

- 中央の場風表示エリアの上部に表示されています。
- 場風は「東」です。

### 5. **自風**

- プレイヤーの位置に対応する場風表示エリアに表示されています。
- 自風は「北」です。

### 6. **手牌**

- 画面下部に表示されています。
- 手牌は以下の通りです：
    - 四萬、四萬、四萬、五筒、五筒、六筒、七筒、五索、六索、七索、北、北

### 7. **ツモ牌**

- 手牌の右隣に表示されています。
- ツモ牌は「八筒」です。
```

ピンズとソーズの認識が失敗しました。
泣きます。many-shotで各牌のスクショを取り注釈を付けてgpt-4oに渡すか、テンプレートマッチング方式に移行するしかありません。

## 5. 仕様のさらなる追加

あと少しというところで、以下のメッセージが来ました。

>【社長】
やっぱリアルで麻雀打つことになったからその点数計算をするアプリお願い😅

退職届を提出しつつ、最後の仕事としてやらねばなりません。
次回に続く。