Advertisement
Notimecelduv

Python Bit Board Tic-Tac-Toe

Apr 30th, 2025 (edited)
204
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.38 KB | Source Code | 0 0
  1. # We'll use this package to get random moves for the computer player.
  2. from random import randint
  3.  
  4. """
  5. GLOSSARY
  6. - A **line** is a row, column or diagonal.
  7. - A **bit word** is a binary number used to represent data of some sort.
  8. - A bit is said to be **set** when it is equal to 1. For example, in `0100` the bit at position 2 (counting from the right from zero) is set.
  9. - A **mask** is a bit word used to check if certain bits in another number are set. Examples:
  10.    - `n & 0011` is used to check if the first two bits in the number `n` are set,
  11.    - `n2 = n | 0001` returns a number `n2` equal to `n` with its first bit set to 1.
  12. - A **bit board** is a bit word used specifically to represent the board in a board game.
  13.  
  14. BOARD REPRESENTATION
  15. The board is represented as a single bit word (a bit board).
  16.  
  17. bits 00-08 = X occupancy
  18. bits 09-17 = O occupancy
  19. bit 18     = active player
  20.  
  21. CELL INDICES
  22. By convention, indices from 0 to 9 represent the cells on the board in the order below:
  23.  
  24. 6 7 8
  25. 3 4 5
  26. 0 1 2
  27.  
  28. EXAMPLE
  29. Given the board below:
  30.  
  31. O . X
  32. . X .
  33. . . .
  34.  
  35. the corresponding bit word is: 1 001000000 100010000
  36. 100010000 => X has played to cells 4 and 8
  37. 001000000 => O has played to cell 6
  38. 1         => it is O's turn to move
  39. """
  40.  
  41. NB_ROWS = 3
  42. """ Number of rows on the board. """
  43.  
  44. NB_COLS = NB_ROWS
  45. """ Number of columns per row. """
  46.  
  47. NB_CELLS = NB_ROWS * NB_COLS
  48. """ Total number of cells on the board. """
  49.  
  50. PLAYER_COMPUTER = 0
  51. """ The AI player. Always goes first. """
  52.  
  53. PLAYER_HUMAN = 1
  54. """ The human player. Always goes second. """
  55.  
  56. PLAYERS_SYMBOLS = ["X", "O"]
  57. """ The characters used to represent each player when printing the board. """
  58.  
  59. STATE_LOSS = 0
  60. """ A value indicating that the game was lost by the active player. """
  61.  
  62. STATE_DRAW = 1
  63. """ A value indicating that the game is over and neither player won. """
  64.  
  65. STATE_ONGOING = 2
  66. """ A value indicating that the game hasn't reached a result yet. """
  67.  
  68. NB_BITS_FULL_OCCUPANCY = NB_CELLS * 2
  69. """ The number of bits to represent the occupancy of both players. """
  70.  
  71. NB_BITS_BOARD = NB_BITS_FULL_OCCUPANCY + 1
  72. """ The number of bits in a bit board (including the active player). """
  73.  
  74. OCCUPANCY_MASK = (1 << NB_CELLS) - 1
  75. """ A mask with 9 set bits. Used to detect if the board is full or single out a player's occupancy. """
  76.  
  77. BIT_MASKS = [1 << i for i in range(NB_BITS_BOARD)]
  78. """ A list of masks each having a single set bit used for various bit manipulations. """
  79.  
  80. WIN_MASKS = []  # type: list[int]
  81. """ A list of masks each having 3 set bits used to quickly detect if either player has filled a line. It is initialized later through a function call. """
  82.  
  83. BIT_INDEX_PLAYER = NB_BITS_FULL_OCCUPANCY
  84. """ The index of the bit representing the active player in a bit board. """
  85.  
  86.  
  87. def coordsToIndex(row: int, col: int) -> int:
  88.     """ Convert a row and column to a board index. """
  89.     return row * NB_COLS + col
  90.  
  91.  
  92. def initWinMasks() -> None:
  93.     """ The masks should look like this:
  94.        000 000 111,
  95.        000 111 000,
  96.        111 000 000,
  97.        001 001 001,
  98.        010 010 010,
  99.        100 100 100,
  100.        100 010 001,
  101.        001 010 100
  102.  
  103.        They each represent a line that can be filled to win the game.
  104.    """
  105.     diagonalMask = 0
  106.     antiDiagonalMask = 0
  107.  
  108.     for row in range(NB_ROWS):
  109.         rowMask = 0
  110.         colMask = 0
  111.  
  112.         for col in range(NB_COLS):
  113.             rowMask |= BIT_MASKS[coordsToIndex(row, col)]
  114.             colMask |= BIT_MASKS[coordsToIndex(col=row, row=col)]
  115.  
  116.         WIN_MASKS.append(rowMask)
  117.         WIN_MASKS.append(colMask)
  118.         diagonalMask |= BIT_MASKS[coordsToIndex(row, row)]
  119.         antiDiagonalMask |= BIT_MASKS[coordsToIndex(row, NB_ROWS - row - 1)]
  120.  
  121.     WIN_MASKS.append(diagonalMask)
  122.     WIN_MASKS.append(antiDiagonalMask)
  123.  
  124.  
  125. def getActivePlayer(board: int) -> int:
  126.     return board >> BIT_INDEX_PLAYER
  127.  
  128.  
  129. def oppositePlayer(player: int) -> int:
  130.     # Since `player` is always equal to 0 or 1, it can be reversed by toggling it (XOR operator).
  131.     return player ^ 1
  132.  
  133.  
  134. def getOccupancy(board: int, player: int) -> int:
  135.     """ Extract the part of a bit board representing the cells occupied by a given player. """
  136.     return board >> (NB_CELLS * player) & OCCUPANCY_MASK
  137.  
  138.  
  139. def isCellOccupied(occ: int, cell: int) -> bool:
  140.     bitMask = BIT_MASKS[cell]
  141.     # If the cell wasn't occupied, then `fullOcc & bitMask` would be equal to 0.
  142.     return (occ & bitMask) != 0
  143.  
  144.  
  145. def getFreeCells(board: int) -> list[int]:
  146.     """ Get a list of unoccupied cell indices. """
  147.     computerOcc = getOccupancy(board, PLAYER_COMPUTER)
  148.     humanOcc = getOccupancy(board, PLAYER_HUMAN)
  149.     fullOcc = computerOcc | humanOcc
  150.     freeCells = []  # type: list[int]
  151.  
  152.     for cell in range(NB_CELLS):
  153.         if not isCellOccupied(fullOcc, cell):
  154.             freeCells.append(cell)
  155.  
  156.     return freeCells
  157.  
  158.  
  159. def getState(board: int) -> int:
  160.     """ Check if the game is lost, drawn or still in play. """
  161.     activePlayer = getActivePlayer(board)
  162.     inactivePlayer = oppositePlayer(activePlayer)
  163.     inactiveOcc = getOccupancy(board, inactivePlayer)
  164.  
  165.     for mask in WIN_MASKS:
  166.         if (inactiveOcc & mask) == mask:
  167.             return STATE_LOSS
  168.  
  169.     activeOcc = getOccupancy(board, activePlayer)
  170.     fullOcc = activeOcc | inactiveOcc
  171.  
  172.     if fullOcc == OCCUPANCY_MASK:
  173.         return STATE_DRAW
  174.  
  175.     return STATE_ONGOING
  176.  
  177.  
  178. def promptHumanMove(board: int) -> int:
  179.     """ Ask user to enter a move via the command line. """
  180.     fullOcc = board & ~BIT_MASKS[BIT_INDEX_PLAYER]
  181.     prompt = "Please choose a free cell from the above board: "
  182.  
  183.     try:
  184.         move = int(input(prompt))
  185.  
  186.         if move < 0 or move >= NB_CELLS:
  187.             print(f"Invalid move: should be in the range [0;{NB_CELLS}[.")
  188.             # Call function again to query user once more.
  189.             return promptHumanMove(board)
  190.  
  191.         if isCellOccupied(fullOcc, move):
  192.             print("This cell is occupied.")
  193.             return promptHumanMove(board)
  194.  
  195.         return move
  196.     except:
  197.         print("Invalid input!")
  198.         return promptHumanMove(board)
  199.  
  200.  
  201. def getComputerMove(board: int) -> int:
  202.     freeCells = getFreeCells(board)
  203.     # Pick a random move for the computer to play.
  204.     randomIndex = randint(0, len(freeCells) - 1)
  205.     return freeCells[randomIndex]
  206.  
  207.  
  208. def printBoard(board: int) -> None:
  209.     """ Display a visual representation of the board. """
  210.  
  211.     # Command line colors
  212.     FG_YELLOW = "\033[93m"
  213.     FG_CYAN = "\033[96m"
  214.     CLR_RESET = "\033[0m"
  215.  
  216.     computerOcc = getOccupancy(board, PLAYER_COMPUTER)
  217.     humanOcc = getOccupancy(board, PLAYER_HUMAN)
  218.  
  219.     for row in range(NB_ROWS):
  220.         for col in range(NB_COLS):
  221.             # Row is reversed in order to print the first row at the bottom.
  222.             index = coordsToIndex(NB_ROWS - row - 1, col)
  223.  
  224.             if isCellOccupied(computerOcc, index):
  225.                 print(
  226.                     FG_CYAN + PLAYERS_SYMBOLS[PLAYER_COMPUTER] + CLR_RESET, end=" ")
  227.                 continue
  228.  
  229.             if isCellOccupied(humanOcc, index):
  230.                 print(
  231.                     FG_YELLOW + PLAYERS_SYMBOLS[PLAYER_HUMAN] + CLR_RESET, end=" ")
  232.                 continue
  233.  
  234.             print(index, end=" ")
  235.  
  236.         print()
  237.  
  238.  
  239. def play() -> None:
  240.     board = 0
  241.     # Counting the number of moves played to avoid an infinite loop.
  242.     nbMovesPlayed = 0
  243.  
  244.     while nbMovesPlayed <= NB_CELLS:
  245.         activePlayer = getActivePlayer(board)
  246.         inactivePlayer = oppositePlayer(activePlayer)
  247.         state = getState(board)
  248.  
  249.         if state == STATE_LOSS:
  250.             print(f"{PLAYERS_SYMBOLS[inactivePlayer]} wins!")
  251.             break
  252.  
  253.         if state == STATE_DRAW:
  254.             print("The game is a draw.")
  255.             break
  256.  
  257.         if activePlayer == PLAYER_COMPUTER:
  258.             move = getComputerMove(board)
  259.             board |= BIT_MASKS[move]
  260.             print(f"The computer played to cell {move}.")
  261.         elif activePlayer == PLAYER_HUMAN:
  262.             move = promptHumanMove(board)
  263.             board |= BIT_MASKS[move + NB_CELLS]
  264.  
  265.         print()
  266.         printBoard(board)
  267.         print()
  268.  
  269.         # Toggle player
  270.         board ^= BIT_MASKS[BIT_INDEX_PLAYER]
  271.         nbMovesPlayed += 1
  272.  
  273.  
  274. def main() -> None:
  275.     initWinMasks()
  276.     play()
  277.  
  278.  
  279. # Ensure the `main` function is only called when this script is run directly.
  280. if __name__ == "__main__":
  281.     main()
  282.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement