The AlphaBeta framework

AlphaBeta is a Cocoa Objective-C framework encapsulating the Alpha-Beta game-tree search algorithm. It can be used to create computer players for a whole range of games including Chess, Checkers, Go, Reversi/Othello, Connect-4 and Tic-Tac-Toe to mention but a few.

More specifically, Alpha-Beta search can be used in all two-player zero sum perfect information games. The term two-player just means that there must be two opposing sides. (Football is considered two-player, for example.) A zero sum game is one where an advantage for one player is an equally large disadvantage for the other. Perfect information basically rules out any game that has an element of chance. This last rule disqualifies Yatzee, Poker and—most definitely—Jenga.

The framework aims to be easy to use: there's no need for prior experience with AI (Artificial Intelligence). You don't actually have to learn anything about game-tree search in order to use this framework. Should you desire to, the resources section has links to more information about game-tree search and relevant algorithms.

Overview

To use AlphaBeta you need to initialise an instance of SBAlphaBeta with an instance of your game state class. This instance will be used as the initial state of the game. A state is a discrete game state—a point in time between moves. A move contains the information required for transforming a state into its successor.

AlphaBeta does not care what types your states are. Only that they implement the SBAlphaBetaSearching protocol. If you wish you could implement this protocol as a category on an existing class. Neither does AlphaBeta care about the type of your moves. (With an exception for pass moves. These must be represented by an NSNull instance.) They must implement a simple informal protocol.

Though not required it is advised that you override -description to return something sensible for both states and moves. This can make debugging easier if you make a false step and feed SBAlphaBeta unexpected data, as the exceptions thrown will make more sense.

Protocols

The protocols listed below are defined in the SBAlphaBeta.h header file. Don't include this directly though. Include the AlphaBeta/AlphaBeta.h header instead.

SBAlphaBetaSearching [required]

Your state class must implement all the methods specified by this protocol. In addition to the NSCoding protocol, its methods and their responsibilities are:

- (double)fitness;

This method should return a state's fitness: a number indicating how fortuitous the state is for the current player. That is, the probability of winning after reaching this state. Use a large positive number to indicate that a state is very good, or a large negative number for very bad.

- (NSArray *)legalMoves;

This method must be implemented to return an array of all the legal moves available to the current player.

Return an empty array to signify that there are no moves possible and that this is an end state, also known as a leaf state.

Return an array containing a single NSNull instance to signify that the current player is forced to pass if your game supports this. If passing is always an option, this method must always return a pass move.

- (void)applyMove:(id)m;

Must transform a state into its successor given a valid move. Must be implemented to handle pass moves, if your game supports these. Given a pass move, this method must at the very least update the receiver's idea of which player's turn it is.

Informal protocol for moves [required]

Moves must implement the following informal protocol. NSArray, NSDictionary, NSString and NSNumber already implement it so if you define your moves in terms of these you get this protocol for free.

-(BOOL)isEqual:(id)object;
-(unsigned)hash;

SBAlphaBetaStatus [optional]

Implement this optional protocol if you want AlphaBeta to be able to tell you which player won at the end of the game. It adds the following methods:

- (BOOL)isDraw;

Must be implemented to return YES if this state is a draw, i.e. none of the players won. This method is only called by AlphaBeta at the end of a game.

- (BOOL)isWin;

Must be implemented to return YES if this state is a win from the perspective of the current player. This method is only called by AlphaBeta at the end of a game.

SBUndoableAlphaBetaSearch [optional]

AlphaBeta normally calls -copy on your states a lot during search. If calling -copy is particularly expensive your states may optionally implement this protocol. It contains only one method:

- (void)undoMove:(id)m;

This is the opposite of -applyMove:. The move passed to it will always be the last move that was applied to the receiver with -applyMove:. The effect of this method should be to produce the previous state.

This means that each move returned by -legalMoves must contain enough information to revert the move. For Reversi, for example, each move could be an array of co-ordinates: the first is the slot to put the current piece, the remaining are for pieces to flip.

