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, ClassVar, cast
8from .
import bitbully_core
9from .bitbully_core
import BoardCore
13 """Represents the state of a Connect Four board. Mostly a thin wrapper around BoardCore."""
16 N_COLUMNS: ClassVar[int] = bitbully_core.N_COLUMNS
17 N_ROWS: ClassVar[int] = bitbully_core.N_ROWS
19 Player = bitbully_core.Player
21 def __init__(self, init_with: Sequence[Sequence[int]] | Sequence[int] | str |
None =
None) ->
None:
22 """Initializes a Board instance.
25 init_with (Sequence[Sequence[int]] | Sequence[int] | str | None):
26 Optional initial board state. Accepts:
27 - 2D array (list, tuple, numpy-array) with shape 7x6 or 6x7
28 - 1D sequence of ints: a move sequence of columns (e.g., [0, 0, 2, 2, 3, 3])
29 - String: A move sequence of columns as string (e.g., "002233")
30 - None for an empty board
33 ValueError: If the provided initial board state is invalid.
36 You can initialize an empty board in multiple ways:
40 # Create an empty board using the default constructor.
41 board = bb.Board() # Starts with no tokens placed.
43 # Alternatively, initialize the board explicitly from a 2D list.
44 # Each inner list represents a column (7 columns total, 6 rows each).
45 # A value of 0 indicates an empty cell; 1 and 2 would represent player tokens.
46 board = bb.Board([[0] * 6 for _ in range(7)]) # Equivalent to an empty board.
48 # You can also set up a specific board position manually using a 6 x 7 layout,
49 # where each inner list represents a row instead of a column.
50 # (Both layouts are accepted by BitBully for convenience.)
51 # For more complex examples using 2D arrays, see the examples below.
52 board = bb.Board([[0] * 7 for _ in range(6)]) # Also equivalent to an empty board.
54 # Display the board in text form.
55 # The __repr__ method shows the current state (useful for debugging or interactive use).
68 The recommended way to initialize an empty board is simply `Board()`.
71 You can also initialize a board with a sequence of moves:
75 # Initialize a board with a sequence of moves played in the center column.
77 # The list [3, 3, 3] represents three moves in column index 3 (zero-based).
78 # Moves alternate automatically between Player 1 (yellow, X) and Player 2 (red, O).
79 # After these three moves, the center column will contain:
80 # - Row 0: Player 1 token (bottom)
81 # - Row 1: Player 2 token
82 # - Row 2: Player 1 token
83 board = bb.Board([3, 3, 3])
85 # Display the resulting board.
86 # The textual output shows the tokens placed in the center column.
101 You can also initialize a board using a string containing a move sequence:
103 import bitbully as bb
105 # Initialize a board using a compact move string.
107 # The string "33333111" represents a sequence of eight moves:
108 # 3 3 3 3 3 → five moves in the center column (index 3)
109 # 1 1 1 → three moves in the second column (index 1)
111 # Moves are applied in order, alternating automatically between Player 1 (yellow, X)
112 # and Player 2 (red, O), just as if you had called `board.play()` repeatedly.
114 # This shorthand is convenient for reproducing board states or test positions
115 # without having to provide long move lists.
117 board = bb.Board("33333111")
119 # Display the resulting board.
120 # The printed layout shows how the tokens stack in each column.
135 You can also initialize a board using a 2D array (list of lists):
137 import bitbully as bb
139 # Use a 6 x 7 list (rows x columns) to set up a specific board position manually.
141 # Each inner list represents a row of the Connect-4 grid.
144 # - 1 → Player 1 token (yellow, X)
145 # - 2 → Player 2 token (red, O)
147 # The top list corresponds to the *top row* (row index 5),
148 # and the bottom list corresponds to the *bottom row* (row index 0).
149 # This layout matches the typical visual display of the board.
152 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
153 [0, 0, 0, 1, 0, 0, 0], # Row 4: Player 1 token in column 3
154 [0, 0, 0, 2, 0, 0, 0], # Row 3: Player 2 token in column 3
155 [0, 2, 0, 1, 0, 0, 0], # Row 2: tokens in columns 1 and 3
156 [0, 1, 0, 2, 0, 0, 0], # Row 1: tokens in columns 1 and 3
157 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom): tokens stacked lowest
160 # Create a Board instance directly from the 2D list.
161 # This allows reconstructing arbitrary positions (e.g., from test data or saved states)
162 # without replaying the move sequence.
163 board = bb.Board(board_array)
165 # Display the resulting board state in text form.
179 You can also initialize a board using a 2D (7 x 6) array with columns as inner lists:
181 import bitbully as bb
183 # Use a 7 x 6 list (columns x rows) to set up a specific board position manually.
185 # Each inner list represents a **column** of the Connect-4 board, from left (index 0)
186 # to right (index 6). Each column contains six entries — one for each row, from
187 # bottom (index 0) to top (index 5).
191 # - 1 → Player 1 token (yellow, X)
192 # - 2 → Player 2 token (red, O)
194 # This column-major layout matches the internal representation used by BitBully,
195 # where tokens are dropped into columns rather than filled row by row.
198 [0, 0, 0, 0, 0, 0], # Column 0 (leftmost)
199 [2, 1, 2, 0, 0, 0], # Column 1
200 [0, 0, 0, 0, 0, 0], # Column 2
201 [1, 2, 1, 2, 1, 0], # Column 3 (center)
202 [0, 0, 0, 0, 0, 0], # Column 4
203 [0, 0, 0, 0, 0, 0], # Column 5
204 [0, 0, 0, 0, 0, 0], # Column 6 (rightmost)
207 # Create a Board instance directly from the 2D list.
208 # This allows reconstructing any arbitrary position (e.g., test cases, saved games)
209 # without replaying all moves individually.
210 board = bb.Board(board_array)
212 # Display the resulting board.
213 # The text output shows tokens as they would appear in a real Connect-4 grid.
227 if init_with
is not None and not self.
reset_board(init_with):
229 "Invalid initial board state provided. Check the examples in the docstring for valid formats."
233 """Checks equality between two Board instances.
236 - Equality checks in BitBully compare the *exact board state* (bit patterns),
237 not just the move history.
238 - Two different move sequences can still yield the same position if they
239 result in identical token configurations.
240 - This is useful for comparing solver states, verifying test positions,
241 or detecting transpositions in search algorithms.
244 value (object): The other Board instance to compare against.
247 bool: True if both boards are equal, False otherwise.
250 NotImplementedError: If the other value is not a Board instance.
254 import bitbully as bb
256 # Create two boards that should represent *identical* game states.
258 assert board1.play("33333111")
261 # Play the same position step by step using a different but equivalent sequence.
262 # Internally, the final bitboard state will match `board1`.
263 assert board2.play("31133331")
265 # Boards with identical token placements are considered equal.
266 # Equality (`==`) and inequality (`!=`) operators are overloaded for convenience.
267 assert board1 == board2
268 assert not (board1 != board2)
270 # ------------------------------------------------------------------------------
272 # Create two boards that differ by one move.
273 board1 = bb.Board("33333111")
274 board2 = bb.Board("33333112") # One extra move in the last column (index 2)
276 # Since the token layout differs, equality no longer holds.
277 assert board1 != board2
278 assert not (board1 == board2)
281 if not isinstance(value, Board):
282 raise NotImplementedError(
"Can only compare with another Board instance.")
283 return bool(self.
_board == value._board)
286 """Checks inequality between two Board instances.
288 See the documentation for [Board.__eq__][src.bitbully.board.Board.__eq__] for details.
291 value (object): The other Board instance to compare against.
294 bool: True if both boards are not equal, False otherwise.
296 return not self.
__eq__(value)
299 """Returns a string representation of the Board instance."""
300 return f
"{self._board}"
303 """Return a human-readable ASCII representation (same as to_string()).
305 See the documentation for [Board.to_string][src.bitbully.board.Board.to_string] for details.
310 """Find all positions reachable from the current position up to a given ply.
312 This is a high-level wrapper around
313 `bitbully_core.BoardCore.allPositions`.
315 Starting from the **current** board, it generates all positions that can be
316 reached by playing additional moves such that the resulting position has:
318 - At most ``up_to_n_ply`` tokens on the board, if ``exactly_n`` is ``False``.
319 - Exactly ``up_to_n_ply`` tokens on the board, if ``exactly_n`` is ``True``.
322 The number of tokens already present in the current position is taken
323 into account. If ``up_to_n_ply`` is smaller than
324 ``self.count_tokens()``, the result is typically empty.
326 This function can grow combinatorially with ``up_to_n_ply`` and the
327 current position, so use it with care for large depths.
331 The maximum total number of tokens (ply) for generated positions.
332 Must be between 0 and 42 (inclusive).
334 If ``True``, only positions with exactly ``up_to_n_ply`` tokens
335 are returned. If ``False``, all positions with a token count
336 between the current number of tokens and ``up_to_n_ply`` are
340 list[Board]: A list of :class:`Board` instances representing all
341 reachable positions that satisfy the ply constraint.
344 ValueError: If ``up_to_n_ply`` is outside the range ``[0, 42]``.
347 Compute all positions at exactly 3 ply from the empty board:
350 import bitbully as bb
352 # Start from an empty board.
355 # Generate all positions that contain exactly 3 tokens.
356 positions = board.all_positions(3, exactly_n=True)
358 # According to OEIS A212693, there are exactly 238 distinct
359 # reachable positions with 3 played moves in standard Connect-4.
360 assert len(positions) == 238
364 - Number of distinct positions at ply *n*:
365 https://oeis.org/A212693
368 if not 0 <= up_to_n_ply <= 42:
369 raise ValueError(f
"up_to_n_ply must be between 0 and 42 (inclusive), got {up_to_n_ply}.")
372 core_positions = self.
_board.allPositions(up_to_n_ply, exactly_n)
375 positions: list[Board] = []
376 for core_board
in core_positions:
378 b._board = core_board
384 """Checks if the current player can win in the next move.
387 move (int | None): Optional column to check for an immediate win. If None, checks all columns.
390 bool: True if the current player can win next, False otherwise.
392 See also: [`Board.has_win`][src.bitbully.board.Board.has_win].
396 import bitbully as bb
398 # Create a board from a move string.
399 # The string "332311" represents a short sequence of alternating moves
400 # that results in a nearly winning position for Player 1 (yellow, X).
401 board = bb.Board("332311")
403 # Display the current board state (see below)
406 # Player 1 (yellow, X) — who is next to move — can win immediately
407 # by placing a token in either column 0 or column 4.
408 assert board.can_win_next(0)
409 assert board.can_win_next(4)
411 # However, playing in other columns does not result in an instant win.
412 assert not board.can_win_next(2)
413 assert not board.can_win_next(3)
415 # You can also call `can_win_next()` without arguments to perform a general check.
416 # It returns True if the current player has *any* winning move available.
417 assert board.can_win_next()
419 The board we created above looks like this:
430 return self.
_board.canWin()
431 return bool(self.
_board.canWin(move))
434 """Creates a copy of the current Board instance.
436 The `copy()` method returns a new `Board` object that represents the
437 *same position* as the original at the time of copying. Subsequent
438 changes to one board do **not** affect the other — they are completely
442 Board: A new Board instance that is a copy of the current one.
445 Create a board, copy it, and verify that both represent the same position:
447 import bitbully as bb
449 # Create a board from a compact move string.
450 board = bb.Board("33333111")
452 # Create an independent copy of the current position.
453 board_copy = board.copy()
455 # Both boards represent the same position and are considered equal.
456 assert board == board_copy
457 assert hash(board) == hash(board_copy)
458 assert board.to_string() == board_copy.to_string()
460 # Display the board state.
463 Expected output (both boards print the same position):
474 Modifying the copy does not affect the original:
476 import bitbully as bb
478 board = bb.Board("33333111")
480 # Create a copy of the current position.
481 board_copy = board.copy()
483 # Play an additional move on the copied board only.
484 assert board_copy.play(0) # Drop a token into the leftmost column.
486 # Now the boards represent different positions.
487 assert board != board_copy
489 # The original board remains unchanged.
493 print("Modified copy:")
522 """Counts the total number of tokens currently placed on the board.
524 This method simply returns how many moves have been played so far in the
525 current position — that is, the number of occupied cells on the 7x6 grid.
527 It does **not** distinguish between players; it only reports the total
528 number of tokens, regardless of whether they belong to Player 1 or Player 2.
531 int: The total number of tokens on the board (between 0 and 42).
534 Count tokens on an empty board:
536 import bitbully as bb
538 board = bb.Board() # No moves played yet.
539 assert board.count_tokens() == 0
541 # The board is completely empty.
555 Count tokens after a few moves:
557 import bitbully as bb
559 # Play three moves in the center column (index 3).
561 assert board.play([3, 3, 3])
563 # Three tokens have been placed on the board.
564 assert board.count_tokens() == 3
579 Relation to the length of a move sequence:
581 import bitbully as bb
583 moves = "33333111" # 8 moves in total
584 board = bb.Board(moves)
586 # The number of tokens on the board always matches
587 # the number of moves that have been played.
588 # (as long as the input was valid)
589 assert board.count_tokens() == len(moves)
592 return self.
_board.countTokens()
595 """Checks if the current player has a winning position.
598 bool: True if the current player has a winning position (4-in-a-row), False otherwise.
600 Unlike `can_win_next()`, which checks whether the current player *could* win
601 on their next move, the `has_win()` method determines whether a winning
602 condition already exists on the board.
603 This method is typically used right after a move to verify whether the game
606 See also: [`Board.can_win_next`][src.bitbully.board.Board.can_win_next].
610 import bitbully as bb
612 # Initialize a board from a move sequence.
613 # The string "332311" represents a position where Player 1 (yellow, X)
614 # is one move away from winning.
615 board = bb.Board("332311")
617 # At this stage, Player 1 has not yet won, but can win immediately
618 # by placing a token in either column 0 or column 4.
619 assert not board.has_win()
620 assert board.can_win_next(0) # Check column 0
621 assert board.can_win_next(4) # Check column 4
622 assert board.can_win_next() # General check (any winning move)
624 # Simulate Player 1 playing in column 4 — this completes
625 # a horizontal line of four tokens and wins the game.
628 # Display the updated board to visualize the winning position.
631 # The board now contains a winning configuration:
632 # Player 1 (yellow, X) has achieved a Connect-4.
633 assert board.has_win()
645 return self.
_board.hasWin()
648 """Returns a hash of the Board instance for use in hash-based collections.
651 int: The hash value of the Board instance.
655 import bitbully as bb
657 # Create two boards that represent the same final position.
658 # The first board is initialized directly from a move string.
659 board1 = bb.Board("33333111")
661 # The second board is built incrementally by playing an equivalent sequence of moves.
662 # Even though the order of intermediate plays differs, the final layout of tokens
663 # (and thus the internal bitboard state) will be identical to `board1`.
665 board2.play("31133331")
667 # Boards with identical configurations produce the same hash value.
668 # This allows them to be used efficiently as keys in dictionaries or members of sets.
669 assert hash(board1) == hash(board2)
671 # Display the board's hash value.
682 """Checks if a move (column) is legal in the current position.
684 A move is considered *legal* if:
686 - The column index is within the valid range (0-6), **and**
687 - The column is **not full** (i.e. it still has at least one empty cell).
689 This method does **not** check for tactical consequences such as
690 leaving an immediate win to the opponent, nor does it stop being
691 usable once a player has already won. It purely validates whether a
692 token can be dropped into the given column according to the basic
693 rules of Connect Four. You have to check for wins separately using
694 [Board.has_win][src.bitbully.board.Board.has_win].
698 move (int): The column index (0-6) to check.
701 bool: True if the move is legal, False otherwise.
704 All moves are legal on an empty board:
706 import bitbully as bb
708 board = bb.Board() # Empty 7x6 board
710 # Every column index from 0 to 6 is a valid move.
712 assert board.is_legal_move(col)
714 # Out-of-range indices are always illegal.
715 assert not board.is_legal_move(-1)
716 assert not board.is_legal_move(7)
720 Detecting an illegal move in a full column:
722 import bitbully as bb
724 # Fill the center column (index 3) with six tokens.
726 assert board.play([3, 3, 3, 3, 3, 3])
728 # The center column is now full, so another move in column 3 is illegal.
729 assert not board.is_legal_move(3)
731 # Other columns are still available (as long as they are not full).
732 assert board.is_legal_move(0)
733 assert board.is_legal_move(6)
748 This function only checks legality, not for situations where a player has won:
750 import bitbully as bb
752 # Player 1 (yellow, X) wins the game.
754 assert board.play("1122334")
756 # Even though Player 1 has already won, moves in non-full columns are still legal.
758 assert board.is_legal_move(col)
772 return self.
_board.isLegalMove(move)
775 """Returns a new Board instance that is the mirror image of the current board.
777 This method reflects the board **horizontally** around its vertical center column:
778 - Column 0 <-> Column 6
779 - Column 1 <-> Column 5
780 - Column 2 <-> Column 4
781 - Column 3 stays in the center
783 The player to move is not changed - only the spatial
784 arrangement of the tokens is mirrored. The original board remains unchanged;
785 `mirror()` always returns a **new** `Board` instance.
788 Board: A new Board instance that is the mirror image of the current one.
791 Mirroring a simple asymmetric position:
793 import bitbully as bb
795 # Play four moves along the bottom row.
797 assert board.play("0123") # Columns: 0, 1, 2, 3
799 # Create a mirrored copy of the board.
800 mirrored = board.mirror()
831 Mirroring a position that is already symmetric:
833 import bitbully as bb
835 # Central symmetry: one token in each outer column and in the center.
836 board = bb.Board([1, 3, 5])
838 mirrored = board.mirror()
840 # The mirrored position is identical to the original.
841 assert board == mirrored
842 assert hash(board) == hash(mirrored)
861 """Returns the number of moves left until the board is full.
863 This is simply the number of *empty* cells remaining on the 7x6 grid.
864 On an empty board there are 42 free cells, so:
866 - At the start of the game: `moves_left() == 42`
867 - After `n` valid moves: `moves_left() == 42 - n`
868 - On a completely full board: `moves_left() == 0`
870 This method is equivalent to:
872 42 - board.count_tokens()
874 but implemented efficiently in the underlying C++ core.
877 int: The number of moves left (0-42).
880 Moves left on an empty board:
882 import bitbully as bb
884 board = bb.Board() # No tokens placed yet.
885 assert board.moves_left() == 42
886 assert board.count_tokens() == 0
890 Relation to the number of moves played:
892 import bitbully as bb
894 # Play five moves in various columns.
895 moves = [3, 3, 1, 4, 6]
897 assert board.play(moves)
899 # Five tokens have been placed, so 42 - 5 = 37 moves remain.
900 assert board.count_tokens() == 5
901 assert board.moves_left() == 37
902 assert board.moves_left() + board.count_tokens() == 42
905 return self.
_board.movesLeft()
907 def play(self, move: int | Sequence[int] | str) -> bool:
908 """Plays one or more moves for the current player.
910 The method updates the internal board state by dropping tokens
911 into the specified columns. Input can be:
912 - a single integer (column index 0 to 6),
913 - an iterable sequence of integers (e.g., `[3, 1, 3]` or `range(7)`),
914 - or a string of digits (e.g., `"33333111"`) representing the move order.
917 move (int | Sequence[int] | str):
918 The column index or sequence of column indices where tokens should be placed.
921 bool: True if the move was played successfully, False if the move was illegal.
925 Play a sequence of moves into the center column (column index 3):
927 import bitbully as bb
930 assert board.play([3, 3, 3]) # returns True on successful move
946 Play a sequence of moves across all columns:
948 import bitbully as bb
951 assert board.play(range(7)) # returns True on successful move
965 Play a sequence using a string:
967 import bitbully as bb
970 assert board.play("33333111") # returns True on successful move
984 if isinstance(move, str):
988 if isinstance(move, int):
992 move_list: list[int] = [int(v)
for v
in cast(Sequence[Any], move)]
996 """Return a new board with the given move applied, leaving the current board unchanged.
1000 The column index (0-6) in which to play the move.
1004 A new Board instance representing the position after the move.
1007 ValueError: If the move is illegal (e.g. column is full or out of range).
1011 import bitbully as bb
1013 board = bb.Board("333") # Some existing position
1014 new_board = board.play_on_copy(4)
1016 # The original board is unchanged.
1017 assert board.count_tokens() == 3
1019 # The returned board includes the new move.
1020 assert new_board.count_tokens() == 4
1021 assert new_board != board
1025 core_new = self.
_board.playMoveOnCopy(move)
1027 if core_new
is None:
1029 raise ValueError(f
"Illegal move: column {move}")
1033 new_board._board = core_new
1036 def reset_board(self, board: Sequence[int] | Sequence[Sequence[int]] | str |
None =
None) -> bool:
1037 """Resets the board or sets (overrides) the board to a specific state.
1040 board (Sequence[int] | Sequence[Sequence[int]] | str | None):
1041 The new board state. Accepts:
1042 - 2D array (list, tuple, numpy-array) with shape 7x6 or 6x7
1043 - 1D sequence of ints: a move sequence of columns (e.g., [0, 0, 2, 2, 3, 3])
1044 - String: A move sequence of columns as string (e.g., "002233...")
1045 - None: to reset to an empty board
1048 bool: True if the board was set successfully, False otherwise.
1051 Reset the board to an empty state:
1053 import bitbully as bb
1055 # Create a temporary board position from a move string.
1056 # The string "0123456" plays one token in each column (0-6) in sequence.
1057 board = bb.Board("0123456")
1059 # Reset the board to an empty state.
1060 # Calling `reset_board()` clears all tokens and restores the starting position.
1061 # No moves → an empty board.
1062 assert board.reset_board()
1076 (Re-)Set the board using a move sequence string:
1078 import bitbully as bb
1080 # This is just a temporary setup; it will be replaced below.
1081 board = bb.Board("0123456")
1083 # Set the board state directly from a move sequence.
1084 # The list [3, 3, 3] represents three consecutive moves in the center column (index 3).
1085 # Moves alternate automatically between Player 1 (yellow) and Player 2 (red).
1087 # The `reset_board()` method clears the current position and replays the given moves
1088 # from an empty board — effectively overriding any existing board state.
1089 assert board.reset_board([3, 3, 3])
1091 # Display the updated board to verify the new position.
1105 You can also set the board using other formats, such as a 2D array or a string.
1106 See the examples in the [Board][src.bitbully.board.Board] docstring for details.
1109 # Briefly demonstrate the different input formats accepted by `reset_board()`.
1110 import bitbully as bb
1112 # Create an empty board instance
1115 # Variant 1: From a list of moves (integers)
1116 # Each number represents a column index (0-6); moves alternate between players.
1117 assert board.reset_board([3, 3, 3])
1119 # Variant 2: From a compact move string
1120 # Equivalent to the list above — useful for quick testing or serialization.
1121 assert board.reset_board("33333111")
1123 # Variant 3: From a 2D list in row-major format (6 x 7)
1124 # Each inner list represents a row (top to bottom).
1125 # 0 = empty, 1 = Player 1, 2 = Player 2.
1127 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
1128 [0, 0, 0, 1, 0, 0, 0], # Row 4
1129 [0, 0, 0, 2, 0, 0, 0], # Row 3
1130 [0, 2, 0, 1, 0, 0, 0], # Row 2
1131 [0, 1, 0, 2, 0, 0, 0], # Row 1
1132 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom)
1134 assert board.reset_board(board_array)
1136 # Variant 4: From a 2D list in column-major format (7 x 6)
1137 # Each inner list represents a column (left to right); this matches BitBully's internal layout.
1139 [0, 0, 0, 0, 0, 0], # Column 0 (leftmost)
1140 [2, 1, 2, 1, 0, 0], # Column 1
1141 [0, 0, 0, 0, 0, 0], # Column 2
1142 [1, 2, 1, 2, 1, 0], # Column 3 (center)
1143 [0, 0, 0, 0, 0, 0], # Column 4
1144 [2, 1, 2, 0, 0, 0], # Column 5
1145 [0, 0, 0, 0, 0, 0], # Column 6 (rightmost)
1147 assert board.reset_board(board_array)
1149 # Display the final board state in text form
1164 return self.
_board.setBoard([])
1165 if isinstance(board, str):
1166 return self.
_board.setBoard(board)
1170 if len(board) > 0
and isinstance(board[0], Sequence)
and not isinstance(board[0], (str, bytes)):
1173 grid: list[list[int]] = [[int(v)
for v
in row]
for row
in cast(Sequence[Sequence[Any]], board)]
1174 return self.
_board.setBoard(grid)
1177 moves: list[int] = [int(v)
for v
in cast(Sequence[Any], board)]
1178 return self.
_board.setBoard(moves)
1180 def to_array(self, column_major_layout: bool =
True) -> list[list[int]]:
1181 """Returns the board state as a 2D array (list of lists).
1183 This layout is convenient for printing, serialization, or converting
1184 to a NumPy array for further analysis.
1187 column_major_layout (bool): Use column-major format if set to `True`,
1188 otherwise the row-major-layout is used.
1191 list[list[int]]: A 7x6 2D list representing the board state.
1194 NotImplementedError: If `column_major_layout` is set to `False`.
1197 === "Column-major Format:"
1199 The returned array is in **column-major** format with shape `7 x 6`
1202 - There are 7 inner lists, one for each column of the board.
1203 - Each inner list has 6 integers, one for each row.
1204 - Row index `0` corresponds to the **bottom row**,
1205 row index `5` to the **top row**.
1208 - `1` -> Player 1 token (yellow, X)
1209 - `2` -> Player 2 token (red, O)
1212 import bitbully as bb
1213 from pprint import pprint
1215 # Create a position from a move sequence.
1216 board = bb.Board("33333111")
1218 # Extract the board as a 2D list (rows x columns).
1219 arr = board.to_array()
1221 # Reconstruct the same position from the 2D array.
1222 board2 = bb.Board(arr)
1224 # Both boards represent the same position.
1225 assert board == board2
1226 assert board.to_array() == board2.to_array()
1228 # print ther result of `board.to_array()`:
1229 pprint(board.to_array())
1233 [[0, 0, 0, 0, 0, 0],
1242 === "Row-major Format:"
1245 TODO: This is not supported yet
1248 if not column_major_layout:
1250 raise NotImplementedError(
"Row-major Layout is yet to be implemented")
1252 return self.
_board.toArray()
1255 """Returns a human-readable ASCII representation of the board.
1257 The returned string shows the **current board position** as a 6x7 grid,
1258 laid out exactly as it would appear when you print a `Board` instance:
1260 - 6 lines of text, one per row (top row first, bottom row last)
1261 - 7 entries per row, separated by two spaces
1262 - `_` represents an empty cell
1263 - `X` represents a token from Player 1 (yellow)
1264 - `O` represents a token from Player 2 (red)
1266 This is useful when you want to explicitly capture the board as a string
1267 (e.g., for logging, debugging, or embedding into error messages) instead
1268 of relying on `print(board)` or `repr(board)`.
1271 str: A multi-line ASCII string representing the board state.
1274 Using `to_string()` on an empty board:
1276 import bitbully as bb
1278 board = bb.Board("33333111")
1280 s = board.to_string()
1294 return self.
_board.toString()
1297 """Returns a unique identifier for the current board state.
1299 The UID is a deterministic integer computed from the internal bitboard
1300 representation of the position. It is **stable**, **position-based**, and
1301 uniquely tied to the exact token layout **and** the side to move.
1305 - Boards with the **same** configuration (tokens + player to move) always
1306 produce the **same** UID.
1307 - Any change to the board (e.g., after a legal move) will almost always
1308 result in a **different** UID.
1309 - Copies of a board created via the copy constructor or `Board.copy()`
1310 naturally share the same UID as long as their states remain identical.
1312 Unlike `__hash__()`, the UID is not optimized for hash-table dispersion.
1313 For use in transposition tables, caching, or dictionary/set keys,
1314 prefer `__hash__()` since it provides a higher-quality hash distribution.
1317 int: A unique integer identifier for the board state.
1320 UID is an integer and not None:
1322 import bitbully as bb
1327 assert isinstance(u, int)
1328 # Empty board has a well-defined, stable UID.
1329 assert board.uid() == u
1333 UID changes when the position changes:
1335 import bitbully as bb
1338 uid_before = board.uid()
1340 assert board.play(1) # Make a move in column 1.
1342 uid_after = board.uid()
1343 assert uid_after != uid_before
1347 Copies share the same UID while they are identical:
1349 import bitbully as bb
1351 board = bb.Board("0123")
1353 # Create an independent copy of the same position.
1354 board_copy = board.copy()
1356 assert board is not board_copy # Different objects
1357 assert board == board_copy # Same position
1358 assert board.uid() == board_copy.uid() # Same UID
1360 # After modifying the copy, they diverge.
1361 assert board_copy.play(4)
1362 assert board != board_copy
1363 assert board.uid() != board_copy.uid()
1367 Different move sequences leading to the same position share the same UID:
1369 import bitbully as bb
1371 board_1 = bb.Board("01234444")
1372 board_2 = bb.Board("44440123")
1374 assert board_1 is not board_2 # Different objects
1375 assert board_1 == board_2 # Same position
1376 assert board_1.uid() == board_2.uid() # Same UID
1378 # After modifying the copy, they diverge.
1379 assert board_1.play(4)
1380 assert board_1 != board_2
1381 assert board_1.uid() != board_2.uid()
1387 """Returns the player whose turn it is to move.
1389 The current player is derived from the **parity** of the number of tokens
1392 - Player 1 (yellow, ``X``) moves first on an empty board.
1393 - After an even number of moves → it is Player 1's turn.
1394 - After an odd number of moves → it is Player 2's turn.
1400 - ``1`` → Player 1 (yellow, ``X``)
1401 - ``2`` → Player 2 (red, ``O``)
1405 import bitbully as bb
1407 # Empty board → Player 1 starts.
1409 assert board.current_player() == 1
1410 assert board.count_tokens() == 0
1412 # After one move, it's Player 2's turn.
1413 assert board.play(3)
1414 assert board.count_tokens() == 1
1415 assert board.current_player() == 2
1417 # After a second move, it's again Player 1's turn.
1418 assert board.play(4)
1419 assert board.count_tokens() == 2
1420 assert board.current_player() == 1
1427 """Checks whether the board has any empty cells left.
1429 A Connect Four board has 42 cells in total (7 columns x 6 rows).
1430 This method returns ``True`` if **all** cells are occupied, i.e.
1431 when [Board.moves_left][src.bitbully.board.Board.moves_left] returns ``0``.
1435 ``True`` if the board is completely full
1436 (no more legal moves possible), otherwise ``False``.
1440 import bitbully as bb
1443 assert not board.is_full()
1444 assert board.moves_left() == 42
1445 assert board.count_tokens() == 0
1447 # Fill the board column by column.
1449 assert board.play("0123456") # one token per column, per row
1451 # Now every cell is occupied.
1452 assert board.is_full()
1453 assert board.moves_left() == 0
1454 assert board.count_tokens() == 42
1460 """Checks whether the game has ended (win or draw).
1462 A game of Connect Four is considered **over** if:
1464 - One of the players has a winning position
1465 (see [Board.has_win][src.bitbully.board.Board.has_win]), **or**
1466 - The board is completely full and no further moves can be played
1467 (see [Board.is_full][src.bitbully.board.Board.is_full]).
1469 This method does **not** indicate *who* won; for that, use
1470 [Board.winner][src.bitbully.board.Board.winner].
1474 ``True`` if the game is over (win or draw), otherwise ``False``.
1479 import bitbully as bb
1481 # Player 1 (X) wins horizontally on the bottom row.
1483 assert board.play("0101010")
1485 assert board.has_win()
1486 assert board.is_game_over()
1487 assert board.winner() == 1
1491 Game over by a draw (full board, no winner):
1493 import bitbully as bb
1495 board, _ = bb.Board.random_board(42, forbid_direct_win=False)
1497 assert board.is_full()
1498 assert not board.has_win()
1499 assert board.is_game_over()
1500 assert board.winner() is None
1506 """Returns the winning player, if the game has been won.
1508 This helper interprets the current board under the assumption that
1509 [Board.has_win][src.bitbully.board.Board.has_win] indicates **the last move** created a
1510 winning configuration. In that case, the winner is the *previous* player:
1512 - If it is currently Player 1's turn, then Player 2 must have just won.
1513 - If it is currently Player 2's turn, then Player 1 must have just won.
1515 If there is no winner (i.e. [Board.has_win][src.bitbully.board.Board.has_win] is ``False``),
1516 this method returns ``None``.
1520 The winning player, or ``None`` if there is no winner.
1522 - ``1`` → Player 1 (yellow, ``X``)
1523 - ``2`` → Player 2 (red, ``O``)
1524 - ``None`` → No winner (game still ongoing or draw)
1529 import bitbully as bb
1531 # Player 1 wins with a horizontal line at the bottom.
1533 assert board.play("1122334")
1535 assert board.has_win()
1536 assert board.is_game_over()
1538 # It is now Player 2's turn to move next...
1539 assert board.current_player() == 2
1541 # ...which implies Player 1 must be the winner.
1542 assert board.winner() == 1
1548 import bitbully as bb
1551 assert board.play("112233") # no connect-four yet
1553 assert not board.has_win()
1554 assert not board.is_game_over()
1555 assert board.winner() is None
1565 """Creates a board by replaying a sequence of moves from the empty position.
1567 This is a convenience constructor around [Board.play][src.bitbully.board.Board.play].
1568 It starts from an empty board and applies the given move sequence, assuming
1569 it is **legal** (no out-of-range columns, no moves in full columns, etc.).
1572 moves (Sequence[int] | str):
1573 The move sequence to replay from the starting position. Accepts:
1575 - A sequence of integers (e.g. ``[3, 3, 3, 1]``)
1576 - A string of digits (e.g. ``"3331"``)
1578 Each value represents a column index (0-6). Players alternate
1579 automatically between moves.
1583 A new `Board` instance representing the final position
1584 after all moves have been applied.
1588 import bitbully as bb
1590 # Create a position directly from a compact move string.
1591 board = bb.Board.from_moves("33333111")
1594 # board = bb.Board()
1595 # assert board.play("33333111")
1598 assert board.count_tokens() == 8
1599 assert not board.has_win()
1603 assert board.play(moves)
1608 """Creates a board directly from a 2D array representation.
1610 This is a convenience wrapper around the main constructor [board.Board][src.bitbully.board.Board]
1611 and accepts the same array formats:
1613 - **Row-major**: 6 x 7 (``[row][column]``), top row first.
1614 - **Column-major**: 7 x 6 (``[column][row]``), left column first.
1616 Values must follow the usual convention:
1618 - ``0`` → empty cell
1619 - ``1`` → Player 1 token (yellow, ``X``)
1620 - ``2`` → Player 2 token (red, ``O``)
1623 arr (Sequence[Sequence[int]]):
1624 A 2D array describing the board state, either in row-major or
1625 column-major layout. See the examples in
1626 [Board][src.bitbully.board.Board] for details.
1630 A new `Board` instance representing the given layout.
1633 Using a 6 x 7 row-major layout:
1635 import bitbully as bb
1638 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
1639 [0, 0, 0, 1, 0, 0, 0], # Row 4
1640 [0, 0, 0, 2, 0, 0, 0], # Row 3
1641 [0, 2, 0, 1, 0, 0, 0], # Row 2
1642 [0, 1, 0, 2, 0, 0, 0], # Row 1
1643 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom)
1646 board = bb.Board.from_array(board_array)
1651 Using a 7 x 6 column-major layout:
1653 import bitbully as bb
1656 [0, 0, 0, 0, 0, 0], # Column 0
1657 [2, 1, 2, 1, 0, 0], # Column 1
1658 [0, 0, 0, 0, 0, 0], # Column 2
1659 [1, 2, 1, 2, 1, 0], # Column 3
1660 [0, 0, 0, 0, 0, 0], # Column 4
1661 [2, 1, 2, 0, 0, 0], # Column 5
1662 [0, 0, 0, 0, 0, 0], # Column 6
1665 board = bb.Board.from_array(board_array)
1667 # Round-trip via to_array:
1668 assert board.to_array() == board_array
1674 def random_board(n_ply: int, forbid_direct_win: bool) -> tuple[Board, list[int]]:
1675 """Generates a random board state by playing a specified number of random moves.
1677 If ``forbid_direct_win`` is ``True``, the generated position is guaranteed
1678 **not** to contain an immediate winning move for the player to move.
1682 Number of random moves to simulate (0-42).
1683 forbid_direct_win (bool):
1684 If ``True``, ensures the resulting board has **no immediate winning move**.
1687 tuple[Board, list[int]]:
1688 A pair ``(board, moves)`` where ``board`` is the generated position
1689 and ``moves`` are the exact random moves performed.
1692 ValueError: If `n_ply` is outside the valid range [0, 42].
1697 import bitbully as bb
1699 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1701 print("Moves:", moves)
1705 # The move list must match the requested ply.
1706 assert len(moves) == 10
1708 # No immediate winning move when forbid_direct_win=True.
1709 assert not board.can_win_next()
1713 Using random boards in tests or simulations:
1715 import bitbully as bb
1717 # Generate 50 random 10-ply positions.
1719 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1720 assert len(moves) == 10
1721 assert not board.has_win() # Game should not be over
1722 assert board.count_tokens() == 10 # All generated boards contain exactly 10 tokens
1723 assert not board.can_win_next() # Since `forbid_direct_win=True`, no immediate threat
1727 Reconstructing the board manually from the move list:
1729 import bitbully as bb
1731 b1, moves = bb.Board.random_board(8, forbid_direct_win=True)
1733 # Recreate the board using the move sequence:
1734 b2 = bb.Board(moves)
1737 assert b1.to_string() == b2.to_string()
1738 assert b1.uid() == b2.uid()
1742 Ensure randomness by generating many distinct sequences:
1744 import bitbully as bb
1748 _, moves = bb.Board.random_board(5, False)
1749 seen.add(tuple(moves))
1751 # Very likely to see more than one unique sequence.
1752 assert len(seen) > 1
1755 if not 0 <= n_ply <= 42:
1756 raise ValueError(f
"n_ply must be between 0 and 42 (inclusive), got {n_ply}.")
1757 board_, moves = BoardCore.randomBoard(n_ply, forbid_direct_win)
1759 board._board = board_
1764 """Encode the current board position into a Huffman-compressed byte sequence.
1766 This is a high-level wrapper around
1767 `bitbully_core.BoardCore.toHuffman`. The returned int encodes the
1768 exact token layout **and** the side to move using the same format as
1769 the BitBully opening databases.
1773 - Deterministic: the same position always yields the same byte sequence.
1774 - Compact: suitable for storage (of positions with little number of tokens),
1775 or lookups in the BitBully database format.
1778 int: A Huffman-compressed representation of the current board
1782 NotImplementedError:
1783 If the position does not contain exactly 8 or 12 tokens, as the
1784 Huffman encoding is only defined for these cases.
1787 Encode a position and verify that equivalent positions have the
1791 import bitbully as bb
1793 # Two different move sequences leading to the same final position.
1794 b1 = bb.Board("01234444")
1795 b2 = bb.Board("44440123")
1797 h1 = b1.to_huffman()
1798 h2 = b2.to_huffman()
1800 # Huffman encoding is purely position-based.
1803 print(f"Huffman code: {h1}")
1807 Huffman code: 10120112
1811 if token_count != 8
and token_count != 12:
1812 raise NotImplementedError(
"to_huffman() is only implemented for positions with 8 or 12 tokens.")
1813 return self.
_board.toHuffman()
1815 def legal_moves(self, non_losing: bool =
False, order_moves: bool =
False) -> list[int]:
1816 """Returns a list of all legal moves (non-full columns) for the current board state.
1820 If ``True``, only returns moves that do **not** allow the opponent
1821 to win immediately on their next turn. The list might be empty
1822 If ``False``, all legal moves are returned.
1824 If ``True``, the returned list is ordered to prioritize moves (potentially more promising first).
1827 list[int]: A list of column indices (0-6) where a token can be legally dropped.
1831 import bitbully as bb
1834 legal_moves = board.legal_moves()
1835 assert set(legal_moves) == set(range(7)) # All columns are initially legal
1836 assert set(legal_moves) == set(board.legal_moves(order_moves=True))
1837 board.legal_moves(order_moves=True) == [3, 2, 4, 1, 5, 0, 6] # Center column prioritized
1842 import bitbully as bb
1845 board.play("3322314")
1847 assert board.legal_moves() == list(range(7))
1848 assert board.legal_moves(non_losing=True) == [5]
1861 return self.
_board.legalMoves(nonLosing=non_losing, orderMoves=order_moves)
1865 """Return the underlying native board representation.
1867 This is intended for internal engine integrations and wrappers.
1868 Users should treat this as read-only.
1872 The underlying native `BoardCore` instance representing the board state.
1875 - The `native` property exposes the underlying engine representation.
1876 - This is intended for engine wrappers (e.g. BitBully) and should be
1877 treated as read-only by users.
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)