University of Alberta logo

CMPUT 229 — Computer Organization and Architecture I

Lab 4: Checkers Game

Creator: Calin Yakimchuk

Introduction

In this lab, students will build a fully functional two-player checkers game using the RARS MMIO simulator. Students will use keyboard interrupts and write their own handler to allow real time player input. Students will represent the game state using efficient 32-bit bitboards to track the positions of pieces, generate legal moves, and enforce game rules such as jumps and promotions.

By the end of this lab, students will:

Background

Checkers Game

Checkers is a two player strategy board game, the name checkers comes from the checkered chess board the game is played on. Below is a quick reference list of the terminology used for this lab.

Board Setup and Play

This is how the board is setup at the start of the game, red and black men are represented by 'r' and 'b' respectively, and red and black kings as 'R' and 'B'. The dark squares (game squares) are represented by '-', and light squares (non-game squares) are represented by a blank. The rank and file are shown on the sides of the board.

    a   b   c   d   e   f   g   h
  +---+---+---+---+---+---+---+---+
8 |   | b |   | b |   | b |   | b | 8
  +---+---+---+---+---+---+---+---+
7 | b |   | b |   | b |   | b |   | 7
  +---+---+---+---+---+---+---+---+
6 |   | b |   | b |   | b |   | b | 6
  +---+---+---+---+---+---+---+---+
5 | - |   | - |   | - |   | - |   | 5
  +---+---+---+---+---+---+---+---+
4 |   | - |   | - |   | - |   | - | 4
  +---+---+---+---+---+---+---+---+
3 | r |   | r |   | r |   | r |   | 3
  +---+---+---+---+---+---+---+---+
2 |   | r |   | r |   | r |   | r | 2
  +---+---+---+---+---+---+---+---+
1 | r |   | r |   | r |   | r |   | 1
  +---+---+---+---+---+---+---+---+
    a   b   c   d   e   f   g   h

The game is played by two players, one player is assigned the black pieces, and the other player is assigned the red pieces. The player assigned the black pieces always moves first. The players take turns moving a single piece.

If a player has the opportunity to make a jump move, they must do so. If a player has multiple jump moves available, they may choose any of them. A player must chain multiple jump moves with a single piece together in a single turn, as long as each jump is a valid move.

Otherwise, the player may make a slide move. You win the game when your opponent has no moves left, this is most commonly achieved by capturing all of your opponent's pieces. If a man piece reaches the opposing end of the board, it is promoted to a king piece immediately. Meaning a piece can be promoted after a jump move, and then immediately jump backwards again in the same turn.

Board Representation

Bitboards

Although the board is made up of 64 squares, only 32 of them have any impact on the game. Thus, the solution only needs to store information about 32 squares. This information can be stored in three 32-bit RISC-V words, where each bit in the word represents a game square on the board. Each one of these words is called a bitboard:

In addition to the three bitboards, the solution will also use the provided TURN global variable to keep track of which player's turn it is. TURN is a 32-bit word that is either:

Square Mapping Conventions

This lab use the following mapping conventions to map the squares to the bit positions in a bitboard.

The Least Significant Bit is bit 0, and the Most Significant Bit is bit 31. The indexes of the squares correspond to the index of the bits in the bitboard (word).

The mapping of game squares to bitboard indexes follows a Rank-File order, meaning that every file (a -> h) is indexed in a rank before moving to the next rank (8 -> 1). A visual representation of the mapping is shown below.

Rank 8 .. Rank 1 -> Row 0 .. Row 7
A-File .. H-File -> Column 0 .. Column 7

The mapping of the squares to indexes is shown below, an easy way to remember the mapping conventions is the index increases from left -> right, top -> bottom. This is the order in which the page of a book is read in English.

square mapping

For example Black's starting bitboard in different formats:

Move Generation

Bitboard Manipulation

The solution for this lab uses bitwise operations on bitboards in order to generate moves, capture and promote pieces, manipulate the game state, and more.

A bitwise logical shift changes the index of all the squares in a bitboard. For example, shifting a bitboard left by four is equivalent to moving all the pieces in the bitboard from their original square_index to square_index + 4.

A bitwise logical left shift is said to be a shift down, because it moves the pieces down the board, and a bitwise logical right shift is said to be a shift up, because it moves the pieces up the board.

Bitwise AND (&) and OR (|) on bitboards work as expected:

In an array representation board, where shifting a square could be represented as board[index + 4] = board[index], the programmer has to consider out of bounds array indexes. Out of bounds indexing is not something that needs to be considered with this labs bitboard representation.

Useful Bitboards

It might be useful for the solution to maintain, or compute, additional bitboards. Here are some examples:

Generating Slide Moves

A slide move is a single square diagonal move of a piece to an unoccupied game square.

