Game Engine with Continuation-Passing Style

Jeff Walden, Mujde Pamuk, Waseem Daher May 16, 2007

1 Introduction

As our final project in 6.891, we chose to implement a very general search system in continuation-passing style, that could also be used to play two-player games such as . Chess is a board game in which players move pieces on the board in an attempt to subdue the other player. Each piece has different capabilities and limitations, and the number of possible moves at any stage of the game is usually fairly large making evaluation of moves a non-trivial problem. Chess is by far the most well studied strategic game in the field of computer game playing. Many advances in computer science have resulted from this research, including developments in problem specification, exhaustive search techniques, parallel algorithms, and others. [1] Along with chess, Go, checkers, Othello, Go-Moku, and Tic-Tac-Toe are categorized as two-player deterministic zero-sum games with perfect information. Search techniques have proven to be quite effective for many of these games, provided the search space is relatively small, or can be restricted with good heuristics. Programs based primarily on efficient search engines are approaching or have surpassed the level of the best human players for many games in this category. A as a system is composed of 5 components: a position representation (encapsulating the state of the game), a move generator to generate all legal moves given a position, a move evaluator to rank these legal moves by optimality, a search component to help a static evaluator to analyze the position at various depths, and finally a user interface. For our project we proposed to follow this basic recipe, using a single method which, when provided with a position, determines the best move to be made. In contrast to many such move evaluators, however, we intended to make the process extremely configurable by using:

• a user-provided board evaluator

• accept and reject continuations for use after a move has been evaluated

• user-defined move traversal (order of evaluation of moves) strategy

• a way to define depth cutoff which does not statically limit the depth of search to a constant number

1 The move finder uses continuation-passing style to provide easy support for exhaustive move generation, using the accept continuation to resume generation as desired. As a search problem, chess move finding is well-suited to use of CPS, particularly for backtracking from moves multiple plays in the future to moves fewer plays in the future while searching. This report is organized in the following manner. Section 2 reviews the literature on chess engines and describes some of the search algorithms that informed our design of the present system. Section 3 describes the design of our system. Section 4 concludes and indicates areas for further work.

2 Literature Review

We organized our literature review around the components we identified in the introduction section: the position representation, move generator, move evaluator and search component. (We chose to exclude the user interface component as it is uninteresting from a technical standpoint.)

2.1 Position Representation Position representations, which encapsulate the state of the game at a given time, have undergone few changes in the last 30 years. Back in the 1970’s, when memory was expensive, using compact board representations was important. The most popular representation was the most obvious way, a 64-byte array, where each byte represents a single square on the board and contains an integer representing the piece located in that square with some extra bytes for castling opportunities and en passant captures. A clever way to present board was developed by Soviet Unions KAISSA team: they used bitboards, a data structure where each bit represents a game position or state. The state of a chess game can be completely represented by 12 bitboards: each for representing each types of pieces by each player. The main advantage of bitboard representation is that move generation calculations can be done in a more efficient way using these structures. As an example if you need to generate the moves of the white knights currently on the board, just find the attack bitboards associated with the positions occupied by the knights and AND them with the logical complement of the bitboard representing ”all squares occupied by white pieces” because the only restriction on knights is that they can not capture their own pieces. An auxiliary type of representation in chess is transposition tables. The idea is there are often many ways to reach the same position. For example, it doesn’t matter whether you play two moves in either order, the game ends up in the same state, this is called transposing. How do transposition tables help chess programming? A position resulting from a number of positions would not need to be re-evaluated again if a transpose of this position was searched and evaluated before and recorded in a transposition table (first incorporated in the Richard Greenblatt’s Mac Hack 6 in the late 1960’s). A transposition table is then a repository of past search results, a concept similar to memoization concept we have seen in the class. There are numerous advantages to this

2 process, including speed, and free depth. A related concept is the use of history tables to store killer moves — moves have had interesting results in the past (i.e., which ones have cut-off search quickly along a continuation) and should therefore be tried again at a later time.

2.2 Move Generation In any given situation, a player may have 30 or more legal moves to choose from, some good, some disastrous. For trained humans, it is easy to characterize the majority of these moves as bad, but coding that information into a computer has proven to be too difficult. Today the strongest programs largely rely on brute force: if you can analyze all possible moves fast enough and predict their consequences far enough down the search tree, it doesn’t matter whether or not you start with a clear idea of what you are trying to accomplish, because you’ll discover a good move eventually. Move generation can be categorized into the following three categories: Selective generation: Examine the board, come up with a small number of likely moves and discard everything else. Incremental generation: Generate a few moves, hoping that one will prove so good or so bad that search along the current line of play can be terminated before generating the others. Complete generation: Generate all moves, hoping that the transposition tables will contain relevant information on one of them and that there will be no need to search anything at all. Selective generation (and its associated search technique, forward pruning) has not been used since the mid-1970s since it was found generating all moves and evaluating all of them were found to be less expensive than selective generation. An exception is null move forward pruning. [2] In games where move generation is easy and/or there are lots of ways to transpose into the same positions, e.g. Othello, complete generation may be most efficient, while in games where move generation rules are complicated, incremental generation will usually work faster. Sophisticated chess programs since at least CHESS 4.5 have adopted the incremental move generation strategy [3] : generate a few moves at a time, search them, and if a cut-off can be caused, there will be no need to generate the rest of the moves. This approach has some advantages such as low memory use and reducing search space easily because often finding a move that will cause a cutoff is a capture (which are relatively cheap to enumerate) [3].