If you go down this route it is possible that you have to make changes to the -legalMoves and -applyMove: methods. The moves returned by -legalMoves must contain enough information to perform an undo operation. This can mean creating them might become more expensive. On the other hand, having more information in the moves might help make -applyMove: faster.

Consider Reversi as an example. If you use undoable states your moves must contain a list of all the slots that were flipped, in addition to the slot where you put your piece. It is impossible to deduce this information from just a set of co-ordinates.

A word of warning: you have to do some profiling to find out what works best for your specific case. In my implementation of Reversi there is little discernable difference in performance of the two.

Methods of SBAlphaBeta

The SBAlphaBeta class provides the following methods. They are defined in the SBAlphaBeta.h header file. Don't include this directly though. Include the AlphaBeta/AlphaBeta.h header instead.

Object creation and initialisation

- (id)init

Initialises an SBAlphaBeta object. You need to call -setState: with an instance of your state class next.

- (id)initWithState:(id)this

Initialises an SBAlphaBeta object with a starting state. If you use this there is no need to call -setState:.

+ (id)newWithState:(id)this

This method is a shortcut for calling -alloc and -initWithState:.

- (void)setState:(id)x

Sets the initial state of the game to the given state. Removes all existing history of states and moves first.

The current state of affairs

- (id)currentState

Returns the current state.

- (unsigned)currentPlayer

Returns 1 or 2, depending on whose turn it is to move. Player 1 is arbitrarily defined to be the player whose turn it is to play at the start of the game. This is not necessarily the same as the state itself thinks of as player 1. (It may not think of the players as @"a" or @"b" instead, for example.)

- (double)currentFitness

Returns currentFitness from the current state.

- (NSArray *)currentLegalMoves

Returns available moves from the current state.

- (BOOL)isForcedPass

Returns true if the current player is forced to pass, that is her only legal move is a pass move.

- (BOOL)isGameOver

Returns true if the game is finished, false otherwise.

- (unsigned)winner

Returns 1 or 2 for the winning player, or 0 if the game ended in a draw. This method is only available if your states implement the SBAlphaBetaStatus protocol.

Performing and undoing moves

- (id)performMove:(id)m

Apply the given move to the current state. Returns the new current state.

- (id)performMoveFromSearchWithDepth:(unsigned)depth

Performs a fixed-depth search to the given depth and applies the best move found.

- (id)performMoveFromSearchWithInterval:(NSTimeInterval)interval

Performs an iterative search for up to interval seconds and applies the best move found.

- (id)undoLastMove

Undo one position from the given state. Returns the new current state.

Searching for moves

- (id)moveFromSearchWithDepth:(unsigned)depth

Returns the best move found from a fixed-depth search to depth.

- (id)moveFromSearchWithInterval:(NSTimeInterval)interval

Returns the best move found from an iterative search for interval seconds. Fractional seconds are supported, so an interval of 0.3 makes for a search that lasts up to 300 milliseconds.

Enquiring about the past

- (id)lastMove

Returns the last move that was applied.

- (unsigned)countPerformedMoves

Returns a count of the number of moves since the initial state.

- (unsigned)depthForSearch

Return the depth reached by the last iterative search. The returned value is undefined if no iterative search has been executed yet.

- (unsigned)stateCountForSearch

Return the number of states visited by the last search.

If the last search was an iterative one, the number of visited states is accumulated across all the completed iterations.

Code & interface maturity

The underlying code is quite mature and by now pretty well tested. However I'm still not entirely happy with the interface. Thus the API may still change between releases. (The broad strokes are there, so if something does change you should find it relatively simple to update to any new versions.)

I maintain three games written in Cocoa for Mac OS X that all use this library for their AI: Desdemona, Auberon, and Phage.

Resources

No downloadable binary yet, as things are still in a bit of a flux. You can get it from Subversion though. (Link below.)

Bugs

None known. Bug reports are very welcome and will be acted upon swiftly.

Author

The AlphaBeta Framework is written by Stig Brautaset, yours truly, and released under the revised BSD license. You are welcome to email me with any queries.

__CODE__ lists more code by me. I occasionally announce new projects on my blog. It also contains the definite list of ways to contact me.