This describes the protocol used by the NBoard Othello GUI when communicating with engines.
The engine is run as a separate process which is started by the GUI; communication between the two programs is via the NBoard Protocol. The protocol is designed to be easy for the engine to implement.
< nboard 2 < set game (;GM[Othello]PC[NBoard]DT[2014-02-21 20:52:27 GMT]PB[./mEdax]PW[chris]RE[?]TI[15:00]TY[8]BO[8 ---------------------------O*------*O--------------------------- *]B[F5]W[F6]B[D3]W[C5]B[E6]W[F7]B[E7]W[F4];) < set depth 6 < ping 1 < go > set myname Edax6 > pong 1 > status Edax is thinking > nodestats 35805 0.00 > === d6 -1.00 0.0 > status Edax is waitingYou can see an example of an NBoard communication by running NBoard and examining debugLog.txt in the NBoard directory. Commands to the engine are preceded by '>' and responses by the engine are preceded by '<'.
This is a list of the commands in the current protocol version. More will likely be added as the protocol evolves. If you get a command you don't understand you should ignore the entire line.
GUIs must send the "nboard {version}" command as their first command to the engine. GUIs must also send the "set depth" command and "set game" command before issuing "hint" and "go" commands. Engines may assume that the GUIs do this properly.
nboard {version: int} |
Version information sent at the beginning of the session. Required: The engine should enter "NBoard mode" and all further commands will be in NBoard format. Note: The current version of the protocol is version 2. For reference, instructions for protocol version 1 are given in the revision history of this document. |
---|---|
set depth {maxDepth: int} |
Set engine midgame search depth. Optional: Set midgame depth to {maxDepth}. Endgame depths are at the engine author's discretion. |
set game {GGF} | Tell the engine that all further commands relate to the position at the end of
the given game, in GGF format.
Required:The engine must update its stored game state. |
set contempt {contempt:int} | Set the engine's contempt factor. Optional: Tell the engine that book draws should be considered as a given value, from black's point of view. 100 = draws to black (+100 disks), -100 = draws to white (by 100 disks),0 = draws are valued at 0.
If no 'set contempt' command is issued, the program should use a contempt value of 0. Since: Protocol version 2. |
move {move:string}/{eval:float or blank}/{time:float} |
Tell the engine that all further commands relate to the position after the given move. The move is 2 characters e.g. "F5". Eval is normally in centi-disks. Time is in seconds. Eval and time may be omitted. If eval is omitted it is assumed to be "unknown"; if time is omitted it is assumed to be 0. Required:Update the game state by making the move. No response required. Since: Protocol version 2. In protocol version 1, the GUI just sent "F5" instead of "move F5". |
hint {n:int} | Tell the engine to give evaluations for the given position. n tells how many
moves to evaluate, e.g. 2 means give evaluations for the top 2 positions. This is used
when the user is analyzing a game. With the "hint" command the engine is not CONSTRained by the time remaining in the game.
Required: The engine sends back an evaluation for at its top move Best: The engine sends back an evaluation for approximately the top n moves. If the engine searches using iterative deepening it should also send back evaluations during search, which makes the GUI feel more responsive to the user. Depending on whether the evalation came from book or a search, the engine sends back search {pv: PV} {eval:Eval} 0 {depth:Depth} {freeform text}
or book {pv: PV} {eval:Eval} {# games:long} {depth:Depth} {freeform text:string}
Two depth codes have special meaning to NBoard: "100%W" tells NBoard that the engine has
solved for a win/loss/draw and the sign of the eval matches the sign of the returned eval.
"100%" tells NBoard that the engine has done an exact solve.
The freeform text can be any other information that the engine wants to convey. NBoard 1.1 and 2.0 do not display this information but later versions or other GUIs may. |
go | Tell the engine to decide what move it would play. This is used when the engine is playing in a game.
With the "go" command the computer is limited by both the maximum search depth and the
time remaining in the game.
Required: The engine responds with " Best: The engine responds with " Important: The engine does not update the board with this move, instead it waits for a "move" command from NBoard. This is because the user may have modified the board while the engine was thinking. Note: To make it easier for the engine author, The NBoard gui sets the engine's status to "" when it receives the response. The engine can override this behaviour by sending a "status" command immediately after the response. Since: Protocol version 1, but the engine being constrained by time remaining is since Protocol version 2. |
ping {ping:int} | Ensure synchronization when the board position is about to change.
Required: Stop thinking and respond with "pong n". If the engine is analyzing a position it must stop analyzing before sending "pong n" otherwise NBoard will think the analysis relates to the current position. |
learn | Learn the current game.
Required: Respond "learned". Best: Add the current game to book. Note: To make it easier for the engine author, The NBoard gui sets the engine's status to "" when it receives the "learned" response. The engine can override this behaviour by sending a "status" command immediately after the response. |
analyze | Perform a retrograde analysis of the current game.
Optional: Perform a retrograde analysis of the current game. For each board position occurring in the game, the
engine sends back a line of the form Since:Protocol version 2. |
status {text} | Sets the status line displayed in the NBoard GUI. |
---|---|
set myname {text} | Sets the engine's name as displayed in the GUI. |
nodestats {node count:long} {time:float} | Sets the engine's node count information as displayed in the GUI.
node count: number of nodes since the beginning of the current search time: number of seconds since the beginning of the current search Since: Protocol version 2. |
// Copyright 2004 Chris Welty // All Rights Reserved #include "GameX.h" #include <deque> #include <sstream> #include "windows.h" /////////////////////////////////// // CGame2 /////////////////////////////////// //! Stored commands from NBoard. static std::deque<std::string> lines; //! This event is set when there is an input line available //! A critical section handles access to a resource. Only one thread can own it at a time. class CriticalSection { public: CriticalSection(); ~CriticalSection(); void Enter(); void Leave(); private: CRITICAL_SECTION m_cs; } cs; //! Construct the critical section object CriticalSection::CriticalSection() { ::InitializeCriticalSection(&m_cs); } //! Destroy the critical section CriticalSection::~CriticalSection() { ::DeleteCriticalSection(&m_cs); } //! Block until the critical section is available; then take control of it. void CriticalSection::Enter() { ::EnterCriticalSection(&m_cs); } //! release control of the critical section void CriticalSection::Leave() { ::LeaveCriticalSection(&m_cs); } //! A waitable windows event //! //! This is an unnamed windows event, so that other processes //! (e.g. multiple copies of ntest) can't affect the event. class Event { public: Event(); ~Event(); void Set(); void Reset(); void Wait(u4 timeout); private: HANDLE m_event; } eventInput; Event::Event() { m_event=CreateEvent(NULL, true, false, NULL); } Event::~Event() { CloseHandle(m_event); } //! Set an event to true. void Event::Set() { SetEvent(m_event); } //! Set an event to false void Event::Reset() { ResetEvent(m_event); } //! Wait on an event with the specified timeout, or forever if timeout == INFINITE void Event::Wait(u4 timeout) { WaitForSingleObject(m_event, timeout); } //! This function returns true if there is input waiting for the engine. //! It is meant to be used in the search function. //! //! It is currently used only when using an external viewer with GameX. //! \todo test to see if this slows down the search. If so need a faster mechanism. bool HasInput() { cs.Enter(); bool fHasInput=!lines.empty(); cs.Leave(); return fHasInput; } //! Add an input line to the deque, and set the eventInput if the deque was empty before static void AddStringToDeque(const std::string& s) { cs.Enter(); if (lines.empty()) { eventInput.Set(); } lines.push_back(s); cs.Leave(); } //! Thread's job: add things to the deque static DWORD WINAPI MoveCinToDeque(void*) { std::string sLine; while (getline(std::cin, sLine)) AddStringToDeque(sLine); // last, post a quit message. Normally we get one from the viewer // but sometimes it doesn't get a chance to before termination. AddStringToDeque("quit"); return 0; } //! Get a line of input from the thread that's parsing lines for us. static bool GetLineFromThread(std::string& sLine) { eventInput.Wait(INFINITE); cs.Enter(); sLine=lines.front(); lines.pop_front(); if (lines.empty()) eventInput.Reset(); cs.Leave(); return true; } //! Create and run a game using the alternate game class for external viewers //! //! User commands: //! - mode <n>: 0=computer inactive, 1=computer plays white, 2=computer plays black, 3=computer plays both CGameX::CGameX(CComputerDefaults cd) { CPlayerComputer* pcomp=NULL; int nboardVersion=0; cd.fsPrint=-1&~CSearchInfo::kPrintMove&~CSearchInfo::kPrintGameAnalysis; cd.fsPrintOpponent=-1; u4 fLearn=CPlayer::kNoAddSoloUnsolvedToBook; // Initialize with a basic board. That way we're guaranteed to always have a legal game. Initialize("8"); // start up the thread that will give us messages u4 threadId; CreateThread(NULL, 0, MoveCinToDeque, NULL, 0, &threadId); std::string sLine; // while (getline(std::cin,sLine)) { while (GetLineFromThread(sLine)) { // save value of GameOver so that we know whether to learn the game const bool fGameWasOver=GameOver(); std::istringstream is(sLine); std::string sCommand; is >> sCommand; if (sCommand=="go") { // engine tells viewer what move it would make. // do NOT update the board, the engine has no idea whether the viewer thinks the computer should move // (e.g. in nboard, the user could switch to "user plays both colors" mode while the engine is thinking). // any board updates are given by a standard move message. if (pcomp) { COsMoveListItem mli; pcomp->GetMoveAndTime(*this, CPlayer::kMyMove | fLearn, mli); std::cout << "=== " << mli << std::endl; } else { std::cout << "warning Must pick a depth with \"set depth <n>\" first" << std::endl; } } else if (sCommand=="hint") { int nBest=1; is >> nBest; // if it's not the computer's move, he can give hints. //COsMoveListItem mli; //pcomp->GetMoveAndTime(*this, CPlayer::kAnalyze | fLearn, mli); std::cout << "status Analyzing" << std::endl; pcomp->Hint(CQPosition(pos.board), nBest); std::cout << "status" << std::endl; } else if (sCommand=="learn") { pcomp->EndGame(*this); std::cout << "learn" << std::endl; } else if (sCommand=="nboard") { is >> nboardVersion; } else if (sCommand=="new") { // this command is not part of the interface standard, but is useful for testing. std::string sBoardType("8"); is >> sBoardType; Initialize(sBoardType.c_str()); } else if (sCommand=="ping") { int n; is >> n; std::cout << "pong " << n << std::endl; } else if (sCommand=="quit") { break; } else if (sCommand=="remove_tree") { if (pcomp && pcomp->book) { pcomp->book->RemoveTree(CQPosition(pos.board).BitBoard()); } } else if (sCommand=="draw_tree") { if (pcomp && pcomp->book) { std::cout<<"--- Draw Tree ---\nAfter "; for (size_t i=0; i<ml.size(); i++) std::cout << ml[i].mv << " "; std::cout << "\n"; pcomp->book->PrintDrawTree(CQPosition(pos.board)); } } else if (sCommand=="set") { std::string param; is >> param; if (param=="game") { // board is changing, need to negamax old game to make sure values remain consistent if (pcomp && pcomp->book) pcomp->book->NegamaxGame(*this); In(is); } else if (param=="depth") { std::istringstream iscp(cd.sCalcParams.c_str()); char c='s'; int depth=12; iscp >> c >> depth; is >> depth; std::ostringstream os; os << c << depth; cd.sCalcParams=os.str(); if (pcomp) delete pcomp; pcomp=new CPlayerComputer(cd); std::cout << "\n"; std::cout << "set myname Ntest" << depth << std::endl; } else if (param=="contempt") { float contempt=0; is >> contempt; // update both the computer defaults (used for new computers) and the current computer. cd.vContempts[1]=CComputerDefaults::FloatToContempt(contempt); cd.vContempts[0]=-cd.vContempts[1]; if (pcomp) { pcomp->cd.vContempts[0]=cd.vContempts[0]; pcomp->cd.vContempts[1]=cd.vContempts[1]; } } else { getline(is, param); } } else if (sCommand=="move") { COsMoveListItem mli; is >> mli; Update(mli); } else if (sCommand.size()==2) { COsMoveListItem mli; mli.mv=COsMove(sCommand); Update(mli); } } }