-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
448 lines (361 loc) · 18.6 KB
/
main.py
File metadata and controls
448 lines (361 loc) · 18.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
import pygame
import sys
from game.game_state import GameState
from ui.menu import MainMenu, PauseMenu
from ui.game_ui import GameUI
from utils.constants import *
from utils.assets import asset_manager
from utils.effects import effects_manager
from utils.sounds import sound_manager
class UnoGame:
def __init__(self):
"""Initialize the UNO game"""
pygame.init()
# Start with square window for menu
self.screen = pygame.display.set_mode((GAME_AREA_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("UNO!")
self.clock = pygame.time.Clock()
self.running = True
# Load assets
asset_manager.load_assets()
# Load effects
effects_manager.load()
# Load sounds
sound_manager.load_sounds()
# Game state
self.state = STATE_MENU
self.game_state = None
# UI components
self.main_menu = MainMenu(self.screen)
self.pause_menu = PauseMenu(self.screen)
self.game_ui = GameUI(self.screen)
# Bot turn timing
self.bot_turn_timer = 0
self.bot_thinking_time = 2250 # milliseconds (increased by 50%)
# Track if penalty cards have been handled this turn
self.penalty_cards_handled = False
def run(self):
"""Main game loop"""
while self.running:
dt = self.clock.tick(FPS)
self.handle_events()
self.update(dt)
self.draw()
pygame.quit()
sys.exit()
def handle_events(self):
"""Handle all events"""
mouse_pos = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
# Handle ESC key for pause
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
if self.state == STATE_PLAYING:
self.state = STATE_PAUSED
self.pause_menu.show()
elif self.state == STATE_PAUSED:
self.state = STATE_PLAYING
self.pause_menu.hide()
# Route events based on game state
if self.state == STATE_MENU or self.state == STATE_GAME_SETUP:
self._handle_menu_events(event)
elif self.state == STATE_PLAYING:
self._handle_game_events(event, mouse_pos)
elif self.state == STATE_PAUSED:
self._handle_pause_events(event)
elif self.state == STATE_GAME_OVER:
self._handle_game_over_events(event)
def _handle_menu_events(self, event):
"""Handle main menu events"""
action, data = self.main_menu.handle_event(event)
if action == 'start_game':
self.start_new_game(data)
elif action == 'exit':
self.running = False
def _handle_game_events(self, event, mouse_pos):
"""Handle in-game events"""
if not self.game_state:
return
# Handle UI events
ui_action = self.game_ui.handle_event(event, self.game_state)
if ui_action:
if ui_action['action'] == 'pause':
self.state = STATE_PAUSED
self.pause_menu.show()
elif ui_action['action'] == 'draw':
self._handle_draw_card()
elif ui_action['action'] == 'choose_color':
self._handle_color_choice(ui_action['color'])
# Handle card clicks for human player
current_player = self.game_state.get_current_player()
if current_player.is_human and event.type == pygame.MOUSEBUTTONDOWN:
clicked_card = current_player.handle_click(mouse_pos)
if clicked_card:
self._handle_card_play(clicked_card)
def _handle_pause_events(self, event):
"""Handle pause menu events"""
action = self.pause_menu.handle_event(event)
if action == 'resume':
self.state = STATE_PLAYING
elif action == 'main_menu':
self.state = STATE_MENU
self.game_state = None
# Resize window back to square for menu
self.screen = pygame.display.set_mode((GAME_AREA_WIDTH, WINDOW_HEIGHT))
self.main_menu = MainMenu(self.screen) # Recreate menu with new screen size
def _handle_game_over_events(self, event):
"""Handle game over screen events"""
if event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN:
self.state = STATE_MENU
self.game_state = None
# Resize window back to square for menu
self.screen = pygame.display.set_mode((GAME_AREA_WIDTH, WINDOW_HEIGHT))
self.main_menu = MainMenu(self.screen) # Recreate menu with new screen size
def _handle_draw_card(self):
"""Handle drawing a card"""
current_player = self.game_state.get_current_player()
if self.game_state.turn_state == TURN_PLAY_OR_DRAW or self.game_state.pending_draw_count > 0:
cards = self.game_state.draw_card(current_player)
if cards:
# Play draw sound
sound_manager.play_draw_sound()
# Animate each drawn card
for card in cards:
self.game_ui.animate_card_draw(card, current_player, self.game_state)
# Check if these were penalty cards (draw2/wild_draw4)
if len(cards) > 1:
# Drew multiple cards - this was a penalty, end turn
self.game_state.next_turn()
elif self.game_state.turn_state == TURN_AFTER_DRAW:
# Regular draw - check if drawn card can be played
drawn_card = cards[0]
if not self.game_state.can_play_after_draw(current_player, drawn_card):
self.game_state.next_turn()
def _handle_card_play(self, card):
"""Handle playing a card"""
current_player = self.game_state.get_current_player()
# Check if card can be played
if self.game_state.turn_state == TURN_PLAY_OR_DRAW:
if card.playable:
# Mark card as animating BEFORE playing it to prevent flash
card.is_animating = True
if self.game_state.play_card(current_player, card):
# Play card sound
sound_manager.play_card_sound()
# Now animate the card after it's been played
self.game_ui.animate_card_play(card, current_player)
# Check if we need to choose a color
if self.game_state.turn_state != TURN_CHOOSE_COLOR:
# Move to next turn if not choosing color
if not self.game_state.game_over:
self.penalty_cards_handled = False # Reset for next player
self.game_state.next_turn()
elif self.game_state.turn_state == TURN_AFTER_DRAW:
# Can only play the card just drawn
if self.game_state.can_play_after_draw(current_player, card):
# Mark card as animating BEFORE playing it
card.is_animating = True
if self.game_state.play_card(current_player, card):
# Play card sound
sound_manager.play_card_sound()
# Animate after playing
self.game_ui.animate_card_play(card, current_player)
# Check if we need to choose a color
if self.game_state.turn_state != TURN_CHOOSE_COLOR:
# Move to next turn if not choosing color
if not self.game_state.game_over:
self.penalty_cards_handled = False # Reset for next player
self.game_state.next_turn()
def _handle_color_choice(self, color):
"""Handle choosing a color for wild card"""
self.game_state.set_active_color(color)
self.game_ui.show_message(f"Color changed to {color}")
if not self.game_state.game_over:
self.penalty_cards_handled = False # Reset for next player
self.game_state.next_turn()
def start_new_game(self, num_bots):
"""Start a new game with specified number of bots"""
# Resize window to include history panel
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
self.game_ui = GameUI(self.screen) # Recreate UI with new screen size
self.game_state = GameState(num_bots)
self.state = STATE_PLAYING
self.bot_turn_timer = 0
def update(self, dt):
"""Update game logic"""
mouse_pos = pygame.mouse.get_pos()
# Update UI components
if self.state == STATE_MENU or self.state == STATE_GAME_SETUP:
self.main_menu.update(mouse_pos)
elif self.state == STATE_PLAYING:
self.game_ui.update(dt, mouse_pos)
# Update effects (dt is already in milliseconds from clock.tick)
effects_manager.update(dt)
# Update game state (for card animations)
self.game_state.update(dt)
# Update playable cards for human player
current_player = self.game_state.get_current_player()
if current_player.is_human:
# Auto-draw penalty cards for human players (only if they didn't play the draw card)
current_index = self.game_state.current_player_index
draw_card_player = self.game_state.draw_card_player_index
if (self.game_state.pending_draw_count > 0 and
not self.penalty_cards_handled and
current_index != draw_card_player):
self.penalty_cards_handled = True
cards = self.game_state.draw_card(current_player)
if cards:
# Play draw sound
sound_manager.play_draw_sound()
# Animate penalty cards being drawn
for card in cards:
self.game_ui.animate_card_draw(card, current_player, self.game_state)
# End turn after drawing penalty cards
self.game_state.next_turn()
self.penalty_cards_handled = False # Reset for next turn
else:
self.game_state.update_playable_cards(current_player)
# Handle bot turns
if not current_player.is_human and not self.game_state.game_over:
self._handle_bot_turn(dt)
# Check for game over
if self.game_state.game_over:
self.state = STATE_GAME_OVER
winner = self.game_state.winner
self.game_ui.show_message(f"{winner.name} wins!", 5000)
elif self.state == STATE_PAUSED:
self.pause_menu.update(mouse_pos)
def _handle_bot_turn(self, dt):
"""Handle bot player turns"""
# Add thinking delay for realism
self.bot_turn_timer += dt
if self.bot_turn_timer < self.bot_thinking_time:
return
self.bot_turn_timer = 0
current_bot = self.game_state.get_current_player()
# Handle color selection for wild cards
if self.game_state.turn_state == TURN_CHOOSE_COLOR:
color = current_bot.choose_color()
self.game_state.set_active_color(color)
self.game_ui.show_message(f"{current_bot.name} chose {color}")
self.game_state.next_turn()
return
# Check if bot must draw cards from draw2/wild_draw4
current_index = self.game_state.current_player_index
draw_card_player = self.game_state.draw_card_player_index
if (self.game_state.pending_draw_count > 0 and
current_index != draw_card_player):
cards = self.game_state.draw_card(current_bot)
if cards:
# Play draw sound
sound_manager.play_draw_sound()
# Animate each drawn card for bot
for card in cards:
self.game_ui.animate_card_draw(card, current_bot, self.game_state)
# After drawing penalty cards, turn ends
self.game_state.next_turn()
return
# Bot decides what to play
top_card = self.game_state.deck.get_top_card()
card_to_play = current_bot.choose_card(top_card, self.game_state.active_color, self.game_state)
if card_to_play:
# Mark card as animating BEFORE playing
card_to_play.is_animating = True
# Play the card
if self.game_state.play_card(current_bot, card_to_play):
# Play card sound
sound_manager.play_card_sound()
# Animate after playing
self.game_ui.animate_card_play(card_to_play, current_bot)
# Check if bot needs to choose color
if self.game_state.turn_state == TURN_CHOOSE_COLOR:
# Will handle in next update
return
# Move to next turn
if not self.game_state.game_over:
self.game_state.next_turn()
else:
# Bot needs to draw
cards = self.game_state.draw_card(current_bot)
if cards:
# Play draw sound
sound_manager.play_draw_sound()
# Animate the drawn card
for card in cards:
self.game_ui.animate_card_draw(card, current_bot, self.game_state)
# Check if bot can play the drawn card
if self.game_state.turn_state == TURN_AFTER_DRAW and cards:
drawn_card = cards[0]
if self.game_state.can_play_after_draw(current_bot, drawn_card):
# Mark card as animating BEFORE playing
drawn_card.is_animating = True
# Try to play it
if self.game_state.play_card(current_bot, drawn_card):
# Play card sound
sound_manager.play_card_sound()
# Animate after playing
self.game_ui.animate_card_play(drawn_card, current_bot)
if self.game_state.turn_state != TURN_CHOOSE_COLOR:
if not self.game_state.game_over:
self.game_state.next_turn()
else:
# Can't play, end turn
self.game_state.next_turn()
else:
# Drew multiple cards (from draw2/draw4), end turn
self.game_state.next_turn()
def draw(self):
"""Draw everything"""
if self.state == STATE_MENU or self.state == STATE_GAME_SETUP:
self.main_menu.draw()
elif self.state == STATE_PLAYING or self.state == STATE_PAUSED or self.state == STATE_GAME_OVER:
if self.game_state:
self.game_ui.draw(self.game_state)
# Draw pause menu on top if paused
if self.state == STATE_PAUSED:
self.pause_menu.draw()
# Draw game over screen
if self.state == STATE_GAME_OVER:
self._draw_game_over()
pygame.display.flip()
def _draw_game_over(self):
"""Draw game over screen"""
# Semi-transparent overlay
overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT))
overlay.set_alpha(192)
overlay.fill(BLACK)
self.screen.blit(overlay, (0, 0))
# Winner text
font_huge = pygame.font.Font(None, 72)
winner_text = font_huge.render(f"{self.game_state.winner.name} Wins!", True, YELLOW)
winner_rect = winner_text.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2 - 100))
self.screen.blit(winner_text, winner_rect)
# Cards remaining
font_medium = pygame.font.Font(None, 32)
y = WINDOW_HEIGHT // 2
# Create list of players with their card counts
player_cards = []
for player in self.game_state.players:
cards_left = player.get_hand_size()
player_cards.append((player.name, cards_left))
# Sort by cards remaining (winner with 0 cards first)
player_cards.sort(key=lambda x: x[1])
# Display each player's cards remaining
for player_name, cards_left in player_cards:
if cards_left == 0:
card_text = font_medium.render(f"{player_name}: Winner! (0 cards)", True, YELLOW)
else:
card_text = font_medium.render(f"{player_name}: {cards_left} cards left", True, WHITE)
card_rect = card_text.get_rect(center=(WINDOW_WIDTH // 2, y))
self.screen.blit(card_text, card_rect)
y += 40
# Continue prompt
font_small = pygame.font.Font(None, 24)
continue_text = font_small.render("Press any key to continue", True, WHITE)
continue_rect = continue_text.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT - 50))
self.screen.blit(continue_text, continue_rect)
if __name__ == "__main__":
game = UnoGame()
game.run()