처음 하는 사람용 따라 하기
준비물
- FlipSight 계정. 봇 등록과 연결 프로그램 내려받기는 로그인한 뒤에 할 수 있습니다.
- 봇 사진 한 장. GIF, JPG, PNG 파일이고 5MB 이하, 가로와 세로가 모두 175px 이상이어야 합니다.
- 봇을 운영할 내 컴퓨터 한 대 봇의 수는 연결 프로그램을 켜 둔 내 컴퓨터에서 계산됩니다. 같은 엔진이라도 컴퓨터 성능과 엔진 설정에 따라 강도가 달라질 수 있고, 컴퓨터나 설정을 바꾸면 레이팅도 그에 맞춰 움직일 수 있습니다.
- Python 3(파이썬). 연결 프로그램을 실행하는 무료 프로그램입니다. 3.9 이상이면 되고 3.11 이상을 권합니다. 5단계에서 확인합니다.
- 내 봇 엔진. 연결 프로그램에 테스트용 연습용 엔진이 포함되어 있습니다.
1. 내 봇 등록하기
로그인한 뒤 봇 페이지(othelloclub.com/flipsight/bot)를 엽니다. 맨 위 내 봇 등록 칸의 봇 이름 (2~24자) 칸에 이름을 적습니다. 지원 게임 항목은 처음에는 일반만 체크된 그대로 둡니다. 봇 사진 업로드에서 사진을 고른 뒤 내 봇 등록 버튼을 누릅니다.
이렇게 보이면 성공: 연결 토큰 (지금 한 번만 표시됩니다) 상자가 나타나고, 그 안에 fsbm_ 으로 시작하는 긴 글자가 보입니다.
2. 토큰 보관하기
이 긴 글자가 토큰(봇 전용 비밀번호)입니다. 연결 프로그램이 서버에 "이 봇이 맞다"고 증명할 때 씁니다. 상자 안의 글자를 모두 복사해 메모장 같은 곳에 잠시 붙여 둡니다. 이 상자를 닫으면 같은 토큰을 다시 볼 수 없습니다.
토큰은 비밀번호처럼 다룹니다. 남에게 보여 주거나 화면을 찍어 올리지 않습니다. 잃어버렸거나 남이 봤다면 봇 목록의 관리 메뉴를 열고 연결 토큰 발급 버튼을 누릅니다. 새 토큰을 받으면 옛 토큰은 그 즉시 쓸 수 없습니다.
3. 연결 프로그램 내려받기
FlipSight 봇 연결기 묶음 (.zip) 링크를 눌러 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이렇게 보이면 성공: 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 입니다. 아래에서 운영체제에 맞는 명령 한 줄을 복사해 붙여 넣고 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)).PasswordmacOS 와 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
macOS용 공식 Edax 4.6 실행 파일은 없습니다. 같은 페이지의 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 는 그 사이에서 내 엔진에게 수를 물어보는 작은 파이썬 파일입니다. 서버 접속과 토큰, 착수 제출은 연결 프로그램이 하므로 이 파일에서는 신경 쓰지 않습니다.
bot_match_client.py→ my_adapter.pybot_match_client.py← my_adapter.py내 엔진은 어느 쪽인가요?
- 파이썬 코드로 수를 고르는 엔진: 아래 예시 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} 한 줄이 나옵니다. 그 줄이 나오면 2번 상자의 명령으로 이 어댑터를 붙여 연결 프로그램을 켭니다(6단계의 토큰이 있는 창에서). 두 상자는 따로 실행합니다. 엔진이 이 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: 파이썬 함수로 수를 고르는 엔진
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초)입니다. 이 시간을 넘기면 그 판은 더 두지 못하고 시간패가 되므로, 엔진에는 budget_ms 대신 think_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)
입력 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)
입력 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)
입력 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 로 알려 줍니다. |
다운로드
다운로드는 로그인 후 이용할 수 있습니다. 첫 번째 항목은 설명서와 tools 폴더의 연결기와 예제 엔진을 묶은 압축파일(.zip)입니다. API 규격(.json)과 사용설명서(.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 인 pending 신청에 agent_accept 를 보내고, running 대국의 각 leg 에서 내 차례인 국면만 계산합니다. 같은 match_id, leg, 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 | 8×8 | 무작위 12돌 | 60,000 ms | 국면마다 다름 |
| 10x10 | grand | 10×10 | 무작위 12돌 | 60,000 ms | 국면마다 다름 |
| 팔각 | 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)
{"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)
{"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)
{"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초 안에 첫 수를 두지 않은 봇이 있으면 그 대국은 무효(aborted)가 되고 레이팅에 반영하지 않습니다. 진행 중 한쪽 봇의 연결이 30초 넘게 끊기고 다른 쪽은 연결되어 있으면, 연결된 쪽의 승리(finish_reason: disconnect)로 끝나고 레이팅에 반영합니다.
FlipSight 봇의 수는 모든 게임 종류에서 서버가 계산합니다. 웹 신청과 API 신청(agent_challenge)을 모두 받고 서버가 바로 시작하므로 대국방을 열어 둘 필요가 없습니다. 서버 계산이 30초 넘게 멈추면 그 대국은 무효(aborted, finish_reason: technical)이고 레이팅에 반영하지 않습니다. 변형 게임은 한 수 계산이 무거워 FlipSight 봇과의 변형 대국은 동시에 2개까지 받습니다. 가득 차면 HTTP 409와 site bot variant matches are full 오류가 반환되니 잠시 뒤 다시 신청하세요.
자동 봇전: 사이트는 봇전 방 2개를 늘 열어 둡니다. 자동 매칭이 켜진 등록 봇이 온라인이고 대국 중이 아니면, 레이팅이 비슷한 봇(다른 등록 봇이 먼저, 없으면 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 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)
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 을 부르면 됩니다. 스트림 없이 폴링만 할 때는 대국 중 1초, 대기 중 5초 간격으로 부르고, 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 | 판 한 변의 칸 수, 8 또는 10 |
state.blocked_squares | integer[] | 쓸 수 없는 칸 번호(팔각만, 번호순 아님) |
state.legs | array | 판 목록(봇끼리 2판, 사람과 1판) |
state.totals | [integer, integer] | 참가자별 두 판 정산 점수 합계(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. 두 봇 모두 지원해야 합니다. 일반 / 안티 / 팔각 / 10x10 / 클래식 리버시 |
요청 예시
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 |