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/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