BitBully 0.0.75
Loading...
Searching...
No Matches
agent_interface.py
1"""Agent interface definitions for Connect-4.
2
3This module defines the minimal, structural interface that Connect-4 agents must
4implement in order to be compatible with the interactive GUI and other
5high-level components. The interface is expressed using ``typing.Protocol`` to
6enable static type checking without requiring inheritance or tight coupling
7between agents and consumers.
8"""
9
10from __future__ import annotations
11
12from typing import Protocol, runtime_checkable
13
14from . import Board
15
16
17@runtime_checkable
18class Connect4Agent(Protocol):
19 """Minimal interface a Connect-4 agent must implement to work with ``GuiC4``.
20
21 This interface is intentionally aligned with the public ``BitBully`` API,
22 but excludes BitBully-specific engine features such as opening-book handling,
23 node counters, transposition tables, and specialized search entry points.
24
25 Required methods:
26 - ``score_all_moves``: Provide integer evaluations for all *legal* moves.
27 - ``best_move``: Select one legal move using BitBully-compatible
28 tie-breaking semantics.
29
30 Notes on scoring:
31 - Scores are integers where **larger values are better** for the side to move.
32 - The absolute scale is agent-defined.
33 - The GUI only relies on *relative ordering* and legality.
34
35 Example:
36 Minimal agent compatible with the `Connect4Agent` protocol:
37
38 ```python
39 import random
40 from bitbully import Board
41
42 # Importing the Protocol is optional at runtime, but useful for:
43 # - static type checking (mypy / pyright)
44 # - documenting that this class satisfies the agent interface
45 from bitbully.agent_protocols import Connect4Agent
46
47
48 class RandomAgent:
49 '''Agent that plays a random legal move.
50
51 This class does NOT inherit from ``Connect4Agent``.
52 It is compatible because it implements the required methods
53 with matching signatures (structural typing).
54 '''
55
56 def score_all_moves(self, board: Board) -> dict[int, int]:
57 # Only legal columns may appear in the result.
58 # The GUI and other consumers rely on this contract.
59 return {c: 0 for c in board.legal_moves()}
60
61 def best_move(self, board: Board) -> int:
62 # Consumers may call only ``best_move`` if they are
63 # not interested in individual move scores.
64 return random.choice(board.legal_moves())
65
66
67 board = Board("332311")
68
69 # The variable annotation enforces the protocol at type-check time.
70 agent: Connect4Agent = RandomAgent()
71
72 move = agent.best_move(board)
73 board.play(move)
74 ```
75 """
76
77 def score_all_moves(self, board: Board) -> dict[int, int]:
78 """Score all legal moves for the given board state.
79
80 Args:
81 board (Board):
82 Current Connect-4 board position.
83
84 Returns:
85 dict[int, int]:
86 Mapping ``{column: score}`` for all *legal* columns (0..6).
87 Columns that are full or illegal **must not** appear in the mapping.
88
89 Notes:
90 - Higher scores indicate better moves.
91 - The returned dictionary may contain between 0 and 7 entries.
92 """
93 ...
94
96 self,
97 board: Board,
98 ) -> int:
99 """Return the best legal move (column index) for the side to move.
100
101 Args:
102 board (Board):
103 Current Connect-4 board position.
104
105 Returns:
106 int:
107 Selected column index in the range ``0..6``.
108 """
109 ...
110
111 def score_move(self, board: Board, column: int, first_guess: int = 0) -> int:
112 """Evaluate a single legal move for the given board state.
113
114 This method is optional and not required by the GUI, but can be useful
115 for agents that support fine-grained move evaluation.
116
117 Args:
118 board (Board):
119 Current Connect-4 board position.
120 column (int):
121 Column index (0..6) of the move to evaluate.
122 first_guess (int):
123 Optional initial guess for iterative or search-based agents.
124 Implementations may safely ignore this parameter.
125
126 Returns:
127 int:
128 Evaluation score for the given move.
129 """
130 ...
int score_move(self, Board board, int column, int first_guess=0)
dict[int, int] score_all_moves(self, Board board)