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.
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.
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.
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.
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;
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.
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.
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.
- (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.
- (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.
- (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.
- (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.
- (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.
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.
No downloadable binary yet, as things are still in a bit of a flux. You can get it from Subversion though. (Link below.)
None known. Bug reports are very welcome and will be acted upon swiftly.
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.