2.3 Search Techniques for Chess Tree search is a central component of any game-playing program. A typical tree search looks at all possible game positions as a tree whose root is the current position; legal game moves determine both the nodes in the tree and the connections between the tree nodes. The problem for any interesting game like chess is that the size of this tree is enormous: if M is the average number of moves per position and D is the depth of the search corresponding to the tree, the tree will contain roughly M D positions. Given that chess games often progress

3 beyond 20 moves and that there can be anywhere from 5-20 different moves at each stage, the size of the tree renders a full search impossible. All practical algorithms merely approximate this full search, with varying degrees of success. We read about the search functions typically described in the literature: Minimax, Alpha- Beta, Aspiration Search, and techniques like NegaScout, memory enhanced test, quiescence search, mtd(f) [4,5]. Our final framework provides support for implementing some but not all of these features; the level of generality required is fairly daunting, especially since many of these algorithms must carry incremental state (e.g. minimax requires access to the structure of the search tree when making its decisions). The further complication of separating the implementation from the algorithm, with respect to the MOVE, POSITION, and PLAYER types our best-move-finding-algorithm interface used, acted as an impediment to fully taking advantage of the full depth of information we could generate and might have occasion to use, depending upon the particular chess algorithm being implemented on the framework. In retrospect we should have spent more time attempting to generalize from specific algorithms, for our final design fails to adequately address the needs of a sophisticated chess AI.

2.4 Evaluation Functions Evaluation functions enables the chess engine to evaluate relative strength of possible moves found through the search in the game tree. There are several features of the game that give advantage to one player over the other. The first and the simplest heuristic is the material balance of players. Material balance is an account of which pieces are on the board for each side with each piece assigned a numeric value, with king having an infinite utility value. Computing material balance is therefore straightforward: a side’s material value is equal to sum of weights of each piece times the number of each piece on a side. The second one is mobility and board control. As an example to understand the impor- tance of mobility, we can take the example of a where the victim has no legal moves left. Assessing mobility is easy, by just counting the number of legal moves a player at a time can legally make. In practice, this statistic may be useless because many moves are pointless. Also trying to limit the opponent’s mobility at all costs may lead the program to destroy its own defensive position in search of “pointless checks”. Still some heuristics such as “bad bishops” are useful. A close concept to mobility is board control. In chess, a side controls a square if it has more pieces attacking it than the opponent. It is usually safe to move to a controlled square, and disastrous to move to one controlled by the opponent. Another important feature is development. That is the strategy to sequence moves by different pieces. For example a well know heuristic is that bishops and knights should be brought into the battle as quickly as possible, that the King should castle early and that rooks and queens should stay quiet until it is time for a crucial attack. Another heuristic that was prevalent in literature is pawn formations. As an example, two or more pawns of the same color on the same file are usually bad, because they hinder each other’s movement. King safety is something all sophisticated chess engines try to ensure, Especially, in the opening and middle game, protecting the king is paramount, and castling is the best way

4 to do it. Also, chess engines keep track of how easy it is for a piece to attack the opposing king, usually measured in terms of distance. Lastly, while we have all these different heuristics we need to find a way to give weight to each of these features. There is no absolute answer to find an optimal linear combination of features in evaluation functions. It is very difficult to refine an evaluation function enough to gain as much performance as one would from an extra ply of search. So it is better to keep the evaluator simple and leave as much processing power as possible to the search techniques. [6]

3 The System

3.1 Search Strategy The search strategy component of our project is easily the most interesting portion of our design. Its goals were ambitious: be able to recreate as many of the standard chess al- gorithms as possible using a generalized framework, such that implementing an algorithm might require fifteen or twenty lines of code spread throughout perhaps four or five methods (and, given the existence of a standard algorithm library, might often require even fewer lines of code), all while introducing as few dependencies upon the concrete implementations of the components of a chess game as possible. The final result is only partially success- ful at these goals. Implementation independence through opaque MOVE, POSITION, and PLAYER types is successfully demonstrated, perhaps most notably in the manner by which our implementation denoted players (using a boolean to denote a player and using not as the function to get one player given the other). Algorithmic generality is both successful and unsuccessful. We have a good method of determining when to search a given subtree of the position space (even allowing for information about the trail of positions visited to influence descent decisions), and we have abstracted out evaluation of moves such that moves don’t even need to be given exact scores. However, our design suffers from not fully supporting many common algorithms. For example, supporting minimax upon our design would be at best a hack involving dubious external state storage, which is probably the biggest problem with it. Designing the support for this is non-trivial, even if the implementation (given a suitable API) would be reasonably simple. Just as notably, the good enough parameter to our method is a hack to ameliorate the absence of an easy way to prune the search space other than by terminating descent from a given position. These problems could likely have been solved by designing first and foremost with the base algorithms in mind; doing so would have been difficult but not entirely infeasible. The implementation of the method is reasonably simple. We first begin by expanding the initial position into all possible moves from it. From there, we iterate through each new position. If we can descend from that position, we generate the resultant positions and append all that pass the good enough predicate to the end of a pending queue; otherwise, we move the position to an expanded queue. We continue in this manner until the pending queue is exhausted, using a tail-recursive loop to implement it. Finally, we choose the best position generated for each of the initial moves, sort those positions by utility, and return each move in order through the success continuation. For the amount of necessary description for the

