Skip to content

Instantly share code, notes, and snippets.

@SnowCait
Last active July 7, 2026 21:26
Show Gist options
  • Select an option

  • Save SnowCait/f613b1d97ab850420ada8bcb00842970 to your computer and use it in GitHub Desktop.

Select an option

Save SnowCait/f613b1d97ab850420ada8bcb00842970 to your computer and use it in GitHub Desktop.
SPEC.md はコミット可能なシンプル版

地鳳 Mahjong Bot: AI プレイヤー向け通信仕様

1. 概要

地鳳 Mahjong Bot は、独自 WebSocket API ではなく Nostr relay 上の Nostr event で通信する。

AI プレイヤーは、Nostr relay に接続し、自分宛ての麻雀 event を購読し、サーバー BOT からの GET sutehai? / GET naku? に返信する。

2. 参照 URL

実装:

Nostr 公式仕様:

3. relay

AI プレイヤーは、少なくとも次の relay に接続する。

wss://relay.nostr.wirednet.jp/
wss://yabu.me/

4. 麻雀チャンネル ID

半荘戦:
c8d5c2709a5670d6f621ac8020ac3e4fc3057a4961a15319f7c0818309407723

東風戦:
06ddcb27b27f667d6487b5128625f25cb2148cf87bff0502aaffe5ca705dc626

5. サーバー BOT 公開鍵

サーバー BOT の公開鍵は repository 内に固定値として定義されていない。

実装では NOSTR_PRIVATE_KEY_JONGBARI からサーバー秘密鍵を読み込み、そこから公開鍵を導出する。

AI 実装側では、実際に稼働しているサーバー BOT の npub と hex pubkey を設定する。

const SERVER_NPUB = "<server npub>";
const SERVER_PUBKEY_HEX = "<server hex pubkey>";

6. 使用する kind

kind 用途
42 通常の麻雀メッセージ。NIP-28 public chat channel message
20000 麻雀メッセージとしても処理される ephemeral event
30315 卓ステータス。d tag に channel id を入れる

AI プレイヤーは、サーバー BOT への返信では、受信 event と同じ kind を使う。

reply.kind = request.kind; // 42 または 20000

7. 購読 filter

AI プレイヤーは、自分宛ての kind:42 / kind:20000 を購読する。

[
  "REQ",
  "mahjong-player-main",
  {
    "kinds": [42, 20000],
    "#p": ["<AI_PUBKEY_HEX>"],
    "#e": [
      "c8d5c2709a5670d6f621ac8020ac3e4fc3057a4961a15319f7c0818309407723",
      "06ddcb27b27f667d6487b5128625f25cb2148cf87bff0502aaffe5ca705dc626"
    ],
    "since": 1760000000
  }
]

since には接続時点の Unix timestamp 秒を指定する。

8. ステータス購読

卓状態を読む場合は kind:30315 を購読する。

[
  "REQ",
  "mahjong-status",
  {
    "kinds": [30315],
    "#d": [
      "c8d5c2709a5670d6f621ac8020ac3e4fc3057a4961a15319f7c0818309407723",
      "06ddcb27b27f667d6487b5128625f25cb2148cf87bff0502aaffe5ca705dc626"
    ],
    "authors": ["<SERVER_PUBKEY_HEX>"],
    "limit": 2
  }
]

主な status content:

募集中 1/4
募集中 2/4
募集中 3/4
対局中
next待ち

9. 新規卓開始・参加

9.1 卓を開始する

nostr:<SERVER_NPUB> gamestart

event 例:

