1+ import tkinter as tk
2+ import random
3+ from PIL import Image , ImageTk
4+ import pygame
5+ import sys
6+ import os
7+
8+ # --- Helper Function for PyInstaller ---
9+ def resource_path (relative_path ):
10+ """ Get absolute path to resource, works for dev and for PyInstaller """
11+ try :
12+ # PyInstaller creates a temp folder and stores path in _MEIPASS
13+ base_path = sys ._MEIPASS
14+ except Exception :
15+ base_path = os .path .abspath ("." )
16+ return os .path .join (base_path , relative_path )
17+
18+ # --- Game Constants ---
19+ WINDOW_WIDTH = 800
20+ WINDOW_HEIGHT = 600
21+ PLAYER_SIZE = 70
22+ PLAYER_SPEED = 35
23+ FRUIT_SIZE = 45
24+ UPDATE_DELAY = 30
25+ MAX_FRUITS = 4
26+ STARTING_LIVES = 3
27+
28+ # --- Asset File Names (using the helper function) ---
29+ INTRO_GIF_FILE = resource_path ("intro.gif" )
30+ MUSIC_FILE = resource_path ("1.mp3" )
31+ CATCH_SOUND_FILE = resource_path ("1.mp3" ) # Make sure you have a file named "1.mp3"
32+ HIGHSCORE_FILE = resource_path ("highscore.txt" )
33+
34+ # --- UI Customization ---
35+ BG_COLOR = "#a2d2ff"
36+ PLAYER_EMOJI = "🧺"
37+ FRUITS_EMOJI = ["🍎" , "🍓" , "🍊" , "🍇" , "🍌" , "🍒" , "🍑" ]
38+ HEART_EMOJI = "❤️"
39+ PUNCHLINES = [
40+ "Looks like you couldn't 'ketchup'!" ,
41+ "That was the 'apple' of my eye!" ,
42+ "You're 'berry' bad at this!" ,
43+ "'Orange' you glad you tried?" ,
44+ "That slip-up was 'bananas'!" ,
45+ ]
46+
47+ class FruityFrenzyGame :
48+ def __init__ (self , master ):
49+ self .master = master
50+ self .master .title ("Fruity Frenzy" )
51+ self .master .geometry (f"{ WINDOW_WIDTH } x{ WINDOW_HEIGHT } " )
52+ self .master .resizable (False , False )
53+
54+ # --- IMPORTANT: Handle window close event gracefully ---
55+ self .master .protocol ("WM_DELETE_WINDOW" , self .on_close )
56+
57+ pygame .mixer .init ()
58+ self .load_music ()
59+ self .load_sounds ()
60+
61+ self .high_score = self .load_high_score ()
62+ self .canvas = tk .Canvas (master , bg = BG_COLOR , width = WINDOW_WIDTH , height = WINDOW_HEIGHT , highlightthickness = 0 )
63+ self .canvas .pack ()
64+
65+ self .score = 0
66+ self .lives = STARTING_LIVES
67+ self .game_running = False
68+ self .fruits = []
69+ self .gif_frames = []
70+ self .gif_frame_index = 0
71+
72+ self .setup_intro_screen ()
73+ self .setup_game_elements ()
74+ self .setup_game_over_screen ()
75+
76+ self .show_intro_screen ()
77+
78+ def on_close (self ):
79+ """Handles the window closing event to prevent crashes."""
80+ print ("Closing game safely..." )
81+ self .game_running = False # Stop the game loop
82+ pygame .quit () # Cleanly shut down pygame
83+ self .master .destroy () # Safely close the Tkinter window
84+
85+ def setup_intro_screen (self ):
86+ try :
87+ self .load_gif_frames ()
88+ self .intro_gif_label = tk .Label (self .master , bg = BG_COLOR )
89+ self .intro_title = self .canvas .create_text (
90+ WINDOW_WIDTH / 2 , WINDOW_HEIGHT / 2 - 100 ,
91+ text = "Fruity Frenzy" , font = ("Goudy Stout" , 50 ),
92+ fill = "white" , state = tk .HIDDEN
93+ )
94+ self .start_button = tk .Button (
95+ self .master , text = "Start Game" , font = ("Helvetica" , 25 , "bold" ),
96+ command = self .start_game , relief = "raised" , borderwidth = 5 ,
97+ bg = "#4CAF50" , fg = "white"
98+ )
99+ except (FileNotFoundError , tk .TclError ):
100+ self .intro_gif_label = None
101+ print (f"Error: '{ INTRO_GIF_FILE } ' not found or is invalid. The intro screen will be static." )
102+ self .start_button = tk .Button (self .master , text = "Start Game (intro.gif not found)" , command = self .start_game )
103+
104+
105+ def setup_game_elements (self ):
106+ self .player = self .canvas .create_text (0 , 0 , text = PLAYER_EMOJI , font = ("Arial" , PLAYER_SIZE ), state = tk .HIDDEN )
107+ for _ in range (MAX_FRUITS ):
108+ fruit_item = self .canvas .create_text (0 , 0 , text = "" , font = ("Arial" , FRUIT_SIZE ), state = tk .HIDDEN )
109+ self .fruits .append ({'item' : fruit_item , 'speed' : 1 })
110+ self .score_label = self .canvas .create_text (15 , 15 , anchor = "nw" , text = "" , font = ("Helvetica" , 20 , "bold" ), fill = "white" , state = tk .HIDDEN )
111+ self .lives_label = self .canvas .create_text (WINDOW_WIDTH / 2 , 28 , anchor = "center" , text = "" , font = ("Helvetica" , 22 , "bold" ), fill = "#ff4d4d" , state = tk .HIDDEN )
112+ self .highscore_label = self .canvas .create_text (WINDOW_WIDTH - 15 , 15 , anchor = "ne" , text = "" , font = ("Helvetica" , 20 , "bold" ), fill = "white" , state = tk .HIDDEN )
113+
114+ def setup_game_over_screen (self ):
115+ self .game_over_text = self .canvas .create_text (WINDOW_WIDTH / 2 , WINDOW_HEIGHT / 2 - 50 , text = "GAME OVER" , font = ("Helvetica" , 50 , "bold" ), fill = "#ff5733" , state = tk .HIDDEN )
116+ self .punchline_text = self .canvas .create_text (WINDOW_WIDTH / 2 , WINDOW_HEIGHT / 2 , text = "" , font = ("Helvetica" , 20 , "italic" ), fill = "black" , state = tk .HIDDEN )
117+ self .restart_button = tk .Button (self .master , text = "Restart" , font = ("Helvetica" , 20 ), command = self .start_game , relief = "raised" , borderwidth = 3 , bg = "#4CAF50" , fg = "white" )
118+
119+ def show_intro_screen (self ):
120+ if self .intro_gif_label and self .gif_frames :
121+ self .intro_gif_label .place (x = 0 , y = 0 , relwidth = 1 , relheight = 1 )
122+ self .update_gif (0 )
123+ self .canvas .itemconfig (self .intro_title , state = tk .NORMAL )
124+ self .start_button .place (relx = 0.5 , rely = 0.5 , y = 50 , anchor = "center" )
125+ self .master .bind ("<KeyPress>" , lambda e : None )
126+ self .play_music ()
127+
128+ def start_game (self ):
129+ self .start_button .place_forget ()
130+ if self .intro_gif_label : self .intro_gif_label .place_forget ()
131+ self .canvas .itemconfig (self .intro_title , state = tk .HIDDEN )
132+ self .restart_button .place_forget ()
133+ self .canvas .itemconfig (self .game_over_text , state = tk .HIDDEN )
134+ self .canvas .itemconfig (self .punchline_text , state = tk .HIDDEN )
135+
136+ self .game_running = True
137+ self .score = 0
138+ self .lives = STARTING_LIVES
139+ self .update_score_label ()
140+ self .update_lives_label ()
141+ self .canvas .itemconfig (self .highscore_label , text = f"High Score: { self .high_score } " , state = tk .NORMAL )
142+ self .canvas .itemconfig (self .score_label , state = tk .NORMAL )
143+ self .canvas .itemconfig (self .lives_label , state = tk .NORMAL )
144+ self .canvas .itemconfig (self .player , state = tk .NORMAL )
145+ self .canvas .coords (self .player , WINDOW_WIDTH / 2 , WINDOW_HEIGHT - PLAYER_SIZE )
146+
147+ for fruit in self .fruits :
148+ self .canvas .itemconfig (fruit ['item' ], state = tk .NORMAL )
149+ self .reset_fruit (fruit )
150+
151+ self .master .bind ("<KeyPress>" , self .move_player )
152+ self .update_game ()
153+
154+ def load_gif_frames (self ):
155+ gif = Image .open (INTRO_GIF_FILE )
156+ for i in range (gif .n_frames ):
157+ gif .seek (i )
158+ frame = gif .resize ((WINDOW_WIDTH , WINDOW_HEIGHT ), Image .Resampling .LANCZOS )
159+ self .gif_frames .append (ImageTk .PhotoImage (frame ))
160+
161+ def update_gif (self , frame_index ):
162+ if self .game_running or not self .gif_frames : return
163+ frame = self .gif_frames [frame_index ]
164+ self .intro_gif_label .config (image = frame )
165+ next_frame_index = (frame_index + 1 ) % len (self .gif_frames )
166+ self .master .after (50 , self .update_gif , next_frame_index )
167+
168+ def load_music (self ):
169+ try :
170+ pygame .mixer .music .load (MUSIC_FILE )
171+ except pygame .error :
172+ print (f"Error: '{ MUSIC_FILE } ' not found. Music will not be played." )
173+
174+ def load_sounds (self ):
175+ try :
176+ self .catch_sound = pygame .mixer .Sound (CATCH_SOUND_FILE )
177+ except pygame .error :
178+ self .catch_sound = None
179+ print (f"Error: '{ CATCH_SOUND_FILE } ' not found. Catch sound will not be played." )
180+
181+ def play_catch_sound (self ):
182+ if self .catch_sound :
183+ self .catch_sound .play ()
184+
185+ def play_music (self ):
186+ try :
187+ pygame .mixer .music .play (loops = - 1 )
188+ except pygame .error :
189+ pass
190+
191+ def update_game (self ):
192+ if not self .game_running : return
193+ player_bbox = self .canvas .bbox (self .player )
194+ for fruit in self .fruits :
195+ self .canvas .move (fruit ['item' ], 0 , fruit ['speed' ])
196+ fruit_bbox = self .canvas .bbox (fruit ['item' ])
197+ if not fruit_bbox : continue
198+ if player_bbox and (player_bbox [0 ] < fruit_bbox [2 ] and player_bbox [2 ] > fruit_bbox [0 ] and player_bbox [1 ] < fruit_bbox [3 ] and player_bbox [3 ] > fruit_bbox [1 ]):
199+ self .score += 1
200+ self .play_catch_sound ()
201+ self .update_score_label ()
202+ self .reset_fruit (fruit )
203+ elif self .canvas .coords (fruit ['item' ])[1 ] > WINDOW_HEIGHT :
204+ self .lives -= 1
205+ self .update_lives_label ()
206+ self .reset_fruit (fruit )
207+ if self .lives <= 0 :
208+ self .end_game ()
209+ return
210+ self .master .after (UPDATE_DELAY , self .update_game )
211+
212+ def end_game (self ):
213+ self .game_running = False
214+ if self .score > self .high_score :
215+ self .high_score = self .score
216+ self .save_high_score ()
217+ self .canvas .itemconfig (self .highscore_label , text = f"High Score: { self .high_score } " )
218+ self .canvas .itemconfig (self .game_over_text , state = tk .NORMAL )
219+ self .canvas .itemconfig (self .punchline_text , text = random .choice (PUNCHLINES ), state = tk .NORMAL )
220+ self .restart_button .place (relx = 0.5 , rely = 0.5 , y = 60 , anchor = "center" )
221+
222+ def reset_fruit (self , fruit ):
223+ fruit ['speed' ] = random .uniform (4.0 , 9.0 )
224+ x_start = random .randint (FRUIT_SIZE , WINDOW_WIDTH - FRUIT_SIZE )
225+ y_start = - random .randint (FRUIT_SIZE , WINDOW_HEIGHT // 2 )
226+ self .canvas .itemconfig (fruit ['item' ], text = random .choice (FRUITS_EMOJI ))
227+ self .canvas .coords (fruit ['item' ], x_start , y_start )
228+
229+ def move_player (self , event ):
230+ if not self .game_running : return
231+ x , y = self .canvas .coords (self .player )
232+ if event .keysym == "Left" and x > PLAYER_SIZE / 2 : self .canvas .move (self .player , - PLAYER_SPEED , 0 )
233+ elif event .keysym == "Right" and x < WINDOW_WIDTH - PLAYER_SIZE / 2 : self .canvas .move (self .player , PLAYER_SPEED , 0 )
234+ elif event .keysym == "Up" and y > WINDOW_HEIGHT / 2 : self .canvas .move (self .player , 0 , - PLAYER_SPEED )
235+ elif event .keysym == "Down" and y < WINDOW_HEIGHT - PLAYER_SIZE / 2 : self .canvas .move (self .player , 0 , PLAYER_SPEED )
236+
237+ def update_score_label (self ): self .canvas .itemconfig (self .score_label , text = f"Score: { self .score } " )
238+ def update_lives_label (self ): self .canvas .itemconfig (self .lives_label , text = HEART_EMOJI * self .lives )
239+ def load_high_score (self ):
240+ try :
241+ with open (HIGHSCORE_FILE , "r" ) as f : return int (f .read ())
242+ except (FileNotFoundError , ValueError ): return 0
243+ def save_high_score (self ):
244+ with open (HIGHSCORE_FILE , "w" ) as f : f .write (str (self .high_score ))
245+
246+
247+ if __name__ == "__main__" :
248+ root = tk .Tk ()
249+ game = FruityFrenzyGame (root )
250+ root .mainloop ()
0 commit comments