The generation of slide moves requires an algorithm that transforms a source bitboard of any number and position of pieces, to a bitboard of all possible slide moves from those pieces. One such algorithm generates the slide moves down, and slide moves up. The color of the pieces in the source bitboard (which is the same as the current TURN) determines which direction (up or down) is forward (MAN and KING slides) and which direction is backward (KING slides only).

The algorithm uses bitwise shifts and operations to generate the slide moves. However not all squares need to be shifted the same number of spaces to generate the slide moves. This issue can be solved by using masks to isolate squares that need to be shifted by a certain value.

The diagram below illustrates which shifts generate which slide move for some example squares. The small numbers in the squares represent the index of in the square in the bitboard. The large numbers show the change in index from the source of the arrow to the destination of the arrow. A negative number indicates a right shift, and a positive number indicates a left shift.

Gen Slides Img

For instance, for downward moves, every square has at most two downward slide moves, any square can be left shifted by four to generate the first downward slide move. In the case that the source square is in the bottom row (row 7, rank 1), shifting left to generate the slide moves will shift the set bits off the end of the bitboard, thus meaning that no move is contributed to the slide moves bitboard. This is not an issue, as there are no valid downward slide moves for squares in the bottom row (row 7 | rank 1), so every square can be left shifted by 4 without issue.

Some squares have a second possible move, it falls into one of two cases:

Slides Down Img

To handle the different shifts for different squares, the solution will use four masks to isolate the squares that need to be shifted down three, up three, down five, up five. Conveniently, the squares that need to be shifted down by three are the same squares that need to be shifted up by five, and the squares that need to be shifted down by five are the same squares that need to be shifted up by three.

The algorithm for generating upward and downward slide moves are nearly identical. To generate upward slide moves, shift right instead of left, and use the THREE_U and FIVE_U masks instead of the THREE_D and FIVE_D masks.

The masks to isolate the squares that need to be shifted by 3 or 5 are as follows

THREE_D = 0xE0E0 E0E0

Three Down Img

FIVE_U = 0xE0E0 E0E0

Five Up Img

FIVE_D = 0x0707 0707

Five Down Img

THREE_U = 0x0707 0707

Three Up Img

The algorithm below generates slide moves for a SRC bitboard of black pieces. The algorithm can be easily adapted to generate slide moves for red pieces by swapping the directions of the King and Man/King slides. The algorithm below preforms no checks, and assumes that the source bitboard SRC contains only black pieces.

S_KINGS = SRC & KINGS
NOCC = ~(BLACK | RED)

# Upward Slides (Man and King slides for black)
MOVES = (SRC << 4)
MOVES |= ((SRC & THREE_D) << 3)
MOVES |= ((SRC & FIVE_D) << 5)

# Downward Slides (King slides for black)
MOVES |= (S_KINGS >> 4)
MOVES |= ((S_KINGS & THREE_U) >> 3)
MOVES |= ((S_KINGS & FIVE_U) >> 5)

MOVES &= NOCC

Generating Jump Moves

A jump move is a two square diagonal move of a piece jumping over an opposing piece to an unoccupied square.

To generate any possible jump move, the algorithm needs a way to transform a bitboard of any piece, to the bitboard that represents the four diagonal jump moves.

To generate jump moves, first start with generating the slide moves, then instead of finding the unoccupied squares, find the squares that are occupied by an opposing piece, and then shift those squares to the square that is two squares diagonally away from the the source piece.

There is a pattern that the bitboard indices follow for determining the jump move targets. If the first shift to determine where an opposing piece might be is a shift by four, then the second shift to determine the jump move target is a shift by three or five. Vice versa: if the first shift to determine where an opposing piece might be is a shift by three or five, then the second shift to determine the jump move target is a shift by four.

Gen Jumps Img

The masks to isolate the squares that need to be shifted by 3 or 5 are as follows (identical to previous masks):

Assuming the SRC bitboard only contains red pieces, we can apply the following bitboard transformations to obtain a bitboard of all jump moves from pieces in SRC

S_KINGS = SRC & KINGS
NOCC = ~(BLACK | RED)

# Upward Jumps (Man jumps for red)
ADJ_OPP = (SRC >> 4) & BLACK        # Adjacent Opponent Pieces

MOVES = ((ADJ_OPP & THREE_U) >> 3)
MOVES |= ((ADJ_OPP & FIVE_U) >> 5)

ADJ_OPP = ((SRC & THREE_U) >> 3)
ADJ_OPP |= ((SRC & FIVE_U) >> 5)
ADJ_OPP &= BLACK                    # Adjacent Opponent Pieces

MOVES |= ADJ_OPP >> 4


# Downward Jumps (Man and King jumps for red)
ADJ_OPP = (S_KINGS << 4) & BLACK    # Adjacent Opponent Pieces

MOVES |= ((ADJ_OPP & THREE_D) << 3)
MOVES |= ((ADJ_OPP & FIVE_D) << 5)

