solver
This module provides the Connect Four AI agent "BitBully" with opening book support.
Classes:
| Name | Description |
|---|---|
BitBully |
A Connect Four AI agent with optional opening book support. |
Attributes:
| Name | Type | Description |
|---|---|---|
OpeningBookName |
TypeAlias
|
Name of the opening book used by the BitBully engine. |
TieBreakStrategy |
TypeAlias
|
Strategy for breaking ties between equally good moves. |
OpeningBookName
module-attribute
Name of the opening book used by the BitBully engine.
Possible values:
- "default": Alias for "12-ply-dist".
- "8-ply": 8-ply opening book (win/loss only).
- "12-ply": 12-ply opening book (win/loss only).
- "12-ply-dist": 12-ply opening book with distance-to-win information.
TieBreakStrategy
module-attribute
BitBully
BitBully(opening_book: OpeningBookName | None = 'default', *, tie_break: TieBreakStrategy | None = None, rng: Random | None = None, max_depth: int = -1)
A Connect Four AI agent with optional opening book support.
Todo: - We have to describe the scoring scheme (range of values and their meaning).
This class is a high-level Python wrapper around
bitbully_core.BitBullyCore.
It integrates the packaged BitBully Databases opening books and
operates on bitbully.Board objects.
Notes
- If an opening book is enabled, it is used automatically for early-game positions.
- For deeper positions or positions outside the database horizon, the engine falls back to search-based evaluation.
Example
from bitbully import BitBully, Board
agent = BitBully()
board, _ = Board.random_board(n_ply=14, forbid_direct_win=True)
print(board)
# All three search methods should agree on the score
score_mtdf = agent.mtdf(board)
score_negamax = agent.negamax(board)
score_null_window = agent.null_window(board)
assert score_negamax == score_null_window == score_mtdf
Example
from bitbully import BitBully, Board
board = Board() # empty board
agent = BitBully()
scores = agent.score_all_moves(board) # get scores for all moves
assert len(scores) == 7 # there are 7 columns
assert scores == {3: 1, 2: 0, 4: 0, 1: -1, 5: -1, 0: -2, 6: -2} # center column is best
print(scores)
Expected Output:
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
OpeningBookName | None
|
Which opening book to load.
|
'default'
|
|
TieBreakStrategy | None
|
Default strategy for breaking ties between equally scoring moves.
If |
None
|
|
Random | None
|
Optional RNG for reproducible "random" tie-breaking. |
None
|
|
int
|
Maximum search depth in plies. When reached, a rollout using
non-losing moves is performed instead of continuing the search.
|
-1
|
TODO: Example for initialization with different books.
Methods:
| Name | Description |
|---|---|
__repr__ |
Return a concise string representation of the BitBully agent. |
available_opening_books |
Return the available opening book identifiers. |
best_move |
Return the best legal move (column index) for the current player. |
get_node_counter |
Return the number of nodes visited since the last reset. |
is_book_loaded |
Check whether an opening book is loaded. |
load_book |
Load an opening book from a file path. |
mtdf |
Evaluate a position using the MTD(f) algorithm. |
negamax |
Evaluate a position using negamax search. |
null_window |
Evaluate a position using a null-window search. |
reset_book |
Unload the currently loaded opening book (if any). |
reset_node_counter |
Reset the internal node counter. |
reset_transposition_table |
Clear the internal transposition table. |
score_all_moves |
Score all legal moves for the given board state. |
score_move |
Evaluate a single move for the given board state. |
score_to_moves_left |
Convert a solver score to the number of moves until the game ends. |
Attributes:
| Name | Type | Description |
|---|---|---|
max_depth |
|
|
opening_book_type |
OpeningBookName | None
|
|
rng |
|
|
tie_break |
|
Source code in src/bitbully/solver.py
available_opening_books
classmethod
available_opening_books() -> tuple[OpeningBookName, ...]
Return the available opening book identifiers.
Returns:
| Type | Description |
|---|---|
tuple[OpeningBookName, ...]
|
tuple[OpeningBookName, ...]:
All supported opening book names, including |
Example
Source code in src/bitbully/solver.py
best_move
best_move(board: Board, *, tie_break: TieBreakStrategy | None = None, rng: Random | None = None, max_depth: int | None = None) -> int
Return the best legal move (column index) for the current player.
All legal moves are scored using :meth:score_all_moves. The move(s)
with the highest score are considered best, and ties are resolved
according to tie_break.
Tie-breaking strategies
None(default): Use the agent's default tie-breaking strategy (self.tie_break)."center"(default): Prefer the move closest to the center column (3). If still tied, choose the smaller column index."leftmost": Choose the smallest column index among tied moves."random": Choose uniformly at random among tied moves. An optionalrngcan be provided for reproducibility.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Board
|
The current board state. |
required |
|
TieBreakStrategy | None
|
Strategy used to resolve ties between equally scoring moves. |
None
|
|
Random | None
|
Random number generator used when |
None
|
|
int | None
|
Maximum search depth override.
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The selected column index (0-6). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If there are no legal moves (board is full) or if an unknown tie-breaking strategy is specified. |
Example
Example
from bitbully import BitBully, Board
import random
agent = BitBully()
board = Board("341") # some arbitrary position
print(board)
assert agent.best_move(board, tie_break="center") == 3 # Several moves are tied; center is preferred
assert agent.best_move(board, tie_break="leftmost") == 1 # Leftmost among tied moves
assert agent.best_move(board, tie_break="random") in {1, 3, 4} # Random among tied moves
rng = random.Random(42) # use own random number generator
assert agent.best_move(board, tie_break="random", rng=rng) in {1, 3, 4}
Example
Use depth-limited search for a fast (approximate) best move:
Source code in src/bitbully/solver.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | |
get_node_counter
get_node_counter() -> int
Return the number of nodes visited since the last reset.
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Number of visited nodes. |
Example
-
BitBully API Reference
solverBitBullyreset_node_counter
Source code in src/bitbully/solver.py
is_book_loaded
is_book_loaded() -> bool
Check whether an opening book is loaded.
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
|
Example
Source code in src/bitbully/solver.py
load_book
load_book(book: OpeningBookName | PathLike[str] | str) -> None
Load an opening book from a file path.
This is a thin wrapper around
bitbully_core.BitBullyCore.loadBook.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
OpeningBookName | PathLike[str] | str
|
Name/Identifier (see |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the book identifier/path is invalid or if loading the book fails. |
Example
from bitbully import BitBully
from pathlib import Path
which_book = BitBully.available_opening_books()[0] # e.g., "default"
agent = BitBully(opening_book=None) # start without book
assert agent.is_book_loaded() is False
agent.load_book(which_book) # load "default" book
assert agent.is_book_loaded() is True
Example
from bitbully import BitBully
from pathlib import Path
import bitbully_databases as bbd
which_book = BitBully.available_opening_books()[2] # e.g., "12-ply"
db_path = bbd.BitBullyDatabases.get_database_path(which_book)
agent = BitBully(opening_book=None) # start without book
assert agent.is_book_loaded() is False
agent.load_book(db_path)
assert agent.is_book_loaded() is True
Source code in src/bitbully/solver.py
mtdf
Evaluate a position using the MTD(f) algorithm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Board
|
The board position to evaluate. |
required |
|
int
|
Initial guess for the score (often 0). |
0
|
|
int | None
|
Maximum search depth override.
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The evaluation score. |
Example
Example
Depth-limited MTD(f) (rollout at depth 12):
Source code in src/bitbully/solver.py
negamax
negamax(board: Board, alpha: int = -1000, beta: int = 1000, depth: int = 0, *, max_depth: int | None = None) -> int
Evaluate a position using negamax search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Board
|
The board position to evaluate. |
required |
|
int
|
Alpha bound. |
-1000
|
|
int
|
Beta bound. |
1000
|
|
int
|
Search depth in plies. |
0
|
|
int | None
|
Maximum search depth override.
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The evaluation score returned by the engine. |
Example
Example
Depth-limited negamax (rollout at depth 12):
Source code in src/bitbully/solver.py
null_window
Evaluate a position using a null-window search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Board
|
The board position to evaluate. |
required |
|
int | None
|
Maximum search depth override.
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The evaluation score. |
Example
Example
Depth-limited null-window search:
Source code in src/bitbully/solver.py
reset_book
Unload the currently loaded opening book (if any).
This resets the engine to search-only mode until another opening book is loaded.
Example
Source code in src/bitbully/solver.py
reset_node_counter
Reset the internal node counter.
See Also: get_node_counter for usage.
reset_transposition_table
score_all_moves
Score all legal moves for the given board state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Board
|
The current board state. |
required |
|
int | None
|
Maximum search depth override.
|
None
|
Returns:
| Type | Description |
|---|---|
dict[int, int]
|
dict[int, int]: A dictionary of up to 7 column-value pairs, one per reachable column (0-6). Higher values generally indicate better moves for the player to move. If a column is full, it will not be listed in the returned dictionary. |
Example
Example
When a column is full, it is omitted from the results:
Example
Use depth-limited search for faster (approximate) scoring:
Source code in src/bitbully/solver.py
score_move
Evaluate a single move for the given board state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Board
|
The current board state. |
required |
|
int
|
Column index (0-6) of the move to evaluate. |
required |
|
int
|
Initial guess for the score (often 0). |
0
|
|
int | None
|
Maximum search depth override.
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The evaluation score of the move. |
Example
Example
Score a move using depth-limited search (faster, approximate):
Raises:
| Type | Description |
|---|---|
ValueError
|
If the column is outside the valid range or if the column is full. |
Notes
- This is a wrapper around
bitbully_core.BitBullyCore.scoreMove.
Source code in src/bitbully/solver.py
score_to_moves_left
staticmethod
Convert a solver score to the number of moves until the game ends.
Given a score returned by the solver (e.g., from
:meth:mtdf or :meth:score_all_moves) and the board state,
returns how many moves remain until the game concludes under
perfect play.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
int
|
The solver score. Positive means the current player wins; negative means the current player loses; 0 means draw. |
required |
|
Board
|
The board state for which the score was computed. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Number of moves remaining until the game ends. |
Example
A draw uses all remaining moves:
Example
Maximum score means the current player wins on the very next move:
Example
Combining with the solver to find how many moves until the game is decided: