初めての方向けの手順
用意するもの
- FlipSightのアカウント。ボット登録と接続プログラムのダウンロードはログイン後に行えます。
- ボットの写真1枚。GIF、JPG、PNGで5MB以下、縦横とも175px以上の画像です。
- ボットを動かす自分のコンピューター1台 ボットの手は、接続プログラムを動かしている自分のコンピューターで計算されます。同じエンジンでも、コンピューターの性能やエンジンの設定によって強さが変わることがあり、コンピューターや設定を変えるとレーティングもそれに合わせて動くことがあります。
- Python 3。接続プログラムを動かす無料のプログラムです。3.9以上で動き、3.11以上をおすすめします。手順5で確認します。
- 自分のボットエンジン。接続プログラムにはテスト用の練習エンジンが付属しています。
1. ボットを登録する
ログインしてボットページ(othelloclub.com/ja/flipsight/bot)を開きます。いちばん上の自分のボットを登録の欄で、ボット名(2~24文字)に名前を入力します。対応ゲームは最初は標準だけにチェックが入ったままにします。ボット写真をアップロードで写真を選び、自分のボットを登録ボタンを押します。
成功の目安:接続トークン(今回のみ表示)の枠が表示され、fsbm_で始まる長い文字列が入っています。
2. トークンを保管する
この長い文字列がトークン(ボット専用のパスワード)です。接続プログラムがサーバーに「このボット本人です」と証明するときに使います。枠の中をすべてコピーし、メモ帳などに一時的に貼っておきます。枠を閉じると同じトークンはもう表示できません。
トークンはパスワードと同じように扱います。人に見せたり、画面を撮って投稿したりしないでください。なくしたときや人に見られたときは、ボット一覧の管理を開いて接続トークンを発行を押します。新しいトークンを受け取ると、古いトークンはすぐに使えなくなります。
3. 接続プログラムをダウンロードする
FlipSight ボット接続プログラム一式 (.zip)を押してZIPファイルをダウンロードし、展開します。展開したフォルダーをこの手順では「ボットフォルダー」と呼びます。デスクトップなど見つけやすい場所に置きます。
成功の目安:ボットフォルダーの中にREADME.mdファイルとtoolsフォルダーがあります。
4. ボットフォルダーで端末を開く
端末はコマンドを文字で入力する画面です。お使いのOSに合わせて開きます。
macOS:Command+Spaceを押してTerminalと入力し、開きます。cdと空白1つを入力し、ボットフォルダーを画面にドラッグしてからEnterを押します。
Linux:ファイルマネージャーでボットフォルダーを右クリックし、「端末で開く」を選びます。メニュー名はディストリビューションによって違うことがあります。ない場合は端末を開き、macOSと同じようにcdで移動します。
Windows:エクスプローラーでボットフォルダーを開きます。上のアドレス欄にpowershellと入力してEnterを押すと、そのフォルダーでPowerShellの画面が開きます。
確認:下のコマンドを入力してEnterを押します。一覧にREADME.mdとtoolsが表示されれば成功です。このコマンドはmacOS、Linux、Windows PowerShellで共通です。
ls5. Pythonを確認する
同じ画面で、お使いのOS用のコマンドを1つコピーして貼り付け、Enterを押します。コード枠の右上にあるコピーボタンを使えます。
macOS / Linux
python3 --versionWindows PowerShell
py --version成功の目安:Python 3.11.9のように3で始まる番号が表示されます。3.9以上なら使えます。コマンドが見つからないと表示されたら下のようにインストールし、画面を閉じて手順4からやり直します。
macOS:python.orgからmacOS用インストーラー(.pkg)を入手してインストールします。終わったら「アプリケーション」のPythonフォルダーにあるInstall Certificates.commandを一度実行します。安全な通信(HTTPS)に必要な証明書を準備する手順です。
Linux:ディストリビューションのソフトウェア管理やパッケージ管理でpython3をインストールします。
Windows:python.orgからPython install managerをインストールします。
6. トークンを入力する
トークンを環境変数(今開いている端末の画面の中だけで使う一時的な設定)に入れます。名前はFS_BOT_TOKENです。下からお使いのOS用の1行のコマンドをコピーして貼り付け、Enterを1回押します。するとBot tokenと表示され、入力待ちになります。そこで手順2でコピーしたトークンを貼り付けてEnterを押します。
macOS / Linux
printf 'Bot token: '; read -r -s FS_BOT_TOKEN; printf '\n'; export FS_BOT_TOKENWindows PowerShell
$env:FS_BOT_TOKEN = [Net.NetworkCredential]::new('', (Read-Host 'Bot token' -AsSecureString)).PasswordmacOSとLinuxでは貼り付けたトークンが画面に表示されず、Windowsでは文字の代わりに*が表示されます。どちらも正常です。トークンをコマンドに直接書かないので、コマンド履歴に残りません。この設定はこの画面の中でだけ有効です。画面を閉じたり新しい画面を開いたりしたときは、手順4と6をやり直します。
Windows:PowerShellでの貼り付けはCtrl+Vか右クリックです。この1行のコマンドは、Windowsに標準で入っているWindows PowerShell 5.1と新しいPowerShell 7の両方で使える書き方ですが、実際のWindows機ではまだ確認前です。
成功の目安:エラーなしで次のコマンドを入力する行が表示されます。
7. 接続プログラムを起動する
同じ画面で、下のコマンドをそのままコピーして貼り付け、Enterを押します。コマンドの中のファイル名は変えません。練習用エンジン(打てるマスのうち番号がいちばん小さいマスに打つ、とても弱いエンジン)で接続します。
macOS / Linux
python3 tools/bot_match_client.py --engine 'python3 tools/bot_match_example_engine.py'Windows PowerShell
py tools/bot_match_client.py --engine 'py tools/bot_match_example_engine.py'実行中に文字が表示されず、次の入力待ちに戻らないのは正常です。ボットをオンラインに保つには、このウィンドウを開いたまま接続プログラムとエンジンを実行し続け、コンピューターがスリープしないようにしてください。このウィンドウを閉じるかCtrl+Cを押すと、接続プログラムとエンジンが終了し、ボットはオフラインになります。再びオンラインにするには、上のコマンドをもう一度実行してください。他のボットからの対局申し込みは接続プログラムが自動で承諾します。
練習用エンジンで動かしている間は自動ボット対戦が組まれず、自分で申し込んだボット同士の対局もレーティングに反映されません。10の手順のように自分のエンジンに替えて起動し直すと、その時から自動対戦とレーティングの対象になります。
成功の目安:ボットページの登録ボット一覧で、自分のボット名の右の点が緑色です。点にマウスを重ねるとオンラインと表示されます。灰色(オフライン)なら更新を押します。
8. 最初の対局で試す
ボットページの対戦する自分のボットで自分のボットを選びます。FlipSightボットの一覧から好きなボットを選んで対戦申込を押し、表示された画面でもう一度確認します。どのゲームの種類でもFlipSightボットの手をサーバーが計算するので、対局室を開いておく必要はありません。自動マッチングがオン(初期設定)なら、接続プログラムを動かしている間、サイトがボット対戦を組むこともあります。
成功の目安:対局室に盤が2面表示され、自分のボットの手番ごとに石が置かれます。練習用エンジンはとても弱いので、負けるのが普通です。
9. 画面を閉じても動かし続ける(任意)
手順7の代わりに下のコマンドで起動すると、画面を閉じてもボットが動き続けます。トークンを入力した同じ画面(手順6)で実行します。パソコンの電源が切れたりスリープしたりすると止まります。対局中に止めると、その対局は時間切れ負けか切断負けになります。
macOS / Linux
macOS / Linux:起動のコマンドを実行したあとは画面を閉じてかまいません。メッセージはボットフォルダーのbot.logファイルにたまります。止めるときは新しい端末を開いて停止のコマンドを実行します。2つの箱をまとめて貼り付けると起動してすぐ止まるので、必要な箱を1つだけコピーします。
起動
nohup python3 tools/bot_match_client.py --engine 'python3 tools/bot_match_example_engine.py' > bot.log 2>&1 &停止
pkill -f bot_match_client.pyWindows PowerShell
Windows:タスクバーに最小化された画面が1つでき、ボットはその中で動きます。今のPowerShellの画面は閉じてかまいません。止めるときは最小化された画面を開いてCtrl+Cを押すか、その画面を閉じます。
起動
Start-Process py -ArgumentList 'tools/bot_match_client.py --engine "py tools/bot_match_example_engine.py"' -WorkingDirectory $PWD -WindowStyle Minimized10. 自分のエンジンに切り替える
練習用エンジンで接続を確認したら自分のエンジンに切り替えます。Edax・Egaroucidは下の専用オプション、それ以外のNBoard対応エンジンは--nboard、自作エンジンはJSONを使います。トークンはそのまま使えます。
変則ゲーム(アンチ、10x10、八角、クラシックリバーシ)は、エンジンがそのルールを知っている場合だけ対応ゲームにチェックします。チェックしたゲームは、ほかのボットや人から申し込まれます。変更するときはボット一覧の管理で対応ゲームを直し、保存を押します。進行中の対局や承認待ちの申し込みがあるときは変更できません。
EdaxなどNBoard対応エンジンを接続
NBoardはNTestと組み合わせて使えるオセロの画面プログラムです。NBoardプロトコルは、このプログラムとエンジン間の公開通信規格で、NTest、Edaxなど、この規格に対応したエンジンで使えます。
以下のインストール・起動例は、Edax 4.6とEgaroucid for Console 7.8.2を基準にしています。
Edax 4.6は接続プログラムに含まれていません。Edax 4.6のAssetsからOSに合うファイルをダウンロードし、ボットフォルダーに置いて下の準備コマンドで展開してください。Windows・Linuxのコマンドはx86-64用です。
Windows PowerShell (x86-64): edax-4.6-MS-windows-x86.zip
準備(最初の1回だけ)
Expand-Archive -LiteralPath edax-4.6-MS-windows-x86.zip -DestinationPath edax-4.6起動
py tools/bot_match_client.py --edax "edax-4.6\wEdax-x86-64.exe"Linux (x86-64): edax-4.6-linux-x86.tar.gz
準備(最初の1回だけ)
mkdir -p edax-4.6
tar -xzf edax-4.6-linux-x86.tar.gz -C edax-4.6
chmod +x edax-4.6/lEdax-x86-64起動
python3 tools/bot_match_client.py --edax "edax-4.6/lEdax-x86-64"macOS: edax-reversi-4.6.tar.gz
Edax 4.6の公式macOS実行ファイルはありません。同じページのSource code (tar.gz)とedax-4.6-linux-x86.tar.gzをダウンロードしてください。ソースからMac用をビルドし、Linuxのアーカイブからは共通の評価ファイルdata/eval.datだけを取り出します。Xcodeコマンドラインツールが必要です。下の準備コマンドはソースをコンパイルします。
準備(最初の1回だけ)
xcode-select --installtar -xzf edax-reversi-4.6.tar.gz
mv edax-reversi-4.6 edax-4.6
mkdir -p edax-4.6/bin
tar -xzf edax-4.6-linux-x86.tar.gz -C edax-4.6 data/eval.dat
(cd edax-4.6/src && make build ARCH=native OS=osx)起動
python3 tools/bot_match_client.py --edax "edax-4.6/bin/mEdax-native"Edaxは、以下のように--edaxに実行ファイルのパスだけを指定しても接続できます。別のアダプターは不要で、接続プログラムが残り持ち時間から各手の思考時間を配分します。
コマンドのパスを自分の実行ファイルの絶対パスに変えてください。空白を含むパスは二重引用符で囲みます。評価ファイルが自動で見つからない場合は--edax-evalでeval.datを指定してください。
NBoard接続は通常(8×8)だけに対応します。このボットは対応ゲームで標準だけにチェックしておきます。変則の対局が来ると接続プログラムはエンジンに問い合わせないので手が打てず、その対局は無効か時間切れ負けになります。
Egaroucid: --egaroucid
Egaroucid for Consoleを用意してください。画面用アプリではなくコンソール版の実行ファイルを指定し、resourcesフォルダーは元の場所に置きます。macOS・Linuxでは公式手順でビルドした実行ファイルを使います。独自のsetboard、settime、goコマンドで局面と残り時間を渡すため、アダプターを作る必要はありません。
Windows PowerShell
py tools/bot_match_client.py --egaroucid "egaroucid\Egaroucid_for_console.exe"macOS / Linux
python3 tools/bot_match_client.py --egaroucid "egaroucid/bin/Egaroucid_for_Console.out"どちらも通常の8×8専用で、2局同時の場合は各局に1つずつエンジンを起動します。--max-think-secondsは1手の安全上限です(既定10秒)。Egaroucidが上限に達したら探索を終了して代わりに合法手を打ち、次の手番でエンジンを再起動します。
自作エンジン:JSONで接続
ここは自作エンジンをつなぎたい方だけが読めば十分です。Edax、Egaroucid、NBoard対応エンジンは上の方法で接続が終わっているので、読み飛ばしてください。
接続プログラムは、自分のボットの手番が来るたびに my_adapter.py を1回実行します。今の局面(石の位置、どちらの手番か、打てるマス、残り時間)を1行で渡し、my_adapter.py が打つマスを1行で答えると、その手を代わりにサーバーへ送ります。my_adapter.py はその間で自分のエンジンに手を尋ねる小さな Python ファイルです。サーバーへの接続、トークン、着手の送信は接続プログラムが行うので、このファイルでは気にしません。
bot_match_client.py→ my_adapter.pybot_match_client.py← my_adapter.py自分のエンジンはどれですか?
- Python のコードで手を選ぶエンジン:下の例 A を使います。
- 実行ファイルで、1回実行すると答えを1行出して終わるエンジン:下の例 B を使います。
- 実行ファイルで、起動したまま命令をやり取りし続けるエンジン:NBoard規格に対応していれば上の--nboardで接続します。別の規格なら例Bをその規格に合わせて直し、エンジンを起動したままmy_adapter.pyが1行ずつやり取りを繰り返すようにします。
ステップ1:接続だけを確かめるファイル (my_adapter.py)
ボットフォルダー(README.md と tools がある場所)に my_adapter.py というファイルを作り、下の内容をそのまま貼り付けて保存します。このファイルはエンジンを使わず、打てる最初のマスを答えるだけです。自分のエンジンをつなぐ前に、接続できるかを確かめるためのものです。
import json
import sys
def choose_move(position):
legal = position["legal_moves"]
return legal[0] if legal else -1
line = sys.stdin.readline()
position = json.loads(line)
move = choose_move(position)
print(json.dumps({"move": move}), flush=True)
サーバーに接続する前に入出力を確認
1の箱のコマンドは例の局面をアダプターに直接入れてみます。正常なら{"move": 19}が1行表示されます。その行が出たら、2の箱のコマンドでこのアダプターを使って接続プログラムを起動します(手順6のトークンがある画面で)。2つの箱は別々に実行します。エンジンがこのJSON形式を直接読めるなら、--engineにエンジンの起動コマンドをそのまま書いてもかまいません。
1. アダプターだけを試す
macOS / Linux
printf '%s\n' '{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[19,26,37,44],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":0,"2":0},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}' | python3 my_adapter.pyWindows PowerShell
'{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[19,26,37,44],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":0,"2":0},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}' | py my_adapter.py2. アダプターで接続プログラムを起動
macOS / Linux
python3 tools/bot_match_client.py --engine 'python3 my_adapter.py'Windows PowerShell
py tools/bot_match_client.py --engine 'py my_adapter.py'ステップ2:自分のエンジンを呼ぶファイルに替える
ステップ1ができたら、my_adapter.py の中身を下の例 A か例 B に丸ごと置き換えます。直すのは EDIT と書かれた行だけで、ほかはそのままにします。置き換えたら上のテスト用コマンド1をもう一度実行し、答えが1行出るか確かめてください。エンジンが選んだ手なので、19 以外のマスが出ても正常です。
例 A:Python の関数で手を選ぶエンジン
my_engine.py というファイルに手を選ぶ search 関数があると仮定した例です。EDIT 1 でファイル名と関数名を、EDIT 2 で渡す値を自分のエンジンに合わせて直します。関数は打つマスの番号(0~63)か d3 のような座標を返せば十分です。
import json
import sys
from pathlib import Path
from my_engine import search # EDIT 1: your engine file and function name
def think_ms(position):
# Time for this move: share the remaining clock over my remaining moves.
my_moves_left = position["board"].count(0) // 2 + 1
share = position["remaining_ms"] / my_moves_left
return int(min(share, position["budget_ms"]) * 0.8)
def choose_move(position):
if not position["legal_moves"]:
return -1
# EDIT 2: pass what your function needs
return search(
board=position["board"],
color=position["color"],
legal_moves=position["legal_moves"],
time_limit_ms=think_ms(position),
)
position = json.loads(sys.stdin.readline())
legal = position["legal_moves"]
try:
move = choose_move(position)
except Exception as error:
# The engine failed: note why in my_adapter.log and play the first legal square.
with open(Path(__file__).with_name("my_adapter.log"), "a") as log:
log.write(repr(error) + "\n")
move = legal[0] if legal else -1
print(json.dumps({"move": move}), flush=True)例 B:実行ファイルのエンジン
実行ファイルが盤面64文字、手番、考える時間(ミリ秒)を1行で受け取り、D3 のような座標を1行で答えると仮定した例です(.=空き、X=黒、O=白)。形式はエンジンごとに違うので、EDIT 1 で実行ファイル名を、EDIT 2 でエンジンへ送る行を、EDIT 3 で答えを読む部分を自分のエンジンに合わせて直します。
my_adapter.py → my_engine
...........................OX......XO........................... X 1777my_engine → my_adapter.py
D3import json
import subprocess
import sys
from pathlib import Path
# EDIT 1: your engine program file (kept in the same folder as my_adapter.py)
ENGINE = Path(__file__).with_name("my_engine.exe" if sys.platform == "win32" else "my_engine")
def think_ms(position):
# Time for this move: share the remaining clock over my remaining moves.
my_moves_left = position["board"].count(0) // 2 + 1
share = position["remaining_ms"] / my_moves_left
return int(min(share, position["budget_ms"]) * 0.8)
def choose_move(position):
if not position["legal_moves"]:
return -1
# EDIT 2: build the line your engine reads
board = "".join(".XO"[cell] for cell in position["board"])
color = "X" if position["color"] == 1 else "O"
reply = subprocess.run(
[str(ENGINE)],
input=f"{board} {color} {think_ms(position)}\n",
text=True,
capture_output=True,
check=True,
timeout=position["budget_ms"] / 1000 * 0.9,
)
# EDIT 3: take the square out of your engine's answer
return reply.stdout.strip()
position = json.loads(sys.stdin.readline())
legal = position["legal_moves"]
try:
move = choose_move(position)
except Exception as error:
# The engine failed: note why in my_adapter.log and play the first legal square.
with open(Path(__file__).with_name("my_adapter.log"), "a") as log:
log.write(repr(error) + "\n")
move = legal[0] if legal else -1
print(json.dumps({"move": move}), flush=True)考える時間:ボット対局は1局ごとの持ち時間が1分なので、1手に長く使うとすぐ時間切れ負けになります。例の think_ms は残り時間(remaining_ms)を自分の残り手数で割って、この手に使う時間を決めます。budget_ms はこの手に使ってよい時間ではなく、接続プログラムが答えを待つ最長時間(既定は10秒)です。これを過ぎるとその局はそれ以上打てず時間切れ負けになるので、エンジンには budget_ms ではなく think_ms の値を渡してください。
うまく動かないとき:例のファイルは、エンジンの呼び出しに失敗すると理由を my_adapter.log に書き、打てる最初のマスを代わりに打ちます。手がおかしいときは、まずこのファイルを開いてください。接続プログラムはアダプターのエラーメッセージを画面に表示しません。print は答えの1行だけに使い、確認用の文字はファイルに書いてください。
速さ:接続プログラムは対局ごとにmy_adapter.pyを起動したままにし、1手ごとに1行送ります。例のように1行答えて終了するファイルなら次の手で起動し直すので、エンジンも毎回起動し直します。エンジンの起動に時間がかかる場合や前の手の計算を引き継ぎたい場合は、my_adapter.pyを「1行読んで1行答える」を繰り返す形にしてください。入力を最後まで読まず、答えるたびにflushします。
以下は、接続プログラムと my_adapter.py がやり取りする内容を説明した参考資料です。上の例のファイルをそのまま使うなら、読まなくてもかまいません。
入力JSON(接続プログラム → アダプター)
以下は実行コードではなく、接続プログラムが送るデータの例です。Pythonファイルとして保存する必要はありません。対局中は現在のデータが毎手自動で届きます。
下は通常8×8の初期局面の例です。実際には接続プログラムが現在の局面を入れて送ります。boardはa1から右へ読み、次の段へ進むマスの配列で、0=空き、1=黒、2=白です。color=1なら黒の手を選びます。legal_movesは今打てるマスの番号です。remaining_msは残りの持ち時間、budget_msは接続プログラムが今回の手の答えを待つ最長時間で、単位はミリ秒(1000=1秒)です。変則ゲームのフィールドと例は下の「変則ゲーム」の節にあります。
{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[19,26,37,44],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":0,"2":0},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}応答JSON(アダプター → 接続プログラム)
応答は下の例のいずれかをJSON1行で書き、改行で終えます。moveにはマス番号か座標を入れ、大文字と小文字は区別しません。8×8のd3は(3−1)×8+3=19です。必ず受け取ったlegal_movesの中から選びます。ボット同士の対局で打てる場所がなければ-1かpassを送ります。人との対局の強制パスはサーバーが処理します。stdoutには応答の1行だけを書き、ほかの案内文を混ぜません。
{"move":19}{"move":"d3"}{"move":-1}種目別のエンジンへの要求の例
接続プログラムが--engineのエンジンに送る黒の最初の要求を、ルールのコードで作った例です。実際の対局はどの種目もランダムな12石の局面から始まるので、boardとlegal_movesは対局ごとに変わります。この例は表示のたびに変わらないよう、開始の手を盤の中央に近いマスに固定して作りました。match_idと持ち時間の値も実際の対局では変わります。
game_mode: std、anti、grand、octagon、reversiのいずれか。人との対局のXOTはstdで届きます。board_size: 8。grandとoctagonでは10。board: 長さboard_size²の配列。a1から右へ読み、次の段へ進みます。0=空き(八角の使えないマスを含む)、1=黒、2=白。blocked_squares: 八角の使えないマスの番号。ほかのゲームでは空の配列。placed: 色ごとにこれまで置いた石の数(開始時のランダムな手を含む)。キーは文字列の"1"と"2"。クラシックリバーシの32個制限に使います。
アンチ (Anti) (anti)
入力JSON(接続プログラム → アダプター)
{
"board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"color": 1,
"legal_moves": [2, 3, 12, 25, 42, 44, 45],
"game_mode": "anti",
"board_size": 8,
"blocked_squares": [],
"placed": {
"1": 4,
"2": 4
},
"remaining_ms": 60000,
"match_id": 123,
"leg": 0,
"revision": 0,
"history": [],
"round_no": null,
"room_created_at": null,
"budget_ms": 10000
}応答JSON(アダプター → 接続プログラム)
{"move":2}10x10 (Grand) (grand)
入力JSON(接続プログラム → アダプター)
{
"board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"color": 1,
"legal_moves": [13, 14, 25, 42, 63, 65, 66],
"game_mode": "grand",
"board_size": 10,
"blocked_squares": [],
"placed": {
"1": 4,
"2": 4
},
"remaining_ms": 60000,
"match_id": 123,
"leg": 0,
"revision": 0,
"history": [],
"round_no": null,
"room_created_at": null,
"budget_ms": 10000
}応答JSON(アダプター → 接続プログラム)
{"move":13}八角 (Octagon) (octagon)
入力JSON(接続プログラム → アダプター)
{
"board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"color": 1,
"legal_moves": [13, 14, 25, 42, 63, 65, 66],
"game_mode": "octagon",
"board_size": 10,
"blocked_squares": [0, 1, 10, 8, 9, 19, 80, 90, 91, 89, 98, 99],
"placed": {
"1": 4,
"2": 4
},
"remaining_ms": 60000,
"match_id": 123,
"leg": 0,
"revision": 0,
"history": [],
"round_no": null,
"room_created_at": null,
"budget_ms": 10000
}応答JSON(アダプター → 接続プログラム)
{"move":13}クラシックリバーシ (reversi)
入力JSON(接続プログラム → アダプター)
{
"board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 2, 2, 1, 1, 0, 0, 0, 0, 2, 2, 1, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"color": 1,
"legal_moves": [18, 25, 33, 41, 42, 50, 51],
"game_mode": "reversi",
"board_size": 8,
"blocked_squares": [],
"placed": {
"1": 6,
"2": 6
},
"remaining_ms": 60000,
"match_id": 123,
"leg": 0,
"revision": 0,
"history": [],
"round_no": null,
"room_created_at": null,
"budget_ms": 10000
}応答JSON(アダプター → 接続プログラム)
{"move":18}よくあるつまずき
| 症状 | 原因と対処 |
|---|---|
| ボットが灰色の点(オフライン)で表示される。 | 接続プログラムの画面が動いているか確認します。画面を閉じた、Ctrl+Cを押した、パソコンがスリープした場合は止まっています。手順6と7をやり直し、更新を押します。 |
コマンドが見つからないと表示される。command not found / is not recognized | Pythonが入っていないか、インストール後に画面を開き直していません。手順5を見て、新しい画面を開き手順4からやり直します。 |
ファイルを開けないと表示される。can't open file | ボットフォルダー以外の場所で実行しています。手順4のようにボットフォルダーへ移動し、lsでREADME.mdとtoolsが表示されるか確認します。 |
トークンがないと表示されてすぐ終わる。FS_BOT_TOKEN is missing or invalid | この画面にはトークンがありません。新しい画面を開いた場合は手順6をやり直します。 |
トークンが拒否されたと表示されて止まる。Bot credential rejected | トークンの貼り付けを間違えたか、古いトークンです。新しいトークンを発行したりボットを無効にしたりすると、古いトークンは使えません。最新のトークンで手順6からやり直します。 |
同じエラー行が何度も表示される。Connection/state error | インターネット接続を確認します。macOSなら手順5のInstall Certificates.commandを実行したか確認します。対局中だけ表示されるなら、エンジンの手をサーバーが反則として拒否している可能性があります。接続プログラムは同じ手を送り直すので、エンジンを直す必要があります。 |
| 対局申込ボタンが押せない、または申し込みが承認されない。 | 両方のボットが緑の点である必要があります。対戦する自分のボットで自分のボットを選んだか確認します。ほかの対局中のボットは新しい申し込みを受けません。承認されない申し込みは120秒で期限切れになります。 |
手を打たずに時間切れ負けや無効になる。Engine failed for match | エンジンが時間内に答えなかったか、答えの形式が間違っています。エンジンの起動コマンドと1行のJSON出力を確認します。開始から30秒以内に初手を打たないと、その対局は無効になります。 |
| 変則ゲームの申し込みが来ない。 | 申込画面には両方のボットが対応するゲームだけが表示されます。自分のボットの対応ゲームでそのゲームにチェックしたか、相手のボットも対応しているか確認します。NBoardで接続したボットは標準だけにチェックします。 |
用語の説明
| 用語 | 意味 |
|---|---|
| API | プログラム同士が決まった形式で要求と応答をやり取りする窓口です。接続プログラムが代わりに処理します。 |
| トークン | 自分のボット専用のパスワードです。接続プログラムがサーバーにボット本人であることを証明するときに使います。 |
| 端末 | コマンドを文字で入力する画面です。WindowsではPowerShellを使います。 |
| 環境変数 | ひとつの端末画面から起動するプログラムに渡す一時的な設定です。画面を閉じると消えます。 |
| JSON | データを文字で書く形式です。例:{"move":19} |
| エンジン | 局面を見て手を選ぶプログラムです。 |
| アダプター | 自分のエンジンと接続プログラムの間でデータ形式を変換する小さなプログラムです。 |
| NBoard | NBoardはNTestと組み合わせて使えるオセロの画面プログラムです。NBoardプロトコルは、このプログラムとエンジン間の公開通信規格で、NTest、Edaxなど、この規格に対応したエンジンで使えます。 |
| 合法手 | 今打てるマスです。サーバーがlegal_movesで知らせます。 |
ダウンロード
ダウンロードにはログインが必要です。最初の項目は説明書とtoolsフォルダーの接続プログラムと例のエンジンをまとめた圧縮ファイル(.zip)です。API仕様(.json)と説明書(.md)は個別ファイルです。
詳しい仕様
ここから先は、エンジンやAPIを直接扱う開発者向けの詳細です。
接続プログラムのオプション
tools/bot_match_client.pyに指定できる起動オプションです。--engineと--nboard / --edax / --egaroucidのいずれか1つは必ず指定します。トークンはオプションではなく、環境変数FS_BOT_TOKENからだけ読み込みます。終了コードは0(正常終了)、2(トークン拒否)、3(--max-runtime到達)です。
| パラメーター | 説明 |
|---|---|
--engine "COMMAND" | 対局ごとにこのコマンドを1回起動したままにし、1手ごとにJSONを1行送って1行受け取ります(入力は閉じません)。1行答えて終了するエンジンは次の手で起動し直します。 |
--nboard "COMMAND" | NBoardエンジンを局ごとに1つ、合計2つ動かしたままにします。通常(std)専用です。 |
--edax PATH | Edaxは、以下のように--edaxに実行ファイルのパスだけを指定しても接続できます。別のアダプターは不要で、接続プログラムが残り持ち時間から各手の思考時間を配分します。 |
--edax-eval PATH | Edaxの評価ファイルeval.datのパスです。省略すると実行ファイルのフォルダー、またはその親のdata/eval.datを探します。 |
--egaroucid PATH | Egaroucid for Consoleを用意してください。画面用アプリではなくコンソール版の実行ファイルを指定し、resourcesフォルダーは元の場所に置きます。macOS・Linuxでは公式手順でビルドした実行ファイルを使います。独自のsetboard、settime、goコマンドで局面と残り時間を渡すため、アダプターを作る必要はありません。 |
--nboard-depth N | NBoardエンジンにset depth Nを送ります。0~60で、0ならエンジンの既定値です。 (--edax: 0 → 60). |
--max-think-seconds S | 1手でエンジンを待つ最大時間(秒)です。既定は10、最大300。残り時間から0.75秒を引いた値より長くはしません。接続プログラム側の安全上限で、サーバーには1手ごとの時間制限はありません。 0なら別の1手上限を設けず、対局の残り時間を使います。 |
--engine-name NAME | ボット情報に報告するエンジン名(120文字以内)です。コマンドやパスは入れません。 |
--cpu-name NAME | 報告するCPU名(120文字以内)です。省略すると自動で調べます。 |
--once-match ID | この対局だけを処理し、終わったら(finished、aborted、expired、rejected)終了します。人との対局の負のIDも使えます。試験では--max-runtimeと一緒に使います。 |
--max-runtime S | 指定した秒数が過ぎるとエンジンを片付けて終了します(終了コード3)。 |
--no-stream | イベントストリームを使わず、ポーリングだけを行います。 |
--api URL, --stream-url URL | APIとストリームのアドレスです。通常は既定値を使います。HTTPSのみで、トークンも一緒に送られるため、ストリームはAPIと同じホストに限ります。 |
APIを直接実装する場合
agent_pollまたは認証付きイベントストリームで申し込みと局面を受け取ります。自分がbot_bのpending申し込みにagent_acceptを送り、running対局の各legで自分の手番の局面だけを計算します。同じmatch_id、leg、revisionごとに1回だけ着手し、状態が変わったら古い計算を捨てます。人との対局は負のmatch_idとleg=0で、round_noとroom_created_atを受け取ったまま送ります。通常の部屋の強制パスはサーバーが処理し、ボット同士の対局だけがパスを送ります。
agent_poll / bot-stream → pending (bot_b = my_id)
→ agent_accept → running → legs[0] + legs[1]
→ agent_move(match_id, leg, revision, move) → finished / aborted変則ゲーム
ボットは対応ゲームでチェックしたゲームだけを受けます。エンジンへの要求と状態のgame_modeで種目がわかります。盤の大きさ、使えないマス、打てるマスはサーバーが計算して送るので、エンジンは受け取ったlegal_movesの中から選べば十分です。通常8×8用の計算で10×10やクラシックリバーシの手を作らないでください。
| ゲーム | game_mode | 盤 | 開始時の石 | 持ち時間(局ごと、色ごと) | 黒の最初のlegal_moves |
|---|---|---|---|---|---|
| 標準 | std | 8×8 | ランダムな12石の局面 | 60,000 ms | 局面ごとに異なる |
| アンチ (Anti) | anti | 8×8 | ランダムな12石の局面 | 60,000 ms | 局面ごとに異なる |
| 10x10 (Grand) | grand | 10×10 | ランダムな12石の局面 | 60,000 ms | 局面ごとに異なる |
| 八角 (Octagon) | octagon | 10×10 (88) | ランダムな12石の局面 | 60,000 ms | 局面ごとに異なる |
| クラシックリバーシ | reversi | 8×8 | ランダムな12石の局面 | 60,000 ms | 局面ごとに異なる |
共通:黒(1)が先に打ちます。2局は同じ開始局面から黒白を入れ替えて同時に進みます。ボット同士の対局で打てる場所がないとlegal_movesが空の配列で届き、ボットがmove=-1(パス)を自分で送る必要があります。サーバーは代わりにパスせず、その間も自分の持ち時間は減ります。打てる場所があるのにパスすると反則です。両方の色が打てなくなるとその局は終わります。空きマスはどちらにも与えず、実際の石の数だけを2局合計し、同数なら引き分けです。すべてのボット戦は、ランダムに作った12石の局面から始まります。すべてのゲーム種類で、各局の各ボットの持ち時間は1分です。
アンチ:置き方と返し方は通常と同じです。2局の石の合計が少ない方が勝ちです。
10x10:10×10の盤(100マス)で通常のルールで打ちます。ルール上の初期配置は中央の4マスで、ボット戦はそこにランダムな8手を加えた12石から始まります。
八角:10x10と同じ盤と初期配置で、四隅の3マスずつ、合計12マスを使いません(使えるマス88)。使えないマスはblocked_squaresにマス番号で届き、boardでは空きマスと同じ0で届きます。配列は番号順ではありません。使えないマスには打てず、石の並びもそこで途切れます。
クラシックリバーシ:ルール上は空の盤から始めます。中央の4マス(27、28、35、36)に空きがある間は、そこにだけ返さずに置きます。4マスが埋まると通常のルールです。各色が置ける石は最初の4手を含めて32個までです。placedは色ごとにこれまで置いた石の数で、キーは文字列の"1"(黒)と"2"(白)です。ボット戦はサーバーが中央の4手を含む12手をランダムに打った局面から始まるので、最初の要求のplacedは2色の合計が12です(通常は{"1":6,"2":6})。32個を置き終えた色はlegal_movesが空の配列になり、手番ごとに-1を送ります。
種目別のエンジンへの要求の例
接続プログラムが--engineのエンジンに送る黒の最初の要求を、ルールのコードで作った例です。実際の対局はどの種目もランダムな12石の局面から始まるので、boardとlegal_movesは対局ごとに変わります。この例は表示のたびに変わらないよう、開始の手を盤の中央に近いマスに固定して作りました。match_idと持ち時間の値も実際の対局では変わります。
アンチ (Anti) (anti)
{"board":[0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,2,2,0,0,0,0,0,1,2,2,1,0,0,0,0,2,2,2,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[2,3,12,25,42,44,45],"game_mode":"anti","board_size":8,"blocked_squares":[],"placed":{"1":4,"2":4},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}10x10 (Grand) (grand)
{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[13,14,25,42,63,65,66],"game_mode":"grand","board_size":10,"blocked_squares":[],"placed":{"1":4,"2":4},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}八角 (Octagon) (octagon)
{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[13,14,25,42,63,65,66],"game_mode":"octagon","board_size":10,"blocked_squares":[0,1,10,8,9,19,80,90,91,89,98,99],"placed":{"1":4,"2":4},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}クラシックリバーシ (reversi)
{"board":[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,2,2,1,1,0,0,0,0,2,2,1,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[18,25,33,41,42,50,51],"game_mode":"reversi","board_size":8,"blocked_squares":[],"placed":{"1":6,"2":6},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}エンジンへの要求のフィールド
| フィールド | 型 | 説明 |
|---|---|---|
game_mode | string | std、anti、grand、octagon、reversiのいずれか。人との対局のXOTはstdで届きます。 |
board_size | integer 8 | 10 | 8。grandとoctagonでは10。 |
board | integer[board_size²] | 長さboard_size²の配列。a1から右へ読み、次の段へ進みます。0=空き(八角の使えないマスを含む)、1=黒、2=白。 |
blocked_squares | integer[] | 八角の使えないマスの番号。ほかのゲームでは空の配列。 |
placed | {"1": integer, "2": integer} | 色ごとにこれまで置いた石の数(開始時のランダムな手を含む)。キーは文字列の"1"と"2"。クラシックリバーシの32個制限に使います。 |
legal_moves | integer[] | 今打てるマスの番号。空の配列ならmove=-1を返します。 |
color | integer 1 | 2 | 打つ色。1=黒、2=白。 |
budget_ms | integer | 今回の手に使える時間(ミリ秒)。--max-think-secondsと、残り時間から0.75秒を引いた値の小さい方。超えると接続プログラムがエンジンを止めます。 |
remaining_ms | integer | この局の自分の色の残り時間(ミリ秒)。この手番ですでに使った時間を引いた値です。 |
match_id | integer ≠ 0 | 対局id。人との対局は負の値(-部屋番号)。 |
leg | integer 0 | 1 | 局の番号0または1。人との対局は0。 |
revision | integer | この局の状態番号。agent_moveにそのまま送ります。 |
history | array | この局で打った手の一覧。判断はboardで行います。 |
round_no | integer | null | 人との対局のラウンド番号。ボット同士の対局ではnull。 |
room_created_at | string | null | 人との対局の部屋の作成時刻。ボット同士の対局ではnull。 |
ボット同士の変則対局の申し込み
agent_challengeにgame_modeを入れます。省略するとstdです。両方のボットのsupported_gamesに含まれている必要があります。相手が対応するゲームはagent_poll応答のbots[].supported_gamesで確認します。どちらかが対応していないとHTTP 409と下のエラーが返ります。
{"action":"agent_challenge","target_id":2,"game_mode":"anti"}
{"action":"agent_challenge","target_name":"Opponent","game_mode":"grand"}HTTP 409
{"ok":false,"error":"unsupported game mode","server_now_ms":1800000000000}対応ゲーム(supported_games)の変更
登録画面では標準にあらかじめチェックが入っており、対応ゲームを登録していなかった既存のボットはstdです。変更はWebのボットページの管理でだけでき、ボットのトークン(API)では変更できません。進行中の対局や承認待ちの申し込みがあると拒否されます。空の一覧では保存できません。
NBoard接続と変則
--nboard接続は通常(std)だけに対応します。変則の対局が来ると、接続プログラムはエンジンに問い合わせずにその手番をあきらめます(Engine failed for matchのメッセージ)。手を送れないので、初手の前なら無効、そのあとならその局の時間切れ負けになります。NBoardのボットは対応ゲームを通常だけにします。
人と登録ボットの対局
人もボットが登録したゲームで申し込めます。通常に対応していると、STDとXOTの両方が来ることがあります。XOTはgame_modeがstdで届き、すでに8手進んだ盤から始まります(その手はhistoryに入っています)。持ち時間は申し込んだ人が選び、レーティングは適用されません。人との対局は1局(leg 0)で、match_idは負の値です。打てる場所がないときのパスはサーバーが処理するので、-1は送りません。 自動マッチングがオンのオンライン登録ボットは、対応するSTD/XOTのクイックマッチにも参加します。会員のクイックマッチはレーティング対象、ゲストは対象外です。
反則手と時間
反則手や古いrevisionはHTTP 409(illegal_move、stale_revision、not_your_turn)で拒否され、盤は変わりません。その間も持ち時間は減り続けます。接続プログラムは同じ手を同じrevisionで送り直すので、エンジンを直さないと最後にはその局が時間切れ負けになります。サーバーには1手ごとの時間制限はなく、色ごとの残り時間だけを見ます。10秒は接続プログラムの--max-think-secondsの既定値です。
認証
HTTPSのJSON POSTとAuthorization: Bearerを使います。トークンはそのボット専用です。URL、公開コード、ログに含めないでください。
Content-Type: application/json
Authorization: Bearer <FS_BOT_TOKEN>POST https://othelloclub.com/flipsight/bot_match_api.php
対戦ルール
すべてのボット戦は、ランダムに作った12石の局面から始まります。 同じ局面で白黒を入れ替えた2局を同時に行い、石数の合計で勝敗を決めます。アンチは石数が少ない方が勝ちます。
すべてのボット戦は、ランダムに作った12石の局面から始まります。 すべてのゲーム種類で、各局の各ボットの持ち時間は1分です。
開始から30秒以内に初手を打たないボットがいる場合、その対戦は無効(aborted)となり、レーティングに反映されません。対戦中に一方のボットの接続が30秒を超えて切れ、もう一方が接続中の場合は、接続中のボットの勝ち(finish_reason: disconnect)となり、レーティングに反映されます。
FlipSightボットの手は、どのゲームの種類でもサーバーが計算します。ウェブからの申し込みとAPIからの申し込み(agent_challenge)のどちらも受け付け、サーバーがすぐに対局を始めるので、対局室を開いておく必要はありません。サーバーの計算が30秒を超えて止まると、その対局は無効(aborted、finish_reason: technical)となり、レーティングに反映しません。変則ゲームは1手の計算が重いため、FlipSightボットとの変則対局は同時に2局までです。埋まっているとHTTP 409とsite bot variant matches are fullエラーが返るので、少し後でもう一度申し込んでください。
自動ボット対戦:サイトはボット対戦の部屋を常に2つ開いています。自動マッチングがオンの登録ボットがオンラインで対局中でないと、レーティングの近いボット(ほかの登録ボットを優先し、いなければFlipSightボット)との通常(std)のボット対戦の申し込みがそのボットに届きます(そのボットがbot_b)。接続プログラムが自動で承諾すると、すぐに始まります。120秒以内に承諾しないか拒否すると、そのボットは10分間自動マッチングから外れます。自動ボット対戦もレーティングに反映します。アリーナの対局中も自動ボット対戦は続きます。自動マッチングは初期設定でオンで、ボットページの管理でオフにできます。 練習用エンジンで起動したボット(environment.practiceがtrue)は自動ボット対戦の候補から外れ、その間のボット同士の対局はレーティングに反映されません。
人が直接申し込む登録ボット戦は、対応ゲームと時間を選び通常の対局室で1局行います。直接の申し込みはサーバー側で必ずレーティング対象外にします。 自動マッチングがオンのオンライン登録ボットは、対応するSTD/XOTのクイックマッチにも参加します。会員のクイックマッチはレーティング対象、ゲストは対象外です。 登録ボットはボット管理の同時対局数(既定2)までしか対局を受けません。人との対局1局とボット同士の対戦1件(2局)をそれぞれ1と数えます。
pending → running → finished / aborted
pending → rejected / expired
kind=human: id=-room_id, legs[0], round_no, room_created_at
agent_move: move=19 / "d3" / "D3" (8×8)
10×10: "j10" = "J0" = 99
pass: -1 / "pass" / "PA" / "PS" / "--" (paired games only)
index=row*board_size+column (0-based)対戦の流れ
すべてのボット戦は、ランダムに作った12石の局面から始まります。 同じ局面で白黒を入れ替えた2局を同時に行い、石数の合計で勝敗を決めます。アンチは石数が少ない方が勝ちます。
自分の参加者番号はbot_aなら0、bot_bなら1です。参加者0はlegs[0]で黒(1)、legs[1]で白(2)、参加者1はその逆です。leg.statusがactiveでleg.turnが自分の色なら自分の手番です。2局で同時に自分の手番になることもあります。
合法手がない場合は、move -1(パス)を自分で送る必要があります。サーバーは代わりにパスしません。人との対局では、通常の対局室と同じくサーバーがパスを処理します。
自分の手番での残り時間 = clocks_ms[自分の色 - 1] - (server_now_ms - turn_started_ms)。clocks_msは色の順(黒、白)で、totalsとwinnerは参加者の順です(winnerは0または1、引き分けはnull)。
マス番号
マス番号 = 行 × 8 + 列で、a1=0、h1=7、a2=8、h8=63です。例えば19はd3、37はf5です。board配列も同じ順で、0=空、1=黒、2=白です。
a b c d e f g h
1 0 1 2 3 4 5 6 7
2 8 9 10 11 12 13 14 15
3 16 17 18 19 20 21 22 23
4 24 25 26 27 28 29 30 31
5 32 33 34 35 36 37 38 39
6 40 41 42 43 44 45 46 47
7 48 49 50 51 52 53 54 55
8 56 57 58 59 60 61 62 6310×10(grand、octagon)はマス番号=行×10+列です。列はa~jで、座標の10段目は10または0と書きます。a1=0、j1=9、a2=10、j10=99(j0と同じ)です。同じ座標でも盤の大きさで番号が変わります。たとえばd3は8×8では19、10×10では23です。
a b c d e f g h i j
1 0 1 2 3 4 5 6 7 8 9
2 10 11 12 13 14 15 16 17 18 19
3 20 21 22 23 24 25 26 27 28 29
4 30 31 32 33 34 35 36 37 38 39
5 40 41 42 43 44 45 46 47 48 49
6 50 51 52 53 54 55 56 57 58 59
7 60 61 62 63 64 65 66 67 68 69
8 70 71 72 73 74 75 76 77 78 79
9 80 81 82 83 84 85 86 87 88 89
10 90 91 92 93 94 95 96 97 98 99八角 (Octagon) (octagon)
a b c d e f g h i j
1 ## ## 2 3 4 5 6 7 ## ##
2 ## 11 12 13 14 15 16 17 18 ##
3 20 21 22 23 24 25 26 27 28 29
4 30 31 32 33 34 35 36 37 38 39
5 40 41 42 43 44 45 46 47 48 49
6 50 51 52 53 54 55 56 57 58 59
7 60 61 62 63 64 65 66 67 68 69
8 70 71 72 73 74 75 76 77 78 79
9 ## 81 82 83 84 85 86 87 88 ##
10 ## ## 92 93 94 95 96 97 ## ##イベントストリーム
同じBearerトークンでGET https://othelloclub.com/flipsight_api/bot-stream に接続すると、1行に1つのJSONイベント(NDJSON)を受信します。helloは接続確認、matchは自分の対戦の変更(ボット同士の対戦では対戦オブジェクト全体を含む)、pingは10秒ごとの接続維持信号です。matchを受信したらagent_pollを1回呼んでください。接続が切れたら1秒から30秒まで間隔を延ばして再接続し、その間はポーリングで進めます。ストリーム接続中はボットがオンラインと表示されます。1つのボットで開けるストリームは3つまでです。
イベント例
curl -N 'https://othelloclub.com/flipsight_api/bot-stream' \
-H "Authorization: Bearer $FS_BOT_TOKEN"
{"type":"hello","bot_id":1,"server_now_ms":1800000000000}
{"type":"match","match":{"id":123,"bot_a":1,"bot_b":2,"status":"running","state":{...}},"server_now_ms":1800000000500}
{"type":"match","match":{"id":-45,"kind":"human","room_id":45},"server_now_ms":1800000003000}
{"type":"ping","server_now_ms":1800000010000}エンジン接続
FlipSight ボット接続プログラム(Python 3.9以上、標準ライブラリのみ)はWindows、macOS、Linuxで動作し、エンジンを2つの方式で接続します。--engineは対局ごとにエンジンを1回起動したままにし、1手ごとにJSONを1行送って{"move": マス番号}を1行受け取ります。1行答えて終了するエンジンも次の手で起動し直すのでそのまま動きますが、入力を最後まで読むエンジン(sys.stdin.read()など)は答えられないので、1行ずつ読むようにしてください。リクエストのlegal_movesは合法なマスの一覧で、空なら-1を返します。--nboardはNBoardプロトコルのエンジンを1局に1つずつ起動したままにし、--nboard-depthで探索深さを指定します。
Edaxは、以下のように--edaxに実行ファイルのパスだけを指定しても接続できます。別のアダプターは不要で、接続プログラムが残り持ち時間から各手の思考時間を配分します。
Egaroucid for Consoleを用意してください。画面用アプリではなくコンソール版の実行ファイルを指定し、resourcesフォルダーは元の場所に置きます。macOS・Linuxでは公式手順でビルドした実行ファイルを使います。独自のsetboard、settime、goコマンドで局面と残り時間を渡すため、アダプターを作る必要はありません。
入力JSON(接続プログラム → アダプター)
以下は実行コードではなく、接続プログラムが送るデータの例です。Pythonファイルとして保存する必要はありません。対局中は現在のデータが毎手自動で届きます。
{"board":[0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,2,2,0,0,0,0,0,1,2,2,1,0,0,0,0,2,2,2,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[2,3,12,25,42,44,45],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":4,"2":4},"remaining_ms":40000,"match_id":123,"leg":1,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}応答JSON(アダプター → 接続プログラム)
{"move":2}状態の同期
ストリーム接続中は、イベント受信時と10秒ごとにagent_pollを呼びます。ストリームなしでポーリングする場合は、対局中は1秒ごと、待機中は5秒ごとに呼び、1秒に1回より頻繁には呼ばないでください。通信時間も持ち時間に含まれます。着手の応答が失われたら新しい状態を取得し、revisionが変わっていない場合にのみ同じ手を再送します。
API操作
レスポンス例は形式を示すものです。コピーボタンはリクエストをコピーするだけで、APIは呼び出しません。
POST agent_poll
状態を取得
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
action | string "agent_poll" | 必須 | 呼び出すアクション名 |
environment | object | 任意 | 接続PCのCPU、OS、エンジン名(各120文字以内、任意)とpractice(trueかfalse、任意)。practiceがtrueなら練習中という意味で、自動ボット対戦の候補から外れ、その間に申し込んだり承諾したりしたボット同士の対局はレーティングに反映されません。接続プログラムは練習用エンジンで起動するとこの値を自動で送ります。サーバーは最後に受け取った値を覚えているので、練習を終えるときはpracticeなし、またはfalseで送ります。 |
リクエスト例
curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
-H "Authorization: Bearer $FS_BOT_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"action":"agent_poll","environment":{"cpu":"Apple M4","engine":"MyEngine","os":"Darwin"}}'レスポンス例
{
"ok": true,
"bots": [
{
"id": 1,
"name": "Opponent",
"owner_id": "example",
"owner_name": "Example",
"owner": {
"player_id": "example",
"name": "Example",
"photo": "/img/no_profile.gif",
"country_code": "",
"country_name": "",
"country_flag_url": "",
"donor": false,
"verified_player_id": ""
},
"engine_level": null,
"rating": {
"rating": 1200,
"rd": 300,
"official": false,
"official_progress": {
"kind": "provisional",
"current": 0,
"required": 50
},
"color": "pz-rating-unofficial",
"high_rating": 1200,
"wins": 0,
"losses": 0,
"draws": 0,
"streak": 0,
"best_streak": 0
},
"supported_games": ["std"],
"auto_match": true,
"bio": "",
"registered_at": "2026-11-12 18:00:00",
"registered_at_ts": 1794474000,
"environment": {
"cpu": "Apple M4",
"engine": "MyEngine",
"os": "Darwin",
"practice": false
},
"is_builtin": false,
"online": true,
"disabled": false,
"deleted": false,
"photo": "/flipsight/bot_match_api.php?action=photo&bot_id=1&v=1794474000000"
},
{
"id": 2,
"name": "MyBot",
"owner_id": "example",
"owner_name": "Example",
"owner": {
"player_id": "example",
"name": "Example",
"photo": "/img/no_profile.gif",
"country_code": "",
"country_name": "",
"country_flag_url": "",
"donor": false,
"verified_player_id": ""
},
"engine_level": null,
"rating": {
"rating": 1200,
"rd": 300,
"official": false,
"official_progress": {
"kind": "provisional",
"current": 0,
"required": 50
},
"color": "pz-rating-unofficial",
"high_rating": 1200,
"wins": 0,
"losses": 0,
"draws": 0,
"streak": 0,
"best_streak": 0
},
"supported_games": ["std", "anti"],
"auto_match": true,
"bio": "",
"registered_at": "2026-11-12 18:00:00",
"registered_at_ts": 1794474000,
"environment": {
"cpu": "Apple M4",
"engine": "MyEngine",
"os": "Darwin",
"practice": false
},
"is_builtin": false,
"online": true,
"disabled": false,
"deleted": false,
"photo": "/flipsight/bot_match_api.php?action=photo&bot_id=2&v=1794474000000"
}
],
"record_bots": [],
"matches": [
{
"id": 123,
"bot_a": 1,
"bot_b": 2,
"game_mode": "std",
"time_limit_sec": 60,
"status": "running",
"state": {
"version": 1,
"game_mode": "std",
"time_limit_sec": 60,
"status": "active",
"revision": 1,
"started_ms": 1799999980000,
"last_now_ms": 1799999984200,
"finished_ms": null,
"opening_moves": [19, 20, 29, 34, 26, 11, 43, 37],
"opening_history": [
{
"color": 1,
"move": 19
},
{
"color": 2,
"move": 20
},
{
"color": 1,
"move": 29
},
{
"color": 2,
"move": 34
},
{
"color": 1,
"move": 26
},
{
"color": 2,
"move": 11
},
{
"color": 1,
"move": 43
},
{
"color": 2,
"move": 37
}
],
"legs": [
{
"board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 2, 1, 2, 2, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"turn": 2,
"black_participant": 0,
"placed": {
"1": 5,
"2": 4
},
"clocks_ms": [55800, 60000],
"turn_started_ms": 1799999984200,
"revision": 1,
"history": [
{
"color": 1,
"participant": 0,
"move": 44,
"at_ms": 1799999984200
}
],
"status": "active",
"discs": [5, 8],
"finished_ms": null,
"legal_moves": [18, 21, 22, 25, 30, 33, 38, 42, 50, 51, 52]
},
{
"board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"turn": 1,
"black_participant": 1,
"placed": {
"1": 4,
"2": 4
},
"clocks_ms": [60000, 60000],
"turn_started_ms": 1799999980000,
"revision": 0,
"history": [],
"status": "active",
"discs": [3, 9],
"finished_ms": null,
"legal_moves": [2, 3, 12, 25, 42, 44, 45]
}
],
"totals": [14, 11],
"winner": null,
"finish_reason": null,
"board_size": 8,
"blocked_squares": []
},
"auto": false,
"expires_ms": 1800000080000,
"ranked": true,
"rating_applied": false,
"rating_before": [null, null],
"rating_after": [null, null],
"created_at": "2026-11-12 19:00:00"
}
],
"me": "example",
"bot": {
"id": 2,
"name": "MyBot",
"owner_id": "example",
"owner_name": "Example",
"owner": {
"player_id": "example",
"name": "Example",
"photo": "/img/no_profile.gif",
"country_code": "",
"country_name": "",
"country_flag_url": "",
"donor": false,
"verified_player_id": ""
},
"engine_level": null,
"rating": {
"rating": 1200,
"rd": 300,
"official": false,
"official_progress": {
"kind": "provisional",
"current": 0,
"required": 50
},
"color": "pz-rating-unofficial",
"high_rating": 1200,
"wins": 0,
"losses": 0,
"draws": 0,
"streak": 0,
"best_streak": 0
},
"supported_games": ["std", "anti"],
"auto_match": true,
"bio": "",
"registered_at": "2026-11-12 18:00:00",
"registered_at_ts": 1794474000,
"environment": {
"cpu": "Apple M4",
"engine": "MyEngine",
"os": "Darwin",
"practice": false
},
"is_builtin": false,
"online": true,
"disabled": false,
"deleted": false,
"photo": "/flipsight/bot_match_api.php?action=photo&bot_id=2&v=1794474000000"
},
"server_now_ms": 1800000000000
}対戦オブジェクト(matches[])
| フィールド | 型 | 説明 |
|---|---|---|
id | integer | 対戦ID(人との対局は負の値) |
kind | string | 人との対局ならhuman |
bot_a | integer | 参加者0のボットID(人なら0) |
bot_b | integer | 参加者1のボットID(人なら0) |
game_mode | string | ゲーム(std、anti、grand、octagon、reversi) |
time_limit_sec | integer | 局ごと、色ごとの持ち時間(秒) |
status | string | pending, running, finished, aborted, rejected, expired |
expires_ms | integer | 承諾期限(ミリ秒) |
ranked | boolean | レーティング対戦かどうか |
state.status | string | active、finishedまたはaborted |
state.game_mode | string | ゲーム(std、anti、grand、octagon、reversi) |
state.board_size | integer 8 | 10 | 盤の1辺のマス数、8または10 |
state.blocked_squares | integer[] | 使えないマスの番号(八角のみ、番号順ではない) |
state.legs | array | 局の一覧(ボット同士は2局、人とは1局) |
state.totals | [integer, integer] | 参加者ごとの2局の判定点合計(result_discsがなければdiscsを使用) |
state.winner | integer | null | 勝った参加者 0 または 1、引き分けはnull |
state.finish_reason | string | null | discs, timeout, double_timeout, disconnect, no_first_move |
局オブジェクト(state.legs[])
| フィールド | 型 | 説明 |
|---|---|---|
board | integer[64 | 100] | 64 / 100マス、0=空 1=黒 2=白 |
turn | integer | 手番の色、1(黒)または2(白) |
legal_moves | integer[] | 手番の色が打てるマス。終わった局は空の配列 |
placed | {"1": integer, "2": integer} | 色ごとに置いた石の数、キーは"1"(黒)、"2"(白) |
black_participant | integer | この局で黒を持つ参加者 |
clocks_ms | [number, number] | 色の順(黒、白)の残り時間の基準値 |
turn_started_ms | integer | 現在の手番が始まったサーバー時刻 |
revision | integer | この局が変わるたびに1増加 |
status | string | active, finished, timeout, stopped |
history | array | 打った手の一覧(color, participant, move, at_ms) |
discs | [integer, integer] | 色の順(黒、白)の石数 |
result_discs | [integer, integer] (optional) | 時間切れになった局の合計用の判定点[黒, 白](任意のフィールド)。ある場合はdiscsの代わりに合計します。時間切れ側と相手の点数はstdとreversiが0対64、grandが0対100、octagonが0対88、antiが64対0です。もう一方の局は続き、両局が終わったあと合計で勝敗を決めます。過去の終了済みの記録は再計算しません。 |
POST agent_challenge
対戦申込
相手はtarget_id(ボット番号)またはtarget_name(ボット名)で指定します。番号と名前はagent_pollの応答のbots一覧で確認します。FlipSightの標準ボット(is_builtin: true)にも、どのゲームの種類でもAPIで申し込むことができ、サーバーがすぐに対局を始めます。標準ボットとの変則対局がすでに2局進行中だと、HTTP 409とsite bot variant matches are fullエラーが返ります。
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
action | string "agent_challenge" | 必須 | 呼び出すアクション名 |
target_id | integer ≥ 1 | 任意 | 申し込む相手ボットのID |
target_name | string | 任意 | 申し込む相手ボットの名前(target_id の代わり) |
game_mode | string std | anti | octagon | grand | reversi | 任意 | ボット同士の対局のゲーム。省略するとstd。両方のボットが対応している必要があります。 標準 / アンチ (Anti) / 八角 (Octagon) / 10x10 (Grand) / クラシックリバーシ |
リクエスト例
curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
-H "Authorization: Bearer $FS_BOT_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"action":"agent_challenge","target_id":2,"game_mode":"anti"}'レスポンス例
{
"ok": true,
"match_id": 123,
"server_now_ms": 1800000000000
}POST agent_accept
承諾
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
action | string "agent_accept" | 必須 | 呼び出すアクション名 |
match_id | integer ≥ 1 | 必須 | 対戦ID(人との対局は負の値) |
リクエスト例
curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
-H "Authorization: Bearer $FS_BOT_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"action":"agent_accept","match_id":123}'レスポンス例
{
"ok": true,
"match": {
"id": 123,
"status": "running",
"bot_a": 1,
"bot_b": 2
},
"server_now_ms": 1800000000000
}POST agent_reject
辞退
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
action | string "agent_reject" | 必須 | 呼び出すアクション名 |
match_id | integer ≥ 1 | 必須 | 対戦ID(人との対局は負の値) |
リクエスト例
curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
-H "Authorization: Bearer $FS_BOT_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"action":"agent_reject","match_id":123}'レスポンス例
{
"ok": true,
"match": {
"id": 123,
"status": "rejected",
"bot_a": 1,
"bot_b": 2
},
"server_now_ms": 1800000000000
}POST agent_move
着手を送信
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
action | string "agent_move" | 必須 | 呼び出すアクション名 |
match_id | integer ≠ 0 | 必須 | 対戦ID(人との対局は負の値) |
round_no | integer ≥ 0 | 任意 | 人との対局のみ: 対戦オブジェクトのround_noをそのまま |
room_created_at | string | 任意 | 人との対局のみ: 対戦オブジェクトのroom_created_atをそのまま |
leg | integer 0 | 1 | 必須 | 局 0 または 1(人との対局は0) |
revision | integer ≥ 0 | 必須 | 着手を計算した局のrevision。局が変わっていれば409 stale_revision |
move | integer -1..99 | string | 必須 | マス 0-(board_size²−1)、パスは-1 / d3 / D3 / pass |
リクエスト例
curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
-H "Authorization: Bearer $FS_BOT_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"action":"agent_move","match_id":123,"leg":1,"revision":0,"move":2}'レスポンス例
{
"ok": true,
"match": {
"id": 123,
"status": "running",
"bot_a": 1,
"bot_b": 2
},
"server_now_ms": 1800000000000
}エラー処理
エラーは{"ok": false, "error": "…"}の形式です。401は停止してトークンを確認します。409は現在の状態を取得し、手番、revision、合法手を確認します。429は1つのボットのストリームが多すぎることを示します。接続エラーと503は間隔を延ばして再試行し、不正な着手は再送しないでください。
{"ok":false,"error":"stale_revision","server_now_ms":1800000000000}| HTTP | error |
|---|---|
| 400 | JSON object required / unknown action |
| 401 | invalid bot credential |
| 403 | access denied / invalid csrf |
| 405 | POST required |
| 409 | stale_revision / not_your_turn / illegal_move / leg_finished / match not running / match timed out / stale room revision / invalid move coordinate / move outside board / unsupported game mode / both bots must be connected / bot already has an active match / bot busy / challenge expired or answered / site bot variant matches are full / target bot not found |
| 413 | request too large |
| 429 | too many streams (bot-stream) |
| 503 | bot match storage unavailable |