ADJ_OPP = ((S_KINGS & THREE_D) << 3)
ADJ_OPP |= ((S_KINGS & FIVE_D) << 5)
ADJ_OPP &= BLACK                    # Adjacent Opponent Pieces

MOVES |= ADJ_OPP << 4

MOVES &= NOCC

Interrupts

Exceptions and interrupts are two types of events that can disrupt a RISC-V program's execution. An exception is directly caused by the instructions a program is executing, such as an address out of range error. Interrupts are triggered by external sources and can occur at any point during program execution, such as a timer interrupt. When an interrupt or exception occurs, the normal flow of the program is paused, and control is transferred to an interrupt handler.

A handler will perform any actions required by the interrupt or exception. After the handler completes its task, if it hasn't terminated the program (as handlers often do in response to exceptions), the program's execution usually resumes at the instruction that was about to be executed when the interrupt or exception occurred.

In some cases, control cannot safely be returned to the interrupted code after an exception. In such cases, the program is terminated. This lab is only concerned with handling keyboard interrupts, all exceptions and other interrupts should terminate the program by jumping to handlerTerminate.

Control and Status Registers

This lab uses external interrupts from hardware. The role of the following CSRs (Control and Status Registers) are important for the use of interrupts:

These CSRs can be set by using the CSR instructions. For example, to enable user-level interrupts in ustatus use "CSR Read/Write Immediate" instruction: csrrwi zero, 0, 0x1. Or use pseudo-instructions to read and write to the CSR registers. For example:

csrr    t0, 4     # read from CSR #4 to t0
csrw    t0, 6     # write from t0 to CSR #6
csrwi   0, 0x4    # write 0x4 to CSR #0

Saving Registers in the Interrupt Handler

When an interrupt or exception is raised, the program is paused and control is transferred to the interrupt handler. The utvec CSR holds the address of the interrupt handler that should be called when an interrupt or exception occurs. An interrupt handler is analogous to a normal function but there are some key differences. An interrupt can occur at any time, therefore the handler must guarantee that:

To ensure that the program can safely resume execution after returning from the handler, the registers used by the handler must be saved upon entering the interrupt handler and restored before returning. The registers cannot be saved using the stack pointer because the stack pointer may be corrupted. Therefore, in common.s we have allocated memory labelled DATA_TRAP where the handler may save registers. In common.s the address of DATA_TRAP has been loaded into the uscratch register. The csrrw instruction can be used to swap the contents of uscratch and a register, then use the address from uscratch to save the registers that the handler will use.

The first and last instructions executed in the handler should be csrrw a0, 64, a0, where a0 is chosen by convention. Here is some sample code that saves two registers in the interrupt data trap:

handler:

# swap a0 and uscratch
csrrw   a0, 64, a0     # a0 <- Addr[DATA_TRAP], uscratch <- PROGRAMa0

# save all used registers except a0
sw      t0, 0(a0)         # save PROGRAMt0
sw      s0, 4(a0)         # save PROGRAMs0

...

# restore all used registers except a0
lw    t0, 0(a0)           # restore PROGRAMt0
lw    s0, 4(a0)           # restore PROGRAMs0

# restore a0
csrrw	a0, 64, a0        # a0 <- PROGRAMa0, uscratch <- Addr[DATA_TRAP]

uret                      # return from the interrupt handler using uret

Non-re-entrant handler: If you allocate a specific address to save a given register --- for example, register s0 is always saved in Addr[DATA_TRAP]+4 --- then the handler is not re-entrant. Interruptions cannot be enabled while handling an interruption, because doing so could cause the first saved value of s0 to be overwritten.

This lab will handle implementing a non-re-entrant handler. It is important to remember that interrupts are automatically disabled when control is transferred to the handler, and automatically re-enabled when returning from the handler, so the solution does not need to enable or disable interrupts in the handler.

The solution will still need to enable interrupts at the beginning of the program by setting the 0th bit of ustatus to 1 to enable user interrupts, and the 8th bit of uie to 1 to enable keyboard interrupts.

Handler Considerations

The solution cannot use reading or printing system calls (ecall) for this assignment. Instead, the program must use the MMIO and interrupts to interact with the keyboard and the display.

When executing a program in RARS using the custom handler, runtime errors won't be shown in RARS as usual. Instead, a provided section of the handler labelled handlerTerminate can be used to print the line where the error occurred, and the error code, before terminating the program. For example, the following exception happened at the instruction at address 0x00400f70 and was caused by a load access fault.

Error: Unhandled interrupt with exception code: 0x00000005
Originating from the instruction at address: 0x00400f70

Use the tables below to identify errors.

Make sure the handler does not call any functions. This is because the handler is not a function and hence it must restore every register that it uses, even the temporary t and a registers. On the other hand, functions are not required to preserve t and a registers, only the s registers.

Memory-Mapped I/O

