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 # ----------------------
33 def __init__( self
, 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
):
47 if type == event
.type:
48 if number
== -1: # Any number is fine
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
:
56 elif type == pygame
.KEYDOWN \
57 or type == pygame
.KEYUP
:
58 if number
== event
.key
:
63 ans
= "Input( '%s'" % self
.name
65 ans
+= ", %d, %d" % ( p
[0], p
[1] )
69 # ----------------------
71 class HashException( Exception ):
74 # ----------------------
76 def mkdir_if_needed( filename
):
77 dr
= os
.path
.split( filename
)[0]
78 if dr
.strip() != "" and not os
.path
.isdir( dr
):
81 # ----------------------
83 def read_version( config
):
84 f
= file( os
.path
.join( config
.install_dir
, "version" ), 'r' )
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
)
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.
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)) )
117 new_colour
= ( colour
[0] * dim
, colour
[1] * dim
, colour
[2] * dim
)
121 # ----------------------
123 def clear_events( event_type
):
124 pygame
.time
.set_timer( event_type
, 0 )
125 pygame
.event
.clear( event_type
)
128 def __init__( self
, hiscores_filename
, default_score_table
):
129 self
.scores
= default_score_table
130 self
.filename
= hiscores_filename
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
):
147 f
= file( self
.filename
, 'r' )
152 if lines_since_hash
== 5: # Pathetic attempt at security
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()
165 del self
.scores
[current_array
][:]
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] ) ) )
176 pass # If the file didn't exist or couldn't be read, we continue
179 def hash_array( self
, array
):
184 ans
+= str( pair
[1] )
191 def save_array( self
, f
, array
):
192 f
.write( "%s\n" % self
.hash_array( array
) );
194 f
.write( "%s : %d\n" % pair
);
197 mkdir_if_needed( self
.filename
)
198 f
= file( self
.filename
, 'w' )
199 for arr
in self
.scores
:
200 self
.save_array( f
, arr
)
202 os
.fsync( f
.fileno() )
205 # ----------------------
209 def __init__( self
, config_filename
):
211 # Set the default config, and override if we find things in the
213 self
.default_config()
216 f
= file( config_filename
, 'r' )
219 self
.process_line( ln
)
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
229 self
.unsaved
.append( "filename" )
230 self
.unsaved
.append( "unsaved" )
232 def process_line( self
, ln
):
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()
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] )
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] == "'" ):
265 return (int)( value
)
268 def default_config( self
):
270 "default_config() should be implemented in the base class." )
274 mkdir_if_needed( self
.filename
)
275 f
= file( self
.filename
, 'w' )
276 keys
= self
.__dict
__.keys()
279 if k
not in self
.unsaved
:
281 if isinstance( v
, str ):
282 f
.write( "%s = '%s'\n" % ( k
, str(v
) ) )
284 f
.write( "%s = %s\n" % ( k
, str(v
) ) )
286 os
.fsync( f
.fileno() )
289 # ----------------------
292 def __init__( self
, text
, code
):
296 # ----------------------
300 def __init__( self
):
301 self
.selected_index
= 0
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
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
) )
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 # --------------------------
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
):
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
344 self
.bottom_pos
= 0.85
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
):
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
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
):
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
410 txt_type
= MenuRenderer
.TEXT_TYPE_MENU_UNSELECTED
413 self
.write_text( self
.menu
.items
[item_idx
].text
,
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
]
422 if txt_type
== MenuRenderer
.TEXT_TYPE_SMALL
:
423 ft
= self
.small_print_font
424 colour
= self
.colour_small_print
426 ft
= self
.menu_item_font
427 if txt_type
== MenuRenderer
.TEXT_TYPE_MENU_SELECTED
:
428 colour
= self
.colour_menu_selected
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
)
445 def move_somewhere( self
, move_method
):
446 old_index
= self
.menu
.selected_index
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
)
457 self
.move_somewhere( self
.menu
.move_up
)
459 # ----------------------
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
469 self
.music_sample
= None
470 self
.samples_loaded
= False
471 self
.sample_filenames
= {}
472 self
.sample_groups
= {}
475 if "music_on" not in config
.__dict
__:
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
,
484 self
.music_filename
= ""
486 self
.sounds_dir
= os
.path
.join( config
.install_dir
, "sounds" )
490 def add_sample_group( self
, groupname
, filenames
):
491 if self
.sound_inited
:
492 self
.sample_filenames
[groupname
] = []
494 self
.sample_filenames
[groupname
].append(
495 os
.path
.join( self
.sounds_dir
, fn
+ ".wav" ) )
496 if self
.config
.sound_effects_on
:
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():
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:
520 if self
.config
.music_on
:
521 if not self
.music_loaded
:
523 if not self
.music_playing
:
525 if self
.music_sample
!= None:
526 self
.music_sample
.set_volume( self
.volume
/ 3 )
527 #print "set music volume %f" % (self.volume / 3)
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:
542 return int( round( self
.volume
* 100 ) )
545 def decrease_volume( self
):
546 if self
.sound_inited
and self
.volume
> 0:
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()
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
)
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
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
:
609 if pygame
.mixer
.get_init():
610 self
.sound_inited
= True
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
:
625 if self
.config
.sound_effects_on
:
626 if not self
.samples_loaded
:
628 self
.sound_effects_on
= True
630 self
.sound_effects_on
= False