BitBully 0.0.71
Loading...
Searching...
No Matches
board.py
1"""This module defines the Board class for managing the state of a Connect Four game."""
2
3from __future__ import annotations # for forward references in type hints (Python 3.7+)
4
5from collections.abc import Sequence
6from typing import Any, ClassVar, cast
7
8from . import bitbully_core
9from .bitbully_core import BoardCore
10
11
12class Board:
13 """Represents the state of a Connect Four board. Mostly a thin wrapper around BoardCore."""
14
15 # class-level constants
16 N_COLUMNS: ClassVar[int] = bitbully_core.N_COLUMNS
17 N_ROWS: ClassVar[int] = bitbully_core.N_ROWS
18
19 Player = bitbully_core.Player
20
21 def __init__(self, init_with: Sequence[Sequence[int]] | Sequence[int] | str | None = None) -> None:
22 """Initializes a Board instance.
23
24 Args:
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
31
32 Raises:
33 ValueError: If the provided initial board state is invalid.
34
35 Example:
36 You can initialize an empty board in multiple ways:
37 ```python
38 import bitbully as bb
39
40 # Create an empty board using the default constructor.
41 board = bb.Board() # Starts with no tokens placed.
42
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.
47
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.
53
54 # Display the board in text form.
55 # The __repr__ method shows the current state (useful for debugging or interactive use).
56 board
57 ```
58 Expected output:
59 ```text
60 _ _ _ _ _ _ _
61 _ _ _ _ _ _ _
62 _ _ _ _ _ _ _
63 _ _ _ _ _ _ _
64 _ _ _ _ _ _ _
65 _ _ _ _ _ _ _
66 ```
67
68 The recommended way to initialize an empty board is simply `Board()`.
69
70 Example:
71 You can also initialize a board with a sequence of moves:
72 ```python
73 import bitbully as bb
74
75 # Initialize a board with a sequence of moves played in the center column.
76
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])
84
85 # Display the resulting board.
86 # The textual output shows the tokens placed in the center column.
87 board
88 ```
89
90 Expected output:
91 ```text
92 _ _ _ _ _ _ _
93 _ _ _ _ _ _ _
94 _ _ _ _ _ _ _
95 _ _ _ X _ _ _
96 _ _ _ O _ _ _
97 _ _ _ X _ _ _
98 ```
99
100 Example:
101 You can also initialize a board using a string containing a move sequence:
102 ```python
103 import bitbully as bb
104
105 # Initialize a board using a compact move string.
106
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)
110 #
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.
113 #
114 # This shorthand is convenient for reproducing board states or test positions
115 # without having to provide long move lists.
116
117 board = bb.Board("33333111")
118
119 # Display the resulting board.
120 # The printed layout shows how the tokens stack in each column.
121 board
122 ```
123
124 Expected output:
125 ```text
126 _ _ _ _ _ _ _
127 _ _ _ X _ _ _
128 _ _ _ O _ _ _
129 _ O _ X _ _ _
130 _ X _ O _ _ _
131 _ O _ X _ _ _
132 ```
133
134 Example:
135 You can also initialize a board using a 2D array (list of lists):
136 ```python
137 import bitbully as bb
138
139 # Use a 6 x 7 list (rows x columns) to set up a specific board position manually.
140
141 # Each inner list represents a row of the Connect-4 grid.
142 # Convention:
143 # - 0 → empty cell
144 # - 1 → Player 1 token (yellow, X)
145 # - 2 → Player 2 token (red, O)
146 #
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.
150
151 board_array = [
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
158 ]
159
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)
164
165 # Display the resulting board state in text form.
166 board
167 ```
168 Expected output:
169 ```text
170 _ _ _ _ _ _ _
171 _ _ _ X _ _ _
172 _ _ _ O _ _ _
173 _ O _ X _ _ _
174 _ X _ O _ _ _
175 _ O _ X _ _ _
176 ```
177
178 Example:
179 You can also initialize a board using a 2D (7 x 6) array with columns as inner lists:
180 ```python
181 import bitbully as bb
182
183 # Use a 7 x 6 list (columns x rows) to set up a specific board position manually.
184
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).
188 #
189 # Convention:
190 # - 0 → empty cell
191 # - 1 → Player 1 token (yellow, X)
192 # - 2 → Player 2 token (red, O)
193 #
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.
196
197 board_array = [
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)
205 ]
206
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)
211
212 # Display the resulting board.
213 # The text output shows tokens as they would appear in a real Connect-4 grid.
214 board
215 ```
216 Expected output:
217 ```text
218 _ _ _ _ _ _ _
219 _ _ _ X _ _ _
220 _ _ _ O _ _ _
221 _ O _ X _ _ _
222 _ X _ O _ _ _
223 _ O _ X _ _ _
224 ```
225 """
226 self._board = BoardCore()
227 if init_with is not None and not self.reset_board(init_with):
228 raise ValueError(
229 "Invalid initial board state provided. Check the examples in the docstring for valid formats."
230 )
231
232 def __eq__(self, value: object) -> bool:
233 """Checks equality between two Board instances.
234
235 Notes:
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.
242
243 Args:
244 value (object): The other Board instance to compare against.
245
246 Returns:
247 bool: True if both boards are equal, False otherwise.
248
249 Raises:
250 NotImplementedError: If the other value is not a Board instance.
251
252 Example:
253 ```python
254 import bitbully as bb
255
256 # Create two boards that should represent *identical* game states.
257 board1 = bb.Board()
258 assert board1.play("33333111")
259
260 board2 = bb.Board()
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")
264
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)
269
270 # ------------------------------------------------------------------------------
271
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)
275
276 # Since the token layout differs, equality no longer holds.
277 assert board1 != board2
278 assert not (board1 == board2)
279 ```
280 """
281 if not isinstance(value, Board):
282 raise NotImplementedError("Can only compare with another Board instance.")
283 return bool(self._board == value._board)
284
285 def __ne__(self, value: object) -> bool:
286 """Checks inequality between two Board instances.
287
288 See the documentation for [Board.__eq__][src.bitbully.board.Board.__eq__] for details.
289
290 Args:
291 value (object): The other Board instance to compare against.
292
293 Returns:
294 bool: True if both boards are not equal, False otherwise.
295 """
296 return not self.__eq__(value)
297
298 def __repr__(self) -> str:
299 """Returns a string representation of the Board instance."""
300 return f"{self._board}"
301
302 def __str__(self) -> str:
303 """Return a human-readable ASCII representation (same as to_string()).
304
305 See the documentation for [Board.to_string][src.bitbully.board.Board.to_string] for details.
306 """
307 return self.to_string()
308
309 def all_positions(self, up_to_n_ply: int, exactly_n: bool) -> list[Board]:
310 """Find all positions reachable from the current position up to a given ply.
311
312 This is a high-level wrapper around
313 `bitbully_core.BoardCore.allPositions`.
314
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:
317
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``.
320
321 Note:
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.
325
326 This function can grow combinatorially with ``up_to_n_ply`` and the
327 current position, so use it with care for large depths.
328
329 Args:
330 up_to_n_ply (int):
331 The maximum total number of tokens (ply) for generated positions.
332 Must be between 0 and 42 (inclusive).
333 exactly_n (bool):
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
337 included.
338
339 Returns:
340 list[Board]: A list of :class:`Board` instances representing all
341 reachable positions that satisfy the ply constraint.
342
343 Raises:
344 ValueError: If ``up_to_n_ply`` is outside the range ``[0, 42]``.
345
346 Example:
347 Compute all positions at exactly 3 ply from the empty board:
348
349 ```python
350 import bitbully as bb
351
352 # Start from an empty board.
353 board = bb.Board()
354
355 # Generate all positions that contain exactly 3 tokens.
356 positions = board.all_positions(3, exactly_n=True)
357
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
361 ```
362
363 Reference:
364 - Number of distinct positions at ply *n*:
365 https://oeis.org/A212693
366
367 """
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}.")
370
371 # Delegate to the C++ core, which returns a list of BoardCore objects.
372 core_positions = self._board.allPositions(up_to_n_ply, exactly_n)
373
374 # Wrap each BoardCore in a high-level Board instance.
375 positions: list[Board] = []
376 for core_board in core_positions:
377 b = Board() # start with an empty high-level Board
378 b._board = core_board # replace its internal BoardCore
379 positions.append(b)
380
381 return positions
382
383 def can_win_next(self, move: int | None = None) -> bool:
384 """Checks if the current player can win in the next move.
385
386 Args:
387 move (int | None): Optional column to check for an immediate win. If None, checks all columns.
388
389 Returns:
390 bool: True if the current player can win next, False otherwise.
391
392 See also: [`Board.has_win`][src.bitbully.board.Board.has_win].
393
394 Example:
395 ```python
396 import bitbully as bb
397
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")
402
403 # Display the current board state (see below)
404 print(board)
405
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)
410
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)
414
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()
418 ```
419 The board we created above looks like this:
420 ```text
421 _ _ _ _ _ _ _
422 _ _ _ _ _ _ _
423 _ _ _ _ _ _ _
424 _ _ _ O _ _ _
425 _ O _ O _ _ _
426 _ X X X _ _ _
427 ```
428 """
429 if move is None:
430 return self._board.canWin()
431 return bool(self._board.canWin(move))
432
433 def copy(self) -> Board:
434 """Creates a copy of the current Board instance.
435
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
439 independent.
440
441 Returns:
442 Board: A new Board instance that is a copy of the current one.
443
444 Example:
445 Create a board, copy it, and verify that both represent the same position:
446 ```python
447 import bitbully as bb
448
449 # Create a board from a compact move string.
450 board = bb.Board("33333111")
451
452 # Create an independent copy of the current position.
453 board_copy = board.copy()
454
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()
459
460 # Display the board state.
461 print(board)
462 ```
463 Expected output (both boards print the same position):
464 ```text
465 _ _ _ _ _ _ _
466 _ _ _ X _ _ _
467 _ _ _ O _ _ _
468 _ O _ X _ _ _
469 _ X _ O _ _ _
470 _ O _ X _ _ _
471 ```
472
473 Example:
474 Modifying the copy does not affect the original:
475 ```python
476 import bitbully as bb
477
478 board = bb.Board("33333111")
479
480 # Create a copy of the current position.
481 board_copy = board.copy()
482
483 # Play an additional move on the copied board only.
484 assert board_copy.play(0) # Drop a token into the leftmost column.
485
486 # Now the boards represent different positions.
487 assert board != board_copy
488
489 # The original board remains unchanged.
490 print("Original:")
491 print(board)
492
493 print("Modified copy:")
494 print(board_copy)
495 ```
496 Expected output:
497 ```text
498 Original:
499
500 _ _ _ _ _ _ _
501 _ _ _ X _ _ _
502 _ _ _ O _ _ _
503 _ O _ X _ _ _
504 _ X _ O _ _ _
505 _ O _ X _ _ _
506
507 Modified copy:
508
509 _ _ _ _ _ _ _
510 _ _ _ X _ _ _
511 _ _ _ O _ _ _
512 _ O _ X _ _ _
513 _ X _ O _ _ _
514 X O _ X _ _ _
515 ```
516 """
517 new_board = Board()
518 new_board._board = self._board.copy()
519 return new_board
520
521 def count_tokens(self) -> int:
522 """Counts the total number of tokens currently placed on the board.
523
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.
526
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.
529
530 Returns:
531 int: The total number of tokens on the board (between 0 and 42).
532
533 Example:
534 Count tokens on an empty board:
535 ```python
536 import bitbully as bb
537
538 board = bb.Board() # No moves played yet.
539 assert board.count_tokens() == 0
540
541 # The board is completely empty.
542 print(board)
543 ```
544 Expected output:
545 ```text
546 _ _ _ _ _ _ _
547 _ _ _ _ _ _ _
548 _ _ _ _ _ _ _
549 _ _ _ _ _ _ _
550 _ _ _ _ _ _ _
551 _ _ _ _ _ _ _
552 ```
553
554 Example:
555 Count tokens after a few moves:
556 ```python
557 import bitbully as bb
558
559 # Play three moves in the center column (index 3).
560 board = bb.Board()
561 assert board.play([3, 3, 3])
562
563 # Three tokens have been placed on the board.
564 assert board.count_tokens() == 3
565
566 print(board)
567 ```
568 Expected output:
569 ```text
570 _ _ _ _ _ _ _
571 _ _ _ _ _ _ _
572 _ _ _ _ _ _ _
573 _ _ _ X _ _ _
574 _ _ _ O _ _ _
575 _ _ _ X _ _ _
576 ```
577
578 Example:
579 Relation to the length of a move sequence:
580 ```python
581 import bitbully as bb
582
583 moves = "33333111" # 8 moves in total
584 board = bb.Board(moves)
585
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)
590 ```
591 """
592 return self._board.countTokens()
593
594 def has_win(self) -> bool:
595 """Checks if the current player has a winning position.
596
597 Returns:
598 bool: True if the current player has a winning position (4-in-a-row), False otherwise.
599
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
604 has been won.
605
606 See also: [`Board.can_win_next`][src.bitbully.board.Board.can_win_next].
607
608 Example:
609 ```python
610 import bitbully as bb
611
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")
616
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)
623
624 # Simulate Player 1 playing in column 4 — this completes
625 # a horizontal line of four tokens and wins the game.
626 assert board.play(4)
627
628 # Display the updated board to visualize the winning position.
629 print(board)
630
631 # The board now contains a winning configuration:
632 # Player 1 (yellow, X) has achieved a Connect-4.
633 assert board.has_win()
634 ```
635 Expected output:
636 ```text
637 _ _ _ _ _ _ _
638 _ _ _ _ _ _ _
639 _ _ _ _ _ _ _
640 _ _ _ O _ _ _
641 _ O _ O _ _ _
642 _ X X X X _ _
643 ```
644 """
645 return self._board.hasWin()
646
647 def __hash__(self) -> int:
648 """Returns a hash of the Board instance for use in hash-based collections.
649
650 Returns:
651 int: The hash value of the Board instance.
652
653 Example:
654 ```python
655 import bitbully as bb
656
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")
660
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`.
664 board2 = bb.Board()
665 board2.play("31133331")
666
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)
670
671 # Display the board's hash value.
672 hash(board1)
673 ```
674 Expected output:
675 ```text
676 971238920548618160
677 ```
678 """
679 return self._board.hash()
680
681 def is_legal_move(self, move: int) -> bool:
682 """Checks if a move (column) is legal in the current position.
683
684 A move is considered *legal* if:
685
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).
688
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].
695
696
697 Args:
698 move (int): The column index (0-6) to check.
699
700 Returns:
701 bool: True if the move is legal, False otherwise.
702
703 Example:
704 All moves are legal on an empty board:
705 ```python
706 import bitbully as bb
707
708 board = bb.Board() # Empty 7x6 board
709
710 # Every column index from 0 to 6 is a valid move.
711 for col in range(7):
712 assert board.is_legal_move(col)
713
714 # Out-of-range indices are always illegal.
715 assert not board.is_legal_move(-1)
716 assert not board.is_legal_move(7)
717 ```
718
719 Example:
720 Detecting an illegal move in a full column:
721 ```python
722 import bitbully as bb
723
724 # Fill the center column (index 3) with six tokens.
725 board = bb.Board()
726 assert board.play([3, 3, 3, 3, 3, 3])
727
728 # The center column is now full, so another move in column 3 is illegal.
729 assert not board.is_legal_move(3)
730
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)
734
735 print(board)
736 ```
737 Expected output:
738 ```text
739 _ _ _ O _ _ _
740 _ _ _ X _ _ _
741 _ _ _ O _ _ _
742 _ _ _ X _ _ _
743 _ _ _ O _ _ _
744 _ _ _ X _ _ _
745 ```
746
747 Example:
748 This function only checks legality, not for situations where a player has won:
749 ```python
750 import bitbully as bb
751
752 # Player 1 (yellow, X) wins the game.
753 board = bb.Board()
754 assert board.play("1122334")
755
756 # Even though Player 1 has already won, moves in non-full columns are still legal.
757 for col in range(7):
758 assert board.is_legal_move(col)
759
760 print(board)
761 ```
762 Expected output:
763 ```text
764 _ _ _ _ _ _ _
765 _ _ _ _ _ _ _
766 _ _ _ _ _ _ _
767 _ _ _ _ _ _ _
768 _ O O O _ _ _
769 _ X X X X _ _
770 ```
771 """
772 return self._board.isLegalMove(move)
773
774 def mirror(self) -> Board:
775 """Returns a new Board instance that is the mirror image of the current board.
776
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
782
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.
786
787 Returns:
788 Board: A new Board instance that is the mirror image of the current one.
789
790 Example:
791 Mirroring a simple asymmetric position:
792 ```python
793 import bitbully as bb
794
795 # Play four moves along the bottom row.
796 board = bb.Board()
797 assert board.play("0123") # Columns: 0, 1, 2, 3
798
799 # Create a mirrored copy of the board.
800 mirrored = board.mirror()
801
802 print("Original:")
803 print(board)
804
805 print("Mirrored:")
806 print(mirrored)
807 ```
808
809 Expected output:
810 ```text
811 Original:
812
813 _ _ _ _ _ _ _
814 _ _ _ _ _ _ _
815 _ _ _ _ _ _ _
816 _ _ _ _ _ _ _
817 _ _ _ _ _ _ _
818 X O X O _ _ _
819
820 Mirrored:
821
822 _ _ _ _ _ _ _
823 _ _ _ _ _ _ _
824 _ _ _ _ _ _ _
825 _ _ _ _ _ _ _
826 _ _ _ _ _ _ _
827 _ _ _ O X O X
828 ```
829
830 Example:
831 Mirroring a position that is already symmetric:
832 ```python
833 import bitbully as bb
834
835 # Central symmetry: one token in each outer column and in the center.
836 board = bb.Board([1, 3, 5])
837
838 mirrored = board.mirror()
839
840 # The mirrored position is identical to the original.
841 assert board == mirrored
842 assert hash(board) == hash(mirrored)
843
844 print(board)
845 ```
846 Expected output:
847 ```text
848 _ _ _ _ _ _ _
849 _ _ _ _ _ _ _
850 _ _ _ _ _ _ _
851 _ _ _ _ _ _ _
852 _ _ _ _ _ _ _
853 _ X _ O _ X _
854 ```
855 """
856 new_board = Board()
857 new_board._board = self._board.mirror()
858 return new_board
859
860 def moves_left(self) -> int:
861 """Returns the number of moves left until the board is full.
862
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:
865
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`
869
870 This method is equivalent to:
871 ```
872 42 - board.count_tokens()
873 ```
874 but implemented efficiently in the underlying C++ core.
875
876 Returns:
877 int: The number of moves left (0-42).
878
879 Example:
880 Moves left on an empty board:
881 ```python
882 import bitbully as bb
883
884 board = bb.Board() # No tokens placed yet.
885 assert board.moves_left() == 42
886 assert board.count_tokens() == 0
887 ```
888
889 Example:
890 Relation to the number of moves played:
891 ```python
892 import bitbully as bb
893
894 # Play five moves in various columns.
895 moves = [3, 3, 1, 4, 6]
896 board = bb.Board()
897 assert board.play(moves)
898
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
903 ```
904 """
905 return self._board.movesLeft()
906
907 def play(self, move: int | Sequence[int] | str) -> bool:
908 """Plays one or more moves for the current player.
909
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.
915
916 Args:
917 move (int | Sequence[int] | str):
918 The column index or sequence of column indices where tokens should be placed.
919
920 Returns:
921 bool: True if the move was played successfully, False if the move was illegal.
922
923
924 Example:
925 Play a sequence of moves into the center column (column index 3):
926 ```python
927 import bitbully as bb
928
929 board = bb.Board()
930 assert board.play([3, 3, 3]) # returns True on successful move
931 board
932 ```
933
934 Expected output:
935
936 ```
937 _ _ _ _ _ _ _
938 _ _ _ _ _ _ _
939 _ _ _ _ _ _ _
940 _ _ _ X _ _ _
941 _ _ _ O _ _ _
942 _ _ _ X _ _ _
943 ```
944
945 Example:
946 Play a sequence of moves across all columns:
947 ```python
948 import bitbully as bb
949
950 board = bb.Board()
951 assert board.play(range(7)) # returns True on successful move
952 board
953 ```
954 Expected output:
955 ```text
956 _ _ _ _ _ _ _
957 _ _ _ _ _ _ _
958 _ _ _ _ _ _ _
959 _ _ _ _ _ _ _
960 _ _ _ _ _ _ _
961 X O X O X O X
962 ```
963
964 Example:
965 Play a sequence using a string:
966 ```python
967 import bitbully as bb
968
969 board = bb.Board()
970 assert board.play("33333111") # returns True on successful move
971 board
972 ```
973 Expected output:
974 ```text
975 _ _ _ _ _ _ _
976 _ _ _ X _ _ _
977 _ _ _ O _ _ _
978 _ O _ X _ _ _
979 _ X _ O _ _ _
980 _ O _ X _ _ _
981 ```
982 """
983 # Case 1: string -> pass through directly
984 if isinstance(move, str):
985 return self._board.play(move)
986
987 # Case 2: int -> pass through directly
988 if isinstance(move, int):
989 return self._board.play(move)
990
991 # From here on, move is a Sequence[...] (but not str or int).
992 move_list: list[int] = [int(v) for v in cast(Sequence[Any], move)]
993 return self._board.play(move_list)
994
995 def play_on_copy(self, move: int) -> Board:
996 """Return a new board with the given move applied, leaving the current board unchanged.
997
998 Args:
999 move (int):
1000 The column index (0-6) in which to play the move.
1001
1002 Returns:
1003 Board:
1004 A new Board instance representing the position after the move.
1005
1006 Raises:
1007 ValueError: If the move is illegal (e.g. column is full or out of range).
1008
1009 Example:
1010 ```python
1011 import bitbully as bb
1012
1013 board = bb.Board("333") # Some existing position
1014 new_board = board.play_on_copy(4)
1015
1016 # The original board is unchanged.
1017 assert board.count_tokens() == 3
1018
1019 # The returned board includes the new move.
1020 assert new_board.count_tokens() == 4
1021 assert new_board != board
1022 ```
1023 """
1024 # Delegate to C++ (this returns a BoardCore instance)
1025 core_new = self._board.playMoveOnCopy(move)
1026
1027 if core_new is None:
1028 # C++ signals illegal move by returning a null board
1029 raise ValueError(f"Illegal move: column {move}")
1030
1031 # Wrap in a new high-level Board object
1032 new_board = Board()
1033 new_board._board = core_new
1034 return new_board
1035
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.
1038
1039 Args:
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
1046
1047 Returns:
1048 bool: True if the board was set successfully, False otherwise.
1049
1050 Example:
1051 Reset the board to an empty state:
1052 ```python
1053 import bitbully as bb
1054
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")
1058
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()
1063 board
1064 ```
1065 Expected output:
1066 ```text
1067 _ _ _ _ _ _ _
1068 _ _ _ _ _ _ _
1069 _ _ _ _ _ _ _
1070 _ _ _ _ _ _ _
1071 _ _ _ _ _ _ _
1072 _ _ _ _ _ _ _
1073 ```
1074
1075 Example:
1076 (Re-)Set the board using a move sequence string:
1077 ```python
1078 import bitbully as bb
1079
1080 # This is just a temporary setup; it will be replaced below.
1081 board = bb.Board("0123456")
1082
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).
1086 #
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])
1090
1091 # Display the updated board to verify the new position.
1092 board
1093 ```
1094 Expected output:
1095 ```text
1096 _ _ _ _ _ _ _
1097 _ _ _ _ _ _ _
1098 _ _ _ _ _ _ _
1099 _ _ _ X _ _ _
1100 _ _ _ O _ _ _
1101 _ _ _ X _ _ _
1102 ```
1103
1104 Example:
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.
1107
1108 ```python
1109 # Briefly demonstrate the different input formats accepted by `reset_board()`.
1110 import bitbully as bb
1111
1112 # Create an empty board instance
1113 board = bb.Board()
1114
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])
1118
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")
1122
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.
1126 board_array = [
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)
1133 ]
1134 assert board.reset_board(board_array)
1135
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.
1138 board_array = [
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)
1146 ]
1147 assert board.reset_board(board_array)
1148
1149 # Display the final board state in text form
1150 board
1151 ```
1152
1153 Expected output:
1154 ```text
1155 _ _ _ _ _ _ _
1156 _ _ _ X _ _ _
1157 _ X _ O _ _ _
1158 _ O _ X _ O _
1159 _ X _ O _ X _
1160 _ O _ X _ O _
1161 ```
1162 """
1163 if board is None:
1164 return self._board.setBoard([])
1165 if isinstance(board, str):
1166 return self._board.setBoard(board)
1167
1168 # From here on, board is a Sequence[...] (but not str).
1169 # Distinguish 2D vs 1D by inspecting the first element.
1170 if len(board) > 0 and isinstance(board[0], Sequence) and not isinstance(board[0], (str, bytes)):
1171 # Case 2: 2D -> list[list[int]]
1172 # Convert inner sequences to lists of ints
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)
1175
1176 # Case 3: 1D -> list[int]
1177 moves: list[int] = [int(v) for v in cast(Sequence[Any], board)]
1178 return self._board.setBoard(moves)
1179
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).
1182
1183 This layout is convenient for printing, serialization, or converting
1184 to a NumPy array for further analysis.
1185
1186 Args:
1187 column_major_layout (bool): Use column-major format if set to `True`,
1188 otherwise the row-major-layout is used.
1189
1190 Returns:
1191 list[list[int]]: A 7x6 2D list representing the board state.
1192
1193 Raises:
1194 NotImplementedError: If `column_major_layout` is set to `False`.
1195
1196 Example:
1197 === "Column-major Format:"
1198
1199 The returned array is in **column-major** format with shape `7 x 6`
1200 (`[column][row]`):
1201
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**.
1206 - Convention:
1207 - `0` -> empty cell
1208 - `1` -> Player 1 token (yellow, X)
1209 - `2` -> Player 2 token (red, O)
1210
1211 ```python
1212 import bitbully as bb
1213 from pprint import pprint
1214
1215 # Create a position from a move sequence.
1216 board = bb.Board("33333111")
1217
1218 # Extract the board as a 2D list (rows x columns).
1219 arr = board.to_array()
1220
1221 # Reconstruct the same position from the 2D array.
1222 board2 = bb.Board(arr)
1223
1224 # Both boards represent the same position.
1225 assert board == board2
1226 assert board.to_array() == board2.to_array()
1227
1228 # print ther result of `board.to_array()`:
1229 pprint(board.to_array())
1230 ```
1231 Expected output:
1232 ```text
1233 [[0, 0, 0, 0, 0, 0],
1234 [2, 1, 2, 0, 0, 0],
1235 [0, 0, 0, 0, 0, 0],
1236 [1, 2, 1, 2, 1, 0],
1237 [0, 0, 0, 0, 0, 0],
1238 [0, 0, 0, 0, 0, 0],
1239 [0, 0, 0, 0, 0, 0]]
1240 ```
1241
1242 === "Row-major Format:"
1243
1244 ``` markdown
1245 TODO: This is not supported yet
1246 ```
1247 """
1248 if not column_major_layout:
1249 # TODO: Implement in C++
1250 raise NotImplementedError("Row-major Layout is yet to be implemented")
1251
1252 return self._board.toArray()
1253
1254 def to_string(self) -> str:
1255 """Returns a human-readable ASCII representation of the board.
1256
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:
1259
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)
1265
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)`.
1269
1270 Returns:
1271 str: A multi-line ASCII string representing the board state.
1272
1273 Example:
1274 Using `to_string()` on an empty board:
1275 ```python
1276 import bitbully as bb
1277
1278 board = bb.Board("33333111")
1279
1280 s = board.to_string()
1281 print(s)
1282 ```
1283
1284 Expected output:
1285 ```text
1286 _ _ _ _ _ _ _
1287 _ _ _ X _ _ _
1288 _ _ _ O _ _ _
1289 _ O _ X _ _ _
1290 _ X _ O _ _ _
1291 _ O _ X _ _ _
1292 ```
1293 """
1294 return self._board.toString()
1295
1296 def uid(self) -> int:
1297 """Returns a unique identifier for the current board state.
1298
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.
1302
1303 Key properties:
1304
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.
1311
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.
1315
1316 Returns:
1317 int: A unique integer identifier for the board state.
1318
1319 Example:
1320 UID is an integer and not None:
1321 ```python
1322 import bitbully as bb
1323
1324 board = bb.Board()
1325 u = board.uid()
1326
1327 assert isinstance(u, int)
1328 # Empty board has a well-defined, stable UID.
1329 assert board.uid() == u
1330 ```
1331
1332 Example:
1333 UID changes when the position changes:
1334 ```python
1335 import bitbully as bb
1336
1337 board = bb.Board()
1338 uid_before = board.uid()
1339
1340 assert board.play(1) # Make a move in column 1.
1341
1342 uid_after = board.uid()
1343 assert uid_after != uid_before
1344 ```
1345
1346 Example:
1347 Copies share the same UID while they are identical:
1348 ```python
1349 import bitbully as bb
1350
1351 board = bb.Board("0123")
1352
1353 # Create an independent copy of the same position.
1354 board_copy = board.copy()
1355
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
1359
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()
1364 ```
1365
1366 Example:
1367 Different move sequences leading to the same position share the same UID:
1368 ```python
1369 import bitbully as bb
1370
1371 board_1 = bb.Board("01234444")
1372 board_2 = bb.Board("44440123")
1373
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
1377
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()
1382 ```
1383 """
1384 return self._board.uid()
1385
1386 def current_player(self) -> int:
1387 """Returns the player whose turn it is to move.
1388
1389 The current player is derived from the **parity** of the number of tokens
1390 on the board:
1391
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.
1395
1396 Returns:
1397 int:
1398 The player to move:
1399
1400 - ``1`` → Player 1 (yellow, ``X``)
1401 - ``2`` → Player 2 (red, ``O``)
1402
1403 Example:
1404 ```python
1405 import bitbully as bb
1406
1407 # Empty board → Player 1 starts.
1408 board = bb.Board()
1409 assert board.current_player() == 1
1410 assert board.count_tokens() == 0
1411
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
1416
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
1421 ```
1422 """
1423 # Empty board: Player 1
1424 return 1 if self.count_tokens() % 2 == 0 else 2
1425
1426 def is_full(self) -> bool:
1427 """Checks whether the board has any empty cells left.
1428
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``.
1432
1433 Returns:
1434 bool:
1435 ``True`` if the board is completely full
1436 (no more legal moves possible), otherwise ``False``.
1437
1438 Example:
1439 ```python
1440 import bitbully as bb
1441
1442 board = bb.Board()
1443 assert not board.is_full()
1444 assert board.moves_left() == 42
1445 assert board.count_tokens() == 0
1446
1447 # Fill the board column by column.
1448 for _ in range(6):
1449 assert board.play("0123456") # one token per column, per row
1450
1451 # Now every cell is occupied.
1452 assert board.is_full()
1453 assert board.moves_left() == 0
1454 assert board.count_tokens() == 42
1455 ```
1456 """
1457 return self.moves_left() == 0
1458
1459 def is_game_over(self) -> bool:
1460 """Checks whether the game has ended (win or draw).
1461
1462 A game of Connect Four is considered **over** if:
1463
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]).
1468
1469 This method does **not** indicate *who* won; for that, use
1470 [Board.winner][src.bitbully.board.Board.winner].
1471
1472 Returns:
1473 bool:
1474 ``True`` if the game is over (win or draw), otherwise ``False``.
1475
1476 Example:
1477 Game over by a win:
1478 ```python
1479 import bitbully as bb
1480
1481 # Player 1 (X) wins horizontally on the bottom row.
1482 board = bb.Board()
1483 assert board.play("0101010")
1484
1485 assert board.has_win()
1486 assert board.is_game_over()
1487 assert board.winner() == 1
1488 ```
1489
1490 Example:
1491 Game over by a draw (full board, no winner):
1492 ```python
1493 import bitbully as bb
1494
1495 board, _ = bb.Board.random_board(42, forbid_direct_win=False)
1496
1497 assert board.is_full()
1498 assert not board.has_win()
1499 assert board.is_game_over()
1500 assert board.winner() is None
1501 ```
1502 """
1503 return self.has_win() or self.is_full()
1504
1505 def winner(self) -> int | None:
1506 """Returns the winning player, if the game has been won.
1507
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:
1511
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.
1514
1515 If there is no winner (i.e. [Board.has_win][src.bitbully.board.Board.has_win] is ``False``),
1516 this method returns ``None``.
1517
1518 Returns:
1519 int | None:
1520 The winning player, or ``None`` if there is no winner.
1521
1522 - ``1`` → Player 1 (yellow, ``X``)
1523 - ``2`` → Player 2 (red, ``O``)
1524 - ``None`` → No winner (game still ongoing or draw)
1525
1526 Example:
1527 Detecting a winner:
1528 ```python
1529 import bitbully as bb
1530
1531 # Player 1 wins with a horizontal line at the bottom.
1532 board = bb.Board()
1533 assert board.play("1122334")
1534
1535 assert board.has_win()
1536 assert board.is_game_over()
1537
1538 # It is now Player 2's turn to move next...
1539 assert board.current_player() == 2
1540
1541 # ...which implies Player 1 must be the winner.
1542 assert board.winner() == 1
1543 ```
1544
1545 Example:
1546 No winner yet:
1547 ```python
1548 import bitbully as bb
1549
1550 board = bb.Board()
1551 assert board.play("112233") # no connect-four yet
1552
1553 assert not board.has_win()
1554 assert not board.is_game_over()
1555 assert board.winner() is None
1556 ```
1557 """
1558 if not self.has_win():
1559 return None
1560 # Previous player = opposite of current_player
1561 return 2 if self.current_player() == 1 else 1
1562
1563 @classmethod
1564 def from_moves(cls, moves: Sequence[int] | str) -> Board:
1565 """Creates a board by replaying a sequence of moves from the empty position.
1566
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.).
1570
1571 Args:
1572 moves (Sequence[int] | str):
1573 The move sequence to replay from the starting position. Accepts:
1574
1575 - A sequence of integers (e.g. ``[3, 3, 3, 1]``)
1576 - A string of digits (e.g. ``"3331"``)
1577
1578 Each value represents a column index (0-6). Players alternate
1579 automatically between moves.
1580
1581 Returns:
1582 Board:
1583 A new `Board` instance representing the final position
1584 after all moves have been applied.
1585
1586 Example:
1587 ```python
1588 import bitbully as bb
1589
1590 # Create a position directly from a compact move string.
1591 board = bb.Board.from_moves("33333111")
1592
1593 # Equivalent to:
1594 # board = bb.Board()
1595 # assert board.play("33333111")
1596
1597 print(board)
1598 assert board.count_tokens() == 8
1599 assert not board.has_win()
1600 ```
1601 """
1602 board = cls()
1603 assert board.play(moves)
1604 return board
1605
1606 @classmethod
1607 def from_array(cls, arr: Sequence[Sequence[int]]) -> Board:
1608 """Creates a board directly from a 2D array representation.
1609
1610 This is a convenience wrapper around the main constructor [board.Board][src.bitbully.board.Board]
1611 and accepts the same array formats:
1612
1613 - **Row-major**: 6 x 7 (``[row][column]``), top row first.
1614 - **Column-major**: 7 x 6 (``[column][row]``), left column first.
1615
1616 Values must follow the usual convention:
1617
1618 - ``0`` → empty cell
1619 - ``1`` → Player 1 token (yellow, ``X``)
1620 - ``2`` → Player 2 token (red, ``O``)
1621
1622 Args:
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.
1627
1628 Returns:
1629 Board:
1630 A new `Board` instance representing the given layout.
1631
1632 Example:
1633 Using a 6 x 7 row-major layout:
1634 ```python
1635 import bitbully as bb
1636
1637 board_array = [
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)
1644 ]
1645
1646 board = bb.Board.from_array(board_array)
1647 print(board)
1648 ```
1649
1650 Example:
1651 Using a 7 x 6 column-major layout:
1652 ```python
1653 import bitbully as bb
1654
1655 board_array = [
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
1663 ]
1664
1665 board = bb.Board.from_array(board_array)
1666
1667 # Round-trip via to_array:
1668 assert board.to_array() == board_array
1669 ```
1670 """
1671 return cls(arr)
1672
1673 @staticmethod
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.
1676
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.
1679
1680 Args:
1681 n_ply (int):
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**.
1685
1686 Returns:
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.
1690
1691 Raises:
1692 ValueError: If `n_ply` is outside the valid range [0, 42].
1693
1694 Example:
1695 Basic usage:
1696 ```python
1697 import bitbully as bb
1698
1699 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1700
1701 print("Moves:", moves)
1702 print("Board:")
1703 print(board)
1704
1705 # The move list must match the requested ply.
1706 assert len(moves) == 10
1707
1708 # No immediate winning move when forbid_direct_win=True.
1709 assert not board.can_win_next()
1710 ```
1711
1712 Example:
1713 Using random boards in tests or simulations:
1714 ```python
1715 import bitbully as bb
1716
1717 # Generate 50 random 10-ply positions.
1718 for _ in range(50):
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
1724 ```
1725
1726 Example:
1727 Reconstructing the board manually from the move list:
1728 ```python
1729 import bitbully as bb
1730
1731 b1, moves = bb.Board.random_board(8, forbid_direct_win=True)
1732
1733 # Recreate the board using the move sequence:
1734 b2 = bb.Board(moves)
1735
1736 assert b1 == b2
1737 assert b1.to_string() == b2.to_string()
1738 assert b1.uid() == b2.uid()
1739 ```
1740
1741 Example:
1742 Ensure randomness by generating many distinct sequences:
1743 ```python
1744 import bitbully as bb
1745
1746 seen = set()
1747 for _ in range(20):
1748 _, moves = bb.Board.random_board(5, False)
1749 seen.add(tuple(moves))
1750
1751 # Very likely to see more than one unique sequence.
1752 assert len(seen) > 1
1753 ```
1754 """
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)
1758 board = Board()
1759 board._board = board_
1760
1761 return board, moves
1762
1763 def to_huffman(self) -> int:
1764 """Encode the current board position into a Huffman-compressed byte sequence.
1765
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.
1770
1771 The encoding is:
1772
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.
1776
1777 Returns:
1778 int: A Huffman-compressed representation of the current board
1779 state.
1780
1781 Raises:
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.
1785
1786 Example:
1787 Encode a position and verify that equivalent positions have the
1788 same Huffman code:
1789
1790 ```python
1791 import bitbully as bb
1792
1793 # Two different move sequences leading to the same final position.
1794 b1 = bb.Board("01234444")
1795 b2 = bb.Board("44440123")
1796
1797 h1 = b1.to_huffman()
1798 h2 = b2.to_huffman()
1799
1800 # Huffman encoding is purely position-based.
1801 assert h1 == h2
1802
1803 print(f"Huffman code: {h1}")
1804 ```
1805 Expected output:
1806 ```text
1807 Huffman code: 10120112
1808 ```
1809 """
1810 token_count = self.count_tokens()
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()
1814
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.
1817
1818 Args:
1819 non_losing (bool):
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.
1823 order_moves (bool):
1824 If ``True``, the returned list is ordered to prioritize moves (potentially more promising first).
1825
1826 Returns:
1827 list[int]: A list of column indices (0-6) where a token can be legally dropped.
1828
1829 Example:
1830 ```python
1831 import bitbully as bb
1832
1833 board = bb.Board()
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
1838 ```
1839
1840 Example:
1841 ```python
1842 import bitbully as bb
1843
1844 board = bb.Board()
1845 board.play("3322314")
1846 print(board)
1847 assert board.legal_moves() == list(range(7))
1848 assert board.legal_moves(non_losing=True) == [5]
1849 ```
1850
1851 Expected output:
1852 ```text
1853 _ _ _ _ _ _ _
1854 _ _ _ _ _ _ _
1855 _ _ _ _ _ _ _
1856 _ _ _ X _ _ _
1857 _ _ O O _ _ _
1858 _ O X X X _ _
1859 ```
1860 """
1861 return self._board.legalMoves(nonLosing=non_losing, orderMoves=order_moves)
1862
1863 @property
1864 def native(self) -> BoardCore:
1865 """Return the underlying native board representation.
1866
1867 This is intended for internal engine integrations and wrappers.
1868 Users should treat this as read-only.
1869
1870 Returns:
1871 BoardCore:
1872 The underlying native `BoardCore` instance representing the board state.
1873
1874 Notes:
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.
1878 """
1879 return self._board
int current_player(self)
Definition board.py:1386
Board from_array(cls, Sequence[Sequence[int]] arr)
Definition board.py:1607
int to_huffman(self)
Definition board.py:1763
Board copy(self)
Definition board.py:433
Board play_on_copy(self, int move)
Definition board.py:995
bool __eq__(self, object value)
Definition board.py:232
bool is_full(self)
Definition board.py:1426
None __init__(self, Sequence[Sequence[int]]|Sequence[int]|str|None init_with=None)
Definition board.py:21
int count_tokens(self)
Definition board.py:521
str to_string(self)
Definition board.py:1254
bool reset_board(self, Sequence[int]|Sequence[Sequence[int]]|str|None board=None)
Definition board.py:1036
list[Board] all_positions(self, int up_to_n_ply, bool exactly_n)
Definition board.py:309
str __repr__(self)
Definition board.py:298
list[list[int]] to_array(self, bool column_major_layout=True)
Definition board.py:1180
bool can_win_next(self, int|None move=None)
Definition board.py:383
Board from_moves(cls, Sequence[int]|str moves)
Definition board.py:1564
int|None winner(self)
Definition board.py:1505
int __hash__(self)
Definition board.py:647
Board mirror(self)
Definition board.py:774
bool is_game_over(self)
Definition board.py:1459
bool play(self, int|Sequence[int]|str move)
Definition board.py:907
tuple[Board, list[int]] random_board(int n_ply, bool forbid_direct_win)
Definition board.py:1674
str __str__(self)
Definition board.py:302
BoardCore native(self)
Definition board.py:1864
bool has_win(self)
Definition board.py:594
int moves_left(self)
Definition board.py:860
bool is_legal_move(self, int move)
Definition board.py:681
bool __ne__(self, object value)
Definition board.py:285
list[int] legal_moves(self, bool non_losing=False, bool order_moves=False)
Definition board.py:1815