Memory-mapped IO allows interaction with external devices through an interface pretending to be system memory. This mapping allows the processor to communicate with these devices using the load-word and store-word instructions.

Keyboard & Display

This lab uses the Keyboard and Display MMIO Simulator, available under the Tools menu in RARS, to interact with the simulator. The contents of the terminal are shown in the display section, and keys are typed in the keyboard section. Be sure to click Connect To Program after assembling the program and before running it. If you resize the window, click the Reset button to propagate the size change.

Generally, devices have two registers associated with them, a control, and a data register. The control register relays information about the device's state, and the data register relays data to or from a device. A description of the control and data registers for the keyboard and display can be found below.

Register Memory Address Description Label Containing Address
Keyboard control 0xFFFF0000 For keyboard interrupts to be enabled, bit 1 of this register must be set.

Bit 0 of this register is automatically set when the MMIO terminal receives a key, bit 0 is automatically cleared when the key is read from the KEY_DATA register.

The solution only needs to set bit 1 of this register to enable keyboard interrupts.
KEY_CONTROL
Keyboard data 0xFFFF0004 The ASCII value of the last key pressed is stored here. KEY_DATA
Display control 0xFFFF0008 Bit 0 of this register indicates whether the processor can write to the display. While this bit is 0 the processor cannot write to the display.

Thus, the program must wait until this bit is 1 to write to the MMIO display.
DISPLAY_CONTROL
Display data 0xFFFF000C When a character is placed into this register, given that the DISPLAY_CONTROL ready bit (bit 0) is 1, that character is drawn onto the display. If the character is the bell character (ASCII code 0x07) the display will move the cursor, the bits 8-19 and 20-31 corresponding to the row and column respectively.

You should not have to work with this register directly in this lab, as the mechanics of printing to the MMIO display are handled by the printChar and printStr functions provided.
DISPLAY_DATA

A separate keyboard interrupt occurs for every key pressed when keyboard interrupts are enabled. Therefore, the user program receives input one character at a time.

Assignment

Gameplay Behaviour Details

The implementation details below are important because if the solution does not follow them, the game may function as expected but fail the unit tests that the lab uses to mark the functions. Strict adherence ensures that the solution meets both functional and assessment requirements.

Inputs

The solution receives keyboard user inputs through the PENDING_KEY global variable. The handler function is responsible for processing keyboard interrupts, and storing the pressed key in PENDING_KEY. When a key that does not correspond to a valid input is pressed, the key should be stored in PENDING_KEY and checkers should handle the key (do nothing) and reset PENDING_KEY to zero.

Inputs [a->h,1->8]

When inputting a move, the player must input the file then the rank of the desired square. When the rank is inputted, the program should ensure that the square is game square, if the square is a game square, print an x to the MMIO terminal to indicate the square is selected, and add the bitboard containing the square to the end of MOVES_ARRAY. If the square is not a game square, or if MOVES_ARRAY is full (128 bitboards), discard input, don't add to MOVES_ARRAY and wait for next input.

Inputs [q]

If at any point q is inputted, MOVES_ARRAY should be emptied, the game should be re-displayed to remove any x marks, and any partially inputted moves should be discarded.

Inputs [SPACE]

If at any point 'SPACE' is inputted, the game should discard any partially inputted moves, store a sentinel value at the end of MOVES_ARRAY, and doTurn should be called to process the moves in MOVES_ARRAY. MOVES_ARRAY should be reset after doTurn and the game state should be re-displayed. Depending on the outcome of doTurn:

King Promotions

When a piece reaches the opposite end of the board, it should be promoted to a King. The makeMove function will promote all pieces of the color of the current TURN (the same color that makeMove moves) that are on the opposite end of the board.

Primary Gameplay Loop

The primary gameplay loop in checkers should continue until a player wins the game. The loop should wait until a key is stored in PENDING_KEY by the handler, then the loop should process the key, update the game state as necessary and store zero back into PENDING_KEY to indicate that the program is ready to handle another key.

Data Structures

The solution uses several global variables to represent the game state, store moves, and pass input to the program. Additional variables are defined in common.s, these include the ASCII representation of all the characters used in the solution, the address of the MMIO registers used for interacting with the RARS MMIO terminal, and the masks for shifting bitboards. Looking at, and understanding the definitions of the variables at the top of common.s may be helpful for understanding how the solution works.

Additional global variables may be added as needed in the checkers.s file for the solution. The common.s file should not be modified.

The RED global variable is a bitboard where the set bits represent squares that contain a red piece (King or Man).

The solution will maintain the RED bitboard to represent the squares that contain a red piece in the current game state.

The BLACK global variable is a bitboard where the set bits represent squares that contain a black piece (King or Man).

The solution will maintain the BLACK bitboard to represent the squares that contain a black piece in the current game state.

The KINGS global variable is a bitboard where the set bits represent squares that contain a King piece (Red or Black).

The solution will maintain the KINGS bitboard to represent the squares that contain a king piece in the current game state.

