最近リアルが忙しい六角レンチです
地球寒すぎませんかね?
それはさておきぶろみね君にリバーシ機能を追加したのでリバーシの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にして送信
変わった時に送られるので相手の準備完了ボタンがまた押されたとき(準備に戻った時)は当然、自分が準備完了を送信したときにも送られてくるので注意(無限ループに陥る)
user1
とuser2
がどちらもTrue
になってしばらくたつとstated
が送られてくる
データ構造
{id: チャンネル識別用のID,
type: 'changeReadyStates',
body: {user1 : bool,
user2 : bool}}
対戦中に送られるメッセージ
started
開始時に送られてきます
一緒にゲームの情報も送られてきます
前述した自分が先行か確かめる方法ですが、ゲームの情報の中にblack
というパラメータがあり、このblack
の数字でどちらが先行か決まるのでここで決めた方がいいです。(black
の取る値は1か2でuser1、user2どちらが黒かを指している)
また、ゲームの情報のstartedAt
が始まった時間。isStarted
がTrue
にそれぞれ変わっています。
あと設定されたゲームの設定もゲームの情報の中に入ります。
データ構造
{id : チャンネル識別用のid,
type : 'started',
body : {game : ゲームの情報}}
bodyがゲームの情報ではなく、bodyの中にあるgameがゲームの情報なので注意
ended
終了時に送られてきます
started
と同じくゲームの情報も送られてきます
started
と同様にendedAt
が終わった時間に。isEnded
がTrue
に変わります。
また、勝者がいる場合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の流れ
-
reversi
チャンネルへ接続 - メッセージを受け取るまで待つ
-
matched
を受け取った場合、reversiGame
チャンネルへgameId
を入れて接続する。
invited
を受け取った場合、reversi/match
へ招待してきたuserId
を使ってPOSTする。
うまくいくとレスポンスでゲームの情報が返ってくるのでreversiGame
チャンネルへgameId
を入れて接続する。 - 相手の設定が変更されると
updateSettings
が送られてくるのでそれを見て必要なら何かやる
ここでもしcanceled
が送られてきたらチャンネルから切断し、2に戻る -
changeReadyStates
で相手の準備ができた場合、自分も準備ができたらメッセージready
を送る -
started
で始まったら情報から自分が先行か等を見て駒を置いたりする -
log
で相手が置いたら置く場所を考えて石を置く -
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作ろうね