5 interface of the method, its actual implementation is quite minimal. For further details, refer to the method documentation in finder.scm for type information and usage details; library implementations and examples of simple algorithms implementable upon this framework are in descent.scm and move-sort.scm.

3.2 Board Representation, Move and Player We represent board as a type that encapsulates all information about an arrangement of pieces upon a board. Move is a simple record type with only source and target fields. Player is a simple Boolean type #t for white pieces and #f black pieces.

3.3 Move Generator The move generator generates all moves given a position of the board. We have relied on Mark Watson’s chess program in his book “Programming in Scheme: Learn Scheme programming using AI examples” to generate all possible legal moves. The move generator component has two methods: one that will generate all legal moves given a single position. The second procedure move-generator applies the first method to all of a player’s current pieces on the board as well as looking for castlability. The reason for having a separate function for the two is because we would like to be able to use generate moves for other purposes, such as checking for control space of a player, checking a checkmate or conditions for checking castlability. Although we have utilized Watson’s code, move-generation wasn’t as straightforward as we would have expected because (1) the code was tightly coupled with the mutable board representation, that made it difficult to re-use in our system (2) continuation passing style meant we could not easily keep track past moves to check for conditions such as en passant, castling etc.

3.4 Evaluator Evaluator is a static evaluation method that will compute the “goodness” of a board layout. We check for several heuristics. We calculate the number of times the computer’s pieces control each board square. We also calculate the number of times the opponent’s pieces control each square. We modify the value based on material advantage, square control, and center control. Lastly we look at pawn placement heuristics, looping over the central four squares.

3.5 Graphics The graphics component of the system enjoys the unique position of being largely decoupled from the rest of the system; the only requirement is that there be some agreement on the representation of the chess board between the caller and callee.

6 Even this is abstracted as much as possible; the board is set up with a virtual coordinate system of (0,0) to (8,8), courtesy of the scheme graphics library1. (0,0) being the lower-left- hand corner of the chess board. A loop goes over the board representation and identifies the piece in each square, and asks the piece to itself. Each piece has its own draw method that is fairly agnostic about the structure of the board as a whole – all it takes is starting coordinates and color, and knows how to do everything else. (Thus, the same methods can be used to draw pieces of either color)2. In the vein of abstractions and compatibility, the board also obeys a subset of the xboard ad-hoc protocol for chess boards, and can thus be used as a graphics output for programs like gnuchess or any of the other hundreds of programs that speak this language. With the help of a tiny Python script, our entire application can be connected to gnuchess (or any other xboard-compliant chess game), and they can play one another.

4 Conclusion

In implementing the system, we learned a great deal about playing and generalized, abstract methodologies. To play a first-rate chess game, many specific chess- only optimizations need to be made, ones which remove generality from the system, but our aim was to produce more of a proof-of-concept system in continuation-passing style that, incidentally, plays a game of chess. And given that, it’s fair to say we did reasonably. The source code for our project can be found online at http://web.mit.edu/jwalden/Public/891/.

5 References

[1] Osborne, Martin J., Rubinstein. A Course in Game Theory.

[2] T.A. Marsland, The Anatomy of Chess Programs, University of Alberta.

[3] Atkin, L. and Slate, D. 1988. Chess 4.5-The Northwestern University chess program. In Computer Chess Compendium, D. Levy, New York, NY, 80-103.

[4] Reinefeld, A.. Spielbaum-Suchverfahren. Informatik-Fachbericht 200, Springer-Verlag, Berlin 1989.

[5] Plaat, Aske. “Research Re: search & Re-search,” PhD thesis, 1996.

[6] Gomboc, D., Tuning evaluation functions by maximizing concordance Department of Computing Science, University of Alberta, Edmonton, Alberta, Canada.

1This being basically one of the only nice things about said library. If I had known it was in such bad shape, I would have suggested bringing this up to speed as a project in its own right! 2It’s simultaneously a point of pride and embarassment that I admit that the graphics are hand-drawn as vectors, rather than loaded as bitmaps, because the image-blitting code was not exactly behaving as expected

7