pingv4 0.1.0__cp39-abi3-manylinux_2_34_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pingv4/__init__.py +15 -0
- pingv4/_core.abi3.so +0 -0
- pingv4/_core.pyi +203 -0
- pingv4/bot/__init__.py +8 -0
- pingv4/bot/base.py +77 -0
- pingv4/bot/minimax.py +334 -0
- pingv4/game.py +496 -0
- pingv4/py.typed +0 -0
- pingv4-0.1.0.dist-info/METADATA +355 -0
- pingv4-0.1.0.dist-info/RECORD +13 -0
- pingv4-0.1.0.dist-info/WHEEL +4 -0
- pingv4-0.1.0.dist-info/entry_points.txt +2 -0
- pingv4-0.1.0.dist-info/licenses/LICENSE +21 -0
pingv4/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pingv4._core import ConnectFourBoard, CellState
|
|
2
|
+
from pingv4.bot import AbstractBot, RandomBot, MinimaxBot
|
|
3
|
+
from pingv4.game import Connect4Game, ManualPlayer, GameConfig, PlayerConfig
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ConnectFourBoard",
|
|
7
|
+
"CellState",
|
|
8
|
+
"AbstractBot",
|
|
9
|
+
"Connect4Game",
|
|
10
|
+
"GameConfig",
|
|
11
|
+
"PlayerConfig",
|
|
12
|
+
"ManualPlayer",
|
|
13
|
+
"RandomBot",
|
|
14
|
+
"MinimaxBot",
|
|
15
|
+
]
|
pingv4/_core.abi3.so
ADDED
|
Binary file
|
pingv4/_core.pyi
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from typing import List, Tuple, Optional
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
|
|
4
|
+
class CellState(IntEnum):
|
|
5
|
+
"""
|
|
6
|
+
Enumeration representing the state of a cell on a Connect Four board.
|
|
7
|
+
|
|
8
|
+
Each value corresponds to the player occupying a cell.
|
|
9
|
+
|
|
10
|
+
:cvar Yellow: Indicates a cell occupied by the yellow player.
|
|
11
|
+
:cvar Red: Indicates a cell occupied by the red player.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
Yellow = 0
|
|
15
|
+
Red = 1
|
|
16
|
+
|
|
17
|
+
class ConnectFourBoard:
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Initializes an empty Connect Four Board.
|
|
21
|
+
"""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def num_rows(self) -> int:
|
|
26
|
+
"""
|
|
27
|
+
:return: The total number of rows.
|
|
28
|
+
:rtype: int
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def num_cols(self) -> int:
|
|
34
|
+
"""
|
|
35
|
+
:return: The total number of columns.
|
|
36
|
+
:rtype: int
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def hash(self) -> int:
|
|
42
|
+
"""
|
|
43
|
+
Return a hash representing the current board state.
|
|
44
|
+
|
|
45
|
+
The hash depends only on the configuration of pieces on the board
|
|
46
|
+
and is deterministic for a given state.
|
|
47
|
+
|
|
48
|
+
:return: A hash value for the current board state.
|
|
49
|
+
:rtype: int
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def column_heights(self) -> List[int]:
|
|
55
|
+
"""
|
|
56
|
+
Return the current heights of all columns.
|
|
57
|
+
|
|
58
|
+
Each element represents the number of pieces currently placed
|
|
59
|
+
in the corresponding column.
|
|
60
|
+
|
|
61
|
+
:return: A list of column heights indexed by column.
|
|
62
|
+
:rtype: List[int]
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def is_in_progress(self) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if the game is still in progress.
|
|
70
|
+
|
|
71
|
+
:return: True if the game is in progress (no victory or draw), False otherwise.
|
|
72
|
+
:rtype: bool
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def current_player(self) -> Optional[CellState]:
|
|
78
|
+
"""
|
|
79
|
+
Get the current player whose turn it is.
|
|
80
|
+
|
|
81
|
+
:return: The current player if game is in progress, None otherwise.
|
|
82
|
+
:rtype: Optional[CellState]
|
|
83
|
+
"""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_victory(self) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Check if the game has ended in a victory.
|
|
90
|
+
|
|
91
|
+
:return: True if a player has won, False otherwise.
|
|
92
|
+
:rtype: bool
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def winner(self) -> Optional[CellState]:
|
|
98
|
+
"""
|
|
99
|
+
Get the winning player.
|
|
100
|
+
|
|
101
|
+
:return: The winning player if there is a victory, None otherwise.
|
|
102
|
+
:rtype: Optional[CellState]
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_draw(self) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if the game has ended in a draw.
|
|
110
|
+
|
|
111
|
+
:return: True if the board is full with no winner, False otherwise.
|
|
112
|
+
:rtype: bool
|
|
113
|
+
"""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def cell_states(self) -> List[List[Optional[CellState]]]:
|
|
118
|
+
"""
|
|
119
|
+
Return the state of all cells on the board.
|
|
120
|
+
|
|
121
|
+
.. warning::
|
|
122
|
+
Cell states are stored in **column-major** order. Access as
|
|
123
|
+
``cell_states[col_idx][row_idx]`` where ``col_idx`` is the column
|
|
124
|
+
index (0-6) and ``row_idx`` is the row index (0-5, bottom to top).
|
|
125
|
+
|
|
126
|
+
:return: A nested list of cell states indexed by [column][row].
|
|
127
|
+
:rtype: List[List[Optional[CellState]]]
|
|
128
|
+
"""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
def get_valid_moves(self) -> List[int]:
|
|
132
|
+
"""
|
|
133
|
+
Return all moves that can be made given the current game state.
|
|
134
|
+
|
|
135
|
+
Returns empty list if game is in draw/victory state.
|
|
136
|
+
|
|
137
|
+
:return: A list of column indexes that are valid moves
|
|
138
|
+
:rtype: List[int]
|
|
139
|
+
"""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
def make_move(self, col_idx: int) -> "ConnectFourBoard":
|
|
143
|
+
"""
|
|
144
|
+
Make a move in the specified column.
|
|
145
|
+
|
|
146
|
+
Returns a new board with the move applied. The current board
|
|
147
|
+
remains unchanged (immutable).
|
|
148
|
+
|
|
149
|
+
:param col_idx: The zero-indexed column to drop the piece into.
|
|
150
|
+
:type col_idx: int
|
|
151
|
+
:return: A new board with the move applied.
|
|
152
|
+
:rtype: ConnectFourBoard
|
|
153
|
+
:raises ValueError: If column index is out of bounds, column is full,
|
|
154
|
+
or game is not in progress (victory/draw).
|
|
155
|
+
"""
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
def __getitem__(self, idx: Tuple[int, int]) -> Optional[CellState]:
|
|
159
|
+
"""
|
|
160
|
+
Return the state of a specific cell on the board.
|
|
161
|
+
|
|
162
|
+
.. warning::
|
|
163
|
+
Access is **column-major**: the first index is the column, the second
|
|
164
|
+
is the row. Use ``board[col_idx, row_idx]`` where ``col_idx`` is the
|
|
165
|
+
column (0-6) and ``row_idx`` is the row (0-5, bottom to top).
|
|
166
|
+
|
|
167
|
+
:param idx: A zero-indexed ``(col_idx, row_idx)`` tuple identifying the cell.
|
|
168
|
+
:type idx: Tuple[int, int]
|
|
169
|
+
:return: The cell state if occupied, or None if the cell is empty.
|
|
170
|
+
:rtype: Optional[CellState]
|
|
171
|
+
"""
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
def __hash__(self) -> int:
|
|
175
|
+
"""
|
|
176
|
+
Return the hash value of the board.
|
|
177
|
+
|
|
178
|
+
Enables the board to be used in hash-based collections.
|
|
179
|
+
|
|
180
|
+
:return: The hash value of the board.
|
|
181
|
+
:rtype: int
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
def __eq__(self, other: object) -> bool:
|
|
186
|
+
"""
|
|
187
|
+
Compare this board with another object for equality.
|
|
188
|
+
|
|
189
|
+
Boards are equal if they are the same type and have identical
|
|
190
|
+
piece configurations.
|
|
191
|
+
|
|
192
|
+
:param other: The object to compare against.
|
|
193
|
+
:return: True if equal, False otherwise.
|
|
194
|
+
:rtype: bool
|
|
195
|
+
"""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
def __str__(self) -> str:
|
|
199
|
+
"""
|
|
200
|
+
:return: The string representation of the board
|
|
201
|
+
:rtype: str
|
|
202
|
+
"""
|
|
203
|
+
...
|
pingv4/bot/__init__.py
ADDED
pingv4/bot/base.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
from pingv4._core import CellState, ConnectFourBoard
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractBot(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for all ConnectFour bots
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, player: CellState) -> None:
|
|
14
|
+
"""
|
|
15
|
+
:param player: The CellState (Red or Yellow) this bot is playing as
|
|
16
|
+
:type player: CellState
|
|
17
|
+
"""
|
|
18
|
+
self.player: Final[CellState] = player
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def strategy_name(self) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Human-readable name of the bot's strategy.
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def author_name(self) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Full name of the bot's author.
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def author_netid(self) -> str:
|
|
39
|
+
"""
|
|
40
|
+
NetID (or equivalent identifier) of the bot's author.
|
|
41
|
+
"""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_move(self, board: ConnectFourBoard) -> int:
|
|
46
|
+
"""
|
|
47
|
+
Decide which column to place your next coin in
|
|
48
|
+
|
|
49
|
+
:param board: The current ConnectFour Board state
|
|
50
|
+
:type board: ConnectFourBoard
|
|
51
|
+
:return: A valid column index (0 <= i <= 6)
|
|
52
|
+
:rtype: int
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RandomBot(AbstractBot):
|
|
58
|
+
"""A simple bot that plays random valid moves."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, player: CellState) -> None:
|
|
61
|
+
super().__init__(player)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def strategy_name(self) -> str:
|
|
65
|
+
return "RandomBot"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def author_name(self) -> str:
|
|
69
|
+
return "SDIYBT"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def author_netid(self) -> str:
|
|
73
|
+
return "SDIYBT"
|
|
74
|
+
|
|
75
|
+
def get_move(self, board: ConnectFourBoard) -> int:
|
|
76
|
+
valid_moves = board.get_valid_moves()
|
|
77
|
+
return random.choice(valid_moves)
|
pingv4/bot/minimax.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from typing import Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from pingv4._core import CellState, ConnectFourBoard
|
|
4
|
+
from pingv4.bot.base import AbstractBot
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Transposition table entry types
|
|
8
|
+
EXACT = 0
|
|
9
|
+
LOWERBOUND = 1
|
|
10
|
+
UPPERBOUND = 2
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MinimaxBot(AbstractBot):
|
|
14
|
+
"""
|
|
15
|
+
A competent Connect Four bot using Minimax with Alpha-Beta pruning.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Alpha-beta pruning for efficient search
|
|
19
|
+
- Transposition table using board.hash for caching
|
|
20
|
+
- Move ordering (center-first) for better pruning
|
|
21
|
+
- Iterative deepening for time management
|
|
22
|
+
- Sophisticated positional evaluation
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, player: CellState, max_depth: int = 6) -> None:
|
|
26
|
+
super().__init__(player)
|
|
27
|
+
self.max_depth = max_depth
|
|
28
|
+
self.opponent = CellState.Yellow if player == CellState.Red else CellState.Red
|
|
29
|
+
|
|
30
|
+
# Transposition table: hash -> (depth, score, flag, best_move)
|
|
31
|
+
self._tt: Dict[int, Tuple[int, float, int, Optional[int]]] = {}
|
|
32
|
+
|
|
33
|
+
# Center-preference move ordering (center columns searched first)
|
|
34
|
+
self._move_order = [3, 2, 4, 1, 5, 0, 6]
|
|
35
|
+
|
|
36
|
+
# Precomputed weights for positional evaluation
|
|
37
|
+
# Center columns are more valuable
|
|
38
|
+
self._col_weights = [1, 2, 3, 4, 3, 2, 1]
|
|
39
|
+
|
|
40
|
+
# Window scoring weights
|
|
41
|
+
self._window_scores = {
|
|
42
|
+
4: 100000, # Four in a row (win)
|
|
43
|
+
3: 50, # Three with open space
|
|
44
|
+
2: 5, # Two with open spaces
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def strategy_name(self) -> str:
|
|
49
|
+
return f"MinimaxBot (depth={self.max_depth})"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def author_name(self) -> str:
|
|
53
|
+
return "Pingv4"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def author_netid(self) -> str:
|
|
57
|
+
return "pingv4"
|
|
58
|
+
|
|
59
|
+
def get_move(self, board: ConnectFourBoard) -> int:
|
|
60
|
+
"""Select the best move using iterative deepening minimax."""
|
|
61
|
+
valid_moves = board.get_valid_moves()
|
|
62
|
+
|
|
63
|
+
if not valid_moves:
|
|
64
|
+
raise ValueError("No valid moves available")
|
|
65
|
+
|
|
66
|
+
# Check for immediate winning move
|
|
67
|
+
for move in valid_moves:
|
|
68
|
+
next_board = board.make_move(move)
|
|
69
|
+
if next_board.is_victory and next_board.winner == self.player:
|
|
70
|
+
return move
|
|
71
|
+
|
|
72
|
+
# Check for blocking opponent's winning move
|
|
73
|
+
for move in valid_moves:
|
|
74
|
+
next_board = board.make_move(move)
|
|
75
|
+
# Simulate opponent playing in same column on current board
|
|
76
|
+
# by checking if after our move, opponent would win there
|
|
77
|
+
test_board = self._simulate_opponent_move(board, move)
|
|
78
|
+
if (
|
|
79
|
+
test_board
|
|
80
|
+
and test_board.is_victory
|
|
81
|
+
and test_board.winner == self.opponent
|
|
82
|
+
):
|
|
83
|
+
return move
|
|
84
|
+
|
|
85
|
+
# Iterative deepening
|
|
86
|
+
best_move = valid_moves[0]
|
|
87
|
+
for depth in range(1, self.max_depth + 1):
|
|
88
|
+
move, _ = self._search_root(board, depth)
|
|
89
|
+
if move is not None:
|
|
90
|
+
best_move = move
|
|
91
|
+
|
|
92
|
+
return best_move
|
|
93
|
+
|
|
94
|
+
def _simulate_opponent_move(
|
|
95
|
+
self, board: ConnectFourBoard, col: int
|
|
96
|
+
) -> Optional[ConnectFourBoard]:
|
|
97
|
+
"""Simulate what would happen if opponent played in this column."""
|
|
98
|
+
# Check if the column has room and simulate opponent move
|
|
99
|
+
if board.column_heights[col] < board.num_rows:
|
|
100
|
+
# Create a hypothetical board where it's opponent's turn
|
|
101
|
+
# This is a simplification - we just check if this column would be dangerous
|
|
102
|
+
try:
|
|
103
|
+
# Make our move, then imagine opponent there
|
|
104
|
+
return board.make_move(col)
|
|
105
|
+
except ValueError:
|
|
106
|
+
return None
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def _search_root(
|
|
110
|
+
self, board: ConnectFourBoard, depth: int
|
|
111
|
+
) -> Tuple[Optional[int], float]:
|
|
112
|
+
"""Root-level search with move ordering from transposition table."""
|
|
113
|
+
valid_moves = board.get_valid_moves()
|
|
114
|
+
if not valid_moves:
|
|
115
|
+
return None, 0.0
|
|
116
|
+
|
|
117
|
+
# Order moves: TT best move first, then center-preference
|
|
118
|
+
ordered_moves = self._order_moves(valid_moves, board.hash)
|
|
119
|
+
|
|
120
|
+
best_move = ordered_moves[0]
|
|
121
|
+
best_score = float("-inf")
|
|
122
|
+
alpha = float("-inf")
|
|
123
|
+
beta = float("inf")
|
|
124
|
+
|
|
125
|
+
for move in ordered_moves:
|
|
126
|
+
next_board = board.make_move(move)
|
|
127
|
+
score = -self._negamax(next_board, depth - 1, -beta, -alpha, -1)
|
|
128
|
+
|
|
129
|
+
if score > best_score:
|
|
130
|
+
best_score = score
|
|
131
|
+
best_move = move
|
|
132
|
+
|
|
133
|
+
alpha = max(alpha, score)
|
|
134
|
+
|
|
135
|
+
return best_move, best_score
|
|
136
|
+
|
|
137
|
+
def _negamax(
|
|
138
|
+
self,
|
|
139
|
+
board: ConnectFourBoard,
|
|
140
|
+
depth: int,
|
|
141
|
+
alpha: float,
|
|
142
|
+
beta: float,
|
|
143
|
+
color: int,
|
|
144
|
+
) -> float:
|
|
145
|
+
"""
|
|
146
|
+
Negamax with alpha-beta pruning and transposition table.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
board: Current board state
|
|
150
|
+
depth: Remaining search depth
|
|
151
|
+
alpha: Alpha bound
|
|
152
|
+
beta: Beta bound
|
|
153
|
+
color: 1 if maximizing for current player, -1 otherwise
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Evaluation score from the perspective of the current player
|
|
157
|
+
"""
|
|
158
|
+
alpha_orig = alpha
|
|
159
|
+
board_hash = board.hash
|
|
160
|
+
|
|
161
|
+
# Transposition table lookup
|
|
162
|
+
if board_hash in self._tt:
|
|
163
|
+
tt_depth, tt_score, tt_flag, tt_move = self._tt[board_hash]
|
|
164
|
+
if tt_depth >= depth:
|
|
165
|
+
if tt_flag == EXACT:
|
|
166
|
+
return tt_score
|
|
167
|
+
elif tt_flag == LOWERBOUND:
|
|
168
|
+
alpha = max(alpha, tt_score)
|
|
169
|
+
elif tt_flag == UPPERBOUND:
|
|
170
|
+
beta = min(beta, tt_score)
|
|
171
|
+
|
|
172
|
+
if alpha >= beta:
|
|
173
|
+
return tt_score
|
|
174
|
+
|
|
175
|
+
# Terminal state check
|
|
176
|
+
if not board.is_in_progress:
|
|
177
|
+
if board.is_victory:
|
|
178
|
+
# Large negative score (opponent won on their move)
|
|
179
|
+
return -100000 - depth # Prefer faster wins
|
|
180
|
+
else:
|
|
181
|
+
return 0 # Draw
|
|
182
|
+
|
|
183
|
+
# Depth limit - evaluate position
|
|
184
|
+
if depth <= 0:
|
|
185
|
+
return color * self._evaluate(board)
|
|
186
|
+
|
|
187
|
+
valid_moves = board.get_valid_moves()
|
|
188
|
+
ordered_moves = self._order_moves(valid_moves, board_hash)
|
|
189
|
+
|
|
190
|
+
best_score = float("-inf")
|
|
191
|
+
best_move = ordered_moves[0] if ordered_moves else None
|
|
192
|
+
|
|
193
|
+
for move in ordered_moves:
|
|
194
|
+
next_board = board.make_move(move)
|
|
195
|
+
score = -self._negamax(next_board, depth - 1, -beta, -alpha, -color)
|
|
196
|
+
|
|
197
|
+
if score > best_score:
|
|
198
|
+
best_score = score
|
|
199
|
+
best_move = move
|
|
200
|
+
|
|
201
|
+
alpha = max(alpha, score)
|
|
202
|
+
if alpha >= beta:
|
|
203
|
+
break # Beta cutoff
|
|
204
|
+
|
|
205
|
+
# Store in transposition table
|
|
206
|
+
if best_score <= alpha_orig:
|
|
207
|
+
flag = UPPERBOUND
|
|
208
|
+
elif best_score >= beta:
|
|
209
|
+
flag = LOWERBOUND
|
|
210
|
+
else:
|
|
211
|
+
flag = EXACT
|
|
212
|
+
|
|
213
|
+
self._tt[board_hash] = (depth, best_score, flag, best_move)
|
|
214
|
+
|
|
215
|
+
return best_score
|
|
216
|
+
|
|
217
|
+
def _order_moves(self, moves: list, board_hash: int) -> list:
|
|
218
|
+
"""Order moves for better alpha-beta pruning."""
|
|
219
|
+
# Check if we have a best move from transposition table
|
|
220
|
+
tt_best = None
|
|
221
|
+
if board_hash in self._tt:
|
|
222
|
+
tt_best = self._tt[board_hash][3]
|
|
223
|
+
|
|
224
|
+
# Sort by: TT best move first, then center preference
|
|
225
|
+
def move_priority(move: int) -> int:
|
|
226
|
+
if move == tt_best:
|
|
227
|
+
return -100 # Highest priority
|
|
228
|
+
try:
|
|
229
|
+
return -self._move_order.index(move)
|
|
230
|
+
except ValueError:
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
return sorted(moves, key=move_priority)
|
|
234
|
+
|
|
235
|
+
def _evaluate(self, board: ConnectFourBoard) -> float:
|
|
236
|
+
"""
|
|
237
|
+
Evaluate the board position from the current player's perspective.
|
|
238
|
+
|
|
239
|
+
Considers:
|
|
240
|
+
- Winning/losing positions
|
|
241
|
+
- Threats (three in a row with open space)
|
|
242
|
+
- Center control
|
|
243
|
+
- Piece connectivity
|
|
244
|
+
"""
|
|
245
|
+
if board.is_victory:
|
|
246
|
+
if board.winner == self.player:
|
|
247
|
+
return 100000
|
|
248
|
+
else:
|
|
249
|
+
return -100000
|
|
250
|
+
|
|
251
|
+
if board.is_draw:
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
score = 0.0
|
|
255
|
+
|
|
256
|
+
# Evaluate all windows of 4
|
|
257
|
+
score += self._evaluate_windows(board)
|
|
258
|
+
|
|
259
|
+
# Center column control bonus
|
|
260
|
+
score += self._evaluate_center(board)
|
|
261
|
+
|
|
262
|
+
return score
|
|
263
|
+
|
|
264
|
+
def _evaluate_windows(self, board: ConnectFourBoard) -> float:
|
|
265
|
+
"""Evaluate all possible winning windows."""
|
|
266
|
+
score = 0.0
|
|
267
|
+
num_rows = board.num_rows
|
|
268
|
+
num_cols = board.num_cols
|
|
269
|
+
|
|
270
|
+
# Horizontal windows
|
|
271
|
+
for row in range(num_rows):
|
|
272
|
+
for col in range(num_cols - 3):
|
|
273
|
+
window = [board[col + i, row] for i in range(4)]
|
|
274
|
+
score += self._score_window(window)
|
|
275
|
+
|
|
276
|
+
# Vertical windows
|
|
277
|
+
for col in range(num_cols):
|
|
278
|
+
for row in range(num_rows - 3):
|
|
279
|
+
window = [board[col, row + i] for i in range(4)]
|
|
280
|
+
score += self._score_window(window)
|
|
281
|
+
|
|
282
|
+
# Positive diagonal (bottom-left to top-right)
|
|
283
|
+
for row in range(num_rows - 3):
|
|
284
|
+
for col in range(num_cols - 3):
|
|
285
|
+
window = [board[col + i, row + i] for i in range(4)]
|
|
286
|
+
score += self._score_window(window)
|
|
287
|
+
|
|
288
|
+
# Negative diagonal (top-left to bottom-right)
|
|
289
|
+
for row in range(3, num_rows):
|
|
290
|
+
for col in range(num_cols - 3):
|
|
291
|
+
window = [board[col + i, row - i] for i in range(4)]
|
|
292
|
+
score += self._score_window(window)
|
|
293
|
+
|
|
294
|
+
return score
|
|
295
|
+
|
|
296
|
+
def _score_window(self, window: list) -> float:
|
|
297
|
+
"""Score a window of 4 cells."""
|
|
298
|
+
player_count = sum(1 for cell in window if cell == self.player)
|
|
299
|
+
opponent_count = sum(1 for cell in window if cell == self.opponent)
|
|
300
|
+
empty_count = sum(1 for cell in window if cell is None)
|
|
301
|
+
|
|
302
|
+
# Can't score if both players have pieces in window
|
|
303
|
+
if player_count > 0 and opponent_count > 0:
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
if player_count == 4:
|
|
307
|
+
return self._window_scores[4]
|
|
308
|
+
elif player_count == 3 and empty_count == 1:
|
|
309
|
+
return self._window_scores[3]
|
|
310
|
+
elif player_count == 2 and empty_count == 2:
|
|
311
|
+
return self._window_scores[2]
|
|
312
|
+
elif opponent_count == 4:
|
|
313
|
+
return -self._window_scores[4]
|
|
314
|
+
elif opponent_count == 3 and empty_count == 1:
|
|
315
|
+
return -self._window_scores[3] * 1.1 # Slightly prioritize blocking
|
|
316
|
+
elif opponent_count == 2 and empty_count == 2:
|
|
317
|
+
return -self._window_scores[2]
|
|
318
|
+
|
|
319
|
+
return 0
|
|
320
|
+
|
|
321
|
+
def _evaluate_center(self, board: ConnectFourBoard) -> float:
|
|
322
|
+
"""Bonus for controlling the center column."""
|
|
323
|
+
center_col = board.num_cols // 2
|
|
324
|
+
center_count = 0
|
|
325
|
+
opponent_center = 0
|
|
326
|
+
|
|
327
|
+
for row in range(board.num_rows):
|
|
328
|
+
cell = board[center_col, row]
|
|
329
|
+
if cell == self.player:
|
|
330
|
+
center_count += 1
|
|
331
|
+
elif cell == self.opponent:
|
|
332
|
+
opponent_center += 1
|
|
333
|
+
|
|
334
|
+
return (center_count - opponent_center) * 3
|