Updated version to 0.0.7.
[troncode.git] / mopelib / mopelib.py
blobacc89b9e05e95a5870b007169a14021f6eaa5aa7
1 #!/usr/bin/python -u
3 # mopelib - a python module with some useful classes for creating games.
5 # Copyright (C) 2006-2007 Andy Balaam
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 import pygame, os, sys, random, gc, md5
23 from pygame.locals import *
25 # ----------------------
27 TEXT_OVER_ITEM_HEIGHT = 0.9
28 MAX_ITEM_HEIGHT = 0.11
30 # ----------------------
32 class MyInputEvent:
33 def __init__( self, name ):
34 self.pairs = []
35 self.name = name
37 def add_all( self, type ):
38 self.pairs.append( ( type, -1 ) )
40 def add( self, type, number ):
41 self.pairs.append( ( type, number ) )
43 def matches( self, event ):
44 for p in self.pairs:
45 type = p[0]
46 number = p[1]
47 if type == event.type:
48 if number == -1: # Any number is fine
49 return True
50 elif type == pygame.JOYBUTTONDOWN \
51 or type == pygame.JOYBUTTONUP \
52 or type == pygame.MOUSEBUTTONDOWN \
53 or type == pygame.MOUSEBUTTONUP:
54 if number == event.button:
55 return True
56 elif type == pygame.KEYDOWN \
57 or type == pygame.KEYUP:
58 if number == event.key:
59 return True
60 return False
62 def __str__( self ):
63 ans = "Input( '%s'" % self.name
64 for p in self.pairs:
65 ans += ", %d, %d" % ( p[0], p[1] )
66 ans += " )"
67 return ans
69 # ----------------------
71 class HashException( Exception ):
72 pass
74 # ----------------------
76 def mkdir_if_needed( filename ):
77 dr = os.path.split( filename )[0]
78 if dr.strip() != "" and not os.path.isdir( dr ):
79 os.mkdir( dr )
81 # ----------------------
83 def read_version( config ):
84 f = file( os.path.join( config.install_dir, "version" ), 'r' )
85 ln = f.readline()
86 f.close()
88 return ln.strip()
90 # ----------------------
92 def load_and_scale_image( filename, config ):
93 sur = pygame.image.load( os.path.join( config.images_dir, filename ) )
95 if sur.get_size() != config.screen_size:
96 sur = pygame.transform.scale( sur, config.screen_size )
98 sur.convert()
99 return sur
101 # ----------------------
103 def dim_colour( colour, dim ):
104 """Modify a colour to make it darker or lighter. Arguments:
105 - colour: the colour to modify as a 3-tuple for RGB values
106 - dim: 1 for no change, 0<=dim<1 for darker colours and
107 1<dim<=2 for lighter colours
108 returns the modified colour.
110 if dim == 1:
111 new_colour = colour
112 elif dim > 1:
113 new_colour = ( colour[0] + ((255-colour[0])*(dim-1)),
114 colour[1] + ((255-colour[1])*(dim-1)),
115 colour[2] + ((255-colour[2])*(dim-1)) )
116 else:
117 new_colour = ( colour[0] * dim, colour[1] * dim, colour[2] * dim )
119 return new_colour
121 # ----------------------
123 def clear_events( event_type ):
124 pygame.time.set_timer( event_type, 0 )
125 pygame.event.clear( event_type )
127 class Hiscores:
128 def __init__( self, hiscores_filename, default_score_table ):
129 self.scores = default_score_table
130 self.filename = hiscores_filename
132 try:
133 self.read_scores()
134 except HashException:
135 print "Fiddling detected: high scores reset to defaults!"
136 self.scores = default_score_table
138 if len( self.scores[0] ) != self.num_tables:
139 print "Creating high score table."
140 self.scores = default_score_table
142 def read_scores( self ):
143 current_array = -1
144 next_hash = None
146 try:
147 f = file( self.filename, 'r' )
148 ln = f.readline()
149 lines_since_hash = 5
150 while( ln ):
152 if lines_since_hash == 5: # Pathetic attempt at security
154 lines_since_hash = 0
155 if next_hash != None:
156 expected_next_hash = self.hash_array(
157 self.scores[current_array] )
158 if expected_next_hash != next_hash:
159 raise HashException()
161 next_hash = ln.strip()
163 current_array += 1
165 del self.scores[current_array][:]
167 else:
168 lines_since_hash += 1
169 split_ln = ln.split( ":", 1 )
170 if len( split_ln ) == 2:
171 self.scores[current_array].append( ( split_ln[0].strip(), int( split_ln[1] ) ) )
173 ln = f.readline()
174 f.close()
175 except IOError:
176 pass # If the file didn't exist or couldn't be read, we continue
179 def hash_array( self, array ):
180 ans = ""
181 for pair in array:
182 ans += pair[0]
183 ans += "."
184 ans += str( pair[1] )
185 ans += "|"
187 m = md5.new()
188 m.update( ans )
189 return m.hexdigest()
191 def save_array( self, f, array ):
192 f.write( "%s\n" % self.hash_array( array ) );
193 for pair in array:
194 f.write( "%s : %d\n" % pair );
196 def save( self ):
197 mkdir_if_needed( self.filename )
198 f = file( self.filename, 'w' )
199 for arr in self.scores:
200 self.save_array( f, arr )
201 f.flush()
202 os.fsync( f.fileno() )
203 f.close()
205 # ----------------------
207 class Config:
209 def __init__( self, config_filename ):
211 # Set the default config, and override if we find things in the
212 # config file
213 self.default_config()
215 try:
216 f = file( config_filename, 'r' )
217 ln = f.readline()
218 while( ln ):
219 self.process_line( ln )
220 ln = f.readline()
221 f.close()
222 except IOError:
223 pass # If the file didn't exist or couldn't be read, we continue
225 # Ensure no-one tries to exploit us with a frigged config file
226 self.filename = config_filename
228 self.unsaved = []
229 self.unsaved.append( "filename" )
230 self.unsaved.append( "unsaved" )
232 def process_line( self, ln ):
233 ln = ln.strip()
234 if len( ln ) > 0 and ln[0] != "#" and ln.find( '=' ) != -1:
235 split_ln = ln.split( '=' )
236 if len( split_ln ) == 2:
237 key = split_ln[0].strip()
238 value = split_ln[1]
240 self.__dict__[key] = self.parse_value( value )
242 def parse_value( self, value ):
244 value = value.strip()
245 if value[:5] == "Input":
246 tup = self.parse_value( value[5:] )
247 ret = MyInputEvent( tup[0] )
248 for i in range( 1, len( tup ), 2 ):
249 ret.add( tup[i], tup[i+1] )
250 return ret
252 elif value[0] == "(" and value[-1] == ")":
253 return tuple( map( self.parse_value,
254 value[1:-1].split( ',' ) ) )
256 elif value[0] == "[" and value[-1] == "]":
257 return map( self.parse_value,
258 value[1:-1].split( ',' ) )
260 elif ( value[0] == '"' and value[-1] == '"' ) \
261 or ( value[0] == "'" and value[-1] == "'" ):
262 return value[1:-1]
264 else:
265 return (int)( value )
268 def default_config( self ):
269 raise Exception(
270 "default_config() should be implemented in the base class." )
273 def save( self ):
274 mkdir_if_needed( self.filename )
275 f = file( self.filename, 'w' )
276 keys = self.__dict__.keys()
277 keys.sort()
278 for k in keys:
279 if k not in self.unsaved:
280 v = self.__dict__[k]
281 if isinstance( v, str ):
282 f.write( "%s = '%s'\n" % ( k, str(v) ) )
283 else:
284 f.write( "%s = %s\n" % ( k, str(v) ) )
285 f.flush()
286 os.fsync( f.fileno() )
287 f.close()
289 # ----------------------
291 class MenuItem:
292 def __init__( self, text, code ):
293 self.code = code
294 self.text = text
296 # ----------------------
298 class Menu:
300 def __init__( self ):
301 self.selected_index = 0
302 self.items = []
304 def set_selected_item( self, item_text ):
305 for i in range( len( self.items ) ):
306 if self.items[i].text == item_text:
307 self.selected_index = i
308 break
310 def get_selected_item( self ):
311 return self.items[self.selected_index]
313 def add_item( self, text, code ):
314 self.items.append( MenuItem( text, code ) )
316 def move_up( self ):
317 self.selected_index -= 1
318 if self.selected_index == -1:
319 self.selected_index = len( self.items ) - 1
321 def move_down( self ):
322 self.selected_index += 1
323 if self.selected_index == len( self.items ):
324 self.selected_index = 0
326 # --------------------------
328 class MenuRenderer:
330 TEXT_TYPE_SMALL = 0
331 TEXT_TYPE_MENU_SELECTED = 1
332 TEXT_TYPE_MENU_UNSELECTED = 2
334 def __init__( self, screen, config, background_surface,
335 colour_menu_unselected, colour_menu_selected, colour_small_print ):
336 self.screen = screen
337 self.config = config
338 self.background_surface = background_surface
339 self.colour_menu_unselected = colour_menu_unselected
340 self.colour_menu_selected = colour_menu_selected
341 self.colour_small_print = colour_small_print
343 self.top_pos = 0.15
344 self.bottom_pos = 0.85
346 self.item_height = 0
347 self.text_height = 0
349 self.rendered_txt = []
350 self.rendered_txt.append( {} )
351 self.rendered_txt.append( {} )
352 self.rendered_txt.append( {} )
354 def set_menu( self, menu, title ):
355 self.menu = menu
356 self.title_txt = title
358 new_item_height = ( ( self.bottom_pos - self.top_pos )
359 / len( self.menu.items ) )
360 new_text_height = new_item_height * TEXT_OVER_ITEM_HEIGHT
362 if( new_item_height != self.item_height or
363 new_text_height != self.text_height ):
364 self.item_height = new_item_height
365 self.text_height = new_text_height
367 if self.item_height > MAX_ITEM_HEIGHT:
368 self.item_height = MAX_ITEM_HEIGHT
369 self.text_height = MAX_ITEM_HEIGHT * TEXT_OVER_ITEM_HEIGHT
371 self.menu_item_font = pygame.font.Font(
372 None, int( self.config.screen_size[1] * self.text_height ) )
374 self.small_print_font = pygame.font.Font(
375 None, int( self.config.screen_size[1] * 0.06 ) )
377 def repaint_full( self ):
378 self.screen.blit( self.background_surface, (0,0) )
380 cur_pos = self.top_pos
381 for item in self.menu.items:
382 if self.menu.get_selected_item() == item:
383 txt_type = MenuRenderer.TEXT_TYPE_MENU_SELECTED
384 else:
385 txt_type = MenuRenderer.TEXT_TYPE_MENU_UNSELECTED
387 self.write_text( item.text, cur_pos, txt_type )
389 cur_pos += self.item_height
391 self.write_text( self.title_txt, 0.01,
392 MenuRenderer.TEXT_TYPE_SMALL )
394 self.write_text( "Press %s and %s to navigate" % (
395 self.config.keys_up.name, self.config.keys_down.name ), 0.9,
396 MenuRenderer.TEXT_TYPE_SMALL )
398 self.write_text( "and %s to select" % self.config.keys_return.name,
399 0.95, MenuRenderer.TEXT_TYPE_SMALL )
401 pygame.display.update()
403 def repaint_items( self, item_indices ):
404 dirty_rects = []
405 for item_idx in item_indices:
406 pos = self.top_pos + ( self.item_height * item_idx )
407 if item_idx == self.menu.selected_index:
408 txt_type = MenuRenderer.TEXT_TYPE_MENU_SELECTED
409 else:
410 txt_type = MenuRenderer.TEXT_TYPE_MENU_UNSELECTED
412 dirty_rects.append(
413 self.write_text( self.menu.items[item_idx].text,
414 pos, txt_type ) )
416 pygame.display.update( dirty_rects )
418 def write_text( self, txt, y_pos, txt_type ):
419 if txt in self.rendered_txt[txt_type]:
420 sf = self.rendered_txt[txt_type][txt]
421 else:
422 if txt_type == MenuRenderer.TEXT_TYPE_SMALL:
423 ft = self.small_print_font
424 colour = self.colour_small_print
425 else:
426 ft = self.menu_item_font
427 if txt_type == MenuRenderer.TEXT_TYPE_MENU_SELECTED:
428 colour = self.colour_menu_selected
429 else:
430 colour = self.colour_menu_unselected
432 sf = ft.render( txt, True, colour )
433 self.rendered_txt[txt_type][txt] = sf
435 pos = ( (self.config.screen_size[0] - sf.get_width() )/2,
436 (self.config.screen_size[1] - sf.get_height() ) * y_pos )
438 dirty_rect = ( pos[0], pos[1], sf.get_width(), sf.get_height() )
440 self.screen.blit( self.background_surface, pos, dirty_rect )
441 self.screen.blit( sf, pos )
443 return dirty_rect
445 def move_somewhere( self, move_method ):
446 old_index = self.menu.selected_index
447 move_method()
448 new_index = self.menu.selected_index
450 if old_index != new_index:
451 self.repaint_items( [old_index, new_index] )
453 def move_down( self ):
454 self.move_somewhere( self.menu.move_down )
456 def move_up( self ):
457 self.move_somewhere( self.menu.move_up )
459 # ----------------------
461 class SoundManager:
463 def __init__( self, config, music_filename = None ):
464 self.sound_inited = False
465 self.music_loaded = False
466 self.music_is_quiet = False
467 self.music_playing = False
468 self.volume = 0
469 self.music_sample = None
470 self.samples_loaded = False
471 self.sample_filenames = {}
472 self.sample_groups = {}
473 self.config = config
475 if "music_on" not in config.__dict__:
476 config.music_on = 1
477 if "sound_effects_on" not in config.__dict__:
478 config.sound_effects_on = 1
480 if music_filename != None:
481 self.music_filename = os.path.join( config.music_dir,
482 music_filename )
483 else:
484 self.music_filename = ""
486 self.sounds_dir = os.path.join( config.install_dir, "sounds" )
488 self.setup( None )
490 def add_sample_group( self, groupname, filenames ):
491 if self.sound_inited:
492 self.sample_filenames[groupname] = []
493 for fn in filenames:
494 self.sample_filenames[groupname].append(
495 os.path.join( self.sounds_dir, fn + ".wav" ) )
496 if self.config.sound_effects_on:
497 self.samples_load()
499 def play_sample( self, groupname ):
500 if self.sound_inited:
501 if self.sound_effects_on and self.volume > 0:
502 group = self.sample_groups[groupname]
503 group[ random.randint( 0, len( group ) - 1 ) ].play()
504 #print "play sample '%s'" % groupname
506 def set_volume( self ):
507 if self.sound_inited:
508 #print "set sample volumes %f" % self.volume
509 for sg in self.sample_groups.values():
510 for s in sg:
511 s.set_volume( self.volume )
513 self.set_music_volume()
515 def set_music_volume( self ):
516 if self.music_is_quiet:
517 if self.volume <= 0.3:
518 self.music_stop()
519 else:
520 if self.config.music_on:
521 if not self.music_loaded:
522 self.music_load()
523 if not self.music_playing:
524 self.music_start()
525 if self.music_sample != None:
526 self.music_sample.set_volume( self.volume / 3 )
527 #print "set music volume %f" % (self.volume / 3)
530 else:
531 if self.music_sample != None:
532 self.music_sample.set_volume( self.volume )
533 #print "set music volume %f" % (self.volume)
536 def increase_volume( self ):
537 if self.sound_inited and self.volume < 1:
538 self.volume += 0.1
539 if self.volume > 1:
540 self.volume = 1
541 self.set_volume()
542 return int( round( self.volume * 100 ) )
545 def decrease_volume( self ):
546 if self.sound_inited and self.volume > 0:
547 self.volume -= 0.1
548 if self.volume < 0:
549 self.volume = 0
550 self.set_volume()
551 return int( round( self.volume * 100 ) )
553 def music_start( self ):
554 if self.sound_inited and not self.music_playing:
555 if self.music_sample != None:
556 self.music_sample.play( -1 )
557 #print "play music (music_start)"
558 self.music_playing = True
560 def music_stop( self ):
561 if self.sound_inited and self.music_playing:
562 if self.music_sample != None:
563 self.music_sample.stop()
564 #print "stop music"
565 self.music_playing = False
567 def music_quiet( self ):
568 if self.sound_inited:
569 self.music_is_quiet = True
570 self.set_music_volume()
572 def music_loud( self ):
573 if self.sound_inited:
574 self.music_is_quiet = False
575 self.set_music_volume()
577 def samples_load( self ):
578 if self.sound_inited:
579 self.samples_loaded = True
580 for groupname in self.sample_filenames.keys():
581 self.sample_groups[groupname] = []
582 for fn in self.sample_filenames[groupname]:
583 if os.path.isfile( fn ):
584 snd = pygame.mixer.Sound( fn )
585 #print "load sample '%s'" % fn
586 self.sample_groups[groupname].append( snd )
587 else:
588 print "Could not find sound file '%s'" % fn
590 def music_load( self ):
591 if self.sound_inited:
592 self.music_loaded = True
593 if self.music_filename != "":
594 if os.path.isfile( self.music_filename ):
595 self.music_sample = pygame.mixer.Sound( self.music_filename )
596 #print "load music '%s'" % self.music_filename
597 else:
598 print "Could not find music file '%s'." % self.music_filename
600 def setup( self, gamestate ):
601 if "volume" in self.config.__dict__:
602 self.volume = self.config.volume / 100.0
604 if not self.sound_inited:
605 pass
606 #pygame.mixer.init()
607 #print "init mixer"
609 if pygame.mixer.get_init():
610 self.sound_inited = True
611 else:
612 self.sound_inited = False
613 if "init_error" not in self.__dict__:
614 print "Unable to initialise the sound mixer."
615 self.init_error = True
617 if self.sound_inited:
618 if self.config.music_on:
619 if not self.music_loaded:
620 self.music_load()
621 self.music_start()
622 else:
623 self.music_stop()
625 if self.config.sound_effects_on:
626 if not self.samples_loaded:
627 self.samples_load()
628 self.sound_effects_on = True
629 else:
630 self.sound_effects_on = False
631 self.set_volume()