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/game.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
import random
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional, Tuple, Type, Union
|
|
5
|
+
|
|
6
|
+
from pingv4 import AbstractBot, CellState, ConnectFourBoard
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GameConfig(BaseModel, frozen=True):
|
|
10
|
+
"""Configuration options for Connect4Game."""
|
|
11
|
+
|
|
12
|
+
# Bot timing
|
|
13
|
+
bot_delay_seconds: float = 1.0
|
|
14
|
+
|
|
15
|
+
# Animation
|
|
16
|
+
animation_speed: int = 25
|
|
17
|
+
|
|
18
|
+
# Window dimensions
|
|
19
|
+
window_width: int = 700
|
|
20
|
+
window_height: int = 700
|
|
21
|
+
|
|
22
|
+
# Board display
|
|
23
|
+
cell_size: int = 80
|
|
24
|
+
|
|
25
|
+
# Board constants
|
|
26
|
+
board_rows: int = 6
|
|
27
|
+
board_cols: int = 7
|
|
28
|
+
|
|
29
|
+
# Colors
|
|
30
|
+
background_color: Tuple[int, int, int] = (30, 30, 40)
|
|
31
|
+
board_color: Tuple[int, int, int] = (0, 80, 180)
|
|
32
|
+
empty_color: Tuple[int, int, int] = (20, 20, 30)
|
|
33
|
+
red_color: Tuple[int, int, int] = (220, 50, 50)
|
|
34
|
+
yellow_color: Tuple[int, int, int] = (240, 220, 50)
|
|
35
|
+
hover_color: Tuple[int, int, int] = (100, 100, 120)
|
|
36
|
+
text_color: Tuple[int, int, int] = (255, 255, 255)
|
|
37
|
+
win_highlight_color: Tuple[int, int, int] = (50, 255, 50)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def board_margin_x(self) -> int:
|
|
41
|
+
"""Calculate horizontal margin to center the board."""
|
|
42
|
+
return (self.window_width - self.board_cols * self.cell_size) // 2
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def board_margin_y(self) -> int:
|
|
46
|
+
"""Vertical margin from top of window."""
|
|
47
|
+
return 100
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ManualPlayer:
|
|
51
|
+
"""Represents a human player who makes moves manually."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, player: CellState) -> None:
|
|
54
|
+
self.player = player
|
|
55
|
+
self.strategy_name = "Manual Player"
|
|
56
|
+
self.author_name = "Human"
|
|
57
|
+
self.author_netid = "N/A"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Player can be specified as:
|
|
61
|
+
# - None (manual player)
|
|
62
|
+
# - AbstractBot subclass (will be instantiated with the assigned color)
|
|
63
|
+
# - ManualPlayer instance
|
|
64
|
+
PlayerConfig = Union[None, Type[AbstractBot], ManualPlayer]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Connect4Game:
|
|
68
|
+
"""
|
|
69
|
+
A graphical Connect Four game supporting both human and bot players.
|
|
70
|
+
|
|
71
|
+
Initialize with player configurations and optional game settings:
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
# Two manual players
|
|
75
|
+
game = Connect4Game()
|
|
76
|
+
game.run()
|
|
77
|
+
|
|
78
|
+
# Bot vs bot with custom timing
|
|
79
|
+
config = GameConfig(bot_delay_seconds=0.5)
|
|
80
|
+
game = Connect4Game(player1=RandomBot, player2=RandomBot, config=config)
|
|
81
|
+
game.run()
|
|
82
|
+
|
|
83
|
+
# Manual player vs bot
|
|
84
|
+
game = Connect4Game(player1=None, player2=RandomBot)
|
|
85
|
+
game.run()
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
player1: PlayerConfig = None,
|
|
91
|
+
player2: PlayerConfig = None,
|
|
92
|
+
config: Optional[GameConfig] = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Initialize a Connect Four game.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
player1: First player - None for manual, or an AbstractBot subclass.
|
|
99
|
+
player2: Second player - None for manual, or an AbstractBot subclass.
|
|
100
|
+
config: Game configuration options. Uses defaults if not provided.
|
|
101
|
+
"""
|
|
102
|
+
self.config = config or GameConfig()
|
|
103
|
+
self._player1_config = player1
|
|
104
|
+
self._player2_config = player2
|
|
105
|
+
|
|
106
|
+
pygame.init()
|
|
107
|
+
self.screen = pygame.display.set_mode(
|
|
108
|
+
(self.config.window_width, self.config.window_height)
|
|
109
|
+
)
|
|
110
|
+
pygame.display.set_caption("Connect Four")
|
|
111
|
+
self.clock = pygame.time.Clock()
|
|
112
|
+
self.font = pygame.font.Font(None, 36)
|
|
113
|
+
self.small_font = pygame.font.Font(None, 28)
|
|
114
|
+
|
|
115
|
+
# Randomly assign colors to players
|
|
116
|
+
self.player1_is_red = random.choice([True, False])
|
|
117
|
+
|
|
118
|
+
if self.player1_is_red:
|
|
119
|
+
red_config = player1
|
|
120
|
+
yellow_config = player2
|
|
121
|
+
else:
|
|
122
|
+
red_config = player2
|
|
123
|
+
yellow_config = player1
|
|
124
|
+
|
|
125
|
+
self.red_player = self._resolve_player(red_config, CellState.Red)
|
|
126
|
+
self.yellow_player = self._resolve_player(yellow_config, CellState.Yellow)
|
|
127
|
+
|
|
128
|
+
self.board = ConnectFourBoard()
|
|
129
|
+
self.hover_col: Optional[int] = None
|
|
130
|
+
self.game_over = False
|
|
131
|
+
self.winner_name: Optional[str] = None
|
|
132
|
+
self.last_move_col: Optional[int] = None
|
|
133
|
+
self.animating = False
|
|
134
|
+
self.animation_col: Optional[int] = None
|
|
135
|
+
self.animation_row_target: Optional[int] = None
|
|
136
|
+
self.animation_y: float = 0
|
|
137
|
+
self.animation_color: Optional[Tuple[int, int, int]] = None
|
|
138
|
+
|
|
139
|
+
print("=" * 50)
|
|
140
|
+
print("COIN FLIP RESULT")
|
|
141
|
+
print("=" * 50)
|
|
142
|
+
print(
|
|
143
|
+
f"Red (goes first): {self.red_player.strategy_name} by {self.red_player.author_name}"
|
|
144
|
+
)
|
|
145
|
+
print(
|
|
146
|
+
f"Yellow: {self.yellow_player.strategy_name} by {self.yellow_player.author_name}"
|
|
147
|
+
)
|
|
148
|
+
print("=" * 50)
|
|
149
|
+
|
|
150
|
+
def _resolve_player(
|
|
151
|
+
self, player_config: PlayerConfig, color: CellState
|
|
152
|
+
) -> Union[ManualPlayer, AbstractBot]:
|
|
153
|
+
"""
|
|
154
|
+
Convert PlayerConfig to a player instance.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
player_config: The player configuration.
|
|
158
|
+
color: The CellState color to assign.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A ManualPlayer or AbstractBot instance.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
TypeError: If player_config is an invalid type.
|
|
165
|
+
"""
|
|
166
|
+
if player_config is None:
|
|
167
|
+
return ManualPlayer(color)
|
|
168
|
+
elif isinstance(player_config, ManualPlayer):
|
|
169
|
+
return player_config
|
|
170
|
+
elif isinstance(player_config, type) and issubclass(player_config, AbstractBot):
|
|
171
|
+
# Bot class provided - instantiate with color
|
|
172
|
+
return player_config(color)
|
|
173
|
+
else:
|
|
174
|
+
raise TypeError(f"Invalid player config type: {type(player_config)}")
|
|
175
|
+
|
|
176
|
+
def get_current_player(self) -> Union[ManualPlayer, AbstractBot]:
|
|
177
|
+
"""Get the player whose turn it currently is."""
|
|
178
|
+
if self.board.current_player == CellState.Red:
|
|
179
|
+
return self.red_player
|
|
180
|
+
return self.yellow_player
|
|
181
|
+
|
|
182
|
+
def is_manual_turn(self) -> bool:
|
|
183
|
+
"""Check if the current turn belongs to a manual player."""
|
|
184
|
+
return isinstance(self.get_current_player(), ManualPlayer)
|
|
185
|
+
|
|
186
|
+
def get_col_from_mouse(self, mouse_x: int) -> Optional[int]:
|
|
187
|
+
"""Convert mouse x-coordinate to board column index."""
|
|
188
|
+
cfg = self.config
|
|
189
|
+
if (
|
|
190
|
+
cfg.board_margin_x
|
|
191
|
+
<= mouse_x
|
|
192
|
+
< cfg.board_margin_x + cfg.board_cols * cfg.cell_size
|
|
193
|
+
):
|
|
194
|
+
col = (mouse_x - cfg.board_margin_x) // cfg.cell_size
|
|
195
|
+
return col
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def make_move(self, col: int) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Initiate a move animation for the specified column.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if the move is valid and animation started, False otherwise.
|
|
204
|
+
"""
|
|
205
|
+
if col not in self.board.get_valid_moves():
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
cfg = self.config
|
|
209
|
+
self.animating = True
|
|
210
|
+
self.animation_col = col
|
|
211
|
+
self.animation_row_target = self.board.column_heights[col]
|
|
212
|
+
self.animation_y = cfg.board_margin_y - cfg.cell_size
|
|
213
|
+
self.animation_color = (
|
|
214
|
+
cfg.red_color
|
|
215
|
+
if self.board.current_player == CellState.Red
|
|
216
|
+
else cfg.yellow_color
|
|
217
|
+
)
|
|
218
|
+
self.last_move_col = col
|
|
219
|
+
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
def finish_move(self) -> None:
|
|
223
|
+
"""Complete the current move after animation finishes."""
|
|
224
|
+
if self.animation_col is not None:
|
|
225
|
+
self.board = self.board.make_move(self.animation_col)
|
|
226
|
+
|
|
227
|
+
if not self.board.is_in_progress:
|
|
228
|
+
self.game_over = True
|
|
229
|
+
if self.board.is_victory:
|
|
230
|
+
winner = self.board.winner
|
|
231
|
+
if winner == CellState.Red:
|
|
232
|
+
self.winner_name = f"{self.red_player.strategy_name} (Red)"
|
|
233
|
+
else:
|
|
234
|
+
self.winner_name = (
|
|
235
|
+
f"{self.yellow_player.strategy_name} (Yellow)"
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
self.winner_name = "Draw"
|
|
239
|
+
|
|
240
|
+
self.animating = False
|
|
241
|
+
self.animation_col = None
|
|
242
|
+
self.animation_row_target = None
|
|
243
|
+
|
|
244
|
+
def update_animation(self) -> None:
|
|
245
|
+
"""Update the falling piece animation."""
|
|
246
|
+
if not self.animating or self.animation_row_target is None:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
cfg = self.config
|
|
250
|
+
target_y = (
|
|
251
|
+
cfg.board_margin_y
|
|
252
|
+
+ (cfg.board_rows - 1 - self.animation_row_target) * cfg.cell_size
|
|
253
|
+
+ cfg.cell_size // 2
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.animation_y += cfg.animation_speed
|
|
257
|
+
if self.animation_y >= target_y:
|
|
258
|
+
self.animation_y = target_y
|
|
259
|
+
self.finish_move()
|
|
260
|
+
|
|
261
|
+
def draw_board(self) -> None:
|
|
262
|
+
"""Draw the game board and all pieces."""
|
|
263
|
+
cfg = self.config
|
|
264
|
+
board_rect = pygame.Rect(
|
|
265
|
+
cfg.board_margin_x - 10,
|
|
266
|
+
cfg.board_margin_y - 10,
|
|
267
|
+
cfg.board_cols * cfg.cell_size + 20,
|
|
268
|
+
cfg.board_rows * cfg.cell_size + 20,
|
|
269
|
+
)
|
|
270
|
+
pygame.draw.rect(self.screen, cfg.board_color, board_rect, border_radius=10)
|
|
271
|
+
|
|
272
|
+
cell_states = self.board.cell_states
|
|
273
|
+
for col in range(cfg.board_cols):
|
|
274
|
+
for row in range(cfg.board_rows):
|
|
275
|
+
screen_row = cfg.board_rows - 1 - row
|
|
276
|
+
x = cfg.board_margin_x + col * cfg.cell_size + cfg.cell_size // 2
|
|
277
|
+
y = cfg.board_margin_y + screen_row * cfg.cell_size + cfg.cell_size // 2
|
|
278
|
+
|
|
279
|
+
cell = cell_states[col][row]
|
|
280
|
+
if cell == CellState.Red:
|
|
281
|
+
color = cfg.red_color
|
|
282
|
+
elif cell == CellState.Yellow:
|
|
283
|
+
color = cfg.yellow_color
|
|
284
|
+
else:
|
|
285
|
+
color = cfg.empty_color
|
|
286
|
+
|
|
287
|
+
pygame.draw.circle(self.screen, color, (x, y), cfg.cell_size // 2 - 5)
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
self.animating
|
|
291
|
+
and self.animation_col is not None
|
|
292
|
+
and self.animation_color is not None
|
|
293
|
+
):
|
|
294
|
+
x = (
|
|
295
|
+
cfg.board_margin_x
|
|
296
|
+
+ self.animation_col * cfg.cell_size
|
|
297
|
+
+ cfg.cell_size // 2
|
|
298
|
+
)
|
|
299
|
+
pygame.draw.circle(
|
|
300
|
+
self.screen,
|
|
301
|
+
self.animation_color,
|
|
302
|
+
(x, int(self.animation_y)),
|
|
303
|
+
cfg.cell_size // 2 - 5,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def draw_hover_indicator(self) -> None:
|
|
307
|
+
"""Draw the hover preview for manual players."""
|
|
308
|
+
if not self.is_manual_turn() or self.game_over or self.animating:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
cfg = self.config
|
|
312
|
+
if (
|
|
313
|
+
self.hover_col is not None
|
|
314
|
+
and self.hover_col in self.board.get_valid_moves()
|
|
315
|
+
):
|
|
316
|
+
x = cfg.board_margin_x + self.hover_col * cfg.cell_size + cfg.cell_size // 2
|
|
317
|
+
y = cfg.board_margin_y - cfg.cell_size // 2
|
|
318
|
+
color = (
|
|
319
|
+
cfg.red_color
|
|
320
|
+
if self.board.current_player == CellState.Red
|
|
321
|
+
else cfg.yellow_color
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
preview_surface = pygame.Surface(
|
|
325
|
+
(cfg.cell_size, cfg.cell_size), pygame.SRCALPHA
|
|
326
|
+
)
|
|
327
|
+
pygame.draw.circle(
|
|
328
|
+
preview_surface,
|
|
329
|
+
(*color, 150),
|
|
330
|
+
(cfg.cell_size // 2, cfg.cell_size // 2),
|
|
331
|
+
cfg.cell_size // 2 - 5,
|
|
332
|
+
)
|
|
333
|
+
self.screen.blit(
|
|
334
|
+
preview_surface, (x - cfg.cell_size // 2, y - cfg.cell_size // 2)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def draw_status(self) -> None:
|
|
338
|
+
"""Draw the game status text."""
|
|
339
|
+
cfg = self.config
|
|
340
|
+
if self.game_over:
|
|
341
|
+
if self.winner_name == "Draw":
|
|
342
|
+
text = "Game Over - It's a Draw!"
|
|
343
|
+
else:
|
|
344
|
+
text = f"{self.winner_name} Wins!"
|
|
345
|
+
color = cfg.win_highlight_color
|
|
346
|
+
else:
|
|
347
|
+
current = self.get_current_player()
|
|
348
|
+
player_color = (
|
|
349
|
+
"Red" if self.board.current_player == CellState.Red else "Yellow"
|
|
350
|
+
)
|
|
351
|
+
if self.is_manual_turn():
|
|
352
|
+
text = (
|
|
353
|
+
f"{current.strategy_name}'s Turn ({player_color}) - Click to play"
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
text = f"{current.strategy_name}'s Turn ({player_color}) - Thinking..."
|
|
357
|
+
color = cfg.text_color
|
|
358
|
+
|
|
359
|
+
text_surface = self.font.render(text, True, color)
|
|
360
|
+
text_rect = text_surface.get_rect(center=(cfg.window_width // 2, 40))
|
|
361
|
+
self.screen.blit(text_surface, text_rect)
|
|
362
|
+
|
|
363
|
+
red_info = f"Red: {self.red_player.strategy_name}"
|
|
364
|
+
yellow_info = f"Yellow: {self.yellow_player.strategy_name}"
|
|
365
|
+
|
|
366
|
+
red_surface = self.small_font.render(red_info, True, cfg.red_color)
|
|
367
|
+
yellow_surface = self.small_font.render(yellow_info, True, cfg.yellow_color)
|
|
368
|
+
|
|
369
|
+
self.screen.blit(red_surface, (20, cfg.window_height - 60))
|
|
370
|
+
self.screen.blit(yellow_surface, (20, cfg.window_height - 30))
|
|
371
|
+
|
|
372
|
+
if self.game_over:
|
|
373
|
+
restart_text = "Press R to restart or ESC to quit"
|
|
374
|
+
restart_surface = self.small_font.render(restart_text, True, cfg.text_color)
|
|
375
|
+
restart_rect = restart_surface.get_rect(
|
|
376
|
+
center=(cfg.window_width // 2, cfg.window_height - 45)
|
|
377
|
+
)
|
|
378
|
+
self.screen.blit(restart_surface, restart_rect)
|
|
379
|
+
|
|
380
|
+
def handle_bot_turn(self) -> None:
|
|
381
|
+
"""Handle the bot's turn by getting and executing its move."""
|
|
382
|
+
if self.game_over or self.animating or self.is_manual_turn():
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
current_player = self.get_current_player()
|
|
386
|
+
if isinstance(current_player, AbstractBot):
|
|
387
|
+
try:
|
|
388
|
+
col = current_player.get_move(self.board)
|
|
389
|
+
if col in self.board.get_valid_moves():
|
|
390
|
+
self.make_move(col)
|
|
391
|
+
else:
|
|
392
|
+
print(
|
|
393
|
+
f"Bot {current_player.strategy_name} returned invalid move: {col}"
|
|
394
|
+
)
|
|
395
|
+
valid_moves = self.board.get_valid_moves()
|
|
396
|
+
if valid_moves:
|
|
397
|
+
self.make_move(random.choice(valid_moves))
|
|
398
|
+
except Exception as e:
|
|
399
|
+
print(f"Bot {current_player.strategy_name} error: {e}")
|
|
400
|
+
valid_moves = self.board.get_valid_moves()
|
|
401
|
+
if valid_moves:
|
|
402
|
+
self.make_move(random.choice(valid_moves))
|
|
403
|
+
|
|
404
|
+
def reset_game(self) -> None:
|
|
405
|
+
"""Reset the game to initial state with new color assignment."""
|
|
406
|
+
self.player1_is_red = random.choice([True, False])
|
|
407
|
+
|
|
408
|
+
if self.player1_is_red:
|
|
409
|
+
red_config = self._player1_config
|
|
410
|
+
yellow_config = self._player2_config
|
|
411
|
+
else:
|
|
412
|
+
red_config = self._player2_config
|
|
413
|
+
yellow_config = self._player1_config
|
|
414
|
+
|
|
415
|
+
self.red_player = self._resolve_player(red_config, CellState.Red)
|
|
416
|
+
self.yellow_player = self._resolve_player(yellow_config, CellState.Yellow)
|
|
417
|
+
|
|
418
|
+
self.board = ConnectFourBoard()
|
|
419
|
+
self.hover_col = None
|
|
420
|
+
self.game_over = False
|
|
421
|
+
self.winner_name = None
|
|
422
|
+
self.last_move_col = None
|
|
423
|
+
self.animating = False
|
|
424
|
+
self.animation_col = None
|
|
425
|
+
self.animation_row_target = None
|
|
426
|
+
|
|
427
|
+
print("\n" + "=" * 50)
|
|
428
|
+
print("NEW GAME - COIN FLIP")
|
|
429
|
+
print("=" * 50)
|
|
430
|
+
print(f"Red (goes first): {self.red_player.strategy_name}")
|
|
431
|
+
print(f"Yellow: {self.yellow_player.strategy_name}")
|
|
432
|
+
print("=" * 50)
|
|
433
|
+
|
|
434
|
+
def run(self) -> None:
|
|
435
|
+
"""Run the game loop until the window is closed."""
|
|
436
|
+
running = True
|
|
437
|
+
bot_delay = 0
|
|
438
|
+
|
|
439
|
+
while running:
|
|
440
|
+
for event in pygame.event.get():
|
|
441
|
+
if event.type == pygame.QUIT:
|
|
442
|
+
running = False
|
|
443
|
+
|
|
444
|
+
elif event.type == pygame.KEYDOWN:
|
|
445
|
+
if event.key == pygame.K_ESCAPE:
|
|
446
|
+
running = False
|
|
447
|
+
elif event.key == pygame.K_r:
|
|
448
|
+
self.reset_game()
|
|
449
|
+
|
|
450
|
+
elif event.type == pygame.MOUSEMOTION:
|
|
451
|
+
mouse_x, _ = event.pos
|
|
452
|
+
self.hover_col = self.get_col_from_mouse(mouse_x)
|
|
453
|
+
|
|
454
|
+
elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
455
|
+
if (
|
|
456
|
+
event.button == 1
|
|
457
|
+
and self.is_manual_turn()
|
|
458
|
+
and not self.game_over
|
|
459
|
+
and not self.animating
|
|
460
|
+
):
|
|
461
|
+
mouse_x, _ = event.pos
|
|
462
|
+
col = self.get_col_from_mouse(mouse_x)
|
|
463
|
+
if col is not None:
|
|
464
|
+
self.make_move(col)
|
|
465
|
+
|
|
466
|
+
self.update_animation()
|
|
467
|
+
|
|
468
|
+
if not self.animating and not self.game_over and not self.is_manual_turn():
|
|
469
|
+
bot_delay += 1
|
|
470
|
+
if bot_delay >= self.config.bot_delay_seconds * 60:
|
|
471
|
+
self.handle_bot_turn()
|
|
472
|
+
bot_delay = 0
|
|
473
|
+
else:
|
|
474
|
+
bot_delay = 0
|
|
475
|
+
|
|
476
|
+
self.screen.fill(self.config.background_color)
|
|
477
|
+
self.draw_board()
|
|
478
|
+
self.draw_hover_indicator()
|
|
479
|
+
self.draw_status()
|
|
480
|
+
|
|
481
|
+
pygame.display.flip()
|
|
482
|
+
self.clock.tick(60)
|
|
483
|
+
|
|
484
|
+
pygame.quit()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == "__main__":
|
|
488
|
+
# Example usage:
|
|
489
|
+
# Two manual players with default config
|
|
490
|
+
game = Connect4Game()
|
|
491
|
+
game.run()
|
|
492
|
+
|
|
493
|
+
# To play with custom settings:
|
|
494
|
+
# config = GameConfig(bot_delay_seconds=0.5, animation_speed=35)
|
|
495
|
+
# game = Connect4Game(player1=RandomBot, player2=RandomBot, config=config)
|
|
496
|
+
# game.run()
|
pingv4/py.typed
ADDED
|
File without changes
|