{
  "kind": 42,
  "tags": [
    ["e", "<CHANNEL_ID>", "", "root"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> gamestart"
}

9.2 卓に参加する

nostr:<SERVER_NPUB> join

event 例:

{
  "kind": 42,
  "tags": [
    ["e", "<CHANNEL_ID>", "", "root"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> join"
}

10. 返信 event の tags

サーバー BOT からの event に返信する場合、最低限次を含める。

[
  ["e", "<CHANNEL_ID>", "", "root"],
  ["e", "<SERVER_EVENT_ID>", "", "reply", "<SERVER_PUBKEY_HEX>"],
  ["p", "<AI_PUBKEY_HEX>"],
  ["p", "<SERVER_PUBKEY_HEX>"]
]

サーバー BOT は p tag に自分の pubkey が含まれる event を処理対象にする。

11. 牌表記

牌は [1-9][mpsz] 形式で表す。

1m 2m 3m 4m 5m 6m 7m 8m 9m
1p 2p 3p 4p 5p 6p 7p 8p 9p
1s 2s 3s 4s 5s 6s 7s 8s 9s
1z 2z 3z 4z 5z 6z 7z
表記 意味
m 萬子
p 筒子
s 索子
1z
2z
3z 西
4z
5z
6z
7z

12. GET sutehai?

12.1 サーバーからの要求

サーバー BOT は、AI プレイヤーに捨て牌判断を求めるとき、次の形式の content を送る。

<手牌emoji> <ツモ牌emoji>
nostr:<AI_NPUB> GET sutehai?

副露後など、ツモ牌がない場合もある。

<手牌emoji>
nostr:<AI_NPUB> GET sutehai?

12.2 AI からの応答

AI プレイヤーは次のいずれかを返す。

nostr:<SERVER_NPUB> sutehai? sutehai <pai>
nostr:<SERVER_NPUB> sutehai? richi <pai>
nostr:<SERVER_NPUB> sutehai? ankan <pai>
nostr:<SERVER_NPUB> sutehai? kakan <pai>
nostr:<SERVER_NPUB> sutehai? tsumo

12.3 通常捨て牌の返信例

{
  "kind": 42,
  "tags": [
    ["e", "<CHANNEL_ID>", "", "root"],
    ["e", "<SERVER_EVENT_ID>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> sutehai? sutehai 7z"
}

13. GET naku?

13.1 サーバーからの要求

サーバー BOT は、鳴き・ロン判断が可能な場合に次の形式の content を送る。

<手牌emoji> <対象牌emoji>
nostr:<AI_NPUB> GET naku? <actions>

<actions> は、可能なアクションの空白区切り。

例:

nostr:<AI_NPUB> GET naku? ron pon chi

13.2 AI からの応答

AI プレイヤーは次のいずれかを返す。

nostr:<SERVER_NPUB> naku? no
nostr:<SERVER_NPUB> naku? ron
nostr:<SERVER_NPUB> naku? kan
nostr:<SERVER_NPUB> naku? pon
nostr:<SERVER_NPUB> naku? chi <pai1> <pai2>

13.3 鳴かない返信例

{
  "kind": 42,
  "tags": [
    ["e", "<CHANNEL_ID>", "", "root"],
    ["e", "<SERVER_EVENT_ID>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> naku? no"
}

14. 鳴き優先順位

複数プレイヤーが naku? に応答した場合、サーバー BOT は次の優先順位で処理する。

ron > pon > kan > chi > no

サーバー BOT は、問い合わせ対象になった全プレイヤーの応答が揃うまで次へ進まない。

15. 主な通知

AI が内部状態を持つ場合、次の通知を parse する。

content pattern 意味
NOTIFY gamestart ゲーム開始・席順
NOTIFY kyokustart 局開始
NOTIFY point 点数
NOTIFY haipai 自分の配牌
NOTIFY dora ドラ表示牌
NOTIFY tsumo 自分のツモ
NOTIFY sutehai 捨て牌
NOTIFY say 発声。tsumo, richi, ron, kan, pon, chi, tenpai, noten
NOTIFY open 副露・カン
NOTIFY agari 和了
NOTIFY ryukyoku 流局
NOTIFY kyokuend 局終了
NOTIFY gameend ゲーム終了

16. 最小 AI 実装

最小 AI は次だけ実装すればよい。

  1. relay に接続する。
  2. 自分宛ての kind:42 / kind:20000 を購読する。
  3. GET sutehai? を受けたら、合法牌を 1 つ選んで sutehai? sutehai <pai> を返す。
  4. GET naku? ... を受けたら、常に naku? no を返す。
  5. event.id で重複排除する。

疑似コード:

for await (const event of subscription) {
  if (event.pubkey !== SERVER_PUBKEY_HEX) continue;
  if (![42, 20000].includes(event.kind)) continue;
  if (!hasPTag(event, AI_PUBKEY_HEX)) continue;
  if (!hasMahjongChannelTag(event)) continue;
  if (seen.has(event.id)) continue;

  seen.add(event.id);

  if (/GET\s+sutehai\?$/s.test(event.content)) {
    const pai = chooseDiscard(event);
    await publishReply(event, `nostr:${SERVER_NPUB} sutehai? sutehai ${pai}`);
    continue;
  }

  if (/GET\s+naku\?\s+((ron|kan|pon|chi)\s*)+$/s.test(event.content)) {
    await publishReply(event, `nostr:${SERVER_NPUB} naku? no`);
    continue;
  }
}

17. 重複排除

複数 relay から同じ event を受信する可能性があるため、必ず event.id で重複排除する。

const seen = new Set<string>();

function shouldProcess(event: NostrEvent): boolean {
  if (seen.has(event.id)) return false;
  seen.add(event.id);
  return true;
}

18. エラー応答

不正な応答や不要な応答に対して、サーバー BOT は次のような message を返す場合がある。

You are not required to send "sutehai?"
You are not required to send "naku?"
You cannot tsumo.
You cannot richi <pai>.
You cannot sutehai <pai> .
You cannot ankan <pai> .
You cannot kakan <pai> .
You cannot ron.
You cannot kan.
You cannot pon.
You cannot chi.

19. 注意事項

この実装では、手牌・ツモ牌・鳴き問い合わせが public Nostr event として送信される。

p tag は宛先指定であり、暗号化やアクセス制御ではない。

そのため、relay またはチャンネルを監視している第三者は、原理上、手牌情報を読める。

20. AI が送信する content 一覧

最小実装:

nostr:<SERVER_NPUB> gamestart
nostr:<SERVER_NPUB> join
nostr:<SERVER_NPUB> sutehai? sutehai <pai>
nostr:<SERVER_NPUB> naku? no

追加実装:

nostr:<SERVER_NPUB> sutehai? richi <pai>
nostr:<SERVER_NPUB> sutehai? ankan <pai>
nostr:<SERVER_NPUB> sutehai? kakan <pai>
nostr:<SERVER_NPUB> sutehai? tsumo
nostr:<SERVER_NPUB> naku? ron
nostr:<SERVER_NPUB> naku? kan
nostr:<SERVER_NPUB> naku? pon
nostr:<SERVER_NPUB> naku? chi <pai1> <pai2>

地鳳 Mahjong Bot: 麻雀 AI プレイヤー向け Nostr 通信仕様

最終確認日: 2026-07-08

1. 概要

この仕様は、Nostr 経由で動作する地鳳 Mahjong Bot に対して、麻雀 AI プレイヤーを実装するための送受信ルールを定義する。

このゲームは独自 WebSocket サーバーを持つのではなく、Nostr relay の WebSocket 通信上で Nostr event を送受信する。

AI プレイヤーは、基本的に次の流れで動作する。

  1. 指定 relay に WebSocket 接続する。
  2. 麻雀チャンネル上の、自分宛ての kind:42 または kind:20000 event を購読する。
  3. サーバー BOT から GET sutehai? または GET naku? を受信する。
  4. 自分の AI 判断に基づき、同じ Nostr スレッドへ sutehai? ... または naku? ... を返信する。

2. 参照 URL

実装

Nostr 仕様

3. 用語

用語 意味
AI プレイヤー 自分が実装する麻雀 AI の Nostr アカウント
サーバー BOT 卓進行・判定・配牌・点数計算を行う Mahjong Bot
relay Nostr relay。WebSocket endpoint を持つ
channel id 麻雀卓を表す Nostr event id
npub NIP-19 形式の公開鍵
hex pubkey 32-byte lowercase hex 形式の公開鍵。Nostr event の pubkeyp tag で使う
content mention content 内の nostr:npub... 形式のメンション
p tag Nostr event tags 内の ["p", "<hex pubkey>"]

4. 固定値

4.1 relay

AI プレイヤーは、少なくとも以下の relay に接続する。

wss://relay.nostr.wirednet.jp/
wss://yabu.me/

4.2 麻雀チャンネル

半荘戦:
c8d5c2709a5670d6f621ac8020ac3e4fc3057a4961a15319f7c0818309407723

東風戦:
06ddcb27b27f667d6487b5128625f25cb2148cf87bff0502aaffe5ca705dc626

4.3 サーバー BOT 公開鍵

サーバー BOT の公開鍵は、この repository 内では固定値として定義されていない。

実装では NOSTR_PRIVATE_KEY_JONGBARI からサーバー鍵を読み込み、その秘密鍵から公開鍵を導出している。

AI プレイヤー実装では、実際に稼働しているサーバー BOT の公開鍵を別途設定すること。

const SERVER_NPUB = "<server npub>";
const SERVER_PUBKEY_HEX = "<server hex pubkey>";

5. Nostr event の基本

5.1 送信 event

AI プレイヤーは、NIP-01 準拠の署名済み Nostr event を relay に送る。

["EVENT", {
  "id": "<event id>",
  "pubkey": "<ai pubkey hex>",
  "created_at": 1760000000,
  "kind": 42,
  "tags": [],
  "content": "...",
  "sig": "<signature>"
}]

5.2 受信 event

relay からの受信は次の形式。

["EVENT", "<subscription_id>", {
  "id": "<event id>",
  "pubkey": "<sender pubkey hex>",
  "created_at": 1760000000,
  "kind": 42,
  "tags": [],
  "content": "...",
  "sig": "<signature>"
}]

5.3 公開鍵表記

tags では hex pubkey を使う。

["p", "<hex pubkey>"]

content では npub を使う。

nostr:<npub> join

6. 購読仕様

AI プレイヤーは、少なくとも自分宛ての麻雀メッセージを購読する。

[
  "REQ",
  "mahjong-player-main",
  {
    "kinds": [42, 20000],
    "#p": ["<AI_PUBKEY_HEX>"],
    "#e": [
      "c8d5c2709a5670d6f621ac8020ac3e4fc3057a4961a15319f7c0818309407723",
      "06ddcb27b27f667d6487b5128625f25cb2148cf87bff0502aaffe5ca705dc626"
    ],
    "since": 1760000000
  }
]

since には接続時点の Unix timestamp 秒を指定する。

過去ログから復元したい場合は since を過去にするか、別 subscription で limit を付けて取得する。

7. ステータス購読

サーバー BOT は、卓状態を kind:30315 で送る。

AI プレイヤーが卓状態を表示・監視したい場合は、次のように購読する。

[
  "REQ",
  "mahjong-status",
  {
    "kinds": [30315],
    "#d": [
      "c8d5c2709a5670d6f621ac8020ac3e4fc3057a4961a15319f7c0818309407723",
      "06ddcb27b27f667d6487b5128625f25cb2148cf87bff0502aaffe5ca705dc626"
    ],
    "authors": ["<SERVER_PUBKEY_HEX>"],
    "limit": 2
  }
]

ステータス event の例。

{
  "kind": 30315,
  "content": "募集中 1/4",
  "tags": [
    ["d", "<channel_id>"]
  ]
}

確認されている主な content は次の通り。

募集中 1/4
募集中 2/4
募集中 3/4
対局中
next待ち

空文字列 "" の場合は、ステータスクリアとして扱う。

8. 送信時の kind

8.1 通常は kind:42

新規に gamestartjoin を送る場合は、原則 kind:42 を使う。

kind:42 は NIP-28 の public chat channel message である。

8.2 返信時は受信 event の kind を引き継ぐ

サーバー BOT からの GET sutehai? または GET naku? に返信する場合は、受信 event と同じ kind を使う。

reply.kind = request.kind; // 42 または 20000

9. 送信 tags

9.1 新規コマンド

新規に gamestart または join を送る場合の tags。

[
  ["e", "<channel_id>", "", "root"],
  ["p", "<SERVER_PUBKEY_HEX>"]
]

9.2 サーバー event への返信

サーバー BOT から受信した event に返信する場合は、root channel と reply event を指定し、サーバー BOT を p tag に含める。

[
  ["e", "<channel_id>", "", "root"],
  ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
  ["p", "<AI_PUBKEY_HEX>"],
  ["p", "<SERVER_PUBKEY_HEX>"]
]

最低限、次は必須とする。

["p", "<SERVER_PUBKEY_HEX>"]

サーバー BOT は p tag に自分の pubkey が含まれている event を処理対象にする。

10. 受信 event の処理条件

AI プレイヤーは、原則として次をすべて満たす event だけを処理する。

  1. kind42 または 20000
  2. pubkeySERVER_PUBKEY_HEX
  3. tags["p", "<AI_PUBKEY_HEX>", ...] が含まれる
  4. tags に対象麻雀チャンネルの e tag が含まれる
  5. contentGET sutehai? または GET naku? ... で終わる

実装上、content の前半には手牌 emoji などが含まれる場合がある。AI は最終行を中心に parse する。

例。

:mahjong_m1::mahjong_m2::mahjong_m3: :mahjong_p5:
nostr:<AI_NPUB> GET sutehai?

11. ゲーム参加コマンド

11.1 新規卓を開始する

nostr:<SERVER_NPUB> gamestart

送信 event 例。

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> gamestart"
}

11.2 募集中の卓に参加する

nostr:<SERVER_NPUB> join

送信 event 例。

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> join"
}

12. サーバーからの主要通知

AI プレイヤーは、少なくとも GET 系を処理すればゲーム参加できる。 ただし、強い AI を作るには NOTIFY 系を parse して内部状態を更新する。

12.1 ゲーム開始通知

nostr:<AI_NPUB> #gamestart NOTIFY gamestart <席> <player1> <player2> <player3> <player4>

例。

nostr:npub1... #gamestart NOTIFY gamestart 東 nostr:npub1... nostr:npub1... nostr:npub1... nostr:npub1...

12.2 局開始通知

<players...> #kyokustart NOTIFY kyokustart <場風> <親npub> <積み棒> <供託点>

例。

nostr:npub1a... nostr:npub1b... nostr:npub1c... nostr:npub1d... #kyokustart NOTIFY kyokustart 東 nostr:npub1a... 0 0

12.3 点数通知

<players...> NOTIFY point <player> = <score>
<players...> NOTIFY point <player> + <score_delta>
<players...> NOTIFY point <player> - <score_delta>

12.4 配牌通知

nostr:<AI_NPUB> NOTIFY haipai nostr:<AI_NPUB> <tehai>
<emoji表示>

例。

nostr:npub1... NOTIFY haipai nostr:npub1... 1m2m3m4p5p6p7s8s9s1z2z3z4z
:mahjong_m1::mahjong_m2::mahjong_m3:

12.5 ドラ通知

<players...> NOTIFY dora <pai>

例。

nostr:npub1a... nostr:npub1b... nostr:npub1c... nostr:npub1d... NOTIFY dora 5p

12.6 ツモ通知

nostr:<AI_NPUB> NOTIFY tsumo nostr:<AI_NPUB> <残り山枚数> <tsumo_pai>

例。

nostr:npub1... NOTIFY tsumo nostr:npub1... 69 7z

12.7 捨て牌通知

<players...> NOTIFY sutehai <player> <pai>

例。

nostr:npub1a... nostr:npub1b... nostr:npub1c... nostr:npub1d... NOTIFY sutehai nostr:npub1a... 7z

12.8 発声通知

<players...> NOTIFY say <player> tsumo
<players...> NOTIFY say <player> richi
<players...> NOTIFY say <player> ron
<players...> NOTIFY say <player> kan
<players...> NOTIFY say <player> pon
<players...> NOTIFY say <player> chi
<players...> NOTIFY say <player> tenpai
<players...> NOTIFY say <player> noten

12.9 副露・カン通知

<players...> NOTIFY open <player> <furo_hai>

例。

nostr:npub1a... nostr:npub1b... nostr:npub1c... nostr:npub1d... NOTIFY open nostr:npub1b... 5p5p5p

12.10 局終了通知

<players...> NOTIFY agari <agari_player> <fu> <yaku,han>...
<players...> NOTIFY ryukyoku
<players...> NOTIFY kyokuend

12.11 ゲーム終了通知

<players...> NOTIFY gameend <player1> <score1> <player2> <score2> <player3> <score3> <player4> <score4>

13. 牌表記

牌は次の文字列表記を使う。

1m 2m 3m 4m 5m 6m 7m 8m 9m
1p 2p 3p 4p 5p 6p 7p 8p 9p
1s 2s 3s 4s 5s 6s 7s 8s 9s
1z 2z 3z 4z 5z 6z 7z

意味。

表記 意味
m 萬子
p 筒子
s 索子
z 字牌
1z
2z
3z 西
4z
5z
6z
7z

サーバー実装のコマンド正規表現は基本的に [1-9][mpsz] を受け付けるが、字牌として意味があるのは 1z から 7z までとする。

14. GET sutehai? 仕様

14.1 サーバーからの要求

サーバー BOT は、AI プレイヤーに捨て牌判断を求めるとき、次の形式の event を送る。

<手牌emoji> <ツモ牌emoji>
nostr:<AI_NPUB> GET sutehai?

副露後など、ツモ牌がない場合は次のような形式になる。

<手牌emoji>
nostr:<AI_NPUB> GET sutehai?

14.2 AI プレイヤーの応答

AI プレイヤーは、次のいずれかを送る。

nostr:<SERVER_NPUB> sutehai? sutehai <pai>
nostr:<SERVER_NPUB> sutehai? richi <pai>
nostr:<SERVER_NPUB> sutehai? ankan <pai>
nostr:<SERVER_NPUB> sutehai? kakan <pai>
nostr:<SERVER_NPUB> sutehai? tsumo

14.3 応答 event 例: 通常捨て牌

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> sutehai? sutehai 7z"
}

14.4 応答 event 例: リーチ

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> sutehai? richi 5p"
}

14.5 応答 event 例: ツモ和了

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> sutehai? tsumo"
}

14.6 アクション選択ルール

AI は、手牌・ツモ牌・場況から次を選択する。

アクション 意味
sutehai <pai> 指定牌を捨てる
richi <pai> リーチして指定牌を捨てる
ankan <pai> 指定牌で暗槓する
kakan <pai> 指定牌で加槓する
tsumo ツモ和了する

不明・判断不能な場合は、合法な sutehai <pai> を返す。

15. GET naku? 仕様

15.1 サーバーからの要求

他家の捨て牌や加槓に対して、ロン・カン・ポン・チーの可能性がある場合、サーバー BOT は AI プレイヤーに次の形式で問い合わせる。

<手牌emoji> <対象牌emoji>
nostr:<AI_NPUB> GET naku? <actions>

<actions> は、可能なアクションの空白区切りである。

例。

:mahjong_m2::mahjong_m3::mahjong_m4: :mahjong_m1:
nostr:npub1... GET naku? ron pon chi

15.2 AI プレイヤーの応答

AI プレイヤーは、次のいずれかを送る。

nostr:<SERVER_NPUB> naku? no
nostr:<SERVER_NPUB> naku? ron
nostr:<SERVER_NPUB> naku? kan
nostr:<SERVER_NPUB> naku? pon
nostr:<SERVER_NPUB> naku? chi <pai1> <pai2>

15.3 応答 event 例: 鳴かない

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> naku? no"
}

15.4 応答 event 例: ロン

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> naku? ron"
}

15.5 応答 event 例: チー

{
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> naku? chi 2m 3m"
}

15.6 アクション選択ルール

アクション 意味
no 鳴かない・ロンしない
ron ロンする
kan 大明槓する
pon ポンする
chi <pai1> <pai2> pai1pai2 を使ってチーする

chi の場合、AI は自分の手牌から実際に使う 2 枚を指定する。

例。

naku? chi 2m 3m

16. サーバー側の鳴き優先順位

複数プレイヤーが naku? に応答した場合、サーバー BOT は実装上、次の優先順位で処理する。

ron > pon > kan > chi > no

三家ロンの場合は途中流局として扱われる。

AI プレイヤーは、GET naku? を受けたら速やかに応答すること。 サーバー BOT は、問い合わせ対象になった全プレイヤーの応答が揃うまで次の処理へ進まない。

17. AI プレイヤーの最小実装

最小実装では、次の処理だけでよい。

  1. relay に接続する。
  2. 自分宛ての kind:42 / kind:20000 を購読する。
  3. GET sutehai? を受けたら、合法牌を 1 つ選んで sutehai? sutehai <pai> を返す。
  4. GET naku? ... を受けたら、常に naku? no を返す。
  5. 重複 event を event.id で除去する。

疑似コード。

for await (const event of nostrSubscription) {
  if (event.pubkey !== SERVER_PUBKEY_HEX) continue;
  if (![42, 20000].includes(event.kind)) continue;
  if (!hasPTag(event, AI_PUBKEY_HEX)) continue;
  if (!hasMahjongChannelRoot(event)) continue;
  if (seen.has(event.id)) continue;

  seen.add(event.id);

  if (event.content.match(/GET\s+sutehai\?$/s)) {
    const pai = chooseDiscard(event);
    await publishReply(event, `nostr:${SERVER_NPUB} sutehai? sutehai ${pai}`);
    continue;
  }

  if (event.content.match(/GET\s+naku\?\s+((ron|kan|pon|chi)\s*)+$/s)) {
    await publishReply(event, `nostr:${SERVER_NPUB} naku? no`);
    continue;
  }
}

18. 推奨実装: 状態管理

より強い AI を作る場合は、次の通知を parse して内部状態を更新する。

通知 更新内容
NOTIFY gamestart 自分の席・プレイヤー一覧
NOTIFY kyokustart 場風・親・積み棒・供託
NOTIFY haipai 自分の手牌
NOTIFY dora ドラ表示牌
NOTIFY tsumo 自分のツモ牌・残り山枚数
NOTIFY sutehai 各プレイヤーの河
NOTIFY say richi リーチ状態
NOTIFY open 副露・カン
NOTIFY point 点数
NOTIFY agari 和了結果
NOTIFY ryukyoku 流局
NOTIFY kyokuend 局終了
NOTIFY gameend 半荘・東風終了

19. 重複処理

複数 relay に接続するため、同じ event を複数回受信する可能性がある。

AI プレイヤーは必ず event.id で重複排除する。

const seen = new Set<string>();

function shouldProcess(event: NostrEvent): boolean {
  if (seen.has(event.id)) return false;
  seen.add(event.id);
  return true;
}

20. 署名と送信

AI プレイヤーが送信する event は、通常の Nostr event として署名する。

署名前 event。

const unsigned = {
  kind: request.kind,
  created_at: Math.floor(Date.now() / 1000),
  tags: [
    ["e", channelId, "", "root"],
    ["e", request.id, "", "reply", request.pubkey],
    ["p", aiPubkeyHex],
    ["p", serverPubkeyHex]
  ],
  content: `nostr:${serverNpub} sutehai? sutehai 7z`
};

署名後、relay に送る。

["EVENT", {
  "id": "<calculated id>",
  "pubkey": "<AI_PUBKEY_HEX>",
  "created_at": 1760000000,
  "kind": 42,
  "tags": [
    ["e", "<channel_id>", "", "root"],
    ["e", "<server_event_id>", "", "reply", "<SERVER_PUBKEY_HEX>"],
    ["p", "<AI_PUBKEY_HEX>"],
    ["p", "<SERVER_PUBKEY_HEX>"]
  ],
  "content": "nostr:<SERVER_NPUB> sutehai? sutehai 7z",
  "sig": "<signature>"
}]

21. エラー応答

サーバー BOT は、不正または不要な応答に対して次のような human-readable message を返す場合がある。

You are not required to send "sutehai?"
You are not required to send "naku?"
You cannot tsumo.
You cannot richi <pai>.
You cannot sutehai <pai> .
You cannot ankan <pai> .
You cannot kakan <pai> .
You cannot ron.
You cannot kan.
You cannot pon.
You cannot chi.

AI プレイヤーは、これらを受けた場合、内部状態を維持しつつログに記録する。

GET sutehai? に対して不正応答した場合、サーバー BOT は再度同じプレイヤーに sutehai? 応答を要求する状態を維持することがある。

22. セキュリティ・プライバシー上の注意

この実装では、手牌・ツモ牌・捨て牌・鳴き問い合わせが public channel message として送信される。

p tag は宛先指定・検索用であり、暗号化やアクセス制御ではない。

したがって、relay またはチャンネルを監視している第三者は、原理上、手牌情報を読める。

この仕様は、公開情報として流れる前提のゲーム実装である。

23. 最小レスポンス一覧

AI プレイヤーが送信する可能性のある content は、最低限次の 7 種類。

nostr:<SERVER_NPUB> gamestart
nostr:<SERVER_NPUB> join
nostr:<SERVER_NPUB> sutehai? sutehai <pai>
nostr:<SERVER_NPUB> sutehai? richi <pai>
nostr:<SERVER_NPUB> sutehai? tsumo
nostr:<SERVER_NPUB> naku? no
nostr:<SERVER_NPUB> naku? ron

カン・ポン・チーまで実装する場合は次も使う。

nostr:<SERVER_NPUB> sutehai? ankan <pai>
nostr:<SERVER_NPUB> sutehai? kakan <pai>
nostr:<SERVER_NPUB> naku? kan
nostr:<SERVER_NPUB> naku? pon
nostr:<SERVER_NPUB> naku? chi <pai1> <pai2>

24. 実装チェックリスト

  • AI 用 Nostr 秘密鍵を用意する
  • AI の hex pubkey と npub を導出する
  • サーバー BOT の hex pubkey と npub を設定する
  • relay へ WebSocket 接続する
  • kind:42 / kind:20000 を購読する
  • #p filter に AI pubkey を入れる
  • #e filter に麻雀チャンネル ID を入れる
  • 受信 event の署名検証を行う
  • event.id で重複排除する
  • GET sutehai? を parse する
  • GET naku? を parse する
  • 返信 event に p tag でサーバー BOT を入れる
  • 返信 event に root e tag と reply e tag を入れる
  • 返信 event を署名する
  • relay へ ["EVENT", event] で送信する
  • relay からの OK をログに残す
  • エラー応答をログに残す
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment