0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

misskeyでリバーシbotを作ったのでリバーシAPIまとめ

Posted at

最近リアルが忙しい六角レンチです
地球寒すぎませんかね?
それはさておきぶろみね君にリバーシ機能を追加したのでリバーシのAPIがどんな感じなのかを記事にしたいと思います

事の発端

misskeyでリバーシが復活。

それに合わせて藍ちゃんとのリバーシ対戦も復活したという情報を聞く
(どうやらioの藍ちゃんはアップデートしてないからまだできないらしい)

藍ちゃんはbotで、リバーシ対戦ができる。ということはリバーシのAPIがあるのでは...?

その謎を解明するため、六角レンチはアマゾンの奥地へと向かった――。



はい。

作ったbot

アカウント
https://misskey.io/@bromine35
リポジトリ
https://github.com/35enidoi/bromine35bot

参考にした物

https://misskey-hub.net/ja/docs/for-developers/api/streaming/
misskey hubのストリーミングAPIのドキュメント

チャンネルへの接続やメッセージの送信をする時のデータの形が書いてあるので絶対読め(1敗)

https://misskey.io/api-doc
misskey.io(最新バージョン)のapiドキュメント

misskeyの最新バージョン(どちらかというとリバーシ機能がついているバージョン)に追従しているサーバーのドキュメントなら多分どれでもいい
リバーシのエンドポイント等が見れる

https://meisskey.one/docs/ja-JP/reversi-bot
昔のバージョンのmisskey(多分v13以前?)にあるドキュメント

最新バージョンにはないけど昔のmisskeyを使ってるインスタンスでdocs/ja-JPにアクセスすると見れる(misskey.devでも見れた)

なんとここにはリバーシbotの作成手順が残っている(しかもしゅいろママの直筆)
どうやら最新バージョンで追加されたリバーシはこの時代のリバーシを復活させた感じっぽく、APIがかなり似ており、マップのデータの計算方法等は完全に同じなのですごい参考にできる

https://github.com/syuilo/ai/tree/master/src/modules/reversi
藍ちゃんの中身のリバーシ実装部分

藍ちゃんはオープンソースなので実装部分を見れる
履いてるパンツの色はどこにも書いてませんでした(どうして)

どのチャンネルに接続しているのかとかを見た

ゲームの情報

APIにはゲームの情報が返ってくることがあります

データ構造はこんな感じ(長いので格納)
{id: ゲームのID(gameId),
createdAt : マッチした時の時間(ISO 8601),
startedAt : None,
endedAt : None,
isStarted : False,
isEnded : False,
form1 : None,
form2 : None,
user1Ready : False,
user2Ready : False,
user1Id : user1のuserID,
user2Id : user2のuserID,
user1 : user1の情報,
user2 : user2の情報,
winnerId : None,
winner : None,
surrenderedUserId : None,
timeoutUserId : None,
black : None,
bw : 'random',
isLlotheo : False,
canPutEverywhere : False,
loopedBoard : False,
timeLimitForEachTurn : 90,
noIrregularRules : False,
logs : [],
map : ['--------', '--------', '--------', '---wb---', '---bw---', '--------', '--------', '--------']}
以降ゲームの情報って言ってたらこれのことです

API

reversi/match

アクセストークンが必要です
このエンドポイントにパラメータuserIdを設定することで招待できます
マッチしない場合(あるいは招待した場合)204が返ってきます
瞬時にマッチした場合、(あるいは招待に招待し返した場合)ゲームの情報が返ってきます。

reversi/show-game

アクセストークン無しでもできます
パラメータgameIdが必要です
成功したらゲームの情報が返ってきます

gameIdはゲームの識別用のIDです
例えばこのゲームのURL
https://misskey.io/reversi/g/9p0nag8f9muy0g4x
これの9p0nag8f9muy0g4xがゲームIDです

websocket

リバーシはwebsocketを使って通信します
昔のmisskeyドキュメントと似たような形で通信します

reversi

リバーシチャンネルです

昔のmisskeyドキュメントに書いてあるgames/reversiがこのチャンネルになります

invited

他人から招待されるとinvitedメッセージが流れてきます
先ほどのAPIreversi/matchでそのユーザーを招待するまで何回も送られてきます

データはこんな感じ
{id : チャンネル識別用のid,
type : 'invited',
body : {user : 招待してきたユーザーの情報}}

matched

APIのreversi/matchで204が返ってきた後、マッチした場合matchedメッセージが送られてきます

データはこんな感じ
{id : チャンネル識別用のid,
type : 'matched',
body : {game : ゲームの情報}}

reversiGame

リバーシゲームのチャンネルです
接続するときにパラメータgameIdが必要なので注意

準備段階の時に送られてくるメッセージ

canceled

マッチがキャンセルされると送られます

データ構造
{id : チャンネル識別用のid,
type : 'canceled',
body : {userId: 相手のユーザーID}}

updateSettings

相手が設定を変更したときに送られてきます
keyで変わった場所、valueが内容です

データ構造
{id : チャンネル識別用のid,
type : 'updateSettings',
body : {userId: 相手のユーザーID,
        key: キーワード
        value: 値}}
map

マップ変更時のキーワードです

valueは昔のmisskeyドキュメントのマップ情報のところに書いてあるマップデータです。
(例えば4x4の場合、['----', '-wb-', '-bw-', '----']です。)

key : map
value : マップデータ(list)

bw

開始時にどちらが黒か(黒が先行)のキーワードです

valueは1, 2, random
1でuser1、2でuser2が先行
randomでランダムです
ただ正直この場所で自分が先行か確かめるよりも後述するstatedメッセージで確かめる方がrandomの時にも対応できるのでそこまで重要じゃないです

key : bw
value : random | 1 | 2 (str | int)

timeLimitForEachTurn

一ターンの制限時間の秒数です
ただ時間切れの処理はクライアント側っぽい?
valueはintで5~3600
(一応全部書くと 5, 10, 30, 60, 90, 120, 180, 3600)

key : timeLimitForEachTurn
value : 5~3600 (int)

isLlotheo

ロセオ(石が少ない方が勝ち)であるかどうかです

key : isLlotheo
value : False | True (bool)

loopedBoard

ループボードであるかどうかです

ちなみにぶろみね君(作ったbot)はループボードの実装が難しすぎてループボード非対応です(悲しい)

key : loopedBoard
value : False | True (bool)

canPutEverywhere

どこにでも置けるかどうかです

key : canPutEverywhere
value : False | True (bool)

changeReadyStates

プレイヤーの準備ができたか(準備完了ボタンが押されたか)が変わった時に送られます

bot側で準備完了を送信するにはストリームにメッセージでtype="ready",body=Trueを送ります

データで表すとこれ
{id: チャンネル識別用のID,
type: 'ready',
body: True}

もちろん準備完了を取り消す場合はbody=Falseにして送信

変わった時に送られるので相手の準備完了ボタンがまた押されたとき(準備に戻った時)は当然、自分が準備完了を送信したときにも送られてくるので注意(無限ループに陥る)

user1user2がどちらもTrueになってしばらくたつとstatedが送られてくる

データ構造
{id: チャンネル識別用のID,
type: 'changeReadyStates',
body: {user1 : bool,
       user2 : bool}}

対戦中に送られるメッセージ

started

開始時に送られてきます
一緒にゲームの情報も送られてきます

前述した自分が先行か確かめる方法ですが、ゲームの情報の中にblackというパラメータがあり、このblackの数字でどちらが先行か決まるのでここで決めた方がいいです。(blackの取る値は1か2でuser1、user2どちらが黒かを指している)

また、ゲームの情報のstartedAtが始まった時間。isStartedTrueにそれぞれ変わっています。
あと設定されたゲームの設定もゲームの情報の中に入ります。

データ構造
{id : チャンネル識別用のid,
type : 'started',
body : {game : ゲームの情報}}

bodyがゲームの情報ではなく、bodyの中にあるgameがゲームの情報なので注意

ended

終了時に送られてきます
startedと同じくゲームの情報も送られてきます

startedと同様にendedAtが終わった時間に。isEndedTrueに変わります。
また、勝者がいる場合winnerIdが勝者のユーザーIDに、winnerが勝者の情報に変わります。

投了による終了の場合、上の勝者がいる場合に加えてsurrenderedUserIdが投了したユーザーIDに変わります。(投了による終了も判別できる)

時間切れによる終了の場合、投了による終了のようにtimeoutUserIdが時間切れしたユーザーIDに変わります。

データ構造
{id : チャンネル識別用のid,
type : 'ended',
body : {game : ゲームの情報}}

log

石が置かれたときに送られてきます
置かれた時なので自分が置いても送られてきます

playerはbool値でTrueで黒、Falseで白を表します。

posは置かれた場所です。
posの計算方法は昔のドキュメントの位置の計算法に書いてあります(y*mapwidth+x=pos)

データ構造
{id : チャンネル識別用のid,
type : 'log',
body : {time : 打つのにかかった時間[ms] (int),
        player : 黒か白か(bool)},
        operation : 'put'(固定、このパラメータの意味は不明),
        pos : 置かれた場所(int),
        id : ID(str)}

石を置く方法

無効な場所(例えばもうすでに置かれている場所や自由に置けるモードじゃない時に一つも取れない場所等)に置こうとした場合、何も起きないので注意

メッセージでtype='putStone'、body={pos : 置く場所(pos)}として送信することで置くことができます
(置く場所はposで表します)

データで表すとこれ
{id : チャンネル識別用のID,
type : 'putStone',
body : {pos : 置く場所(pos)}

リバーシbotの流れ

  1. reversiチャンネルへ接続
  2. メッセージを受け取るまで待つ
  3. matchedを受け取った場合、reversiGameチャンネルへgameIdを入れて接続する。
    invitedを受け取った場合、reversi/matchへ招待してきたuserIdを使ってPOSTする。
    うまくいくとレスポンスでゲームの情報が返ってくるのでreversiGameチャンネルへgameIdを入れて接続する。
  4. 相手の設定が変更されるとupdateSettingsが送られてくるのでそれを見て必要なら何かやる
    ここでもしcanceledが送られてきたらチャンネルから切断し、2に戻る
  5. changeReadyStatesで相手の準備ができた場合、自分も準備ができたらメッセージreadyを送る
  6. startedで始まったら情報から自分が先行か等を見て駒を置いたりする
  7. logで相手が置いたら置く場所を考えて石を置く
  8. endedで終了したらチャンネルから切断し、2に戻る。

botに役に立つかもしれない情報

接続のダブり

reversiチャンネルでinvitedが来たらreversi/matchにPOSTしてreversiGameチャンネルへ接続するのですが、invitedの説明でも言った通りこのメッセージは何回も送られてきます。
その結果同じゲームに何個もreversiGameを接続してしまうことがあります。この場合、何回も石を置く作業をしてしまったりして(botの内部で使う盤面が変になったりする等で)ゲームが止まってしまうことがあります。
この対策としてinvitedにきたユーザーのリストを作っておき、同じユーザーのinvitedが何個も来ても最初のinvited以外ををはじくことで同時に同じゲームに接続してしまうことを回避できます

途中でwebsocketが切れた時の対策

たまにですが、対戦中にwebsocketが切れてしまうことがあります。
この場合、接続しなおしますが、その間に相手が石を置いた場合logが流れてこないのでリバーシが止まってしまいます。
この対策として接続しなおしたらreversi/show-gameでゲームの情報を取得し、logsの中にはそれまでのlogが溜まっているのでbotの内部で使う盤面をmapで初期化し、logsを使って最後の一手前まで再現。
最後の手が自分だった場合、落ちている間に石は打たれていなかったということなのでそのまま再現。
最後の手が相手だった場合、落ちている間に石が打たれたということなのでその一手から置く場所を考えて石を置く。
という感じにして落ちても大丈夫にさせられます。

作った感想

藍ちゃんを除いてmisskey games版リバーシに対応した初めて(多分)のbotなので作ってて自分は今開拓してるんだなぁってなって楽しかったです

ちなみにぶろみね君のリバーシ対応で躓いたのはreversiGameへの接続でgameIdパラメータを載せる必要があるという物です。
これで2日くらい潰れました(ちゃんとストリーミングAPIのドキュメントちゃんと読んでいればこんなことには...)

また、リバーシ対応に当たって一番難しかったのはチャンネルにメッセージを送ることでした
従来のbotの運用でチャンネルにメッセージを送るというのが無かったのでかなり苦労しましたね...
(今までのぶろみね君はLTL(localTimeline)と通知(main)に接続して流れてくる情報を処理するだけだった)

逆にドキュメントが無い問題は藍ちゃんの中身が読みやすいのと昔のドキュメントに書いてあった内容が今のリバーシとかなり似ていたのもあってあまり問題にはならなかったです
(TypeScriptどころかJavaScriptも知らないけど読めるからすごい)

ちなみにこの記事を書いている理由は自分以外にもリバーシbot作りたい人が絶対いるだろうからもうドキュメントみたいな感じでAPIまとめを書いちゃおうっていうのです

みんなもリバーシbot作ろうね

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?