Adding games to Ai Ai in Java (and compatible languages)

I have created a (relatively) simple interface that will allow java programmers to extend Ai Ai with additional games.
To do so, you’ll need:

  • An .mgl file to tell Ai Ai where to look for your class files
  • A class file implementing the ExternalGame interface

Note 1: if you implement a game designed by someone else, please get permission before distributing the files.
Note 2: as this is pure java code, please only run external games from sources you trust.

First, download and unzip Ai Ai, if you haven’t already done so.
If you look in the ai ai installation, you will find a /dev/ folder. In there, you will find the following:

  • ExternalGame.java: this is the interface
  • NoughtsAndCrosses.java: example game implementing the interface
  • NoughtsAndCrosses.mgl: example .mgl file telling Ai Ai how to load the game
  • /bin/external/NoughtsAndCrosses.class: class file that Ai Ai will load

ExternalGame.java

package external;

/**
 * Provides support for games implemented outside the main project and  
 * loaded into Ai Ai at runtime.
 * Any .class file that implements this interface should be runnable.
 * 
 * NOTE: Ai Ai expects to find a default constructor for any class it 
 * loads. Java will create this automatically, unless you expicitly create 
 * a constructor.
 * So, if you do create another constructor (e.g. a copy constructor), 
 * don't forget to create this one too
 * 
 * @author Stephen Tavener
 */
public interface ExternalGame 
{
	/** Return LOSS if specified player has lost the game */
	public static final int LOSS = -1;
	/** Return DRAW if specified player has drawn the game */
	public static final int DRAW = 0;
	/** Return WIN if specified player has won the game */
	public static final int WIN = 1;
	/** Separates [piece type]#[player]#[cell] */
	public static final String DRAWABLE_SEPARATOR = "#";
	
	/**
	 * Initialisation function. Will be called only once, so does not 
         * need to be very efficient.
	 * @param json The contents of the .mgl file used to define this 
         * game. Passed to the game can extract any parameters/settings it 
         * needs.
	 */
	void init (String json);
	
	/**
	 * @return Number of available actions from this state.  If the 
         * game is not in a terminal state, there must always be one 
         * action, even if it is only "Pass".
	 * @throws Exception 
	 */
	int numMoves();

	/**
	 * Changes the current state to a new state by applying the nth 
         * action 
	 * Note that the nth move has no external context, and may not 
         * always refer to the same move.
	 * @param n
	 * @throws Exception 
	 */
	void applyNthMove(int n);
	
	/**
	 * @return the owner of the current state (e.g. the player to 
         * move)
	 */
	int getOwner();

	/**
	 * Checks if the game is in a terminal state
	 * @return true if this is an end state
	 */
	boolean isTerminal();

	/**
	 * Called to obtain the score for a specific player - note that 
         * multi-player games can have multiple winners, losers, etc.
	 * If the game is not in a terminal state, behaviour is 
         * undetermined.
	 * @param player 
	 * @return returns the score for this player, if appropriate - 
         * done this way to support multi-player games, where more than 
         * one winner is possible
	 */
	int mcScore(int player);
	
	/**
	 * Produces a deep copy of this object - necessary, because Java 
         * returns a shallow copy by default, which can lead to corruption 
         * of the state during search.  
	 * 
	 * Some care is required here.  If you have an object O that is 
         * referred to from two board positions, for example, you need to 
         * ensure that it is only copied once; otherwise references which 
         * were formerly referring to the same object will be pointing to 
         * different objects
	 * 
	 * Also, this function will be called frequently, so should be as 
         * fast as possible.
	 * 
	 * @return new game which is a perfect copy of this game
	 */
	ExternalGame deepCopy();	

	/**
	 * Returns the hash for the current position if moveIndex is -1, 
         * index of specified move otherwise
	 * The game MUST return a hash for the current position when 
         * requested, but may return -1 for any other position; 
	 * 
	 * Performance warning: if the game does not supply them, Ai Ai 
         * will generate hashes for moves by: 
	 * (1) calling deep copy to clone the game, 
	 * (2) applying the move, and 
	 * (3) calling hash(-1)
	 * 
	 * Ideally:
	 * - a hash should uniquely identify a position (including player 
         * to move and any other behaviour modification flags, e.g. 
         * castling)
	 * - a position will always have the same hash, even if it occurs 
         *   through a different sequence of moves
	 * - a position will always have the same hash, even if the game   
         *   is saved and reloaded, or copied
	 * 
	 * See https://en.wikipedia.org/wiki/Zobrist_hashing 
	 * 
	 * If you choose to ignore hashing, please add the following line 
         * to your .mgl file:
	 * 		"useGuctHashing" : false,
	 * 
	 *
	 * @param moveIndex child index for which hash is requested, or -1 
         * for current position
	 * @return the current board hash if moveIndex is -1, index of 
         * specified move otherwise
	 */
	long hash(int moveIndex);
	
/* ==================================================
 * (OPTIONAL) HEURISTIC FUNCTIONS BEYOND THIS POINT
 * =============================================== */
	/**
	 * A switch which determines if Alpha Beta can be used with this 
         * game. If isHeuristic() returns true, then the game is expected 
         * to implement the other functions below - if not, they can just 
         * return null.
	 * (NOTE: Alpha Beta search has the potential to be considerably 
         * stronger than UCT, but only if the heuristics are good.)
	 * 
	 * @return true if this game provides heuristic information for 
         * use with alpha beta search 
	 */
	boolean isHeuristic();
	