The TURN global variable is a word that represents which color makes the next move.

  • 0 indicates BLACK is to move
  • 1 indicates RED is to move

TURN should be updated in checkers after a successful turn is made by doTurn. At the beginning of the game, zero should be stored in TURN, as BLACK always moves first.

The MOVES_ARRAY data structure is used to store all pending moves for a turn. MOVES_ARRAY has space for 128 move bitboards (words) plus a sentinel value of 0xFFFFFFFF (-1) for a total of 129 words (516 bytes). A pointer to the head of MOVES_ARRAY is passed to the checkers function.

The checkers function should be responsible for storing moves in and clearing the MOVES_ARRAY data structure.

The PENDING_KEY global variable is a word that contains the ASCII representation of the key pressed by the user that is waiting to be processed by checkers, or zero when no key.

The checkers function is responsible for processing the key stored in PENDING_KEY and resetting PENDING_KEY to zero when no key is waiting to be processed.

The handler is responsible for handling keyboard interrupts, reading the pressed key from the MMIO keyboard data register whose address is stored in the KEY_DATA global variable, and storing the pressed key in PENDING_KEY if it is zero.

Functions to Complete

The solution must implement the following functions:

These functions are described in detail below.

When implementing these functions, to get full marks, remember to complete the register usage section in their header comments to describe the usage of the t and s registers in the function.

The solution must use the implemented functions, as well as the RED, BLACK, KINGS, TURN, MOVES_ARRAY, and PENDING_KEY global variables provided at the top of common.s. These global variables and data structures are described in detail in the data structures section.

Additionally, helper functions are provided that may be useful when producing the solution, these helper functions are detailed in the helper functions section.

checkers
  Entry point for checkers game, handles the main game logic.
    NOTE: The initial game state is not setup for you

  Arguments
    a0: Pointer to head of MOVES_ARRAY

  Returns
    N/A

The checkers function is the main function for the checkers game. It is responsible for initializing the game state, processing user input, and calling the appropriate functions to run the game.

The function takes a pointer to the head of the MOVES_ARRAY as an argument, which is used to store the moves made by the player. The function should initialize the game state, display the game board, and enter a loop that waits for user input. The loop should process the user inputs whem available, calling the appropriate functions to handle the input and update the game state.

The gameplay loop in the checkers function should continue to run until a player wins the game, at which point it should display the winner to the MMIO display using printStr, starting at position (0,20), then return from checkers. The strings to display the winner are stored in the RED_WIN_MSG and BLACK_WIN_MSG global variables.

Gameplay details and implementation requirements are described in detail in the Gameplay Behaviour Details section.

The initial game state is not initialized, the solution must initialize the global gamestate bitboards and TURN to the starting game state.

displayGame
  This function displays the game state on the board in the RARS MMIO
    interface. The skeleton of the game board can be displayed using
    the 'displayBoard' helper function. This function only displays
    the game squares, and should NOT display the non-game squares.
      - 'b' for black man pieces
      - 'B' for black king pieces
      - 'r' for red man pieces
      - 'R' for red king pieces
      - '-' for empty game squares

    NOTE: Use the 'printChar' helper function to print characters
      to the MMIO interface

  Arguments
    N/A

  Returns
    N/A

The displayGame function takes no arguments and returns no values. This function uses the global game state bitboards and the printChar helper function to print the current game state to the MMIO terminal. This function should print the game state on the board template displayed by the displayBoard helper function.

The function prints the content of each game square to the MMIO terminal, using the appropriate symbol for each game square as specified below.

  • b for Black Man pieces
  • r for Red Man pieces
  • B for Black King pieces
  • R for Red King pieces
  • - for empty game squares

Labels to bytes containing the ASCII representation of the printable characters can be found at the top of common.s. The label names are as follows ASCII_b, ASCII_r, ASCII_B, ASCII_R, and ASCII_DASH.

This function has no need to print any characters to non game squares, as a non game square will never contain a piece, and we will never display a non game square as selected with an x.

It may be helpful to display the board template using displayBoard in order to find the MMIO terminal coordinates of the squares that need to be printed to.

genSlideMoves
  This function checks that the source bitboard contains only squares
    that are the color of the current player's turn. It then generates
    a bitboard of all the valid slide moves from the source squares.
    If any of the source squares are invalid, it returns a ZERO_BB.

    NOTE: This function works with multiple src squares, as long
           as they are all valid

  Arguments
    a0: Bitboard of source squares

  Returns
    a0: Bitboard of all valid slide moves from source squares

This function takes a bitboard of source squares, verifies that none of the source squares are invalid, then generates a bitboard of all the squares reachable by slide moves from the source squares. A square is invalid if it is empty or if it contains an opposing piece to the current TURN. If any of the source squares are invalid, the function returns a ZERO_BB.

