1"""This module defines the Board class for managing the state of a Connect Four game."""
3from __future__
import annotations
5from collections.abc
import Sequence
6from typing
import Any, cast
8from bitbully
import bitbully_core
12 """Represents the state of a Connect Four board. Mostly a thin wrapper around BoardCore."""
14 def __init__(self, init_with: Sequence[Sequence[int]] | Sequence[int] | str |
None =
None) ->
None:
15 """Initializes a Board instance.
18 init_with (Sequence[Sequence[int]] | Sequence[int] | str | None):
19 Optional initial board state. Accepts:
20 - 2D array (list, tuple, numpy-array) with shape 7x6 or 6x7
21 - 1D sequence of ints: a move sequence of columns (e.g., [0, 0, 2, 2, 3, 3])
22 - String: A move sequence of columns as string (e.g., "002233")
23 - None for an empty board
26 ValueError: If the provided initial board state is invalid.
29 You can initialize an empty board in multiple ways:
33 # Create an empty board using the default constructor.
34 board = bb.Board() # Starts with no tokens placed.
36 # Alternatively, initialize the board explicitly from a 2D list.
37 # Each inner list represents a column (7 columns total, 6 rows each).
38 # A value of 0 indicates an empty cell; 1 and 2 would represent player tokens.
39 board = bb.Board([[0] * 6 for _ in range(7)]) # Equivalent to an empty board.
41 # You can also set up a specific board position manually using a 6 x 7 layout,
42 # where each inner list represents a row instead of a column.
43 # (Both layouts are accepted by BitBully for convenience.)
44 # For more complex examples using 2D arrays, see the examples below.
45 board = bb.Board([[0] * 7 for _ in range(6)]) # Also equivalent to an empty board.
47 # Display the board in text form.
48 # The __repr__ method shows the current state (useful for debugging or interactive use).
61 The recommended way to initialize an empty board is simply `Board()`.
64 You can also initialize a board with a sequence of moves:
68 # Initialize a board with a sequence of moves played in the center column.
70 # The list [3, 3, 3] represents three moves in column index 3 (zero-based).
71 # Moves alternate automatically between Player 1 (yellow, X) and Player 2 (red, O).
72 # After these three moves, the center column will contain:
73 # - Row 0: Player 1 token (bottom)
74 # - Row 1: Player 2 token
75 # - Row 2: Player 1 token
76 board = bb.Board([3, 3, 3])
78 # Display the resulting board.
79 # The textual output shows the tokens placed in the center column.
94 You can also initialize a board using a string containing a move sequence:
98 # Initialize a board using a compact move string.
100 # The string "33333111" represents a sequence of eight moves:
101 # 3 3 3 3 3 → five moves in the center column (index 3)
102 # 1 1 1 → three moves in the second column (index 1)
104 # Moves are applied in order, alternating automatically between Player 1 (yellow, X)
105 # and Player 2 (red, O), just as if you had called `board.play()` repeatedly.
107 # This shorthand is convenient for reproducing board states or test positions
108 # without having to provide long move lists.
110 board = bb.Board("33333111")
112 # Display the resulting board.
113 # The printed layout shows how the tokens stack in each column.
127 You can also initialize a board using a 2D array (list of lists):
129 import bitbully as bb
131 # Use a 6 x 7 list (rows x columns) to set up a specific board position manually.
133 # Each inner list represents a row of the Connect-4 grid.
136 # - 1 → Player 1 token (yellow, X)
137 # - 2 → Player 2 token (red, O)
139 # The top list corresponds to the *top row* (row index 5),
140 # and the bottom list corresponds to the *bottom row* (row index 0).
141 # This layout matches the typical visual display of the board.
144 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
145 [0, 0, 0, 1, 0, 0, 0], # Row 4: Player 1 token in column 3
146 [0, 0, 0, 2, 0, 0, 0], # Row 3: Player 2 token in column 3
147 [0, 2, 0, 1, 0, 0, 0], # Row 2: tokens in columns 1 and 3
148 [0, 1, 0, 2, 0, 0, 0], # Row 1: tokens in columns 1 and 3
149 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom): tokens stacked lowest
152 # Create a Board instance directly from the 2D list.
153 # This allows reconstructing arbitrary positions (e.g., from test data or saved states)
154 # without replaying the move sequence.
155 board = bb.Board(board_array)
157 # Display the resulting board state in text form.
171 You can also initialize a board using a 2D (7 x 6) array with columns as inner lists:
173 import bitbully as bb
175 # Use a 7 x 6 list (columns x rows) to set up a specific board position manually.
177 # Each inner list represents a **column** of the Connect-4 board, from left (index 0)
178 # to right (index 6). Each column contains six entries — one for each row, from
179 # bottom (index 0) to top (index 5).
183 # - 1 → Player 1 token (yellow, X)
184 # - 2 → Player 2 token (red, O)
186 # This column-major layout matches the internal representation used by BitBully,
187 # where tokens are dropped into columns rather than filled row by row.
190 [0, 0, 0, 0, 0, 0], # Column 0 (leftmost)
191 [2, 1, 2, 0, 0, 0], # Column 1
192 [0, 0, 0, 0, 0, 0], # Column 2
193 [1, 2, 1, 2, 1, 0], # Column 3 (center)
194 [0, 0, 0, 0, 0, 0], # Column 4
195 [0, 0, 0, 0, 0, 0], # Column 5
196 [0, 0, 0, 0, 0, 0], # Column 6 (rightmost)
199 # Create a Board instance directly from the 2D list.
200 # This allows reconstructing any arbitrary position (e.g., test cases, saved games)
201 # without replaying all moves individually.
202 board = bb.Board(board_array)
204 # Display the resulting board.
205 # The text output shows tokens as they would appear in a real Connect-4 grid.
218 self.
_board = bitbully_core.BoardCore()
219 if init_with
is not None and not self.
reset_board(init_with):
221 "Invalid initial board state provided. Check the examples in the docstring for valid formats."
225 """Checks equality between two Board instances.
228 - Equality checks in BitBully compare the *exact board state* (bit patterns),
229 not just the move history.
230 - Two different move sequences can still yield the same position if they
231 result in identical token configurations.
232 - This is useful for comparing solver states, verifying test positions,
233 or detecting transpositions in search algorithms.
236 value (object): The other Board instance to compare against.
239 bool: True if both boards are equal, False otherwise.
242 NotImplementedError: If the other value is not a Board instance.
246 import bitbully as bb
248 # Create two boards that should represent *identical* game states.
250 assert board1.play("33333111")
253 # Play the same position step by step using a different but equivalent sequence.
254 # Internally, the final bitboard state will match `board1`.
255 assert board2.play("31133331")
257 # Boards with identical token placements are considered equal.
258 # Equality (`==`) and inequality (`!=`) operators are overloaded for convenience.
259 assert board1 == board2
260 assert not (board1 != board2)
262 # ------------------------------------------------------------------------------
264 # Create two boards that differ by one move.
265 board1 = bb.Board("33333111")
266 board2 = bb.Board("33333112") # One extra move in the last column (index 2)
268 # Since the token layout differs, equality no longer holds.
269 assert board1 != board2
270 assert not (board1 == board2)
273 if not isinstance(value, Board):
274 raise NotImplementedError(
"Can only compare with another Board instance.")
275 return bool(self.
_board == value._board)
278 """Checks inequality between two Board instances.
280 See the documentation for [`bitbully.Board.__eq__`][src.bitbully.Board.__eq__] for details.
283 value (object): The other Board instance to compare against.
286 bool: True if both boards are not equal, False otherwise.
288 return not self.
__eq__(value)
291 """Returns a string representation of the Board instance."""
292 return f
"{self._board}"
295 """Return a human-readable ASCII representation (same as to_string()).
297 See the documentation for [`bitbully.Board.to_string`][src.bitbully.Board.to_string] for details.
302 """Find all positions reachable from the current position up to a given ply.
304 This is a high-level wrapper around
305 `bitbully_core.BoardCore.allPositions`.
307 Starting from the **current** board, it generates all positions that can be
308 reached by playing additional moves such that the resulting position has:
310 - At most ``up_to_n_ply`` tokens on the board, if ``exactly_n`` is ``False``.
311 - Exactly ``up_to_n_ply`` tokens on the board, if ``exactly_n`` is ``True``.
314 The number of tokens already present in the current position is taken
315 into account. If ``up_to_n_ply`` is smaller than
316 ``self.count_tokens()``, the result is typically empty.
318 This function can grow combinatorially with ``up_to_n_ply`` and the
319 current position, so use it with care for large depths.
323 The maximum total number of tokens (ply) for generated positions.
324 Must be between 0 and 42 (inclusive).
326 If ``True``, only positions with exactly ``up_to_n_ply`` tokens
327 are returned. If ``False``, all positions with a token count
328 between the current number of tokens and ``up_to_n_ply`` are
332 list[Board]: A list of :class:`Board` instances representing all
333 reachable positions that satisfy the ply constraint.
336 ValueError: If ``up_to_n_ply`` is outside the range ``[0, 42]``.
339 Compute all positions at exactly 3 ply from the empty board:
342 import bitbully as bb
344 # Start from an empty board.
347 # Generate all positions that contain exactly 3 tokens.
348 positions = board.all_positions(3, exactly_n=True)
350 # According to OEIS A212693, there are exactly 238 distinct
351 # reachable positions with 3 played moves in standard Connect-4.
352 assert len(positions) == 238
356 - Number of distinct positions at ply *n*:
357 https://oeis.org/A212693
360 if not 0 <= up_to_n_ply <= 42:
361 raise ValueError(f
"up_to_n_ply must be between 0 and 42 (inclusive), got {up_to_n_ply}.")
364 core_positions = self.
_board.allPositions(up_to_n_ply, exactly_n)
367 positions: list[Board] = []
368 for core_board
in core_positions:
370 b._board = core_board
376 """Checks if the current player can win in the next move.
379 move (int | None): Optional column to check for an immediate win. If None, checks all columns.
382 bool: True if the current player can win next, False otherwise.
384 See also: [`bitbully.Board.has_win`][src.bitbully.Board.has_win].
388 import bitbully as bb
390 # Create a board from a move string.
391 # The string "332311" represents a short sequence of alternating moves
392 # that results in a nearly winning position for Player 1 (yellow, X).
393 board = bb.Board("332311")
395 # Display the current board state (see below)
398 # Player 1 (yellow, X) — who is next to move — can win immediately
399 # by placing a token in either column 0 or column 4.
400 assert board.can_win_next(0)
401 assert board.can_win_next(4)
403 # However, playing in other columns does not result in an instant win.
404 assert not board.can_win_next(2)
405 assert not board.can_win_next(3)
407 # You can also call `can_win_next()` without arguments to perform a general check.
408 # It returns True if the current player has *any* winning move available.
409 assert board.can_win_next()
411 The board we created above looks like this:
422 return self.
_board.canWin()
423 return bool(self.
_board.canWin(move))
426 """Creates a copy of the current Board instance.
428 The `copy()` method returns a new `Board` object that represents the
429 *same position* as the original at the time of copying. Subsequent
430 changes to one board do **not** affect the other — they are completely
434 Board: A new Board instance that is a copy of the current one.
437 Create a board, copy it, and verify that both represent the same position:
439 import bitbully as bb
441 # Create a board from a compact move string.
442 board = bb.Board("33333111")
444 # Create an independent copy of the current position.
445 board_copy = board.copy()
447 # Both boards represent the same position and are considered equal.
448 assert board == board_copy
449 assert hash(board) == hash(board_copy)
450 assert board.to_string() == board_copy.to_string()
452 # Display the board state.
455 Expected output (both boards print the same position):
466 Modifying the copy does not affect the original:
468 import bitbully as bb
470 board = bb.Board("33333111")
472 # Create a copy of the current position.
473 board_copy = board.copy()
475 # Play an additional move on the copied board only.
476 assert board_copy.play(0) # Drop a token into the leftmost column.
478 # Now the boards represent different positions.
479 assert board != board_copy
481 # The original board remains unchanged.
485 print("Modified copy:")
514 """Counts the total number of tokens currently placed on the board.
516 This method simply returns how many moves have been played so far in the
517 current position — that is, the number of occupied cells on the 7x6 grid.
519 It does **not** distinguish between players; it only reports the total
520 number of tokens, regardless of whether they belong to Player 1 or Player 2.
523 int: The total number of tokens on the board (between 0 and 42).
526 Count tokens on an empty board:
528 import bitbully as bb
530 board = bb.Board() # No moves played yet.
531 assert board.count_tokens() == 0
533 # The board is completely empty.
547 Count tokens after a few moves:
549 import bitbully as bb
551 # Play three moves in the center column (index 3).
553 assert board.play([3, 3, 3])
555 # Three tokens have been placed on the board.
556 assert board.count_tokens() == 3
571 Relation to the length of a move sequence:
573 import bitbully as bb
575 moves = "33333111" # 8 moves in total
576 board = bb.Board(moves)
578 # The number of tokens on the board always matches
579 # the number of moves that have been played.
580 # (as long as the input was valid)
581 assert board.count_tokens() == len(moves)
584 return self.
_board.countTokens()
587 """Checks if the current player has a winning position.
590 bool: True if the current player has a winning position (4-in-a-row), False otherwise.
592 Unlike `can_win_next()`, which checks whether the current player *could* win
593 on their next move, the `has_win()` method determines whether a winning
594 condition already exists on the board.
595 This method is typically used right after a move to verify whether the game
598 See also: [`bitbully.Board.can_win_next`][src.bitbully.Board.can_win_next].
602 import bitbully as bb
604 # Initialize a board from a move sequence.
605 # The string "332311" represents a position where Player 1 (yellow, X)
606 # is one move away from winning.
607 board = bb.Board("332311")
609 # At this stage, Player 1 has not yet won, but can win immediately
610 # by placing a token in either column 0 or column 4.
611 assert not board.has_win()
612 assert board.can_win_next(0) # Check column 0
613 assert board.can_win_next(4) # Check column 4
614 assert board.can_win_next() # General check (any winning move)
616 # Simulate Player 1 playing in column 4 — this completes
617 # a horizontal line of four tokens and wins the game.
620 # Display the updated board to visualize the winning position.
623 # The board now contains a winning configuration:
624 # Player 1 (yellow, X) has achieved a Connect-4.
625 assert board.has_win()
627 Board from above, expected output:
637 return self.
_board.hasWin()
640 """Returns a hash of the Board instance for use in hash-based collections.
643 int: The hash value of the Board instance.
647 import bitbully as bb
649 # Create two boards that represent the same final position.
650 # The first board is initialized directly from a move string.
651 board1 = bb.Board("33333111")
653 # The second board is built incrementally by playing an equivalent sequence of moves.
654 # Even though the order of intermediate plays differs, the final layout of tokens
655 # (and thus the internal bitboard state) will be identical to `board1`.
657 board2.play("31133331")
659 # Boards with identical configurations produce the same hash value.
660 # This allows them to be used efficiently as keys in dictionaries or members of sets.
661 assert hash(board1) == hash(board2)
663 # Display the board's hash value.
674 """Checks if a move (column) is legal in the current position.
676 A move is considered *legal* if:
678 - The column index is within the valid range (0-6), **and**
679 - The column is **not full** (i.e. it still has at least one empty cell).
681 This method does **not** check for tactical consequences such as
682 leaving an immediate win to the opponent, nor does it stop being
683 usable once a player has already won. It purely validates whether a
684 token can be dropped into the given column according to the basic
685 rules of Connect Four. You have to check for wins separately using
686 [`bitbully.Board.has_win`][src.bitbully.Board.has_win].
690 move (int): The column index (0-6) to check.
693 bool: True if the move is legal, False otherwise.
696 All moves are legal on an empty board:
698 import bitbully as bb
700 board = bb.Board() # Empty 7x6 board
702 # Every column index from 0 to 6 is a valid move.
704 assert board.is_legal_move(col)
706 # Out-of-range indices are always illegal.
707 assert not board.is_legal_move(-1)
708 assert not board.is_legal_move(7)
712 Detecting an illegal move in a full column:
714 import bitbully as bb
716 # Fill the center column (index 3) with six tokens.
718 assert board.play([3, 3, 3, 3, 3, 3])
720 # The center column is now full, so another move in column 3 is illegal.
721 assert not board.is_legal_move(3)
723 # Other columns are still available (as long as they are not full).
724 assert board.is_legal_move(0)
725 assert board.is_legal_move(6)
740 This function only checks legality, not for situations where a player has won:
742 import bitbully as bb
744 # Player 1 (yellow, X) wins the game.
746 assert board.play("1122334")
748 # Even though Player 1 has already won, moves in non-full columns are still legal.
750 assert board.is_legal_move(col)
764 return self.
_board.isLegalMove(move)
767 """Returns a new Board instance that is the mirror image of the current board.
769 This method reflects the board **horizontally** around its vertical center column:
770 - Column 0 <-> Column 6
771 - Column 1 <-> Column 5
772 - Column 2 <-> Column 4
773 - Column 3 stays in the center
775 The player to move is not changed - only the spatial
776 arrangement of the tokens is mirrored. The original board remains unchanged;
777 `mirror()` always returns a **new** `Board` instance.
780 Board: A new Board instance that is the mirror image of the current one.
783 Mirroring a simple asymmetric position:
785 import bitbully as bb
787 # Play four moves along the bottom row.
789 assert board.play("0123") # Columns: 0, 1, 2, 3
791 # Create a mirrored copy of the board.
792 mirrored = board.mirror()
823 Mirroring a position that is already symmetric:
825 import bitbully as bb
827 # Central symmetry: one token in each outer column and in the center.
828 board = bb.Board([1, 3, 5])
830 mirrored = board.mirror()
832 # The mirrored position is identical to the original.
833 assert board == mirrored
834 assert hash(board) == hash(mirrored)
853 """Returns the number of moves left until the board is full.
855 This is simply the number of *empty* cells remaining on the 7x6 grid.
856 On an empty board there are 42 free cells, so:
858 - At the start of the game: `moves_left() == 42`
859 - After `n` valid moves: `moves_left() == 42 - n`
860 - On a completely full board: `moves_left() == 0`
862 This method is equivalent to:
864 42 - board.count_tokens()
866 but implemented efficiently in the underlying C++ core.
869 int: The number of moves left (0-42).
872 Moves left on an empty board:
874 import bitbully as bb
876 board = bb.Board() # No tokens placed yet.
877 assert board.moves_left() == 42
878 assert board.count_tokens() == 0
882 Relation to the number of moves played:
884 import bitbully as bb
886 # Play five moves in various columns.
887 moves = [3, 3, 1, 4, 6]
889 assert board.play(moves)
891 # Five tokens have been placed, so 42 - 5 = 37 moves remain.
892 assert board.count_tokens() == 5
893 assert board.moves_left() == 37
894 assert board.moves_left() + board.count_tokens() == 42
897 return self.
_board.movesLeft()
899 def play(self, move: int | Sequence[int] | str) -> bool:
900 """Plays one or more moves for the current player.
902 The method updates the internal board state by dropping tokens
903 into the specified columns. Input can be:
904 - a single integer (column index 0 to 6),
905 - an iterable sequence of integers (e.g., `[3, 1, 3]` or `range(7)`),
906 - or a string of digits (e.g., `"33333111"`) representing the move order.
909 move (int | Sequence[int] | str):
910 The column index or sequence of column indices where tokens should be placed.
913 bool: True if the move was played successfully, False if the move was illegal.
917 Play a sequence of moves into the center column (column index 3):
919 import bitbully as bb
922 assert board.play([3, 3, 3]) # returns True on successful move
938 Play a sequence of moves across all columns:
940 import bitbully as bb
943 assert board.play(range(7)) # returns True on successful move
957 Play a sequence using a string:
959 import bitbully as bb
962 assert board.play("33333111") # returns True on successful move
976 if isinstance(move, str):
980 if isinstance(move, int):
984 move_list: list[int] = [int(v)
for v
in cast(Sequence[Any], move)]
988 """Return a new board with the given move applied, leaving the current board unchanged.
992 The column index (0-6) in which to play the move.
996 A new Board instance representing the position after the move.
999 ValueError: If the move is illegal (e.g. column is full or out of range).
1003 import bitbully as bb
1005 board = bb.Board("333") # Some existing position
1006 new_board = board.play_on_copy(4)
1008 # The original board is unchanged.
1009 assert board.count_tokens() == 3
1011 # The returned board includes the new move.
1012 assert new_board.count_tokens() == 4
1013 assert new_board != board
1017 core_new = self.
_board.playMoveOnCopy(move)
1019 if core_new
is None:
1021 raise ValueError(f
"Illegal move: column {move}")
1025 new_board._board = core_new
1028 def reset_board(self, board: Sequence[int] | Sequence[Sequence[int]] | str |
None =
None) -> bool:
1029 """Resets the board or sets (overrides) the board to a specific state.
1032 board (Sequence[int] | Sequence[Sequence[int]] | str | None):
1033 The new board state. Accepts:
1034 - 2D array (list, tuple, numpy-array) with shape 7x6 or 6x7
1035 - 1D sequence of ints: a move sequence of columns (e.g., [0, 0, 2, 2, 3, 3])
1036 - String: A move sequence of columns as string (e.g., "002233...")
1037 - None: to reset to an empty board
1040 bool: True if the board was set successfully, False otherwise.
1043 Reset the board to an empty state:
1045 import bitbully as bb
1047 # Create a temporary board position from a move string.
1048 # The string "0123456" plays one token in each column (0-6) in sequence.
1049 board = bb.Board("0123456")
1051 # Reset the board to an empty state.
1052 # Calling `reset_board()` clears all tokens and restores the starting position.
1053 # No moves → an empty board.
1054 assert board.reset_board()
1068 (Re-)Set the board using a move sequence string:
1070 import bitbully as bb
1072 # This is just a temporary setup; it will be replaced below.
1073 board = bb.Board("0123456")
1075 # Set the board state directly from a move sequence.
1076 # The list [3, 3, 3] represents three consecutive moves in the center column (index 3).
1077 # Moves alternate automatically between Player 1 (yellow) and Player 2 (red).
1079 # The `reset_board()` method clears the current position and replays the given moves
1080 # from an empty board — effectively overriding any existing board state.
1081 assert board.reset_board([3, 3, 3])
1083 # Display the updated board to verify the new position.
1097 You can also set the board using other formats, such as a 2D array or a string.
1098 See the examples in the [`bitbully.Board.__init__`][src.bitbully.Board.__init__] docstring for details.
1101 # Briefly demonstrate the different input formats accepted by `reset_board()`.
1102 import bitbully as bb
1104 # Create an empty board instance
1107 # Variant 1: From a list of moves (integers)
1108 # Each number represents a column index (0-6); moves alternate between players.
1109 assert board.reset_board([3, 3, 3])
1111 # Variant 2: From a compact move string
1112 # Equivalent to the list above — useful for quick testing or serialization.
1113 assert board.reset_board("33333111")
1115 # Variant 3: From a 2D list in row-major format (6 x 7)
1116 # Each inner list represents a row (top to bottom).
1117 # 0 = empty, 1 = Player 1, 2 = Player 2.
1119 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
1120 [0, 0, 0, 1, 0, 0, 0], # Row 4
1121 [0, 0, 0, 2, 0, 0, 0], # Row 3
1122 [0, 2, 0, 1, 0, 0, 0], # Row 2
1123 [0, 1, 0, 2, 0, 0, 0], # Row 1
1124 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom)
1126 assert board.reset_board(board_array)
1128 # Variant 4: From a 2D list in column-major format (7 x 6)
1129 # Each inner list represents a column (left to right); this matches BitBully's internal layout.
1131 [0, 0, 0, 0, 0, 0], # Column 0 (leftmost)
1132 [2, 1, 2, 1, 0, 0], # Column 1
1133 [0, 0, 0, 0, 0, 0], # Column 2
1134 [1, 2, 1, 2, 1, 0], # Column 3 (center)
1135 [0, 0, 0, 0, 0, 0], # Column 4
1136 [2, 1, 2, 0, 0, 0], # Column 5
1137 [0, 0, 0, 0, 0, 0], # Column 6 (rightmost)
1139 assert board.reset_board(board_array)
1141 # Display the final board state in text form
1156 return self.
_board.setBoard([])
1157 if isinstance(board, str):
1158 return self.
_board.setBoard(board)
1162 if len(board) > 0
and isinstance(board[0], Sequence)
and not isinstance(board[0], (str, bytes)):
1165 grid: list[list[int]] = [[int(v)
for v
in row]
for row
in cast(Sequence[Sequence[Any]], board)]
1166 return self.
_board.setBoard(grid)
1169 moves: list[int] = [int(v)
for v
in cast(Sequence[Any], board)]
1170 return self.
_board.setBoard(moves)
1172 def to_array(self, column_major_layout: bool =
True) -> list[list[int]]:
1173 """Returns the board state as a 2D array (list of lists).
1175 This layout is convenient for printing, serialization, or converting
1176 to a NumPy array for further analysis.
1179 column_major_layout (bool): Use column-major format if set to `True`,
1180 otherwise the row-major-layout is used.
1183 list[list[int]]: A 7x6 2D list representing the board state.
1186 NotImplementedError: If `column_major_layout` is set to `False`.
1189 === "Column-major Format:"
1191 The returned array is in **column-major** format with shape `7 x 6`
1194 - There are 7 inner lists, one for each column of the board.
1195 - Each inner list has 6 integers, one for each row.
1196 - Row index `0` corresponds to the **bottom row**,
1197 row index `5` to the **top row**.
1200 - `1` -> Player 1 token (yellow, X)
1201 - `2` -> Player 2 token (red, O)
1204 import bitbully as bb
1205 from pprint import pprint
1207 # Create a position from a move sequence.
1208 board = bb.Board("33333111")
1210 # Extract the board as a 2D list (rows x columns).
1211 arr = board.to_array()
1213 # Reconstruct the same position from the 2D array.
1214 board2 = bb.Board(arr)
1216 # Both boards represent the same position.
1217 assert board == board2
1218 assert board.to_array() == board2.to_array()
1220 # print ther result of `board.to_array()`:
1221 pprint(board.to_array())
1225 [[0, 0, 0, 0, 0, 0],
1234 === "Row-major Format:"
1237 TODO: This is not supported yet
1240 if not column_major_layout:
1242 raise NotImplementedError(
"Row-major Layout is yet to be implemented")
1244 return self.
_board.toArray()
1247 """Returns a human-readable ASCII representation of the board.
1249 The returned string shows the **current board position** as a 6x7 grid,
1250 laid out exactly as it would appear when you print a `Board` instance:
1252 - 6 lines of text, one per row (top row first, bottom row last)
1253 - 7 entries per row, separated by two spaces
1254 - `_` represents an empty cell
1255 - `X` represents a token from Player 1 (yellow)
1256 - `O` represents a token from Player 2 (red)
1258 This is useful when you want to explicitly capture the board as a string
1259 (e.g., for logging, debugging, or embedding into error messages) instead
1260 of relying on `print(board)` or `repr(board)`.
1263 str: A multi-line ASCII string representing the board state.
1266 Using `to_string()` on an empty board:
1268 import bitbully as bb
1270 board = bb.Board("33333111")
1272 s = board.to_string()
1286 return self.
_board.toString()
1289 """Returns a unique identifier for the current board state.
1291 The UID is a deterministic integer computed from the internal bitboard
1292 representation of the position. It is **stable**, **position-based**, and
1293 uniquely tied to the exact token layout **and** the side to move.
1297 - Boards with the **same** configuration (tokens + player to move) always
1298 produce the **same** UID.
1299 - Any change to the board (e.g., after a legal move) will almost always
1300 result in a **different** UID.
1301 - Copies of a board created via the copy constructor or `Board.copy()`
1302 naturally share the same UID as long as their states remain identical.
1304 Unlike `__hash__()`, the UID is not optimized for hash-table dispersion.
1305 For use in transposition tables, caching, or dictionary/set keys,
1306 prefer `__hash__()` since it provides a higher-quality hash distribution.
1309 int: A unique integer identifier for the board state.
1312 UID is an integer and not None:
1314 import bitbully as bb
1319 assert isinstance(u, int)
1320 # Empty board has a well-defined, stable UID.
1321 assert board.uid() == u
1325 UID changes when the position changes:
1327 import bitbully as bb
1330 uid_before = board.uid()
1332 assert board.play(1) # Make a move in column 1.
1334 uid_after = board.uid()
1335 assert uid_after != uid_before
1339 Copies share the same UID while they are identical:
1341 import bitbully as bb
1343 board = bb.Board("0123")
1345 # Create an independent copy of the same position.
1346 board_copy = board.copy()
1348 assert board is not board_copy # Different objects
1349 assert board == board_copy # Same position
1350 assert board.uid() == board_copy.uid() # Same UID
1352 # After modifying the copy, they diverge.
1353 assert board_copy.play(4)
1354 assert board != board_copy
1355 assert board.uid() != board_copy.uid()
1359 Different move sequences leading to the same position share the same UID:
1361 import bitbully as bb
1363 board_1 = bb.Board("01234444")
1364 board_2 = bb.Board("44440123")
1366 assert board_1 is not board_2 # Different objects
1367 assert board_1 == board_2 # Same position
1368 assert board_1.uid() == board_2.uid() # Same UID
1370 # After modifying the copy, they diverge.
1371 assert board_1.play(4)
1372 assert board_1 != board_2
1373 assert board_1.uid() != board_2.uid()
1379 def current_player(self) -> int:
1380 """Returns the player whose turn it is to move.
1382 The current player is derived from the **parity** of the number of tokens
1385 - Player 1 (yellow, ``X``) moves first on an empty board.
1386 - After an even number of moves → it is Player 1's turn.
1387 - After an odd number of moves → it is Player 2's turn.
1393 - ``1`` → Player 1 (yellow, ``X``)
1394 - ``2`` → Player 2 (red, ``O``)
1398 import bitbully as bb
1400 # Empty board → Player 1 starts.
1402 assert board.current_player == 1
1403 assert board.count_tokens() == 0
1405 # After one move, it's Player 2's turn.
1406 assert board.play(3)
1407 assert board.count_tokens() == 1
1408 assert board.current_player == 2
1410 # After a second move, it's again Player 1's turn.
1411 assert board.play(4)
1412 assert board.count_tokens() == 2
1413 assert board.current_player == 1
1420 """Checks whether the board has any empty cells left.
1422 A Connect Four board has 42 cells in total (7 columns x 6 rows).
1423 This method returns ``True`` if **all** cells are occupied, i.e.
1424 when [`bitbully.Board.moves_left`][src.bitbully.Board.moves_left] returns ``0``.
1428 ``True`` if the board is completely full
1429 (no more legal moves possible), otherwise ``False``.
1433 import bitbully as bb
1436 assert not board.is_full()
1437 assert board.moves_left() == 42
1438 assert board.count_tokens() == 0
1440 # Fill the board column by column.
1442 assert board.play("0123456") # one token per column, per row
1444 # Now every cell is occupied.
1445 assert board.is_full()
1446 assert board.moves_left() == 0
1447 assert board.count_tokens() == 42
1453 """Checks whether the game has ended (win or draw).
1455 A game of Connect Four is considered **over** if:
1457 - One of the players has a winning position
1458 (see [`bitbully.Board.has_win`][src.bitbully.Board.has_win]), **or**
1459 - The board is completely full and no further moves can be played
1460 (see [`bitbully.Board.is_full`][src.bitbully.Board.is_full]).
1462 This method does **not** indicate *who* won; for that, use
1463 [`bitbully.Board.winner`][src.bitbully.Board.winner].
1467 ``True`` if the game is over (win or draw), otherwise ``False``.
1472 import bitbully as bb
1474 # Player 1 (X) wins horizontally on the bottom row.
1476 assert board.play("0101010")
1478 assert board.has_win()
1479 assert board.is_game_over()
1480 assert board.winner() == 1
1484 Game over by a draw (full board, no winner):
1486 import bitbully as bb
1488 board, _ = bb.Board.random_board(42, forbid_direct_win=False)
1490 assert board.is_full()
1491 assert not board.has_win()
1492 assert board.is_game_over()
1493 assert board.winner() is None
1499 """Returns the winning player, if the game has been won.
1501 This helper interprets the current board under the assumption that
1502 [`bitbully.Board.has_win`][src.bitbully.Board.has_win] indicates **the last move** created a
1503 winning configuration. In that case, the winner is the *previous* player:
1505 - If it is currently Player 1's turn, then Player 2 must have just won.
1506 - If it is currently Player 2's turn, then Player 1 must have just won.
1508 If there is no winner (i.e. [`bitbully.Board.has_win`][src.bitbully.Board.has_win] is ``False``),
1509 this method returns ``None``.
1513 The winning player, or ``None`` if there is no winner.
1515 - ``1`` → Player 1 (yellow, ``X``)
1516 - ``2`` → Player 2 (red, ``O``)
1517 - ``None`` → No winner (game still ongoing or draw)
1522 import bitbully as bb
1524 # Player 1 wins with a horizontal line at the bottom.
1526 assert board.play("1122334")
1528 assert board.has_win()
1529 assert board.is_game_over()
1531 # It is now Player 2's turn to move next...
1532 assert board.current_player == 2
1534 # ...which implies Player 1 must be the winner.
1535 assert board.winner() == 1
1541 import bitbully as bb
1544 assert board.play("112233") # no connect-four yet
1546 assert not board.has_win()
1547 assert not board.is_game_over()
1548 assert board.winner() is None
1558 """Creates a board by replaying a sequence of moves from the empty position.
1560 This is a convenience constructor around [`bitbully.Board.play`][src.bitbully.Board.play].
1561 It starts from an empty board and applies the given move sequence, assuming
1562 it is **legal** (no out-of-range columns, no moves in full columns, etc.).
1565 moves (Sequence[int] | str):
1566 The move sequence to replay from the starting position. Accepts:
1568 - A sequence of integers (e.g. ``[3, 3, 3, 1]``)
1569 - A string of digits (e.g. ``"3331"``)
1571 Each value represents a column index (0-6). Players alternate
1572 automatically between moves.
1576 A new `Board` instance representing the final position
1577 after all moves have been applied.
1581 import bitbully as bb
1583 # Create a position directly from a compact move string.
1584 board = bb.Board.from_moves("33333111")
1587 # board = bb.Board()
1588 # assert board.play("33333111")
1591 assert board.count_tokens() == 8
1592 assert not board.has_win()
1596 assert board.play(moves)
1601 """Creates a board directly from a 2D array representation.
1603 This is a convenience wrapper around the main constructor
1604 [`bitbully.Board.__init__`][src.bitbully.Board.__init__]
1605 and accepts the same array formats:
1607 - **Row-major**: 6 x 7 (``[row][column]``), top row first.
1608 - **Column-major**: 7 x 6 (``[column][row]``), left column first.
1610 Values must follow the usual convention:
1612 - ``0`` → empty cell
1613 - ``1`` → Player 1 token (yellow, ``X``)
1614 - ``2`` → Player 2 token (red, ``O``)
1617 arr (Sequence[Sequence[int]]):
1618 A 2D array describing the board state, either in row-major or
1619 column-major layout. See the examples in
1620 [`bitbully.Board.__init__`][src.bitbully.Board.__init__] for details.
1624 A new `Board` instance representing the given layout.
1627 Using a 6 x 7 row-major layout:
1629 import bitbully as bb
1632 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
1633 [0, 0, 0, 1, 0, 0, 0], # Row 4
1634 [0, 0, 0, 2, 0, 0, 0], # Row 3
1635 [0, 2, 0, 1, 0, 0, 0], # Row 2
1636 [0, 1, 0, 2, 0, 0, 0], # Row 1
1637 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom)
1640 board = bb.Board.from_array(board_array)
1645 Using a 7 x 6 column-major layout:
1647 import bitbully as bb
1650 [0, 0, 0, 0, 0, 0], # Column 0
1651 [2, 1, 2, 1, 0, 0], # Column 1
1652 [0, 0, 0, 0, 0, 0], # Column 2
1653 [1, 2, 1, 2, 1, 0], # Column 3
1654 [0, 0, 0, 0, 0, 0], # Column 4
1655 [2, 1, 2, 0, 0, 0], # Column 5
1656 [0, 0, 0, 0, 0, 0], # Column 6
1659 board = bb.Board.from_array(board_array)
1661 # Round-trip via to_array:
1662 assert board.to_array() == board_array
1668 def random_board(n_ply: int, forbid_direct_win: bool) -> tuple[Board, list[int]]:
1669 """Generates a random board state by playing a specified number of random moves.
1671 If ``forbid_direct_win`` is ``True``, the generated position is guaranteed
1672 **not** to contain an immediate winning move for the player to move.
1676 Number of random moves to simulate (0-42).
1677 forbid_direct_win (bool):
1678 If ``True``, ensures the resulting board has **no immediate winning move**.
1681 tuple[Board, list[int]]:
1682 A pair ``(board, moves)`` where ``board`` is the generated position
1683 and ``moves`` are the exact random moves performed.
1686 ValueError: If `n_ply` is outside the valid range [0, 42].
1691 import bitbully as bb
1693 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1695 print("Moves:", moves)
1699 # The move list must match the requested ply.
1700 assert len(moves) == 10
1702 # No immediate winning move when forbid_direct_win=True.
1703 assert not board.can_win_next()
1707 Using random boards in tests or simulations:
1709 import bitbully as bb
1711 # Generate 50 random 10-ply positions.
1713 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1714 assert len(moves) == 10
1715 assert not board.has_win() # Game should not be over
1716 assert board.count_tokens() == 10 # All generated boards contain exactly 10 tokens
1717 assert not board.can_win_next() # Since `forbid_direct_win=True`, no immediate threat
1721 Reconstructing the board manually from the move list:
1723 import bitbully as bb
1725 b1, moves = bb.Board.random_board(8, forbid_direct_win=True)
1727 # Recreate the board using the move sequence:
1728 b2 = bb.Board(moves)
1731 assert b1.to_string() == b2.to_string()
1732 assert b1.uid() == b2.uid()
1736 Ensure randomness by generating many distinct sequences:
1738 import bitbully as bb
1742 _, moves = bb.Board.random_board(5, False)
1743 seen.add(tuple(moves))
1745 # Very likely to see more than one unique sequence.
1746 assert len(seen) > 1
1749 if not 0 <= n_ply <= 42:
1750 raise ValueError(f
"n_ply must be between 0 and 42 (inclusive), got {n_ply}.")
1751 board_, moves = bitbully_core.BoardCore.randomBoard(n_ply, forbid_direct_win)
1753 board._board = board_
1758 """Encode the current board position into a Huffman-compressed byte sequence.
1760 This is a high-level wrapper around
1761 `bitbully_core.BoardCore.toHuffman`. The returned int encodes the
1762 exact token layout **and** the side to move using the same format as
1763 the BitBully opening databases.
1767 - Deterministic: the same position always yields the same byte sequence.
1768 - Compact: suitable for storage (of positions with little number of tokens),
1769 or lookups in the BitBully database format.
1772 int: A Huffman-compressed representation of the current board
1776 NotImplementedError:
1777 If the position does not contain exactly 8 or 12 tokens, as the
1778 Huffman encoding is only defined for these cases.
1781 Encode a position and verify that equivalent positions have the
1785 import bitbully as bb
1787 # Two different move sequences leading to the same final position.
1788 b1 = bb.Board("01234444")
1789 b2 = bb.Board("44440123")
1791 h1 = b1.to_huffman()
1792 h2 = b2.to_huffman()
1794 # Huffman encoding is purely position-based.
1797 print(f"Huffman code: {h1}")
1801 Huffman code: 10120112
1805 if token_count != 8
and token_count != 12:
1806 raise NotImplementedError(
"to_huffman() is only implemented for positions with 8 or 12 tokens.")
1807 return self.
_board.toHuffman()
1809 def legal_moves(self, non_losing: bool =
False, order_moves: bool =
False) -> list[int]:
1810 """Returns a list of all legal moves (non-full columns) for the current board state.
1814 If ``True``, only returns moves that do **not** allow the opponent
1815 to win immediately on their next turn. The list might be empty
1816 If ``False``, all legal moves are returned.
1818 If ``True``, the returned list is ordered to prioritize moves (potentially more promising first).
1821 list[int]: A list of column indices (0-6) where a token can be legally dropped.
1825 import bitbully as bb
1828 legal_moves = board.legal_moves()
1829 assert set(legal_moves) == set(range(7)) # All columns are initially legal
1830 assert set(legal_moves) == set(board.legal_moves(order_moves=True))
1831 board.legal_moves(order_moves=True) == [3, 2, 4, 1, 5, 0, 6] # Center column prioritized
1834 return self.
_board.legalMoves(nonLosing=non_losing, orderMoves=order_moves)
Board from_array(cls, Sequence[Sequence[int]] arr)
Board play_on_copy(self, int move)
bool __eq__(self, object value)
None __init__(self, Sequence[Sequence[int]]|Sequence[int]|str|None init_with=None)
bool reset_board(self, Sequence[int]|Sequence[Sequence[int]]|str|None board=None)
list[Board] all_positions(self, int up_to_n_ply, bool exactly_n)
list[list[int]] to_array(self, bool column_major_layout=True)
bool can_win_next(self, int|None move=None)
Board from_moves(cls, Sequence[int]|str moves)
bool play(self, int|Sequence[int]|str move)
tuple[Board, list[int]] random_board(int n_ply, bool forbid_direct_win)
bool is_legal_move(self, int move)
bool __ne__(self, object value)
list[int] legal_moves(self, bool non_losing=False, bool order_moves=False)