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:

  • /src/external/ExternalGame.java: this is the old interface
  • /src/external/ExternalGameV2.java: this is the newest version of the interface
  • /src/external/NoughtsAndCrosses.java: example game
  • /src/external/BreakthroughExt.java: more complex example game with heuristics
  • NoughtsAndCrosses.mgl: example .mgl file telling Ai Ai how to load the game
  • BreakthroughExt.mgl: example .mgl file telling Ai Ai how to load the game
  • /bin/external/NoughtsAndCrosses.class: class file that Ai Ai will load
  • /bin/external/BreakthroughExt.class: class file that Ai Ai will load

ExternalGameV2.java

package external;

import java.util.Map;

/**
 * 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 ExternalGameV2 
{
	/** 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.
	 * JSON objects will be represented as one of the following:
	 * Map<String,Object>
	 * List<String>
	 * Number
	 * null
	 * @param json The contents of the .mgl file as a map of key-value pairs.
	 */
	public void init (Map 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".
	 */
	public 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 in the range 0..numMoves()-1
	 */
	public void applyNthMove(int n);
	
	/**
	 * Owner is in the range 1..number of players. Number of players 
	 * defaults to 2, but can be overridden in the mgl file.
	 * @return the owner of the current state (e.g. the player to move)
	 */
	public int getOwner();

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

	/**
	 * Called to obtain the score for a specific player - note that 
	 * multiplayer games can have multiple winners, losers, etc.
	 * If the game is not in a terminal state, behaviour is 
	 * undetermined.
	 * @param player in range 1 to number of players
	 * @return returns the score for this player, 
	 * -1 (loss), 0 (draw), 1 (win)
	 */
	public int mcScore(int player);
	
	/**
	 * Produces a deep copy of this object(game class) - 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
	 * 
	 * This function will be called frequently, so should 
	 * be as fast as possible (Java copies memory very efficiently
	 * so an array of ints for your board representation is fine)
	 * 
	 * @return new game which is a perfect copy of this game
	 */
	public ExternalGameV2 deepCopy();	

	/**
	 * Returns the hash for the current position if moveIndex is -1, 
	 * hash of position after 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
	 * 
	 * Failure to do so may result in a weaker AI, but something is 
	 * better than nothing!
	 * 
	 * 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
	 */
	public 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 
	 */
	public boolean isHeuristic();
	
	/**
	 * Calculates relative scores for each player.
	 * AB search will use the difference between the scores
	 * UCT implementations will ignore this function.
	 * @return array of size numPlayers+1 with scores for 
	 * each player in the corresponding index; 
	 * element 0 is not used 
	 */
	public int[] calcHeuristics();
	
	/**
	 * Optional
	 * Advances the internal state (e.g. next player) without applying a 
	 * move.
	 * Called by AIs that perform null tests (i.e. compare the score 
	 * with the score after doing nothing for a move)! Even the 
	 * Alphabeta AIs won't call this by default, because I haven't 
	 * found it useful in most games.
	 */
	public 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.
	 */
	public boolean isStable();
	
	/**
	 * Implementing classes can order moves according to most likely 
	 * strength. note that there is a trade-off here between processing 
	 * time and AI strength so implement only if necessary,
	 * 
	 * 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.
	 * 
	 * @param searchPlayer player who is searching, 1 to numPlayers
	 * @param maxDepth Maximum search depth for this pass, 
	 *   or -1 if called outside of a search pass
	 * @param currentDepth Current search depth from root for this pass, or
	 *  -1 if called outside of a search pass
	 * @param numVisits UCT based searches only; number of times this node 
	 * has been visited; -1 for other searches
	 * 
	 * @return list of available move indices in recommended order of 
	 * preference (best first)
	 */
	public int[] recommendedMoveOrder(
			int searchPlayer,
			int maxDepth, 
			int currentDepth, 
			int numVisits);
	
	/**
	 * 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.
	 */
	public 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 */ public String actionDescription(int n); /** * @return description of the current state of this game, e.g. * "White to move", "Black won", etc. */ public String stateDescription(); }