genJumpMoves
  This function checks that the source bitboard contains only squares
    that are the color of the current player's turn. It then generates a
    bitboard of all the valid jump moves from the source squares.
    If any of the source squares are invalid, it returns a ZERO_BB.

    NOTE: This function works with multiple src squares, as long
           as they are all valid

  Arguments
    a0: Bitboard of source squares

  Returns
    a0: Bitboard of all valid jump moves from source squares

This function takes a bitboard of source squares, verifies that none of the source squares are invalid, then generates a bitboard of all the squares reachable by jump moves from the source squares. A square is invalid if it is empty or if it contains an opposing piece to the current TURN. If any of the source squares are invalid, the function returns a ZERO_BB.

doTurn
  Validates and executes every move in MOVES_ARRAY, if all the moves are
    valid, they should all be made, the global game state should be
    updated, and the function should return 1. If any of the moves are invalid,
    the game state should be restored to its state before the turn, and return 0.

    NOTE: Assumes MOVES_ARRAY is terminated with a sentinel value of (-1)

    NOTE: Remember that a player MUST make a jump move if
            possible, and MUST continue chaining jump moves together

  Arguments
    a0: Pointer to head of MOVES_ARRAY

  Returns
    a0: 1 if turn is successful, 0 if turn is unsuccessful

The doTurn function is responsible for validating and executing every move in the MOVES_ARRAY for the color of the current TURN. The function takes a pointer to MOVES_ARRAY as an argument, and returns one if the turn is successful (all moves in MOVES_ARRAY are valid and made), or zero if the turn is unsuccessful (some moves in MOVES_ARRAY are invalid, and no moves made).

Remember that jump moves are forced, and must be made if available. If a player has the opportunity to make a jump move or continue a jump move chain and they do not, the turn is considered invalid.

If all moves are valid, the game state bitboards (BLACK, RED, KINGS) should already reflect the game state after the turn (since makeMove is called to execute each valid move), and the function should return one to indicate the turn was successful.

If the turn is considered invalid at any point while processing the moves in MOVES_ARRAY, the function restore the global game state bitboards (BLACK, RED, KINGS) to their state before the turn was attempted, and the function should return zero to indicate the turn was unsuccessful.

The TURN global variable should not be updated by this function, it is the responsibility of the checkers function to update the TURN variable after a successful turn.

makeMove
  Updates the global bitboards with the move of the piece specified
    by a0, to the square specified by a1. This function performs no
    validation checks, it assumes that a0 -> a1 is a single valid
    move for the current player's turn, and that a0 and a1 only have
    a single set bit.

  Arguments
    a0: Bitboard with a single source square
    a1: Bitboard with a single destination square

  Returns
    N/A

The makeMove function takes a source square bitboard a0 and a destination square bitboard a1 as arguments, and updates the global bitboards (BLACK, RED, KINGS) with the move of the piece specified by a0 to the square specified by a1.

This function performs no validation checks, it assumes that a0 to a1 is a single valid move for the current player's turn, and that a0 and a1 each only have a single set bit (representing a single square). This function does not return a value, it only updates the global bitboards with the move.

The move a0 -> a1 may be a slide move or a jump move, and the function should handle both cases correctly. If the move is a jump move, makeMove needs to remove the piece that was jumped over from the global bitboards.

The makeMove function should promote ALL pieces of the color of the current TURN (the same color that makeMove moves) that are on the opposite end of the board to King pieces. The function should NOT promote pieces of the opposing color, even if they are on the opposite end of the board.

The makeMove function is called by the doTurn function to update the global game state with the moves made by the player. It is important to ensure that this function correctly updates the global bitboards, as it is the only function that modifies the board state.

To generate the mask to remove jumped over pieces, the solution may have to extract the row and column indexes of the source and destination bitboards, find the midpoint of the row and column indexes, and then use the midpoints to generate the mask to remove the jumped over piece. The bitsToIndex and cordsToBits helper functions may be useful for this.

handler
  The handler function is responsible for handling all interrupts and
    exceptions. This function should save and restore all registers that
    it uses, including t registers. The handler should
    not call functions exterior to the handler, and should return from
    the function using the uret instruction. If the handler is handling
    an exception or unexpected interrupt, it should jump to the
    'handlerTerminate' label, which will print the error code and the
    address of the instruction that raised the exception.

  Arguments
    N/A

  Returns
    N/A

