新手分步指南
准备工作
- 一个 FlipSight 账号。登录后才能注册机器人和下载连接器。
- 一张机器人照片:GIF、JPG 或 PNG 文件,不超过 5 MB,宽和高都至少 175 px。
- 一台用于运行机器人的个人电脑 机器人的着法在运行连接程序的你的电脑上计算。即使是同一个引擎,强度也会因电脑性能和引擎设置而不同;更换电脑或设置后,等级分也可能随之变化。
- Python 3,一个用来运行连接器的免费程序。3.9 或更高版本可用,推荐 3.11 或更高版本。会在第 5 步检查。
- 自己的机器人引擎。连接程序附带用于测试的练习引擎。
1. 注册机器人
登录后打开机器人页面(othelloclub.com/zh/flipsight/bot)。在顶部的注册我的机器人区域,在机器人名称(2~24个字符)中输入名称。支持的游戏暂时保持只勾选标准。在上传机器人照片中选择图片,然后点击注册我的机器人按钮。
成功时会这样:出现连接令牌(仅显示一次)框,里面是一段以 fsbm_ 开头的长文本。
2. 保存令牌
这段长文本就是令牌,是你的机器人专用的密码。连接器用它向服务器证明自己代表这个机器人。请全部复制,暂时粘贴到文本编辑器等地方。关闭这个框后,就无法再次看到同一个令牌。
请像对待密码一样对待令牌。不要给任何人看,也不要发布它的截图。如果丢失或被别人看到,请在机器人列表中打开管理,点击生成连接令牌。拿到新令牌后,旧令牌会立即失效。
3. 下载连接器
点击下载Python示例下载 ZIP 文件并解压。本指南把解压出的文件夹称为“机器人文件夹”。请把它放在容易找到的地方,例如桌面。
成功时会这样:机器人文件夹里有 README.md 文件和 tools 文件夹。
4. 在机器人文件夹中打开终端
终端是用来输入命令的窗口。请按你的系统用下面的方法打开。
macOS:按 Command+Space,输入 Terminal 并打开它。输入 cd 和一个空格,把机器人文件夹拖进窗口,然后按 Enter。
Linux:在文件管理器中右键点击机器人文件夹,选择“在终端中打开”。不同发行版的菜单名称可能不同。如果没有这个菜单,就打开终端,像 macOS 那样用 cd 进入文件夹。
Windows:在文件资源管理器中打开机器人文件夹。在顶部的地址栏中输入 powershell 并按 Enter。会在该文件夹中打开一个 PowerShell 窗口。
检查:输入下面的命令并按 Enter。列表中出现 README.md 和 tools 就表示成功。这条命令在 macOS、Linux 和 Windows PowerShell 中都一样。
ls5. 检查 Python
在同一个窗口中,复制适合你系统的那一条命令,粘贴后按 Enter。可以使用每个代码框右上角的复制按钮。
macOS / Linux
python3 --versionWindows PowerShell
py --version成功时会这样:显示以 3 开头的版本号,例如 Python 3.11.9。3.9 或更高版本可用。如果提示找不到命令,请按下面的方法安装 Python,关闭窗口,然后从第 4 步重新开始。
macOS:从 python.org 下载 macOS 安装包(.pkg)并安装。然后在“应用程序”里的 Python 文件夹中运行一次 Install Certificates.command。这会准备安全连接(HTTPS)所需的证书。
Linux:用发行版的软件管理器或包管理器安装 python3。
Windows:从 python.org 安装 Python install manager(Python 安装管理器)。
6. 输入令牌
把令牌放进环境变量。环境变量是只在这个终端窗口中存在的临时设置,名称是 FS_BOT_TOKEN。复制下面适合你系统的一行命令,粘贴后按一次 Enter。随后会出现 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)).Password在 macOS 和 Linux 上,粘贴的令牌不会显示;在 Windows 上会显示 * 而不是令牌的字符。两种情况都是正常的。因为令牌没有写进命令里,所以不会留在命令历史中。这个设置只在这个窗口中有效。如果关闭了窗口或打开了新窗口,请重复第 4 步和第 6 步。
Windows:在 PowerShell 中,用 Ctrl+V 或右键点击来粘贴。这条一行命令的写法在 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 机器人的着法由服务器计算,因此无需保持对局室打开。自动匹配开启时(默认),连接器运行期间网站也会为你安排机器人战。
成功时会这样:对局室中显示两个棋盘,每次轮到你的机器人时都会落下一子。练习引擎非常弱,输棋是正常的。
9. 关闭窗口后继续运行(可选)
用下面的命令代替第 7 步启动,关闭窗口后机器人也会继续运行。请在输入令牌的同一个窗口(第 6 步)中运行。电脑关机或进入睡眠时会停止。对局中途停止,该局会因超时或断线判负。
macOS / Linux
macOS / Linux:运行启动命令后可以关闭窗口。信息会写入机器人文件夹中的 bot.log 文件。要停止时,打开新的终端并运行停止命令。只复制你需要的那个代码框:如果把两个代码框一起粘贴,机器人会在启动后立即停止。
启动
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:任务栏中会出现一个最小化的窗口,机器人在其中运行。可以关闭当前的 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下载适合操作系统的文件,放入机器人文件夹,再用下面的准备命令解压。Windows和Linux命令适用于x86-64电脑。
Windows PowerShell (x86-64): edax-4.6-MS-windows-x86.zip
准备(仅首次)
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
准备(仅首次)
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命令行工具,下面的准备命令会编译源码。
准备(仅首次)
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;两局同时进行时,每局运行一个引擎。--max-think-seconds是每步安全上限,默认为10秒。Egaroucid达到上限时会停止搜索,改下一步合法备用着法,并在下一回合重启引擎。
自制引擎:通过 JSON 连接
只有想连接自己编写的引擎时,才需要阅读这一部分。如果你使用 Edax、Egaroucid 或其他 NBoard 引擎,上面的步骤已经全部完成,可以跳过这一部分。
每次轮到你的机器人下棋时,连接器都会运行一次 my_adapter.py。连接器把当前局面作为一行内容交给它(棋子在哪里、轮到谁下、哪些格子可以合法落子、还剩多少时间),my_adapter.py 用一行回复一个格子,连接器再把这一步下到服务器上。my_adapter.py 是夹在中间的一个小 Python 文件,负责向你的引擎要这一步棋。服务器连接、令牌和提交着法都由连接器负责,所以这个文件完全不用处理这些事。
bot_match_client.py→ my_adapter.pybot_match_client.py← my_adapter.py你的引擎属于哪一种?
- 用 Python 代码选择着法的引擎:请使用下面的示例 A。
- 每次运行都输出一行回复然后退出的程序文件:请使用下面的示例 B。
- 保持运行并持续交换命令的可执行文件引擎:若支持 NBoard 规格,用上面的 --nboard 连接。若是其他规格,请按该规格修改示例 B,并让 my_adapter.py 保持引擎运行,循环逐行交换。
第 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}。看到这一行后,在已设置第 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,确认出现一行回复。这一步是你的引擎选的,所以出现 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 个棋盘字符、行棋方和以毫秒为单位的思考时间,并回复一行 D3 这样的坐标(. = 空,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 分钟,所以在一步棋上想得太久很快就会超时判负。示例中的 think_ms 用剩余时间(remaining_ms)除以你剩下的步数,来决定这一步的用时。budget_ms 不是这一步要用掉的时间,而是连接器等待回复的最长时间(默认 10 秒)。如果回复比这更晚,机器人就无法继续这一局并超时判负,所以传给引擎的应该是 think_ms,而不是 budget_ms。
出问题时:如果调用引擎失败,示例文件会把原因写入 my_adapter.log,并改下第一个合法落点。如果着法看起来不对劲,请先打开这个文件。连接器不会在屏幕上显示适配器的错误信息。print 只用于输出那一行回复,调试文字请写到文件里。
速度:连接程序每局让 my_adapter.py 保持运行,每步发送一行。像示例那样回答一行就退出的文件会在下一步重新运行,因此引擎也每次重新启动。若引擎启动很慢或想延续之前各步的计算,请把 my_adapter.py 改为“读一行、答一行”的循环。不要读取输入直到结束,每次回答后 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(适配器 → 连接器)
把下面任一种回复写成一行 JSON,并以换行结尾。move 是格子编号或坐标,不区分大小写。在 8×8 上,d3 是 (3−1)×8+3 = 19。请始终从收到的 legal_moves 中选择。在机器人之间的对战中没有合法着时,发送 -1 或 pass;与人对局时,被迫停着由服务器处理。stdout 只写回复这一行,不要输出其他信息。
{"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 | 引擎没有按时回答,或者回答的格式不对。请检查引擎命令和它输出的一行 JSON。如果机器人在开始后 30 秒内没有下出第一步,对局会被中止。 |
| 收不到变体游戏的挑战。 | 挑战窗口只列出双方机器人都支持的游戏。请确认你的机器人的支持的游戏中勾选了该游戏,并且对方机器人也支持它。通过 NBoard 连接的机器人应只勾选标准。 |
术语表
| 术语 | 含义 |
|---|---|
| API | 程序之间约定好的发送请求和回复的方式。连接器会替你处理。 |
| 令牌 | 你的机器人专用的密码。连接器用它向服务器证明自己是哪个机器人。 |
| 终端 | 用来输入命令的窗口。在 Windows 上使用 PowerShell。 |
| 环境变量 | 传给从同一个终端窗口启动的程序的临时设置。窗口关闭后就会消失。 |
| JSON | 把数据写成文本的一种格式。示例:{"move":19} |
| 引擎 | 查看局面并选择着法的程序。 |
| 适配器 | 在你的引擎和连接器之间转换数据的小程序。 |
| NBoard | NBoard是一款可配合NTest使用的奥赛罗图形界面程序。NBoard协议是该程序与引擎通信的公开格式,适用于NTest、Edax等支持该协议的引擎。 |
| 合法着 | 现在可以下的格子。服务器会在 legal_moves 中列出它们。 |
下载
请登录后下载。.zip是包含说明书、连接程序和示例引擎的压缩包;.json是API规范,.md是使用说明书。
详细规范
本页以下内容是写给直接使用引擎或 API 的开发者的详细参考。
连接器选项
tools/bot_match_client.py 的选项。--engine 和 --nboard / --edax / --egaroucid 必须且只能指定其中一个。令牌不能作为选项传入,只从 FS_BOT_TOKEN 环境变量读取。退出码:0 正常停止,2 令牌被拒绝,3 达到 --max-runtime。
| 参数 | 说明 |
|---|---|
--engine "COMMAND" | 每局启动此命令一次并保持运行,每步交换一行 JSON(不关闭其输入)。回答一行后就退出的引擎会在下一步重新启动。 |
--nboard "COMMAND" | 每一局保持一个 NBoard 引擎,共两个。仅限标准(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 | 每一步等待引擎的最长秒数。默认 10,最大 300。不会超过剩余时间减去 0.75 秒。这是连接器的安全上限;服务器没有每步时间限制。 设为0可取消每步上限,改用对局剩余时间。 |
--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 的待接受挑战发送 agent_accept,然后在进行中的对战里,只计算每一局中轮到你的局面。每个 match_id、局和 revision 最多提交一次,状态变化后丢弃过时的计算。与人对局使用负数 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)先下。两局从同一局面开始,交换黑白,同时进行。在机器人之间的对战中,没有合法着时 legal_moves 会是空数组,机器人必须自己发送 move=-1(停着)。服务器不会代你停着,你的用时会继续减少。有合法着时停着是违规的。双方都不能下时,该局结束。空格不归任何一方;只把两局实际的棋子数相加,合计相同为平局。所有机器人对局都从随机生成的12子局面开始。 所有游戏类型中,每局每个机器人限时1分钟。
反向:落子和翻转与标准奥赛罗相同。两局合计棋子较少的一方获胜。
10x10:在 10×10 棋盘(100 格)上按标准规则对局。初始布局是中央四格;机器人对局会再随机走 8 步,从 12 子开始。
八角:棋盘和初始布局与 10x10 相同,但每个角各有三格、共 12 格不能使用(可用 88 格)。禁用格以格子编号的形式放在 blocked_squares 中,在 board 中则是与空格相同的 0。该数组不按编号顺序排列。禁用格不能落子,连成一线的棋子也在那里中断。
经典黑白棋:按规则从空棋盘开始。中央四格(27、28、35、36)中还有空格时,只能下在那里,并且不翻转任何棋子。四格都填满后,按标准规则进行。每种颜色最多下 32 子,包括最初的四步。placed 是每种颜色到目前为止已下的子数,键是字符串 "1"(黑)和 "2"(白)。机器人对局从服务器随机下完 12 步(包括中央四步)后的局面开始,所以第一条请求中两种颜色的 placed 合计为 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。只能在机器人网页的管理中修改该列表,不能用机器人令牌(API)修改。有进行中或等待接受的对战时,修改会被拒绝。不能保存空列表。
NBoard 和变体
--nboard 连接只支持标准(std)。收到变体游戏时,连接器不会询问引擎,而是放弃这一回合(信息为 Engine failed for match)。因为不会发送任何着法,对局会在第一步之前被中止,或者之后该局超时判负。请只给 NBoard 机器人设置标准这一种支持的游戏。
人与注册机器人对局
玩家可以用你的机器人注册的任何游戏向它发起挑战。如果支持标准,STD 和 XOT 都可能到来。XOT 以 game_mode std 发送,从已经下了 8 步的棋盘开始(这些着法包含在 history 中)。用时由发起挑战的人选择,对局不计等级分。与人对局只有一局(leg 0),match_id 为负数。无法落子时由服务器处理停着,所以不要发送 -1。 开启自动匹配的在线注册机器人也会参加支持的STD/XOT快速匹配。会员快速匹配计等级分,访客不计。
非法着和用时
非法着或过时的 revision 会被 HTTP 409(illegal_move、stale_revision、not_your_turn)拒绝,棋盘不会变化。在此期间用时继续减少。连接器会用同一个 revision 重新发送同一着,所以如果不修正引擎,该局最终会超时判负。服务器没有每步时间限制,只检查每种颜色的剩余时间。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子局面开始。 同一局面交换黑白同时进行两局,以总棋子数决定胜负;反向游戏中棋子较少者获胜。
所有机器人对局都从随机生成的12子局面开始。 所有游戏类型中,每局每个机器人限时1分钟。
如果机器人在开始后 30 秒内没有下出第一步,对战将中止且不计等级分。如果对战中一方机器人断线超过 30 秒而另一方保持连接,则保持连接的机器人获胜(finish_reason: disconnect),结果计入等级分。
所有游戏类型中,FlipSight 机器人的着法都由服务器计算。网页挑战和 API 挑战(agent_challenge)都可以,服务器会立即开始对局,因此无需保持对局室打开。如果服务器的计算停止超过 30 秒,该对局作废(aborted,finish_reason: technical),不计入等级分。变体游戏的着法计算量大,与 FlipSight 机器人的变体对局同时最多 2 局。名额已满时会返回 HTTP 409 和 site bot variant matches are full,请稍后再试。
自动机器人战:网站始终保持两个机器人战房间。开启自动匹配的注册机器人在线且未在对局时,会收到与等级分相近的机器人(优先其他注册机器人,没有则为 FlipSight 机器人)进行标准(std)机器人战的挑战,该机器人为 bot_b。连接器自动接受后对局立即开始。如果 120 秒内未接受或拒绝,该机器人会在 10 分钟内不参加自动匹配。自动机器人战计入等级分。竞技场对局进行中时,自动机器人战也会继续。自动匹配默认开启,可在机器人页面的管理中关闭。 用练习引擎启动的机器人(environment.practice 为 true)不参加自动机器人战,这期间机器人之间的对局也不计入等级分。
人类直接挑战注册机器人时,可选择支持的游戏与时间,在普通房间进行一局。服务器强制直接挑战不计等级分。 开启自动匹配的在线注册机器人也会参加支持的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子局面开始。 同一局面交换黑白同时进行两局,以总棋子数决定胜负;反向游戏中棋子较少者获胜。
如果你是 bot_a,参与者编号为 0;如果是 bot_b,则为 1。参与者 0 在 legs[0] 执黑(1),在 legs[1] 执白(2);参与者 1 相反。当 leg.status 为 active 且 leg.turn 等于你的颜色时轮到你。两局可能同时轮到你。
没有合法着时,必须自己发送 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 63在 10×10(grand、octagon)上,格子编号 = 行 × 10 + 列。列是 a 到 j,第十行在坐标中写作 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,每行接收一个 JSON 事件(NDJSON):hello 确认连接,match 表示你的某个对战有变化(机器人之间对战的事件包含完整的对战对象),ping 每 10 秒发送一次以保持连接。收到 match 后调用一次 agent_poll。连接断开时,以从 1 秒逐渐增加到 30 秒的间隔重连,期间继续轮询。事件流连接期间,你的机器人显示为在线;每个机器人最多可打开 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 上运行,用两种方式连接引擎。--engine 每局启动引擎一次并保持运行,每步发送一行 JSON,读取一行 {"move": 格子编号}。回答一行后就退出的引擎下一步会重新启动,因此仍可使用;但读取输入直到结束的引擎(sys.stdin.read() 等)永远不会回答,请改为逐行读取。请求中的 legal_moves 是可落子的格子列表,若为空则回答 -1。--nboard 让 NBoard 协议引擎保持运行,每局一个,用 --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。不使用事件流轮询时,对战中每秒一次,空闲时每 5 秒一次,且绝不超过每秒一次。网络时间计入你的用时。落子响应丢失时,先查询新状态,只有 revision 仍相同时才重发同一着。
API操作
响应示例展示格式。复制按钮只复制请求,不调用API。
POST agent_poll
查询状态
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string "agent_poll" | 必填 | 要调用的操作名称 |
environment | object | 可选 | 连接程序所在电脑的 CPU、操作系统和引擎名称(各最多 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 | 每边的格数,8 或 10 |
state.blocked_squares | integer[] | 不能使用的格子(仅八角,未排序) |
state.legs | array | 对局(机器人之间 2 局,与人 1 局) |
state.totals | [integer, integer] | 每位参与者两局的子数合计 |
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 列表中查到。所有游戏类型都可以通过 API 向 FlipSight 内置机器人(is_builtin: true)发起挑战,服务器会立即开始对局。如果与内置机器人的变体对局已有 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 表示单个机器人的事件流过多。连接错误和 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 |