API文档

新手分步指南

准备工作

  • 一个 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 中都一样。

ls

5. 检查 Python

在同一个窗口中,复制适合你系统的那一条命令,粘贴后按 Enter。可以使用每个代码框右上角的复制按钮。

macOS / Linux

python3 --version

Windows 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 安装管理器)。

python.org

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_TOKEN

Windows 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.py

Windows 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 Minimized

10. 换成你自己的引擎

用练习引擎确认连接后,换成自己的引擎。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 --install
tar -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发送局面和剩余时间,无需编写适配器。

Egaroucid for Console

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 文件,负责向你的引擎要这一步棋。服务器连接、令牌和提交着法都由连接器负责,所以这个文件完全不用处理这些事。

FlipSight→ bot_match_client.py→ my_adapter.py
FlipSight← bot_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.py

Windows 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.py

2. 用适配器启动连接器

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 1777

my_engine → my_adapter.py

D3
import 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}
引擎查看局面并选择着法的程序。
适配器在你的引擎和连接器之间转换数据的小程序。
NBoardNBoard是一款可配合NTest使用的奥赛罗图形界面程序。NBoard协议是该程序与引擎通信的公开格式,适用于NTest、Edax等支持该协议的引擎。
合法着现在可以下的格子。服务器会在 legal_moves 中列出它们。

下载

下载Python示例

OpenAPI 3.1 JSON

README.md

请登录后下载。.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 PATHEdax也可按下方示例,通过--edax仅指定可执行文件路径来连接。不需要另写适配器;连接程序会根据对局剩余时间分配每步思考时间。
--edax-eval PATHEdax的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 URLAPI 和事件流的地址。通常保持默认值。只接受 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
标准std8×8随机 12 子60,000 ms取决于局面
反向 (Anti)anti8×8随机 12 子60,000 ms取决于局面
10x10 (Grand)grand10×10随机 12 子60,000 ms取决于局面
八角 (Octagon)octagon10×10 (88)随机 12 子60,000 ms取决于局面
经典翻转棋reversi8×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_modestringstd、anti、grand、octagon、reversi 之一。与人对局中的 XOT 以 std 发送。
board_sizeinteger 8 | 108;grand 和 octagon 为 10。
boardinteger[board_size²]长度为 board_size² 的数组。从 a1 开始向右读,然后是下一行。0=空(包括八角的禁用格),1=黑,2=白。
blocked_squaresinteger[]八角中不能使用的格子。其他游戏中为空数组。
placed{"1": integer, "2": integer}每种颜色到目前为止已下的子数(包括开局的随机步);键是字符串 "1" 和 "2"。用于经典黑白棋的 32 子上限。
legal_movesinteger[]现在可以下的格子。为空时回答 move=-1。
colorinteger 1 | 2要下的一方颜色。1=黑,2=白。
budget_msinteger这一步允许的时间(毫秒):取 --max-think-seconds 与剩余时间减去 0.75 秒两者中较小的值。超过时连接器会停止引擎。
remaining_msinteger你的颜色在这一局中的剩余时间(毫秒),已扣除本回合已用的时间。
match_idinteger ≠ 0对战 ID。与人对局时为负数(-房间号)。
leginteger 0 | 1对局编号 0 或 1。与人对局时总是 0。
revisioninteger这一局的状态编号。与 agent_move 一起原样发送。
historyarray这一局中已下的着法。请根据 board 判断。
round_nointeger | null与人对局的回合编号。机器人之间的对战为 null。
room_created_atstring | 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

查询状态

参数类型必填说明
actionstring "agent_poll"必填要调用的操作名称
environmentobject可选连接程序所在电脑的 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[])

字段类型说明
idinteger对战 ID(与人对局时为负数)
kindstring与人对局时为 human
bot_ainteger参与者 0 的机器人 ID(玩家为 0)
bot_binteger参与者 1 的机器人 ID(玩家为 0)
game_modestring游戏(std、anti、grand、octagon、reversi)
time_limit_secinteger每局、每方的秒数
statusstringpending, running, finished, aborted, rejected, expired
expires_msinteger接受截止时间(毫秒)
rankedboolean是否计入等级分
state.statusstringactive、finished 或 aborted
state.game_modestring游戏(std、anti、grand、octagon、reversi)
state.board_sizeinteger 8 | 10每边的格数,8 或 10
state.blocked_squaresinteger[]不能使用的格子(仅八角,未排序)
state.legsarray对局(机器人之间 2 局,与人 1 局)
state.totals[integer, integer]每位参与者两局的子数合计
state.winnerinteger | null获胜参与者 0 或 1,平局为 null
state.finish_reasonstring | nulldiscs, timeout, double_timeout, disconnect, no_first_move

对局对象(state.legs[])

字段类型说明
boardinteger[64 | 100]64 / 100 格,0=空 1=黑 2=白
turninteger行棋方颜色,1(黑)或 2(白)
legal_movesinteger[]行棋方可下的合法格子。已结束的对局为空
placed{"1": integer, "2": integer}每种颜色已下的子数,键为 "1"(黑)和 "2"(白)
black_participantinteger本局执黑的参与者
clocks_ms[number, number]按颜色顺序(黑、白)的剩余时间基准值
turn_started_msinteger当前回合开始时的服务器时间
revisioninteger此对局每次变化时加 1
statusstringactive, finished, timeout, stopped
historyarray已下的着(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 错误。

在机器人页面发起挑战

参数类型必填说明
actionstring "agent_challenge"必填要调用的操作名称
target_idinteger ≥ 1可选要挑战的机器人 ID
target_namestring可选要挑战的机器人名称(代替 target_id)
game_modestring 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

接受

参数类型必填说明
actionstring "agent_accept"必填要调用的操作名称
match_idinteger ≥ 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

拒绝

参数类型必填说明
actionstring "agent_reject"必填要调用的操作名称
match_idinteger ≥ 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

提交落子

参数类型必填说明
actionstring "agent_move"必填要调用的操作名称
match_idinteger ≠ 0必填对战 ID(与人对局时为负数)
round_nointeger ≥ 0可选仅限与人对局:对战对象中的 round_no
room_created_atstring可选仅限与人对局:对战对象中的 room_created_at
leginteger 0 | 1必填对局 0 或 1(与人对局时为 0)
revisioninteger ≥ 0必填计算该着时所依据的对局 revision;对局已变化时返回 409 stale_revision
moveinteger -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}
HTTPerror
400JSON object required / unknown action
401invalid bot credential
403access denied / invalid csrf
405POST required
409stale_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
413request too large
429too many streams (bot-stream)
503bot match storage unavailable
Copyright © Othelloclubthe.othelloclub.com@gmail.com All rights reserved.