	/**
	 * Calculates relative scores for each player.
	 * AB search will use the difference between the scores, but UCT 
         * implementations will return win/loss based on (player 
         * score)/(total score).
	 * To achieve this, each score must be >= 0, and total score must 
         * be greater than zero
	 * Scores are 1-based, so entry 1 is the score for player 1, etc.
	 * @return array of scores for each player
	 */
	int[] calcHeuristics();
	
	/**
	 * Skips a move... for AIs that perform null tests (i.e. compare 
         * the score with the score after doing nothing for a move)!
	 */
	void nullMove();
	
	/**
	 * Used with quintessence search, where the AI continues searching 
         * until a stable position is found.
	 * 
	 * Example; in chess stable implies no captures are possible.
	 * NOTE: If in doubt, return true - if not, the search may not 
         * terminate!
	 *
	 * @return True if the position is stable, and therefore 
         * heuristics are likely to be reliable, false otherwise.
	 */
	boolean isStable();
	
	/**
	 * Implementing classes can order moves according to most likely 
         * strength.  
	 * 
	 * If you return null, ordering will be random (which is good in 
         * terms of user experience)
	 * 
	 * AlphaBeta AIs can then use this ordering during search to 
         * improve cutoffs.
	 * Roughly speaking:
	 * 		Worst order: AB will search to depth n
	 * 		Random order: AB will search to depth 3n/2
	 * 		Best order: AB will search to depth 2n
	 * 
	 * UCT-based AIs may also use this ordering during the exploration 
         * phase of the turn to prioritise or avoid certain moves
	 * 
	 * If a move is clearly bad, do not include it in the list.
	 * 
	 * Examples: 
	 * 	Most placement games should order moves from 
         *      centre outwards. 
	 * 	Capture games should put captures first.
	 * 
	 * NOTE: These are recommendations from the game to the AI only.  
         * The AI is free to re-order this list based on extra heuristics                
         * like killer moves, and may occasionally re-introduce excluded 
         * moves if they were found to be strong elsewhere in the search 
         * tree.
	 * 
	 * @return list of available move indices in recommended order of 
         * preference
	 */
	int[] recommendedMoveOrder();
	
	/**
	 * The drawing method will call this function to obtain a list of 
         * things to draw
	 * 
	 * Each string entry should be in the following format:
	 * 		key#player#cell
	 * Example: "stone#White#a6" would cause the GUI to render a white 
         * go stone at a6.
	 * 
	 * The key and player name are as defined in the .mgl file in the 
         * "pieces" and "agents" sections respectively. 
	 * The cell names are grid references on the board from the bottom 
         * left corner
	 * (letters for column, number for row).  You can view the labels 
         * in Ai Ai using Settings/Show inner labels.
	 * 
	 * NOTE: this method is only called from the GUI, and does not 
         * have to be particularly efficient.
	 * 
	 * @return list of assets for the GUI to draw.
	 */
	String[] drawables();
	
/* ===============================================
 * USER INTERFACE / FEELGOOD FUNCTIONS
 * =============================================== */
	/**
	 * Describes a move in human-readable format. Ai Ai will use the 
         * action description for the following:
	 * 1. To display directly to the user in move lists
	 * 2. To save the game to file.  To restore a game position, Ai Ai 
         * stores the initial random seed, plus the sequence of moves so 
         * far
	 * 3. For user interface choices - in particular, start and end 
         * positions will be inferred from the moves if possible, and 
	 * used to translate mouse clicks into moves.
	 * 
	 * As such, each move description from a given position must be 
         * unique.
	 * 
	 * So, while a game is free to use any notation it chooses, I 
         * recommend the following if you want a good user experience. If 
         * you need 
	 * to disambiguate, Ai Ai will ignore anything after a semicolon
	 * 
	 *   Notation     Description
	 *   Pass         Pass (user takes no action, turn order advances)
	 *   Swap         Swap sides (usually pie rule option on second move)
	 *   [to]         Add a piece to [to], e.g. "b4"
	 *   x[from]      Capture the piece at [from], e.g. "xa2"
	 *   [from]-[to]  Movement from [from] to [to], e.g. "a2-b4"
	 *   [from]x[to]  Piece moves from [from] to [to] capturing piece 
         *                at [to], e.g. "a2xb4"
	 *   [from]->[player]  Change ownership of piece, e.g. "a2->White"
	 * 
	 * (In particular, the GUI will look for moves called "Swap", 
         * "Pass" and add extra options if they are present in the move 
         * list.)
	 * 
	 * @param n nth move
	 * @return nth action in human-readable format
	 */
	String actionDescription(int n);
	
	/**
	 * @return description of the current state of this game, e.g. 
         *         "White to move", "Black won", etc.
	 */
	String stateDescription();
}