NBoard Protocol

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.

Example Session

< 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 waiting
You 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 '<'.

Communication Mechanism

The engine communicates with the GUI via console input and output (i.e. stdin and stdout, cerr and cout). This sounds easy, and it is, but there are two things to be careful of:
  1. The communication is done via pipes, so testing for input may not work in the same way as a true console application. In particular under Windows the _kbhit() function will not work. You can simply do full depth searches and ignore the user trying to abort the search, or use a simple second thread which reads stdin and signals the first when a command has come in.

  2. The response from the engine to NBoard won't be sent until the output buffer is flushed. There are a lot of ways to do this. In C++ viable choices include I expect that at least one of these options is available in other languages.

Commands from NBoard to the engine

All commands from NBoard to the engine are on a single line.

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}

PV: The pv must begin with two characters representing the move considered (e.g. "F5" or "PA") and must not contain any whitespace. "F5d6C3" and "F5-D6-C3" are valid PVs but "F5 D6 C3" will consider D6 to be the eval.

Eval: The eval is from the point-of-view of the player to move and is a double. At the engine's option it can also be an ordered pair of doubles separated by a comma: {draw-to-black value}, {draw-to-white value}.

Depth: depth is the search depth. It must start with an integer but can end with other characters; for instance "100%W" is a valid depth. The depth cannot contain spaces.

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 "=== {move}" where move is e.g. "F5"

Best: The engine responds with "=== {move:String}/{eval:float}/{time:float}". Eval may be omitted if the move is forced. The engine also sends back thinking output as in the "hint" command.

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 analysis {movesMade:int} {eval:double}. movesMade = 0 corresponds to the start position. Passes count towards movesMade, so movesMade can go above 60.

Since:Protocol version 2.

Commands from the engine to NBoard

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.

Sample Code

This is the code used by NTest as of NBoard 1.1. It includes Windows-specific multithreading. It will probably require modification to work in other programs.
// 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);
		}
	}
}