BitBully 0.0.59-a2
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, cast
7
8from bitbully import bitbully_core
9
10
11class Board:
12 """Represents the state of a Connect Four board. Mostly a thin wrapper around BoardCore."""
13
14 def __init__(self, init_with: Sequence[Sequence[int]] | Sequence[int] | str | None = None) -> None:
15 """Initializes a Board instance.
16
17 Args:
18 init_with (Sequence[Sequence[int]] | Sequence[int] | str | None):
19 Optional initial board state. Accepts:
20 - 2D array (list, tuple, numpy-array) with shape 7x6 or 6x7
21 - 1D sequence of ints: a move sequence of columns (e.g., [0, 0, 2, 2, 3, 3])
22 - String: A move sequence of columns as string (e.g., "002233")
23 - None for an empty board
24
25 Raises:
26 ValueError: If the provided initial board state is invalid.
27
28 Example:
29 You can initialize an empty board in multiple ways:
30 ```python
31 import bitbully as bb
32
33 # Create an empty board using the default constructor.
34 board = bb.Board() # Starts with no tokens placed.
35
36 # Alternatively, initialize the board explicitly from a 2D list.
37 # Each inner list represents a column (7 columns total, 6 rows each).
38 # A value of 0 indicates an empty cell; 1 and 2 would represent player tokens.
39 board = bb.Board([[0] * 6 for _ in range(7)]) # Equivalent to an empty board.
40
41 # You can also set up a specific board position manually using a 6 x 7 layout,
42 # where each inner list represents a row instead of a column.
43 # (Both layouts are accepted by BitBully for convenience.)
44 # For more complex examples using 2D arrays, see the examples below.
45 board = bb.Board([[0] * 7 for _ in range(6)]) # Also equivalent to an empty board.
46
47 # Display the board in text form.
48 # The __repr__ method shows the current state (useful for debugging or interactive use).
49 board
50 ```
51 Expected output:
52 ```text
53 _ _ _ _ _ _ _
54 _ _ _ _ _ _ _
55 _ _ _ _ _ _ _
56 _ _ _ _ _ _ _
57 _ _ _ _ _ _ _
58 _ _ _ _ _ _ _
59 ```
60
61 The recommended way to initialize an empty board is simply `Board()`.
62
63 Example:
64 You can also initialize a board with a sequence of moves:
65 ```python
66 import bitbully as bb
67
68 # Initialize a board with a sequence of moves played in the center column.
69
70 # The list [3, 3, 3] represents three moves in column index 3 (zero-based).
71 # Moves alternate automatically between Player 1 (yellow, X) and Player 2 (red, O).
72 # After these three moves, the center column will contain:
73 # - Row 0: Player 1 token (bottom)
74 # - Row 1: Player 2 token
75 # - Row 2: Player 1 token
76 board = bb.Board([3, 3, 3])
77
78 # Display the resulting board.
79 # The textual output shows the tokens placed in the center column.
80 board
81 ```
82
83 Expected output:
84 ```text
85 _ _ _ _ _ _ _
86 _ _ _ _ _ _ _
87 _ _ _ _ _ _ _
88 _ _ _ X _ _ _
89 _ _ _ O _ _ _
90 _ _ _ X _ _ _
91 ```
92
93 Example:
94 You can also initialize a board using a string containing a move sequence:
95 ```python
96 import bitbully as bb
97
98 # Initialize a board using a compact move string.
99
100 # The string "33333111" represents a sequence of eight moves:
101 # 3 3 3 3 3 → five moves in the center column (index 3)
102 # 1 1 1 → three moves in the second column (index 1)
103 #
104 # Moves are applied in order, alternating automatically between Player 1 (yellow, X)
105 # and Player 2 (red, O), just as if you had called `board.play()` repeatedly.
106 #
107 # This shorthand is convenient for reproducing board states or test positions
108 # without having to provide long move lists.
109
110 board = bb.Board("33333111")
111
112 # Display the resulting board.
113 # The printed layout shows how the tokens stack in each column.
114 board
115 ```
116 Expected output:
117 ```text
118 _ _ _ _ _ _ _
119 _ _ _ X _ _ _
120 _ _ _ O _ _ _
121 _ O _ X _ _ _
122 _ X _ O _ _ _
123 _ O _ X _ _ _
124 ```
125
126 Example:
127 You can also initialize a board using a 2D array (list of lists):
128 ```python
129 import bitbully as bb
130
131 # Use a 6 x 7 list (rows x columns) to set up a specific board position manually.
132
133 # Each inner list represents a row of the Connect-4 grid.
134 # Convention:
135 # - 0 → empty cell
136 # - 1 → Player 1 token (yellow, X)
137 # - 2 → Player 2 token (red, O)
138 #
139 # The top list corresponds to the *top row* (row index 5),
140 # and the bottom list corresponds to the *bottom row* (row index 0).
141 # This layout matches the typical visual display of the board.
142
143 board_array = [
144 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
145 [0, 0, 0, 1, 0, 0, 0], # Row 4: Player 1 token in column 3
146 [0, 0, 0, 2, 0, 0, 0], # Row 3: Player 2 token in column 3
147 [0, 2, 0, 1, 0, 0, 0], # Row 2: tokens in columns 1 and 3
148 [0, 1, 0, 2, 0, 0, 0], # Row 1: tokens in columns 1 and 3
149 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom): tokens stacked lowest
150 ]
151
152 # Create a Board instance directly from the 2D list.
153 # This allows reconstructing arbitrary positions (e.g., from test data or saved states)
154 # without replaying the move sequence.
155 board = bb.Board(board_array)
156
157 # Display the resulting board state in text form.
158 board
159 ```
160 Expected output:
161 ```text
162 _ _ _ _ _ _ _
163 _ _ _ X _ _ _
164 _ _ _ O _ _ _
165 _ O _ X _ _ _
166 _ X _ O _ _ _
167 _ O _ X _ _ _
168 ```
169
170 Example:
171 You can also initialize a board using a 2D (7 x 6) array with columns as inner lists:
172 ```python
173 import bitbully as bb
174
175 # Use a 7 x 6 list (columns x rows) to set up a specific board position manually.
176
177 # Each inner list represents a **column** of the Connect-4 board, from left (index 0)
178 # to right (index 6). Each column contains six entries — one for each row, from
179 # bottom (index 0) to top (index 5).
180 #
181 # Convention:
182 # - 0 → empty cell
183 # - 1 → Player 1 token (yellow, X)
184 # - 2 → Player 2 token (red, O)
185 #
186 # This column-major layout matches the internal representation used by BitBully,
187 # where tokens are dropped into columns rather than filled row by row.
188
189 board_array = [
190 [0, 0, 0, 0, 0, 0], # Column 0 (leftmost)
191 [2, 1, 2, 0, 0, 0], # Column 1
192 [0, 0, 0, 0, 0, 0], # Column 2
193 [1, 2, 1, 2, 1, 0], # Column 3 (center)
194 [0, 0, 0, 0, 0, 0], # Column 4
195 [0, 0, 0, 0, 0, 0], # Column 5
196 [0, 0, 0, 0, 0, 0], # Column 6 (rightmost)
197 ]
198
199 # Create a Board instance directly from the 2D list.
200 # This allows reconstructing any arbitrary position (e.g., test cases, saved games)
201 # without replaying all moves individually.
202 board = bb.Board(board_array)
203
204 # Display the resulting board.
205 # The text output shows tokens as they would appear in a real Connect-4 grid.
206 board
207 ```
208 Expected output:
209 ```text
210 _ _ _ _ _ _ _
211 _ _ _ X _ _ _
212 _ _ _ O _ _ _
213 _ O _ X _ _ _
214 _ X _ O _ _ _
215 _ O _ X _ _ _
216 ```
217 """
218 self._board = bitbully_core.BoardCore()
219 if init_with is not None and not self.reset_board(init_with):
220 raise ValueError(
221 "Invalid initial board state provided. Check the examples in the docstring for valid formats."
222 )
223
224 def __eq__(self, value: object) -> bool:
225 """Checks equality between two Board instances.
226
227 Notes:
228 - Equality checks in BitBully compare the *exact board state* (bit patterns),
229 not just the move history.
230 - Two different move sequences can still yield the same position if they
231 result in identical token configurations.
232 - This is useful for comparing solver states, verifying test positions,
233 or detecting transpositions in search algorithms.
234
235 Args:
236 value (object): The other Board instance to compare against.
237
238 Returns:
239 bool: True if both boards are equal, False otherwise.
240
241 Raises:
242 NotImplementedError: If the other value is not a Board instance.
243
244 Example:
245 ```python
246 import bitbully as bb
247
248 # Create two boards that should represent *identical* game states.
249 board1 = bb.Board()
250 assert board1.play("33333111")
251
252 board2 = bb.Board()
253 # Play the same position step by step using a different but equivalent sequence.
254 # Internally, the final bitboard state will match `board1`.
255 assert board2.play("31133331")
256
257 # Boards with identical token placements are considered equal.
258 # Equality (`==`) and inequality (`!=`) operators are overloaded for convenience.
259 assert board1 == board2
260 assert not (board1 != board2)
261
262 # ------------------------------------------------------------------------------
263
264 # Create two boards that differ by one move.
265 board1 = bb.Board("33333111")
266 board2 = bb.Board("33333112") # One extra move in the last column (index 2)
267
268 # Since the token layout differs, equality no longer holds.
269 assert board1 != board2
270 assert not (board1 == board2)
271 ```
272 """
273 if not isinstance(value, Board):
274 raise NotImplementedError("Can only compare with another Board instance.")
275 return bool(self._board == value._board)
276
277 def __ne__(self, value: object) -> bool:
278 """Checks inequality between two Board instances.
279
280 See the documentation for [`bitbully.Board.__eq__`][src.bitbully.Board.__eq__] for details.
281
282 Args:
283 value (object): The other Board instance to compare against.
284
285 Returns:
286 bool: True if both boards are not equal, False otherwise.
287 """
288 return not self.__eq__(value)
289
290 def __repr__(self) -> str:
291 """Returns a string representation of the Board instance."""
292 return f"{self._board}"
293
294 def __str__(self) -> str:
295 """Return a human-readable ASCII representation (same as to_string()).
296
297 See the documentation for [`bitbully.Board.to_string`][src.bitbully.Board.to_string] for details.
298 """
299 return self.to_string()
300
301 def all_positions(self, up_to_n_ply: int, exactly_n: bool) -> list[Board]:
302 """Find all positions reachable from the current position up to a given ply.
303
304 This is a high-level wrapper around
305 `bitbully_core.BoardCore.allPositions`.
306
307 Starting from the **current** board, it generates all positions that can be
308 reached by playing additional moves such that the resulting position has:
309
310 - At most ``up_to_n_ply`` tokens on the board, if ``exactly_n`` is ``False``.
311 - Exactly ``up_to_n_ply`` tokens on the board, if ``exactly_n`` is ``True``.
312
313 Note:
314 The number of tokens already present in the current position is taken
315 into account. If ``up_to_n_ply`` is smaller than
316 ``self.count_tokens()``, the result is typically empty.
317
318 This function can grow combinatorially with ``up_to_n_ply`` and the
319 current position, so use it with care for large depths.
320
321 Args:
322 up_to_n_ply (int):
323 The maximum total number of tokens (ply) for generated positions.
324 Must be between 0 and 42 (inclusive).
325 exactly_n (bool):
326 If ``True``, only positions with exactly ``up_to_n_ply`` tokens
327 are returned. If ``False``, all positions with a token count
328 between the current number of tokens and ``up_to_n_ply`` are
329 included.
330
331 Returns:
332 list[Board]: A list of :class:`Board` instances representing all
333 reachable positions that satisfy the ply constraint.
334
335 Raises:
336 ValueError: If ``up_to_n_ply`` is outside the range ``[0, 42]``.
337
338 Example:
339 Compute all positions at exactly 3 ply from the empty board:
340
341 ```python
342 import bitbully as bb
343
344 # Start from an empty board.
345 board = bb.Board()
346
347 # Generate all positions that contain exactly 3 tokens.
348 positions = board.all_positions(3, exactly_n=True)
349
350 # According to OEIS A212693, there are exactly 238 distinct
351 # reachable positions with 3 played moves in standard Connect-4.
352 assert len(positions) == 238
353 ```
354
355 Reference:
356 - Number of distinct positions at ply *n*:
357 https://oeis.org/A212693
358
359 """
360 if not 0 <= up_to_n_ply <= 42:
361 raise ValueError(f"up_to_n_ply must be between 0 and 42 (inclusive), got {up_to_n_ply}.")
362
363 # Delegate to the C++ core, which returns a list of BoardCore objects.
364 core_positions = self._board.allPositions(up_to_n_ply, exactly_n)
365
366 # Wrap each BoardCore in a high-level Board instance.
367 positions: list[Board] = []
368 for core_board in core_positions:
369 b = Board() # start with an empty high-level Board
370 b._board = core_board # replace its internal BoardCore
371 positions.append(b)
372
373 return positions
374
375 def can_win_next(self, move: int | None = None) -> bool:
376 """Checks if the current player can win in the next move.
377
378 Args:
379 move (int | None): Optional column to check for an immediate win. If None, checks all columns.
380
381 Returns:
382 bool: True if the current player can win next, False otherwise.
383
384 See also: [`bitbully.Board.has_win`][src.bitbully.Board.has_win].
385
386 Example:
387 ```python
388 import bitbully as bb
389
390 # Create a board from a move string.
391 # The string "332311" represents a short sequence of alternating moves
392 # that results in a nearly winning position for Player 1 (yellow, X).
393 board = bb.Board("332311")
394
395 # Display the current board state (see below)
396 print(board)
397
398 # Player 1 (yellow, X) — who is next to move — can win immediately
399 # by placing a token in either column 0 or column 4.
400 assert board.can_win_next(0)
401 assert board.can_win_next(4)
402
403 # However, playing in other columns does not result in an instant win.
404 assert not board.can_win_next(2)
405 assert not board.can_win_next(3)
406
407 # You can also call `can_win_next()` without arguments to perform a general check.
408 # It returns True if the current player has *any* winning move available.
409 assert board.can_win_next()
410 ```
411 The board we created above looks like this:
412 ```text
413 _ _ _ _ _ _ _
414 _ _ _ _ _ _ _
415 _ _ _ _ _ _ _
416 _ _ _ O _ _ _
417 _ O _ O _ _ _
418 _ X X X _ _ _
419 ```
420 """
421 if move is None:
422 return self._board.canWin()
423 return bool(self._board.canWin(move))
424
425 def copy(self) -> Board:
426 """Creates a copy of the current Board instance.
427
428 The `copy()` method returns a new `Board` object that represents the
429 *same position* as the original at the time of copying. Subsequent
430 changes to one board do **not** affect the other — they are completely
431 independent.
432
433 Returns:
434 Board: A new Board instance that is a copy of the current one.
435
436 Example:
437 Create a board, copy it, and verify that both represent the same position:
438 ```python
439 import bitbully as bb
440
441 # Create a board from a compact move string.
442 board = bb.Board("33333111")
443
444 # Create an independent copy of the current position.
445 board_copy = board.copy()
446
447 # Both boards represent the same position and are considered equal.
448 assert board == board_copy
449 assert hash(board) == hash(board_copy)
450 assert board.to_string() == board_copy.to_string()
451
452 # Display the board state.
453 print(board)
454 ```
455 Expected output (both boards print the same position):
456 ```text
457 _ _ _ _ _ _ _
458 _ _ _ X _ _ _
459 _ _ _ O _ _ _
460 _ O _ X _ _ _
461 _ X _ O _ _ _
462 _ O _ X _ _ _
463 ```
464
465 Example:
466 Modifying the copy does not affect the original:
467 ```python
468 import bitbully as bb
469
470 board = bb.Board("33333111")
471
472 # Create a copy of the current position.
473 board_copy = board.copy()
474
475 # Play an additional move on the copied board only.
476 assert board_copy.play(0) # Drop a token into the leftmost column.
477
478 # Now the boards represent different positions.
479 assert board != board_copy
480
481 # The original board remains unchanged.
482 print("Original:")
483 print(board)
484
485 print("Modified copy:")
486 print(board_copy)
487 ```
488 Expected output:
489 ```text
490 Original:
491
492 _ _ _ _ _ _ _
493 _ _ _ X _ _ _
494 _ _ _ O _ _ _
495 _ O _ X _ _ _
496 _ X _ O _ _ _
497 _ O _ X _ _ _
498
499 Modified copy:
500
501 _ _ _ _ _ _ _
502 _ _ _ X _ _ _
503 _ _ _ O _ _ _
504 _ O _ X _ _ _
505 _ X _ O _ _ _
506 X O _ X _ _ _
507 ```
508 """
509 new_board = Board()
510 new_board._board = self._board.copy()
511 return new_board
512
513 def count_tokens(self) -> int:
514 """Counts the total number of tokens currently placed on the board.
515
516 This method simply returns how many moves have been played so far in the
517 current position — that is, the number of occupied cells on the 7x6 grid.
518
519 It does **not** distinguish between players; it only reports the total
520 number of tokens, regardless of whether they belong to Player 1 or Player 2.
521
522 Returns:
523 int: The total number of tokens on the board (between 0 and 42).
524
525 Example:
526 Count tokens on an empty board:
527 ```python
528 import bitbully as bb
529
530 board = bb.Board() # No moves played yet.
531 assert board.count_tokens() == 0
532
533 # The board is completely empty.
534 print(board)
535 ```
536 Expected output:
537 ```text
538 _ _ _ _ _ _ _
539 _ _ _ _ _ _ _
540 _ _ _ _ _ _ _
541 _ _ _ _ _ _ _
542 _ _ _ _ _ _ _
543 _ _ _ _ _ _ _
544 ```
545
546 Example:
547 Count tokens after a few moves:
548 ```python
549 import bitbully as bb
550
551 # Play three moves in the center column (index 3).
552 board = bb.Board()
553 assert board.play([3, 3, 3])
554
555 # Three tokens have been placed on the board.
556 assert board.count_tokens() == 3
557
558 print(board)
559 ```
560 Expected output:
561 ```text
562 _ _ _ _ _ _ _
563 _ _ _ _ _ _ _
564 _ _ _ _ _ _ _
565 _ _ _ X _ _ _
566 _ _ _ O _ _ _
567 _ _ _ X _ _ _
568 ```
569
570 Example:
571 Relation to the length of a move sequence:
572 ```python
573 import bitbully as bb
574
575 moves = "33333111" # 8 moves in total
576 board = bb.Board(moves)
577
578 # The number of tokens on the board always matches
579 # the number of moves that have been played.
580 # (as long as the input was valid)
581 assert board.count_tokens() == len(moves)
582 ```
583 """
584 return self._board.countTokens()
585
586 def has_win(self) -> bool:
587 """Checks if the current player has a winning position.
588
589 Returns:
590 bool: True if the current player has a winning position (4-in-a-row), False otherwise.
591
592 Unlike `can_win_next()`, which checks whether the current player *could* win
593 on their next move, the `has_win()` method determines whether a winning
594 condition already exists on the board.
595 This method is typically used right after a move to verify whether the game
596 has been won.
597
598 See also: [`bitbully.Board.can_win_next`][src.bitbully.Board.can_win_next].
599
600 Example:
601 ```python
602 import bitbully as bb
603
604 # Initialize a board from a move sequence.
605 # The string "332311" represents a position where Player 1 (yellow, X)
606 # is one move away from winning.
607 board = bb.Board("332311")
608
609 # At this stage, Player 1 has not yet won, but can win immediately
610 # by placing a token in either column 0 or column 4.
611 assert not board.has_win()
612 assert board.can_win_next(0) # Check column 0
613 assert board.can_win_next(4) # Check column 4
614 assert board.can_win_next() # General check (any winning move)
615
616 # Simulate Player 1 playing in column 4 — this completes
617 # a horizontal line of four tokens and wins the game.
618 assert board.play(4)
619
620 # Display the updated board to visualize the winning position.
621 print(board)
622
623 # The board now contains a winning configuration:
624 # Player 1 (yellow, X) has achieved a Connect-4.
625 assert board.has_win()
626 ```
627 Board from above, expected output:
628 ```text
629 _ _ _ _ _ _ _
630 _ _ _ _ _ _ _
631 _ _ _ _ _ _ _
632 _ _ _ O _ _ _
633 _ O _ O _ _ _
634 _ X X X X _ _
635 ```
636 """
637 return self._board.hasWin()
638
639 def __hash__(self) -> int:
640 """Returns a hash of the Board instance for use in hash-based collections.
641
642 Returns:
643 int: The hash value of the Board instance.
644
645 Example:
646 ```python
647 import bitbully as bb
648
649 # Create two boards that represent the same final position.
650 # The first board is initialized directly from a move string.
651 board1 = bb.Board("33333111")
652
653 # The second board is built incrementally by playing an equivalent sequence of moves.
654 # Even though the order of intermediate plays differs, the final layout of tokens
655 # (and thus the internal bitboard state) will be identical to `board1`.
656 board2 = bb.Board()
657 board2.play("31133331")
658
659 # Boards with identical configurations produce the same hash value.
660 # This allows them to be used efficiently as keys in dictionaries or members of sets.
661 assert hash(board1) == hash(board2)
662
663 # Display the board's hash value.
664 hash(board1)
665 ```
666 Expected output:
667 ```text
668 971238920548618160
669 ```
670 """
671 return self._board.hash()
672
673 def is_legal_move(self, move: int) -> bool:
674 """Checks if a move (column) is legal in the current position.
675
676 A move is considered *legal* if:
677
678 - The column index is within the valid range (0-6), **and**
679 - The column is **not full** (i.e. it still has at least one empty cell).
680
681 This method does **not** check for tactical consequences such as
682 leaving an immediate win to the opponent, nor does it stop being
683 usable once a player has already won. It purely validates whether a
684 token can be dropped into the given column according to the basic
685 rules of Connect Four. You have to check for wins separately using
686 [`bitbully.Board.has_win`][src.bitbully.Board.has_win].
687
688
689 Args:
690 move (int): The column index (0-6) to check.
691
692 Returns:
693 bool: True if the move is legal, False otherwise.
694
695 Example:
696 All moves are legal on an empty board:
697 ```python
698 import bitbully as bb
699
700 board = bb.Board() # Empty 7x6 board
701
702 # Every column index from 0 to 6 is a valid move.
703 for col in range(7):
704 assert board.is_legal_move(col)
705
706 # Out-of-range indices are always illegal.
707 assert not board.is_legal_move(-1)
708 assert not board.is_legal_move(7)
709 ```
710
711 Example:
712 Detecting an illegal move in a full column:
713 ```python
714 import bitbully as bb
715
716 # Fill the center column (index 3) with six tokens.
717 board = bb.Board()
718 assert board.play([3, 3, 3, 3, 3, 3])
719
720 # The center column is now full, so another move in column 3 is illegal.
721 assert not board.is_legal_move(3)
722
723 # Other columns are still available (as long as they are not full).
724 assert board.is_legal_move(0)
725 assert board.is_legal_move(6)
726
727 print(board)
728 ```
729 Expected output:
730 ```text
731 _ _ _ O _ _ _
732 _ _ _ X _ _ _
733 _ _ _ O _ _ _
734 _ _ _ X _ _ _
735 _ _ _ O _ _ _
736 _ _ _ X _ _ _
737 ```
738
739 Example:
740 This function only checks legality, not for situations where a player has won:
741 ```python
742 import bitbully as bb
743
744 # Player 1 (yellow, X) wins the game.
745 board = bb.Board()
746 assert board.play("1122334")
747
748 # Even though Player 1 has already won, moves in non-full columns are still legal.
749 for col in range(7):
750 assert board.is_legal_move(col)
751
752 print(board)
753 ```
754 Expected output:
755 ```text
756 _ _ _ _ _ _ _
757 _ _ _ _ _ _ _
758 _ _ _ _ _ _ _
759 _ _ _ _ _ _ _
760 _ O O O _ _ _
761 _ X X X X _ _
762 ```
763 """
764 return self._board.isLegalMove(move)
765
766 def mirror(self) -> Board:
767 """Returns a new Board instance that is the mirror image of the current board.
768
769 This method reflects the board **horizontally** around its vertical center column:
770 - Column 0 <-> Column 6
771 - Column 1 <-> Column 5
772 - Column 2 <-> Column 4
773 - Column 3 stays in the center
774
775 The player to move is not changed - only the spatial
776 arrangement of the tokens is mirrored. The original board remains unchanged;
777 `mirror()` always returns a **new** `Board` instance.
778
779 Returns:
780 Board: A new Board instance that is the mirror image of the current one.
781
782 Example:
783 Mirroring a simple asymmetric position:
784 ```python
785 import bitbully as bb
786
787 # Play four moves along the bottom row.
788 board = bb.Board()
789 assert board.play("0123") # Columns: 0, 1, 2, 3
790
791 # Create a mirrored copy of the board.
792 mirrored = board.mirror()
793
794 print("Original:")
795 print(board)
796
797 print("Mirrored:")
798 print(mirrored)
799 ```
800
801 Expected output:
802 ```text
803 Original:
804
805 _ _ _ _ _ _ _
806 _ _ _ _ _ _ _
807 _ _ _ _ _ _ _
808 _ _ _ _ _ _ _
809 _ _ _ _ _ _ _
810 X O X O _ _ _
811
812 Mirrored:
813
814 _ _ _ _ _ _ _
815 _ _ _ _ _ _ _
816 _ _ _ _ _ _ _
817 _ _ _ _ _ _ _
818 _ _ _ _ _ _ _
819 _ _ _ O X O X
820 ```
821
822 Example:
823 Mirroring a position that is already symmetric:
824 ```python
825 import bitbully as bb
826
827 # Central symmetry: one token in each outer column and in the center.
828 board = bb.Board([1, 3, 5])
829
830 mirrored = board.mirror()
831
832 # The mirrored position is identical to the original.
833 assert board == mirrored
834 assert hash(board) == hash(mirrored)
835
836 print(board)
837 ```
838 Expected output:
839 ```text
840 _ _ _ _ _ _ _
841 _ _ _ _ _ _ _
842 _ _ _ _ _ _ _
843 _ _ _ _ _ _ _
844 _ _ _ _ _ _ _
845 _ X _ O _ X _
846 ```
847 """
848 new_board = Board()
849 new_board._board = self._board.mirror()
850 return new_board
851
852 def moves_left(self) -> int:
853 """Returns the number of moves left until the board is full.
854
855 This is simply the number of *empty* cells remaining on the 7x6 grid.
856 On an empty board there are 42 free cells, so:
857
858 - At the start of the game: `moves_left() == 42`
859 - After `n` valid moves: `moves_left() == 42 - n`
860 - On a completely full board: `moves_left() == 0`
861
862 This method is equivalent to:
863 ```
864 42 - board.count_tokens()
865 ```
866 but implemented efficiently in the underlying C++ core.
867
868 Returns:
869 int: The number of moves left (0-42).
870
871 Example:
872 Moves left on an empty board:
873 ```python
874 import bitbully as bb
875
876 board = bb.Board() # No tokens placed yet.
877 assert board.moves_left() == 42
878 assert board.count_tokens() == 0
879 ```
880
881 Example:
882 Relation to the number of moves played:
883 ```python
884 import bitbully as bb
885
886 # Play five moves in various columns.
887 moves = [3, 3, 1, 4, 6]
888 board = bb.Board()
889 assert board.play(moves)
890
891 # Five tokens have been placed, so 42 - 5 = 37 moves remain.
892 assert board.count_tokens() == 5
893 assert board.moves_left() == 37
894 assert board.moves_left() + board.count_tokens() == 42
895 ```
896 """
897 return self._board.movesLeft()
898
899 def play(self, move: int | Sequence[int] | str) -> bool:
900 """Plays one or more moves for the current player.
901
902 The method updates the internal board state by dropping tokens
903 into the specified columns. Input can be:
904 - a single integer (column index 0 to 6),
905 - an iterable sequence of integers (e.g., `[3, 1, 3]` or `range(7)`),
906 - or a string of digits (e.g., `"33333111"`) representing the move order.
907
908 Args:
909 move (int | Sequence[int] | str):
910 The column index or sequence of column indices where tokens should be placed.
911
912 Returns:
913 bool: True if the move was played successfully, False if the move was illegal.
914
915
916 Example:
917 Play a sequence of moves into the center column (column index 3):
918 ```python
919 import bitbully as bb
920
921 board = bb.Board()
922 assert board.play([3, 3, 3]) # returns True on successful move
923 board
924 ```
925
926 Expected output:
927
928 ```
929 _ _ _ _ _ _ _
930 _ _ _ _ _ _ _
931 _ _ _ _ _ _ _
932 _ _ _ X _ _ _
933 _ _ _ O _ _ _
934 _ _ _ X _ _ _
935 ```
936
937 Example:
938 Play a sequence of moves across all columns:
939 ```python
940 import bitbully as bb
941
942 board = bb.Board()
943 assert board.play(range(7)) # returns True on successful move
944 board
945 ```
946 Expected output:
947 ```text
948 _ _ _ _ _ _ _
949 _ _ _ _ _ _ _
950 _ _ _ _ _ _ _
951 _ _ _ _ _ _ _
952 _ _ _ _ _ _ _
953 X O X O X O X
954 ```
955
956 Example:
957 Play a sequence using a string:
958 ```python
959 import bitbully as bb
960
961 board = bb.Board()
962 assert board.play("33333111") # returns True on successful move
963 board
964 ```
965 Expected output:
966 ```text
967 _ _ _ _ _ _ _
968 _ _ _ X _ _ _
969 _ _ _ O _ _ _
970 _ O _ X _ _ _
971 _ X _ O _ _ _
972 _ O _ X _ _ _
973 ```
974 """
975 # Case 1: string -> pass through directly
976 if isinstance(move, str):
977 return self._board.play(move)
978
979 # Case 2: int -> pass through directly
980 if isinstance(move, int):
981 return self._board.play(move)
982
983 # From here on, move is a Sequence[...] (but not str or int).
984 move_list: list[int] = [int(v) for v in cast(Sequence[Any], move)]
985 return self._board.play(move_list)
986
987 def play_on_copy(self, move: int) -> Board:
988 """Return a new board with the given move applied, leaving the current board unchanged.
989
990 Args:
991 move (int):
992 The column index (0-6) in which to play the move.
993
994 Returns:
995 Board:
996 A new Board instance representing the position after the move.
997
998 Raises:
999 ValueError: If the move is illegal (e.g. column is full or out of range).
1000
1001 Example:
1002 ```python
1003 import bitbully as bb
1004
1005 board = bb.Board("333") # Some existing position
1006 new_board = board.play_on_copy(4)
1007
1008 # The original board is unchanged.
1009 assert board.count_tokens() == 3
1010
1011 # The returned board includes the new move.
1012 assert new_board.count_tokens() == 4
1013 assert new_board != board
1014 ```
1015 """
1016 # Delegate to C++ (this returns a BoardCore instance)
1017 core_new = self._board.playMoveOnCopy(move)
1018
1019 if core_new is None:
1020 # C++ signals illegal move by returning a null board
1021 raise ValueError(f"Illegal move: column {move}")
1022
1023 # Wrap in a new high-level Board object
1024 new_board = Board()
1025 new_board._board = core_new
1026 return new_board
1027
1028 def reset_board(self, board: Sequence[int] | Sequence[Sequence[int]] | str | None = None) -> bool:
1029 """Resets the board or sets (overrides) the board to a specific state.
1030
1031 Args:
1032 board (Sequence[int] | Sequence[Sequence[int]] | str | None):
1033 The new board state. Accepts:
1034 - 2D array (list, tuple, numpy-array) with shape 7x6 or 6x7
1035 - 1D sequence of ints: a move sequence of columns (e.g., [0, 0, 2, 2, 3, 3])
1036 - String: A move sequence of columns as string (e.g., "002233...")
1037 - None: to reset to an empty board
1038
1039 Returns:
1040 bool: True if the board was set successfully, False otherwise.
1041
1042 Example:
1043 Reset the board to an empty state:
1044 ```python
1045 import bitbully as bb
1046
1047 # Create a temporary board position from a move string.
1048 # The string "0123456" plays one token in each column (0-6) in sequence.
1049 board = bb.Board("0123456")
1050
1051 # Reset the board to an empty state.
1052 # Calling `reset_board()` clears all tokens and restores the starting position.
1053 # No moves → an empty board.
1054 assert board.reset_board()
1055 board
1056 ```
1057 Expected output:
1058 ```text
1059 _ _ _ _ _ _ _
1060 _ _ _ _ _ _ _
1061 _ _ _ _ _ _ _
1062 _ _ _ _ _ _ _
1063 _ _ _ _ _ _ _
1064 _ _ _ _ _ _ _
1065 ```
1066
1067 Example:
1068 (Re-)Set the board using a move sequence string:
1069 ```python
1070 import bitbully as bb
1071
1072 # This is just a temporary setup; it will be replaced below.
1073 board = bb.Board("0123456")
1074
1075 # Set the board state directly from a move sequence.
1076 # The list [3, 3, 3] represents three consecutive moves in the center column (index 3).
1077 # Moves alternate automatically between Player 1 (yellow) and Player 2 (red).
1078 #
1079 # The `reset_board()` method clears the current position and replays the given moves
1080 # from an empty board — effectively overriding any existing board state.
1081 assert board.reset_board([3, 3, 3])
1082
1083 # Display the updated board to verify the new position.
1084 board
1085 ```
1086 Expected output:
1087 ```text
1088 _ _ _ _ _ _ _
1089 _ _ _ _ _ _ _
1090 _ _ _ _ _ _ _
1091 _ _ _ X _ _ _
1092 _ _ _ O _ _ _
1093 _ _ _ X _ _ _
1094 ```
1095
1096 Example:
1097 You can also set the board using other formats, such as a 2D array or a string.
1098 See the examples in the [`bitbully.Board.__init__`][src.bitbully.Board.__init__] docstring for details.
1099
1100 ```python
1101 # Briefly demonstrate the different input formats accepted by `reset_board()`.
1102 import bitbully as bb
1103
1104 # Create an empty board instance
1105 board = bb.Board()
1106
1107 # Variant 1: From a list of moves (integers)
1108 # Each number represents a column index (0-6); moves alternate between players.
1109 assert board.reset_board([3, 3, 3])
1110
1111 # Variant 2: From a compact move string
1112 # Equivalent to the list above — useful for quick testing or serialization.
1113 assert board.reset_board("33333111")
1114
1115 # Variant 3: From a 2D list in row-major format (6 x 7)
1116 # Each inner list represents a row (top to bottom).
1117 # 0 = empty, 1 = Player 1, 2 = Player 2.
1118 board_array = [
1119 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
1120 [0, 0, 0, 1, 0, 0, 0], # Row 4
1121 [0, 0, 0, 2, 0, 0, 0], # Row 3
1122 [0, 2, 0, 1, 0, 0, 0], # Row 2
1123 [0, 1, 0, 2, 0, 0, 0], # Row 1
1124 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom)
1125 ]
1126 assert board.reset_board(board_array)
1127
1128 # Variant 4: From a 2D list in column-major format (7 x 6)
1129 # Each inner list represents a column (left to right); this matches BitBully's internal layout.
1130 board_array = [
1131 [0, 0, 0, 0, 0, 0], # Column 0 (leftmost)
1132 [2, 1, 2, 1, 0, 0], # Column 1
1133 [0, 0, 0, 0, 0, 0], # Column 2
1134 [1, 2, 1, 2, 1, 0], # Column 3 (center)
1135 [0, 0, 0, 0, 0, 0], # Column 4
1136 [2, 1, 2, 0, 0, 0], # Column 5
1137 [0, 0, 0, 0, 0, 0], # Column 6 (rightmost)
1138 ]
1139 assert board.reset_board(board_array)
1140
1141 # Display the final board state in text form
1142 board
1143 ```
1144
1145 Expected output:
1146 ```text
1147 _ _ _ _ _ _ _
1148 _ _ _ X _ _ _
1149 _ X _ O _ _ _
1150 _ O _ X _ O _
1151 _ X _ O _ X _
1152 _ O _ X _ O _
1153 ```
1154 """
1155 if board is None:
1156 return self._board.setBoard([])
1157 if isinstance(board, str):
1158 return self._board.setBoard(board)
1159
1160 # From here on, board is a Sequence[...] (but not str).
1161 # Distinguish 2D vs 1D by inspecting the first element.
1162 if len(board) > 0 and isinstance(board[0], Sequence) and not isinstance(board[0], (str, bytes)):
1163 # Case 2: 2D -> list[list[int]]
1164 # Convert inner sequences to lists of ints
1165 grid: list[list[int]] = [[int(v) for v in row] for row in cast(Sequence[Sequence[Any]], board)]
1166 return self._board.setBoard(grid)
1167
1168 # Case 3: 1D -> list[int]
1169 moves: list[int] = [int(v) for v in cast(Sequence[Any], board)]
1170 return self._board.setBoard(moves)
1171
1172 def to_array(self, column_major_layout: bool = True) -> list[list[int]]:
1173 """Returns the board state as a 2D array (list of lists).
1174
1175 This layout is convenient for printing, serialization, or converting
1176 to a NumPy array for further analysis.
1177
1178 Args:
1179 column_major_layout (bool): Use column-major format if set to `True`,
1180 otherwise the row-major-layout is used.
1181
1182 Returns:
1183 list[list[int]]: A 7x6 2D list representing the board state.
1184
1185 Raises:
1186 NotImplementedError: If `column_major_layout` is set to `False`.
1187
1188 Example:
1189 === "Column-major Format:"
1190
1191 The returned array is in **column-major** format with shape `7 x 6`
1192 (`[column][row]`):
1193
1194 - There are 7 inner lists, one for each column of the board.
1195 - Each inner list has 6 integers, one for each row.
1196 - Row index `0` corresponds to the **bottom row**,
1197 row index `5` to the **top row**.
1198 - Convention:
1199 - `0` -> empty cell
1200 - `1` -> Player 1 token (yellow, X)
1201 - `2` -> Player 2 token (red, O)
1202
1203 ```python
1204 import bitbully as bb
1205 from pprint import pprint
1206
1207 # Create a position from a move sequence.
1208 board = bb.Board("33333111")
1209
1210 # Extract the board as a 2D list (rows x columns).
1211 arr = board.to_array()
1212
1213 # Reconstruct the same position from the 2D array.
1214 board2 = bb.Board(arr)
1215
1216 # Both boards represent the same position.
1217 assert board == board2
1218 assert board.to_array() == board2.to_array()
1219
1220 # print ther result of `board.to_array()`:
1221 pprint(board.to_array())
1222 ```
1223 Expected output:
1224 ```text
1225 [[0, 0, 0, 0, 0, 0],
1226 [2, 1, 2, 0, 0, 0],
1227 [0, 0, 0, 0, 0, 0],
1228 [1, 2, 1, 2, 1, 0],
1229 [0, 0, 0, 0, 0, 0],
1230 [0, 0, 0, 0, 0, 0],
1231 [0, 0, 0, 0, 0, 0]]
1232 ```
1233
1234 === "Row-major Format:"
1235
1236 ``` markdown
1237 TODO: This is not supported yet
1238 ```
1239 """
1240 if not column_major_layout:
1241 # TODO: Implement in C++
1242 raise NotImplementedError("Row-major Layout is yet to be implemented")
1243
1244 return self._board.toArray()
1245
1246 def to_string(self) -> str:
1247 """Returns a human-readable ASCII representation of the board.
1248
1249 The returned string shows the **current board position** as a 6x7 grid,
1250 laid out exactly as it would appear when you print a `Board` instance:
1251
1252 - 6 lines of text, one per row (top row first, bottom row last)
1253 - 7 entries per row, separated by two spaces
1254 - `_` represents an empty cell
1255 - `X` represents a token from Player 1 (yellow)
1256 - `O` represents a token from Player 2 (red)
1257
1258 This is useful when you want to explicitly capture the board as a string
1259 (e.g., for logging, debugging, or embedding into error messages) instead
1260 of relying on `print(board)` or `repr(board)`.
1261
1262 Returns:
1263 str: A multi-line ASCII string representing the board state.
1264
1265 Example:
1266 Using `to_string()` on an empty board:
1267 ```python
1268 import bitbully as bb
1269
1270 board = bb.Board("33333111")
1271
1272 s = board.to_string()
1273 print(s)
1274 ```
1275
1276 Expected output:
1277 ```text
1278 _ _ _ _ _ _ _
1279 _ _ _ X _ _ _
1280 _ _ _ O _ _ _
1281 _ O _ X _ _ _
1282 _ X _ O _ _ _
1283 _ O _ X _ _ _
1284 ```
1285 """
1286 return self._board.toString()
1287
1288 def uid(self) -> int:
1289 """Returns a unique identifier for the current board state.
1290
1291 The UID is a deterministic integer computed from the internal bitboard
1292 representation of the position. It is **stable**, **position-based**, and
1293 uniquely tied to the exact token layout **and** the side to move.
1294
1295 Key properties:
1296
1297 - Boards with the **same** configuration (tokens + player to move) always
1298 produce the **same** UID.
1299 - Any change to the board (e.g., after a legal move) will almost always
1300 result in a **different** UID.
1301 - Copies of a board created via the copy constructor or `Board.copy()`
1302 naturally share the same UID as long as their states remain identical.
1303
1304 Unlike `__hash__()`, the UID is not optimized for hash-table dispersion.
1305 For use in transposition tables, caching, or dictionary/set keys,
1306 prefer `__hash__()` since it provides a higher-quality hash distribution.
1307
1308 Returns:
1309 int: A unique integer identifier for the board state.
1310
1311 Example:
1312 UID is an integer and not None:
1313 ```python
1314 import bitbully as bb
1315
1316 board = bb.Board()
1317 u = board.uid()
1318
1319 assert isinstance(u, int)
1320 # Empty board has a well-defined, stable UID.
1321 assert board.uid() == u
1322 ```
1323
1324 Example:
1325 UID changes when the position changes:
1326 ```python
1327 import bitbully as bb
1328
1329 board = bb.Board()
1330 uid_before = board.uid()
1331
1332 assert board.play(1) # Make a move in column 1.
1333
1334 uid_after = board.uid()
1335 assert uid_after != uid_before
1336 ```
1337
1338 Example:
1339 Copies share the same UID while they are identical:
1340 ```python
1341 import bitbully as bb
1342
1343 board = bb.Board("0123")
1344
1345 # Create an independent copy of the same position.
1346 board_copy = board.copy()
1347
1348 assert board is not board_copy # Different objects
1349 assert board == board_copy # Same position
1350 assert board.uid() == board_copy.uid() # Same UID
1351
1352 # After modifying the copy, they diverge.
1353 assert board_copy.play(4)
1354 assert board != board_copy
1355 assert board.uid() != board_copy.uid()
1356 ```
1357
1358 Example:
1359 Different move sequences leading to the same position share the same UID:
1360 ```python
1361 import bitbully as bb
1362
1363 board_1 = bb.Board("01234444")
1364 board_2 = bb.Board("44440123")
1365
1366 assert board_1 is not board_2 # Different objects
1367 assert board_1 == board_2 # Same position
1368 assert board_1.uid() == board_2.uid() # Same UID
1369
1370 # After modifying the copy, they diverge.
1371 assert board_1.play(4)
1372 assert board_1 != board_2
1373 assert board_1.uid() != board_2.uid()
1374 ```
1375 """
1376 return self._board.uid()
1377
1378 @property
1379 def current_player(self) -> int:
1380 """Returns the player whose turn it is to move.
1381
1382 The current player is derived from the **parity** of the number of tokens
1383 on the board:
1384
1385 - Player 1 (yellow, ``X``) moves first on an empty board.
1386 - After an even number of moves → it is Player 1's turn.
1387 - After an odd number of moves → it is Player 2's turn.
1388
1389 Returns:
1390 int:
1391 The player to move:
1392
1393 - ``1`` → Player 1 (yellow, ``X``)
1394 - ``2`` → Player 2 (red, ``O``)
1395
1396 Example:
1397 ```python
1398 import bitbully as bb
1399
1400 # Empty board → Player 1 starts.
1401 board = bb.Board()
1402 assert board.current_player == 1
1403 assert board.count_tokens() == 0
1404
1405 # After one move, it's Player 2's turn.
1406 assert board.play(3)
1407 assert board.count_tokens() == 1
1408 assert board.current_player == 2
1409
1410 # After a second move, it's again Player 1's turn.
1411 assert board.play(4)
1412 assert board.count_tokens() == 2
1413 assert board.current_player == 1
1414 ```
1415 """
1416 # Empty board: Player 1
1417 return 1 if self.count_tokens() % 2 == 0 else 2
1418
1419 def is_full(self) -> bool:
1420 """Checks whether the board has any empty cells left.
1421
1422 A Connect Four board has 42 cells in total (7 columns x 6 rows).
1423 This method returns ``True`` if **all** cells are occupied, i.e.
1424 when [`bitbully.Board.moves_left`][src.bitbully.Board.moves_left] returns ``0``.
1425
1426 Returns:
1427 bool:
1428 ``True`` if the board is completely full
1429 (no more legal moves possible), otherwise ``False``.
1430
1431 Example:
1432 ```python
1433 import bitbully as bb
1434
1435 board = bb.Board()
1436 assert not board.is_full()
1437 assert board.moves_left() == 42
1438 assert board.count_tokens() == 0
1439
1440 # Fill the board column by column.
1441 for _ in range(6):
1442 assert board.play("0123456") # one token per column, per row
1443
1444 # Now every cell is occupied.
1445 assert board.is_full()
1446 assert board.moves_left() == 0
1447 assert board.count_tokens() == 42
1448 ```
1449 """
1450 return self.moves_left() == 0
1451
1452 def is_game_over(self) -> bool:
1453 """Checks whether the game has ended (win or draw).
1454
1455 A game of Connect Four is considered **over** if:
1456
1457 - One of the players has a winning position
1458 (see [`bitbully.Board.has_win`][src.bitbully.Board.has_win]), **or**
1459 - The board is completely full and no further moves can be played
1460 (see [`bitbully.Board.is_full`][src.bitbully.Board.is_full]).
1461
1462 This method does **not** indicate *who* won; for that, use
1463 [`bitbully.Board.winner`][src.bitbully.Board.winner].
1464
1465 Returns:
1466 bool:
1467 ``True`` if the game is over (win or draw), otherwise ``False``.
1468
1469 Example:
1470 Game over by a win:
1471 ```python
1472 import bitbully as bb
1473
1474 # Player 1 (X) wins horizontally on the bottom row.
1475 board = bb.Board()
1476 assert board.play("0101010")
1477
1478 assert board.has_win()
1479 assert board.is_game_over()
1480 assert board.winner() == 1
1481 ```
1482
1483 Example:
1484 Game over by a draw (full board, no winner):
1485 ```python
1486 import bitbully as bb
1487
1488 board, _ = bb.Board.random_board(42, forbid_direct_win=False)
1489
1490 assert board.is_full()
1491 assert not board.has_win()
1492 assert board.is_game_over()
1493 assert board.winner() is None
1494 ```
1495 """
1496 return self.has_win() or self.is_full()
1497
1498 def winner(self) -> int | None:
1499 """Returns the winning player, if the game has been won.
1500
1501 This helper interprets the current board under the assumption that
1502 [`bitbully.Board.has_win`][src.bitbully.Board.has_win] indicates **the last move** created a
1503 winning configuration. In that case, the winner is the *previous* player:
1504
1505 - If it is currently Player 1's turn, then Player 2 must have just won.
1506 - If it is currently Player 2's turn, then Player 1 must have just won.
1507
1508 If there is no winner (i.e. [`bitbully.Board.has_win`][src.bitbully.Board.has_win] is ``False``),
1509 this method returns ``None``.
1510
1511 Returns:
1512 int | None:
1513 The winning player, or ``None`` if there is no winner.
1514
1515 - ``1`` → Player 1 (yellow, ``X``)
1516 - ``2`` → Player 2 (red, ``O``)
1517 - ``None`` → No winner (game still ongoing or draw)
1518
1519 Example:
1520 Detecting a winner:
1521 ```python
1522 import bitbully as bb
1523
1524 # Player 1 wins with a horizontal line at the bottom.
1525 board = bb.Board()
1526 assert board.play("1122334")
1527
1528 assert board.has_win()
1529 assert board.is_game_over()
1530
1531 # It is now Player 2's turn to move next...
1532 assert board.current_player == 2
1533
1534 # ...which implies Player 1 must be the winner.
1535 assert board.winner() == 1
1536 ```
1537
1538 Example:
1539 No winner yet:
1540 ```python
1541 import bitbully as bb
1542
1543 board = bb.Board()
1544 assert board.play("112233") # no connect-four yet
1545
1546 assert not board.has_win()
1547 assert not board.is_game_over()
1548 assert board.winner() is None
1549 ```
1550 """
1551 if not self.has_win():
1552 return None
1553 # Previous player = opposite of current_player
1554 return 2 if self.current_player == 1 else 1
1555
1556 @classmethod
1557 def from_moves(cls, moves: Sequence[int] | str) -> Board:
1558 """Creates a board by replaying a sequence of moves from the empty position.
1559
1560 This is a convenience constructor around [`bitbully.Board.play`][src.bitbully.Board.play].
1561 It starts from an empty board and applies the given move sequence, assuming
1562 it is **legal** (no out-of-range columns, no moves in full columns, etc.).
1563
1564 Args:
1565 moves (Sequence[int] | str):
1566 The move sequence to replay from the starting position. Accepts:
1567
1568 - A sequence of integers (e.g. ``[3, 3, 3, 1]``)
1569 - A string of digits (e.g. ``"3331"``)
1570
1571 Each value represents a column index (0-6). Players alternate
1572 automatically between moves.
1573
1574 Returns:
1575 Board:
1576 A new `Board` instance representing the final position
1577 after all moves have been applied.
1578
1579 Example:
1580 ```python
1581 import bitbully as bb
1582
1583 # Create a position directly from a compact move string.
1584 board = bb.Board.from_moves("33333111")
1585
1586 # Equivalent to:
1587 # board = bb.Board()
1588 # assert board.play("33333111")
1589
1590 print(board)
1591 assert board.count_tokens() == 8
1592 assert not board.has_win()
1593 ```
1594 """
1595 board = cls()
1596 assert board.play(moves)
1597 return board
1598
1599 @classmethod
1600 def from_array(cls, arr: Sequence[Sequence[int]]) -> Board:
1601 """Creates a board directly from a 2D array representation.
1602
1603 This is a convenience wrapper around the main constructor
1604 [`bitbully.Board.__init__`][src.bitbully.Board.__init__]
1605 and accepts the same array formats:
1606
1607 - **Row-major**: 6 x 7 (``[row][column]``), top row first.
1608 - **Column-major**: 7 x 6 (``[column][row]``), left column first.
1609
1610 Values must follow the usual convention:
1611
1612 - ``0`` → empty cell
1613 - ``1`` → Player 1 token (yellow, ``X``)
1614 - ``2`` → Player 2 token (red, ``O``)
1615
1616 Args:
1617 arr (Sequence[Sequence[int]]):
1618 A 2D array describing the board state, either in row-major or
1619 column-major layout. See the examples in
1620 [`bitbully.Board.__init__`][src.bitbully.Board.__init__] for details.
1621
1622 Returns:
1623 Board:
1624 A new `Board` instance representing the given layout.
1625
1626 Example:
1627 Using a 6 x 7 row-major layout:
1628 ```python
1629 import bitbully as bb
1630
1631 board_array = [
1632 [0, 0, 0, 0, 0, 0, 0], # Row 5 (top)
1633 [0, 0, 0, 1, 0, 0, 0], # Row 4
1634 [0, 0, 0, 2, 0, 0, 0], # Row 3
1635 [0, 2, 0, 1, 0, 0, 0], # Row 2
1636 [0, 1, 0, 2, 0, 0, 0], # Row 1
1637 [0, 2, 0, 1, 0, 0, 0], # Row 0 (bottom)
1638 ]
1639
1640 board = bb.Board.from_array(board_array)
1641 print(board)
1642 ```
1643
1644 Example:
1645 Using a 7 x 6 column-major layout:
1646 ```python
1647 import bitbully as bb
1648
1649 board_array = [
1650 [0, 0, 0, 0, 0, 0], # Column 0
1651 [2, 1, 2, 1, 0, 0], # Column 1
1652 [0, 0, 0, 0, 0, 0], # Column 2
1653 [1, 2, 1, 2, 1, 0], # Column 3
1654 [0, 0, 0, 0, 0, 0], # Column 4
1655 [2, 1, 2, 0, 0, 0], # Column 5
1656 [0, 0, 0, 0, 0, 0], # Column 6
1657 ]
1658
1659 board = bb.Board.from_array(board_array)
1660
1661 # Round-trip via to_array:
1662 assert board.to_array() == board_array
1663 ```
1664 """
1665 return cls(arr)
1666
1667 @staticmethod
1668 def random_board(n_ply: int, forbid_direct_win: bool) -> tuple[Board, list[int]]:
1669 """Generates a random board state by playing a specified number of random moves.
1670
1671 If ``forbid_direct_win`` is ``True``, the generated position is guaranteed
1672 **not** to contain an immediate winning move for the player to move.
1673
1674 Args:
1675 n_ply (int):
1676 Number of random moves to simulate (0-42).
1677 forbid_direct_win (bool):
1678 If ``True``, ensures the resulting board has **no immediate winning move**.
1679
1680 Returns:
1681 tuple[Board, list[int]]:
1682 A pair ``(board, moves)`` where ``board`` is the generated position
1683 and ``moves`` are the exact random moves performed.
1684
1685 Raises:
1686 ValueError: If `n_ply` is outside the valid range [0, 42].
1687
1688 Example:
1689 Basic usage:
1690 ```python
1691 import bitbully as bb
1692
1693 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1694
1695 print("Moves:", moves)
1696 print("Board:")
1697 print(board)
1698
1699 # The move list must match the requested ply.
1700 assert len(moves) == 10
1701
1702 # No immediate winning move when forbid_direct_win=True.
1703 assert not board.can_win_next()
1704 ```
1705
1706 Example:
1707 Using random boards in tests or simulations:
1708 ```python
1709 import bitbully as bb
1710
1711 # Generate 50 random 10-ply positions.
1712 for _ in range(50):
1713 board, moves = bb.Board.random_board(10, forbid_direct_win=True)
1714 assert len(moves) == 10
1715 assert not board.has_win() # Game should not be over
1716 assert board.count_tokens() == 10 # All generated boards contain exactly 10 tokens
1717 assert not board.can_win_next() # Since `forbid_direct_win=True`, no immediate threat
1718 ```
1719
1720 Example:
1721 Reconstructing the board manually from the move list:
1722 ```python
1723 import bitbully as bb
1724
1725 b1, moves = bb.Board.random_board(8, forbid_direct_win=True)
1726
1727 # Recreate the board using the move sequence:
1728 b2 = bb.Board(moves)
1729
1730 assert b1 == b2
1731 assert b1.to_string() == b2.to_string()
1732 assert b1.uid() == b2.uid()
1733 ```
1734
1735 Example:
1736 Ensure randomness by generating many distinct sequences:
1737 ```python
1738 import bitbully as bb
1739
1740 seen = set()
1741 for _ in range(20):
1742 _, moves = bb.Board.random_board(5, False)
1743 seen.add(tuple(moves))
1744
1745 # Very likely to see more than one unique sequence.
1746 assert len(seen) > 1
1747 ```
1748 """
1749 if not 0 <= n_ply <= 42:
1750 raise ValueError(f"n_ply must be between 0 and 42 (inclusive), got {n_ply}.")
1751 board_, moves = bitbully_core.BoardCore.randomBoard(n_ply, forbid_direct_win)
1752 board = Board()
1753 board._board = board_
1754
1755 return board, moves
1756
1757 def to_huffman(self) -> int:
1758 """Encode the current board position into a Huffman-compressed byte sequence.
1759
1760 This is a high-level wrapper around
1761 `bitbully_core.BoardCore.toHuffman`. The returned int encodes the
1762 exact token layout **and** the side to move using the same format as
1763 the BitBully opening databases.
1764
1765 The encoding is:
1766
1767 - Deterministic: the same position always yields the same byte sequence.
1768 - Compact: suitable for storage (of positions with little number of tokens),
1769 or lookups in the BitBully database format.
1770
1771 Returns:
1772 int: A Huffman-compressed representation of the current board
1773 state.
1774
1775 Raises:
1776 NotImplementedError:
1777 If the position does not contain exactly 8 or 12 tokens, as the
1778 Huffman encoding is only defined for these cases.
1779
1780 Example:
1781 Encode a position and verify that equivalent positions have the
1782 same Huffman code:
1783
1784 ```python
1785 import bitbully as bb
1786
1787 # Two different move sequences leading to the same final position.
1788 b1 = bb.Board("01234444")
1789 b2 = bb.Board("44440123")
1790
1791 h1 = b1.to_huffman()
1792 h2 = b2.to_huffman()
1793
1794 # Huffman encoding is purely position-based.
1795 assert h1 == h2
1796
1797 print(f"Huffman code: {h1}")
1798 ```
1799 Expected output:
1800 ```text
1801 Huffman code: 10120112
1802 ```
1803 """
1804 token_count = self.count_tokens()
1805 if token_count != 8 and token_count != 12:
1806 raise NotImplementedError("to_huffman() is only implemented for positions with 8 or 12 tokens.")
1807 return self._board.toHuffman()
1808
1809 def legal_moves(self, non_losing: bool = False, order_moves: bool = False) -> list[int]:
1810 """Returns a list of all legal moves (non-full columns) for the current board state.
1811
1812 Args:
1813 non_losing (bool):
1814 If ``True``, only returns moves that do **not** allow the opponent
1815 to win immediately on their next turn. The list might be empty
1816 If ``False``, all legal moves are returned.
1817 order_moves (bool):
1818 If ``True``, the returned list is ordered to prioritize moves (potentially more promising first).
1819
1820 Returns:
1821 list[int]: A list of column indices (0-6) where a token can be legally dropped.
1822
1823 Example:
1824 ```python
1825 import bitbully as bb
1826
1827 board = bb.Board()
1828 legal_moves = board.legal_moves()
1829 assert set(legal_moves) == set(range(7)) # All columns are initially legal
1830 assert set(legal_moves) == set(board.legal_moves(order_moves=True))
1831 board.legal_moves(order_moves=True) == [3, 2, 4, 1, 5, 0, 6] # Center column prioritized
1832 ```
1833 """
1834 return self._board.legalMoves(nonLosing=non_losing, orderMoves=order_moves)
Board from_array(cls, Sequence[Sequence[int]] arr)
Definition board.py:1600
int to_huffman(self)
Definition board.py:1757
Board copy(self)
Definition board.py:425
Board play_on_copy(self, int move)
Definition board.py:987
bool __eq__(self, object value)
Definition board.py:224
bool is_full(self)
Definition board.py:1419
None __init__(self, Sequence[Sequence[int]]|Sequence[int]|str|None init_with=None)
Definition board.py:14
int count_tokens(self)
Definition board.py:513
str to_string(self)
Definition board.py:1246
bool reset_board(self, Sequence[int]|Sequence[Sequence[int]]|str|None board=None)
Definition board.py:1028
list[Board] all_positions(self, int up_to_n_ply, bool exactly_n)
Definition board.py:301
str __repr__(self)
Definition board.py:290
list[list[int]] to_array(self, bool column_major_layout=True)
Definition board.py:1172
bool can_win_next(self, int|None move=None)
Definition board.py:375
Board from_moves(cls, Sequence[int]|str moves)
Definition board.py:1557
int|None winner(self)
Definition board.py:1498
int __hash__(self)
Definition board.py:639
Board mirror(self)
Definition board.py:766
bool is_game_over(self)
Definition board.py:1452
bool play(self, int|Sequence[int]|str move)
Definition board.py:899
tuple[Board, list[int]] random_board(int n_ply, bool forbid_direct_win)
Definition board.py:1668
str __str__(self)
Definition board.py:294
bool has_win(self)
Definition board.py:586
int moves_left(self)
Definition board.py:852
bool is_legal_move(self, int move)
Definition board.py:673
bool __ne__(self, object value)
Definition board.py:277
list[int] legal_moves(self, bool non_losing=False, bool order_moves=False)
Definition board.py:1809