By Jeff Mallett - Copyright 2000 Zillions Development
Corporation
Revised 12/1/00
The following technical documentation is intended for third-party developers interested in programming game-specific engines to replace Zillions of Games' default engine. It assumes fluency in programming and is not intended as end-user documentation.
Zillions of Games allows the use of plug-in engines, add-on game AIs hard-coded to come up with moves for specific games. These exist in the form of DLLs that Zillions calls according to a predefined interface. As DLLs they can be written in any language and compiled in a variety of development environments.
The Zillions of Games CD-ROM comes with two engine plug-ins, which handle certain variants from the Go-Moku and Reversi ZRFs. You'll find them in: Zillions/Engines/
A ZRF indicates that it wants to use a plug-in by including an engine command. For example,
(engine "Engines\Jello.dll")
indicates the use of the plug-ing called "Jello.dll" in the Engines directory.
There are only four routines that the plug-in engine needs to support. These are defined in the following C header files. The routines allow Zillions to tell the engine to start a new game, make a move in the real game, search from the current position, and clean up. There are also two optional routines which allow the engine to generate moves for a game and determine the result of the game.
The engine returns a DLL_* constant back to the Zillions, which should be DLL_OK under normal circumstances. If the engine returns a negative error code, Zillions of Games will report this to the user and then unload the engine plug-in. If an engine plug-in is unloaded, either for this reason or because it returned a move that Zillions did not recognize as valid, Zillions will revert to using its built-in, universal engine.
// EngineDLL.h
//
// Copyright 1998-2000 Zillions Development
//
// Shared DLL plug-in for DLL engine and Zillions
#include "windows.h"
typedef enum {
kKEEPSEARCHING = 0,
kSTOPSOON = 1,
kSTOPNOW = 2
} Search_Status;
typedef enum {
DLL_OK = 0,
DLL_OK_DONT_SEND_SETUP = 1, // only supported in 1.0.2 and higher!
DLL_GENERIC_ERROR = -1,
DLL_OUT_OF_MEMORY_ERROR = -2,
DLL_UNKNOWN_VARIANT_ERROR = -3,
DLL_UNKNOWN_PLAYER_ERROR = -4,
DLL_UNKNOWN_PIECE_ERROR = -5,
DLL_WRONG_SIDE_TO_MOVE_ERROR = -6,
DLL_INVALID_POSITION_ERROR = -7,
DLL_NO_MOVES = -8
} DLL_Result;
enum {
UNKNOWN_SCORE = -2140000000L,
LOSS_SCORE = -2130000000L,
DRAW_SCORE = 0,
WIN_SCORE = 2130000000L
};
// ***** REQUIRED ROUTINES
// DLL_Search
//
// The DLL should search from the current position. If it returns DLL_OK it should
// also return the best move found in str; however, it should not make the move
// internally. A separate call to MakeAMove() will follow to make the move the
// engine returns.
//
// -> lSearchTime: Target search time in milliseconds
// -> lDepthLimit: Maximum moves deep the engine should search
// -> lVariety: Variety setting for engine. 0 = no variety, 10 = most variety
// -> pSearchStatus: Pointer to variable where Zillions will report search status
// -> bestMove: Pointer to a string where engine can report the best move found so far
// -> currentMove: Pointer to a string where engine can report the move being searched
// -> plNodes: Pointer to a long where engine can report # of positions searched so far
// -> plScore: Pointer to a long where engine can report current best score in search
// -> plDepth: Pointer to a long where engine can report current search depth
//
// Returns DLL_OK or a negative error code
typedef DLL_Result (FAR PASCAL *SEARCH)(long lSearchTime, long lDepthLimit, long lVariety,
const Search_Status *pSearchStatus, LPSTR bestMove, LPSTR currentMove,
long *plNodes, long *plScore, long *plDepth);
// DLL_MakeAMove
//
// The DLL should try to make the given move internally.
//
// -> move: notation for the move that the engine should make
//
// Returns DLL_OK or a negative error code
typedef DLL_Result (FAR PASCAL *MAKEAMOVE)(LPCSTR move);
// DLL_StartNewGame
//
// The DLL should reset the board for a new game.
//
// -> variant: The variant to be played as it appears in the variant menu
//
// Returns DLL_OK, DLL_OK_DONT_SEND_SETUP, DLL_OUT_OF_MEMORY_ERROR, or
// DLL_GENERIC_ERROR
typedef DLL_Result (FAR PASCAL *STARTNEWGAME)(LPCSTR variant);
// DLL_CleanUp
//
// The DLL should free memory and prepare to be unloaded.
//
// Returns DLL_OK, DLL_OUT_OF_MEMORY_ERROR, or DLL_GENERIC_ERROR
typedef DLL_Result (FAR PASCAL *CLEANUP)(void);
// ***** OPTIONAL ROUTINES
// DLL_IsGameOver
//
// This optional function is called by Zillions to see if a game is over. If
// not present, Zillions uses the goal in the ZRF to decide the winner.
//
// -> lResult: Pointer to the game result which the DLL should fill in when
// called. If the game is over the routine should fill in WIN_SCORE,
// DRAW_SCORE, or LOSS_SCORE. Otherwise the routine should fill in
// UNKNOWN_SCORE.
// -> zcomment: Pointer to a 500-char string in Zillions which the DLL can optionally
// fill in, to make an announcement about why the game is over, such
// as "Draw by third repetition". The DLL should not modify this
// string if there is nothing to report.
//
// Returns DLL_OK or a negative error code
typedef DLL_Result (FAR PASCAL *ISGAMEOVER)(long *lResult, LPSTR zcomment);
// DLL_GenerateMoves
//
// You can use GenerateMoves in your DLL to tell Zillions the legal moves for
// any position in the game.
//
// -> moveBuffer: Pointer to a 1024-char sting which the DLL should fill in when
// called. Initial call should be with moveBuffer set to "". Each call
// to GenerateMoves should fill in the next available move from the
// current position, with a final "" when no more moves are available.
// All moves must be in valid Zillions move string format.
//
// Returns DLL_OK or a negative error code
typedef DLL_Result (FAR PASCAL *GENERATEMOVES)(LPCSTR moveBuffer);
// Engine.h
//
// Copyright 1998-2000 Zillions Development
//
// Header file for plug-in DLL engine to Zillions
#include "EngineDLL.h"
DLL_Result FAR PASCAL DLL_Search(long lSearchTime, long lDepthLimit, long lVariety,
Search_Status *pSearchStatus, LPSTR bestMove, LPSTR currentMove,
long *plNodes, long *plScore, long *plDepth);
DLL_Result FAR PASCAL DLL_MakeAMove(LPCSTR move);
DLL_Result FAR PASCAL DLL_StartNewGame(LPCSTR variant);
DLL_Result FAR PASCAL DLL_CleanUp();
DLL_Result FAR PASCAL DLL_IsGameOver(long *lResult, LPSTR zcomment);
DLL_Result FAR PASCAL DLL_GenerateMoves(LPCSTR moveBuffer);
The engine does not call Zillions of Games. However, during a search it can find out from Zillions whether it should continue searching. When DLL_Search is called, the engine should store away the argument pSearchStatus and then refer to it during the search. If the user requests that the program move now or the time has expired, the value will change to kSTOPSOON. In this case the engine should return a result as soon as possible. If Zillions of Games needs to abort the search prematurely, e.g. the user has chosen to exit the program, the value will change to kSTOPNOW. In this case the engine should return as soon as possible, whether or not a good result is available. The engine should not change the value of *pSearchStatus itself.
During a search the engine should continually report its own search status by updating the values of *plNodes, *plScore, and *plDepth. Zillions uses these values to display feedback on progress to the user. In order to display this feedback during the search, the engine needs to periodically give Zillions a chance to process Windows messages. For example, in C/C++ you might have the following code:
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
*plScore is assumed to be the following units:
-2,140,000,000 (UNKNOWN_SCORE): Score isn't known
-2,130,000,000 (LOSS_SCORE): Immediate loss
-2,130,000,000 (LOSS_SCORE) + x: Loss in x (partial) moves
(-2,130,000,000+150)..-1001: Bad for side on the move
-1000...1000: Close game
1001..(2,130,000,000-150): Good for side on the move
2,130,000,000 (WIN_SCORE) - x: Win in x (partial) moves
As a DLL, the plug-in will also need to have the following two routines defined. The LibMain function will be called when the DLL is loaded, so that gives you a chance to do once-only initialization. (Another method is to simply check a static variable inside DLL_StartNewGame.) The following code gives an example implementation of these two routines that does nothing except initialize the C library's randomizer.
#include <time.h>
int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize)
{
srand( (unsigned)time( NULL ) );
return 1;
}
int FAR PASCAL WEP(void)
{
return 1;
}
With these functions the EXPORTS section of your project's .DEF file might look like this (the last two routines are optional):
EXPORTS LibMain WEP DLL_Search DLL_MakeAMove DLL_StartNewGame DLL_CleanUp DLL_IsGameOver DLL_GenerateMoves
Moves are passed back and forth as move strings. These move strings are the same as those written out to a saved game, such as "Pawn e2 - e4", and don't include the move number.
For most games these strings are also the same as the move strings displayed in the move list (the part following the move number), which makes it easy for you to see what is being passed to your engine. There are two exceptions:
1) The move involves setting piece attributes. For example, when a Rook in
Chess moves, the ZRF sets an attribute saying that the Rook may no longer
be used in castling. The setting of piece attributes is not displayed
to the user on the movelist, but it is sent to the engine in case the engine
needs that information in differentiating moves. Example: "Rook h1 - f1
@ f1 0 0"
2) The move is a partial move. In this case a "partial" string is
added. Example: "partial 2 Checker d4 - b6 x c5"
To see the exact format of moves, simply look at what is saved in the .ZSG.
Note that the sending moves to the engine serves two purposes, both as a method to pass actual game moves and to pass edits made to the board. Thus, your engine needs to handle any edits the user can make to the board, for instance "(White Rook e4)". The Zillions of Games GUI and engine is flexible enough to deal with any position resulting from edits. If your engine can't cope with the existing board position, for example, it is a Chess program that is hard-coded to assume that there is exactly one White King and one Black King, then the engine can indicate this by returning the code DLL_INVALID_POSITION_ERROR.
Edits to the board are passed just as they are displayed in the move list, as a capture or drop surrounded by parentheses.
After calling DLL_StartNewGame Zillions of Games will always pass down a series of board edits to place all the initial pieces on the board. This was done for two reasons:
Of course, your engine may prefer a certain setup, for instance, if it has an opening book. If getting the series of initial board edits is a problem, your DLL_StartNewGame should return DLL_OK_DONT_SEND_SETUP to indicate you're assuming a fixed setup and want to bypass this stage.