This function takes care of handling interrupts, such as user input, the addess of this function should be stored in the utvec CSR (CSR #5) when interrupts are enabled at the start of the program.

It is important to ensure that this function saves and restores all registers that it uses, including t registers, as control can be transferred to this function at any time during execution. This function should store the saved registers using the address stored in the uscratch CSR (CSR #64) as the base address to store registers before use.

When the handler is handling an exception, it should jump to the handlerTerminate label, which will print the error code and the address where the error occurred, then terminates the program.

The handler should be light and quick, to achieve this, the handler should store the pressed key in the PENDING_KEY global variable, which will be used by the checkers function to process user input. This approach of storing the pressed key in a global variable allows the input processing logic to be handled outside of the handler, in the checkers function, avoiding bogging down the handler with more logic.

The ASCII representation of the most recently pressed key is stored in the keyboard data MMIO register, the address of this register is stored in the KEY_DATA global variable in common.s. The handler should read the pressed key from this register, and store it in the PENDING_KEY global variable only if PENDING_KEY is empty (zero). This ensures that the key in PENDING_KEY is only updated after the previous key has been fully processed by checkers.

Additionally, the handler should not call functions exterior to the handler, and should return from the function using the uret instruction, and not use ret or jalr instructions. More information about the handler and interrupts can be found in the interrupts section.

A common cause of issues related to transferring control to the handler is incorrectly enabling interrupts. Ensure that you have correctly set the following MMIO and CSR registers:

  • The address of the handler function is stored in the utvec CSR (CSR #5).
  • That bit zero of the ustatus CSR (CSR #0) is set to one to enable user-level interrupts.
  • That bit eight of the uie CSR (CSR #4) is set to one to enable keyboard interrupts.
  • That bit one of the Keyboard Control MMIO register (Address of which is stored in the KEY_CONTROL global variable in common.s) is set to one to enable keyboard interrupts from the MMIO terminal.

Helper Functions

We have provided you with a set of helper functions that may be useful when implementing the solution. Do not modify these functions, they are used by the test cases to validate the solution.

The following helper functions are provided:

These functions are described in detail below.

If you would like to see how these helper functions are implemented, you can look at their definitions in common.s. Do not modify common.s.

displayBoard
  This function displays the empty game board in the RARS MMIO
    interface. This function should be called once at the start of
    the game to display the empty game board template.

  Arguments
    N/A

  Returns
    N/A

The displayBoard helper function prints the skeleton of the game board to the RARS MMIO terminal.

This function uses the printStr helper function to print the string that contains the skeleton of the game board to the MMIO terminal.

If this funtion is not displaying the game board correctly, ensure that you have resized the MMIO terminal to at least 40x22.

bitsToIndex
  Takes a bitboard with a single bit set and return the numerical
    index of the set bit.

  Arguments
    a0: Bitboard with a single set bit

  Returns
    a0: The index of the set bit

The bitsToIndex helper function takes a bitboard with a single set bit as an argument, and returns the integer index of the set bit.

This function may be useful for helping determining the square that is captured by a jump move.

This function assumes that argument bitboard contains only a single set bit. If the argument bitboard contains multiple set bits, this function will return the index of the most significant set bit.

cordsToBits
  Takes a row and column index (NOT rank and file) as coordinates and
    returns the bitboard of the square that corresponds to the row and
    column. If the square specified by the row and column is not
    a game square, returns a ZERO_BB.

  Arguments
    a0: Index of the row (0-7)
    a1: Index of the column (0-7)

  Returns
    a0: Bitboard representing game square, or 0 for an invalid square

The cordsToBits helper function takes a row and column index as arguments (NOT rank and file), and returns the bitboard with a single set bit that corresponds with the square at the coordinates (col, row). The funtion returns a ZERO_BB when the specified square is NOT a game square.

The solution may convert the rank and file input to row and column index, and then use cordsToBits to both validate the square (check that the square is a game square), and generate the move bitboard to store in MOVES_ARRAY.

This function assumes that both the row and column index are valid and in the bounds of the board [0,7]. The behaviour of this function for out of bounds indexes is not defined.

printChar
  Prints a single character to the Keyboard and Display MMIO Simulator
    terminal at the given coordinates.

  Arguments
    a0: The x coordinate of the point to print to
    a1: The y coordinate of the point to print to
    a2: The ASCII representation of the character to print

  Returns
    N/A

  Reference: This function is a modified version of a printChar
    function made by Zachary Selk, 2019

The printChar helper function takes an x and y coordinate, and an ASCII representation of the character to print. The funtion prints the character to the RARS MMIO terminal at the specified coordinates.

This function may be useful for printing individual characters to the MMIO terminal, such as the game squares ('b', 'B', 'r', 'R', '-'), or selected sqaures ('x').

This function overwrites the character at the position it prints to. Further details on the implementation of this function can be found here.

printStr
  Prints a null-terminated string to the Keyboard and Display MMIO
    Simulator terminal at the given coordinates

  Arguments
    a0: The x coordinate of the starting position to print the string
    a1: The y coordinate of the starting position to print the string
    a2: The address of the null-terminated string to print

  Returns
    N/A

  Reference: This function is a modified version of a printStr
    function made by Zachary Selk, 2019

The printStr helper function takes an x and y coordinate, and a pointer to a null-terminated string. This funtion prints the string to the RARS MMIO terminal starting at the specified coordinates, it prints each character in the string until a null character is reached.

When printStr encounters a newline character ('\n') in the string, it will skip the newline character and print subsequent characters starting at the beginning of the next line to simulate a newline character.

This function may be useful for printing messages to the MMIO terminal, such as the winner of the game. The printStr function is used by displayBoard to print the string that contains the skeleton of the game board to the MMIO terminal.

This function overwrites the characters at the positions it prints to. Further details on the implementation of this function can be found here.

waitForDisplayReady
  A method that will check if the Keyboard and Display MMIO Simulator
    terminal can be written to, busy-waiting until it can.

  Arguments
    N/A

  Returns
    N/A

The waitForDisplayReady helper function executes a loop continually checks if the MMIO display is ready to be printed to. When the MMIO display is ready to be printed to, the loop terminates and waitForDisplayReady returns.

Further details on how this function works, and why it is needed can be found here.

Understanding printChar, printStr, and waitForDisplayReady

The MMIO display is not always ready to print a new character. Thus, whenever the printChar function is called, it must wait for the MMIO display to be ready before it can print. printChar calls the function waitForDisplayReady which executes a busy wait loop that checks the status of the Display Control Register. This register acts as a flag that signals if the display is ready. The address of this register is in the global variable DISPLAY_CONTROL provided to you. If the value in the Display Control Register is set to one, then the busy wait loop exits and the character/string can be printed to the MMIO display. Otherwise, the loop will continue periodically checking the register.

This flag/idle loop mechanism is necessary because the MMIO simulator temporarily clears the Display Control register while printing a character to the display and then re-enables it once printing is complete. The sequence followed during this process is as follows:

  1. When data is written to the Display Data register, the simulator is notified.
  2. The Display Control register is cleared by the simulator.
  3. The character is read from the Display Data register.
  4. The ASCII character is echoed to the display.
  5. A delay is introduced to simulate the processing time required by an actual display unit.
  6. Finally, the simulator re-enables the Display Control register.

printStr simply calls printChar in a loop until it reaches a null character.

Testing

Make sure to thouroughly test the solution to ensure that all the functions work as expected, and that the game plays as described in the instructions.

Bitboard Generation Tool

A python tool bb-generator.py is provided to assist with producing gamestate bitboards for manual testing. This tool can be found in the Code/bb-generatory-tool/ directory. Instructions on how to use this tool can be found upon startup of the tool. To run the tool, you will need to have python installed on your computer, and run the command python3 bb-generator.py in a terminal in the Code/bb-generatory-tool/ directory.

Check My Lab

This lab is supported in CheckMyLab. To get started, navigate to the Checkers lab in CheckMyLab found in the dashboard. From there, students can upload test cases in the 'My test cases table'. Test cases are text files which follow the format described below. Additionally, students can upload their checkers.s file in the My solutions table, which will then be tested against all other valid test cases.

The format of the {test}.txt file is as follows: (test number) (arguments)

The test number is an integer number 1-4 representing the function to be tested.

  1. genSlideMoves
  2. genJumpMoves
  3. makeMove
  4. doTurn

The arguments are specific to the test being preformed, they must be in the order described, separated by a space. Bitboard arguments and outputs (BLACK, RED, KINGS, SRC, DEST, and moves bitboards) are always in the form 0x1234ABCD. Non-bitboard arguments and outputs such as TURN and output of doTurn are either 0 or 1. The arguments and their order for each of the testable functions is described below.

  1. genSlideMoves
    1. BLACK bitboard
    2. RED bitboard
    3. KINGS bitboard
    4. TURN (0 or 1)
    5. SRC bitboard

  2. genJumpMoves
    1. BLACK bitboard
    2. RED bitboard
    3. KINGS bitboard
    4. TURN (0 or 1)
    5. SRC bitboard

  3. makeMove
    1. SRC bitboard
    2. DEST bitboard
    3. BLACK bitboard
    4. RED bitboard
    5. KINGS bitboard
    6. TURN (0 or 1)

  4. doTurn
    1. BLACK bitboard
    2. RED bitboard
    3. KINGS bitboard
    4. TURN (0 or 1)
    5. Move bitboards separated by spaces. Make sure that the last moves bitboard is the sentinel value 0xFFFFFFFF to properly mark the end of MOVES_ARRAY.

CheckMyLab only checks that the test number is valid, and that there is the correct number of arguments for the test. This means that uploaded test cases may result in undefined behavior if not properly constructed, for example since makeMove is only ever called by the solution with a valid move, if the arguments for testing makeMove do not describe a valid move, different implementations of makeMove may have different results causing the test to fail, even though both makeMove implementations function correctly.

Assumptions, Notes, and Hints

Resources

Marking Guide

Assignments too short to be adequately judged for code quality will be given a zero for that portion of the evaluation.

Submission

There is a single file to be submitted for this lab (checkers.s) and it should contain the implementation of the solution functions.