A3 Option 2: Baroque Chess Agents
CSE 473: Introduction to Artificial Intelligence
The University of Washington, Seattle, Spring 2023
OPTION 2: Baroque Chess Agent.

For option 2, you will work with a partner to create a Python module (typically just a single .py file) that will be able to play a game of Baroque Chess and compete in a tournament. Although the tournament is not part of the assignment itself, your program must be technically able to participate in it. (The tournament will take place after the assignment is due; we plan to award some extra credit for programs that place highly in the rankings or that win overall.) Your player's file name should be of the form [free_name]_BC_Player.py, where you get to choose an original name, but that name is followed by "_BC_Player.py". An example might be Magnifico_BC_Player.py. If you need to split your agent to use any additional modules, please name them using the convention: [free_name]_BC_module_[module_name].py. Here an example could be Magnifico_BC_module_static_eval.py.

Game rules: A good overview of the rules of Baroque Chess is given in the Wikipedia article on Baroque Chess. However, since there are a number of variations of Baroque Chess, we have to specify some version, and we will use an operational definition of the rules. ''When in doubt, try it out, at the validation page.''

In addition, CSE 473 staff member Khushi has created a video that goes over our Allen-School rules for Baroque Chess (so that our rules don't get mixed up with Cambridge rules or any other version of the rules). That video is here. She has also created a video that explains in detail how to use the Move-Validator facility that goes with the Allen-School rules. Here is the link to the Move Validator tutorial video. Finally, here are a set of PPTX slides with the rules.


Image courtesy of Chess Guide at multionline.net.
Rules of Baroque Chess (Allen School variant)

Baroque Chess has a number of variations, primarily on the basis of small changes to the rules. We are using Allen-School rules. For one thing this means we will start with the initial state shown below. Players will not have the option to interchange their Kings and Withdrawers (that look like normal Chess Queens), and they will not have the option to interchange their Freezers and Coordinators. (The Freezer looks like an upside-down Rook, and the Coordinator looks like a right-side-up Rook.)

Here is the initial state of the game. The black pieces are in lower-case letters, and the white pieces are in upper-case letters.

c l i w k i l f
p p p p p p p p
- - - - - - - -
- - - - - - - -
- - - - - - - -
- - - - - - - -
P P P P P P P P
F L I W K I L C
Here is the key to interpreting the letters. How the pieces move will be described later.
K = King (just as in normal Chess).
P = Pincer (represented by a normal Chess pawn).
L = Leaper (represented by a normal Chess knight).
I = Imitator (represented by a normal Chess bishop).
W = Withdrawer (represented by a normal Chess queen).
F = Freezer (represented by an upside-down rook).
C = Coordinator (represented by a right-side-up rook).

Each piece has its own "normal" way of moving, which means a way of moving that does not capture an enemy piece or immobilize ("freeze") an enemy piece.

Each piece also has its own "capture" kind of moving, which will result in one or more enemy pieces being captured, or possibly immobilized.

Let's now describe the way these pieces move, beginning with the King, which is easiest to learn and remember. It moves just like a King in normal chess (except there is no such thing as castling in Baroque Chess). It normally can move one square in any of eight directions, provided the square it moves to is unoccupied. There are two exceptions. If the King has been immobilized by an enemy Freezer, then it cannot move until such time as it is no longer immobilized. If the square to which the King would like to move is occupied by an enemy piece (other than an immobilizer), then the King can move into that square, capturing the piece.

Next up are the Pincers. A Pincer moves like a normal Chess rook; in one turn it can move any number of squares horizontally, or any number of squares vertically, provided the move is not blocked by any piece (friendly or enemy). The Pincer may capture an enemy piece by moving up alongside it, completing a "sandwich" (oriented vertically or horizontally) in which the enemy piece is surrounded on two opposite sides -- on one side by the Pincer that just arrived, and on the other size by any of the Pincer's friendly pieces. For example, the pattern P w L is a sandwich in which the black Withdrawer gets pinched between the White Leaper and the White Pincer that just moved there. The black Withdrawer is then removed from the board. It is possible for a Pincer to capture two or even three enemy pieces in one move if it completes that many sandwich patterns where it arrives. Like any other piece, if a Pincer is immobilized, then it cannot move.

All the remaining pieces perform their normal moves in the same way as a normal Chess Queen. They may move any number of squares in one of 8 directions, but they may not jump over any other pieces. However, if they are capturing or otherwise performing their special kind of move, then they move differently.

We'll next consider the Leaper, which normally moves like a Chess Queen, but which captures differntly. In order to capture an enemy piece, there must be a straight, open path (in any of the 8 directions) from the leaper to the enemy piece, and there must be an empty square just beyond the enemy piece, so that the leaper can travel to the enemy piece, jump over it, and land in the empty square without changing direction. The leaper cannot jump over more than one piece (in our rules) and must land immediately beyond the captured piece's square. It must stop there.

The Withdrawer can only capture an enemy piece that it is adjacent to, and it must have space to pull away from the enemy piece, moving along the same line that it already make with the enemy piece. It must withdraw from the enemy piece at least one square in order to capture it. It may withdraw a greater distance, if it is not blocked by another piece.

The Coordinator, represented by the right-side-up rook, moves like most of the other pieces (like a chess Queen), but it captures in a special way that involves its own King. When the Coordinator arrives at a square sC, that square and the King's square sK can be considered as diagonally opposite corners of a rectangle (unless they are in the same row or the same column, in which no capture can take place). The other two corners of the rectangle, call them sX and sY, are attacked. If a friendly piece lies in one or the other, nothing happens. However, any enemy pieces that may lie at sX or sY are captured. It's thus possible for a Coordinator to capture two pieces in one turn.

The Freezer (sometimes called an immobilizer) moves normally like a Chess Queen, and never captures any enemy pieces. However, any enemy piece that is adjacent to it (in any of 8 directions) is immobilized and cannot move until the Freezer moves away or gets captured somehow (possibly by a Coordinator).

The final piece is perhaps the most interesting one. It is the Imitator (sometimes called the Chameleon). Its normal move is like a Queen in Chess. However, it can capture or sometimes immobilize an enemy piece to acting as the same type of piece that it attacks. It can capture a King by stepping one step into the King's square. It can capture a Pincer by moving horizontally or vertically to form a sandwich in which the Pincer is pinched. It can leap over a leaper and capture it. It can withdraw from a Withdrawer and capture it. It can coordinate (with its own King) to capture a Coordinator. It can pull alongside an enemy Freezer and freeze it. (However, the Imitator then becomes frozen itself, and neither the Freezer nor the Imitator can move until one of them is captured.) The only kind of enemy piece is does not interact with is another imitator. The imitator not only has the power to act like an enemy piece when attacking, but is allowed to capture multiple pieces in one turn if it captures each piece in that particular piece's style of capturing (or freezing). For example, it could withdraw from a withdrawer in a horizontal or vertical direction, leap over an enemy leaper, arriving next to one or more enemy Pincers, pinching each one of them and at the same time forming a a rectangle with its own king that captures an enemy Coordinator. If the end square for this Imitator is adjacent to the enemy Freezer, then it would also immobilize it. (All that in one turn!)

Basic Requirements

Your agent should play the Allen School variant of Baroque Chess, except that the Imitators do not have to imitate. Unless you choose to do the extra-credit option of Imitator captures and Imitator freezing, you can consider the Imitator as a sort of dumb piece that can move like a Chess queen (and therefore get in the way of other pieces, say for defensive purposes) but that cannot capture other pieces or freeze a Freezer. Since the Imitator's capture moves make it the most complicated piece to include in your agent, the staff has decided to make its special features an option for teams who have the time and inclination to implement them.

It will be essential that your program use the specified representation of states and import its starter-code module, so that it is compatible with all the other Baroque Chess programs developed in the class. The supplied game-master program requires that the states passed to it be instances of the given state class. (The file BC_state_etc.py is included in the starter code.)

You should implement a move generator that can accept a given state as input and either return all legal moves (and resulting states) or use the yield and next methods of Python generators to lazily generate the legal moves. However, you are NOT required to implement the Imitator Capture moves. You MAY implement the Imitator Capture moves for 5 points of extra credit. If you do implement these moves, your prepare should set (to True) the global variable IMITATOR_CAPTURES_IMPLEMENTED, and you should include in your code a method enable_imitator_captures(status = False) that can be called by the game master software to either enable the feature (with status=True) or disable the feature (status=False). This will allow your agent to compete more fairly with an agent that does not implement the imitator capture rules. We are not specifying the name for this move generator; that's up to you. Presumably your makeMove function will be calling it.

Your program should be designed to anticipate time limits on moves. There are two aspects to this: (1) use iterative deepening search, and (2) poll a clock frequently in order to return a move before time runs out.

As mentioned, Starter code is available. This code provides a game-master to handle the turn-taking between a pair of playing agents, and it provides a class definition for states of the game. Some simple agent stub files are included to help get the project going.

We are playing Baroque Chess with rules that: do not permit (a) any choice of center-counter symmetry (see the Wikipedia description of Baroque Chess), or (b) long-leaper double jumping (it's considered detrimental to having balanced and dynamic games), or (c) "suicide" moves. We will assume that the game ends when either player loses a king.

Your program should implement the following functions, so that they can be called by the game administration software:

  • prepare(player2Nickname ). This function will be called by the game administration software before a game starts, and usually before any of the other functions below are called. If your agent needs to initialize any data structures, this is a good place for that to be done, as the time taken to execute this will not be "on the clock" that is running during game moves.
  • parameterized_minimax(currentState, alphaBeta=False, ply=3, useBasicStaticEval=True). This function is for testing the basic capabilities of your agent. It should be able to work for any combination of values of arguments, within reason, except that it is not required to integrate Zobrist Hashing into your agent. For example, it should be OK with any legal state for currentState; either using or not using alpha-beta pruning; a specified number of ply (maximum depth) from 0 (statically evaluate the current state only) to perhaps 8 (which might not be too bad if there are only a few pieces left on the board).

    When useBasicStaticEval is true, you'll evaluate leaf nodes of your search tree with your own implementation of the following function: White pincers are worth 1, the White king is worth 100, and all other White pieces are worth 2. Black pieces have the same values as their white counterparts, but negative. When useBasicStaticEval is False, you should use your own, more discriminating function. The value of the function is the sum of the values of the pieces on the board in the given state.

    The use of Zobrist hashing in your agent is optional. However, if you do use it, it should be disabled while running the parameterized_minimax function.

    In order for parameterized_minimax to be a good testing function, the order for move generation must be controlled. Although your agent may generate successors in any order when playing a game, in parameterized_minimax it must use a prescribed order. Here is that order, based on the source and destination squares for each move. This order should be respected whether the agent is playing White or Black. Moves of a piece at (a, 1) come before moves of a piece at (a, 2), which come before moves of a piece at (a, 3), etc., which come before moves at (b, 1), etc. Among all moves of a piece from (M, N), we break ties according to the destination squares, using the same ordering as for source squares. Here is the board layout:

    Although your makeMove function should use a form of Iterative-Deepening Depth-First Search to be able to come up with a move no matter how much time is given, you should NOT use iterative deepening in parameterized_minimax.

    The return value of parameterized_minimax should be a dict object with the following attributes:

  • 'CURRENT_STATE_VAL': The value determined for the current state. If ply = 0, then the static evaluation of the current state should be returned. Otherwise, it should be the dynamic (i.e., backed-up) value as computed by minimax.
  • 'N_STATES_EXPANDED': The number of states expanded as part of your minimax search. A state is expanded if and when the first attempt is made to generate any of its children. (If it turns out there are no legal moves from a state, we'll still consider it as expanded when that determination is made during execution.)
  • 'N_STATIC_EVALS': The number of static evals performed as part of your minimax search
  • 'N_CUTOFFS': The number of cutoffs that occurred during the minimax search (0 if alpha-beta was not enabled)

  • introduce(). This function will return a multiline string that introduces your player, giving its full name (you get to make that up), the names and UWNetIDs of its creators (you), and (optionally) some words to describe its character. This function is called by the game-master program at the beginning of a match.

  • nickname(). This function should return a short version of the playing agent's name (16 characters or fewer). This name will be used to identify the player's moves in game transcripts.

  • makeMove(currentState, currentRemark, timeLimit=10). This is probably your most important function. It should return a list of the form [[move, newState], newRemark]. The move is a data item describing the chosen move, of the form ((r, c),(r1, c1)). Here (r, c) gives the starting row and column coordinates of the piece that is being moved, and (r1, c1) give the coordinates of the square where it ends up. (NOTE IN SPRING 2023: You can simply return "OK" as the value of newRemark, and your makeMove function can ignore the value of currentRemark.)

    The newState is the result of making the move from the given currentState. It must be a complete state (an instance of class BC_state) and not just a board.

    The timeLimit represents the number of seconds available for computing and returning the move.

    Your makeMove function should use Iterative-Deepening Depth-First Search (either recursive or in the style of DFS.py with an OPEN list) so that it can implement an "anytime" version of the mimimax search, and it should check the clock often enough to make sure it can return its best move found so far, if there is little time left.

  • staticEval(state). This function will perform a static evaluation of the given state. The value returned should be high if the state is good for WHITE and low if the state is good for BLACK. This function is not the same as your basic static evaluation function mentioned above in association with parameterized_minimax, but should be custom-designed by your team to help your agent be a strong player. The staff plans to test your staticEval function separately at some point during the grading, so it should be possible to use it by executing code such as the following. (This example assumes your agent is named "The_Roman_BC_Player".)
    import The_Roman_BC_Player as player
    staticResult = player.staticEval(some_state)    
        

    The quality of your agent's playing will probably depend heavily on how well your staticEval function works.

  • You may incorporate Zobrist hashing into your agent. It is optional. If you implement and successfully incorporate it into your agent, that is worth 5 points of extra credit.

    What to Turn In:

    Turn in your files via GradeScope. Do not zip up the files.

    1. Your agent file(s). See the naming requirements at the beginning of this page.
    2. Report.pdf: Include the following. Title; Team member names; Intended personality of the agent, if any; Status of agent -- features that work and any that don't; Whether or not you implemented the Imitator imitation features for extra credit; Whether or not you implemented Zobrist hashing for extra credit; Design retrospective of about 200 to 500 words describing how you designed your agent; Optional partnership retrospective including how you operated as a team, what issues you had in terms of collaboration, and how you did or did not overcome those issues, and what if anything you learned in terms of collaboration. A good retrospective will typically include a group portion and an individual statement of several lines of text from each of the two partners.
    3. Transcript1.txt: One game transcript of your player playing against another team's agent.

    Updates and Corrections: Any updates or corrections will be posted here, and/or on ED.