1#!/usr/local/bin/python3.8
2"""
3Copyright (C) 2003  John-Paul Gignac
4
5This program is free software; you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation; either version 2 of the License, or
8(at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program; if not, write to the Free Software
17Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18"""
19
20# Import Modules
21import os, pygame, random, time, math, re, sys, md5
22from pygame.locals import *
23
24# Parse the command line
25highscores_file = "pathological_scores"
26screenshot = 0
27fullscreen = 0
28colorblind = 0
29sound_on = 1
30music_on = 1
31for arg in sys.argv[1:]:
32	if arg == '-s':
33		screenshot = 1
34	elif arg == '-f':
35		fullscreen = 1
36	elif arg == '-cb':
37		colorblind = 1
38	elif arg == '-q':
39		sound_on = 0
40		music_on = 0
41	elif arg[0] == '-':
42		print "Usage: "+sys.argv[0]+" [-cb] [-f] [-s] [highscores-file]\n"
43		sys.exit(1)
44	else:
45		highscores_file = arg
46
47if colorblind:
48	cbext = '-cb.png'
49else:
50	cbext = '.png'
51
52# The location of the setgid script for writing highscores
53# This script is only used if the highscores file is not writable directly
54write_highscores = "/usr/lib/pathological/bin/write-highscores"
55
56# Game constants
57wheel_steps = 9
58frames_per_sec = 100
59timer_width = 36
60timer_margin = 4
61info_height = 20
62initial_lives = 3
63extra_life_frequency = 5000 # Extra life awarded every this many points
64max_spare_lives = 10
65
66# Volume levels
67intro_music_volume = 0.6
68ingame_music_volume = 0.9
69sound_effects_volume = 0.6
70
71# Changing these may affect the playability of levels
72default_colors = (2,3,4,6)  # Blue, Green, Yellow, Red
73default_stoplight = (6,4,3) # Red, Yellow, Green
74default_launch_timer = 6    # 6 passes
75default_board_timer = 30    # 30 seconds per wheel
76marble_speed = 2            # Marble speed in pixels/frame (must be 1, 2 or 4)
77trigger_time = 30           # 30 seconds
78replicator_delay = 35       # 35 frames
79
80# Don't change these constants unless you
81# redo all of the levels
82horiz_tiles = 8
83vert_tiles = 6
84
85# Don't change these constants unless you
86# update the graphics files correspondingly.
87screen_width = 800
88screen_height = 600
89marble_size = 28
90tile_size = 92
91wheel_margin = 4
92stoplight_marble_size = 28
93life_marble_size = 16
94
95# The positions of the holes in the wheels in
96# each of the three rotational positions
97holecenter_radius = (tile_size - marble_size) / 2 - wheel_margin
98holecenters = []
99for i in range(wheel_steps):
100	theta = math.pi * i / (2 * wheel_steps)
101	c = math.floor( 0.5 + math.cos(theta)*holecenter_radius)
102	s = math.floor( 0.5 + math.sin(theta)*holecenter_radius)
103	holecenters.append((
104		(tile_size/2 + s, tile_size/2 - c),
105		(tile_size/2 + c, tile_size/2 + s),
106		(tile_size/2 - s, tile_size/2 + c),
107		(tile_size/2 - c, tile_size/2 - s)))
108
109# Direction references
110dirs = ((0,-1),(1,0),(0,1),(-1,0))
111
112# More global variables
113board_width = horiz_tiles * tile_size
114board_height = vert_tiles * tile_size
115launch_timer_pos = (0,info_height)
116board_pos = (timer_width, info_height + marble_size)
117timer_height = board_height + marble_size
118music_loaded = 0
119
120# Functions to create our resources
121def load_image(name, colorkey=-1, size=None):
122	fullname = os.path.join('graphics', name)
123	try:
124		image = pygame.image.load(fullname)
125	except pygame.error, message:
126		print 'Cannot load image:', fullname
127		raise SystemExit, message
128
129	if size is not None:
130		image = pygame.transform.scale( image, size)
131	image = image.convert()
132
133	if colorkey is not None:
134		if colorkey is -1:
135			colorkey = image.get_at((0,0))
136		image.set_colorkey(colorkey, RLEACCEL)
137	return image
138
139def load_sound(name, volume=1.0):
140	class NoneSound:
141		def play(self): pass
142	if not pygame.mixer or not pygame.mixer.get_init():
143		return NoneSound()
144	fullname = os.path.join('sounds', name)
145	try:
146		sound = pygame.mixer.Sound(fullname)
147	except pygame.error, message:
148		print 'Cannot load sound:', fullname
149		return NoneSound()
150
151	sound.set_volume( volume * sound_effects_volume)
152
153	return sound
154
155def play_sound(sound):
156	if sound_on: sound.play()
157
158def start_music(name, volume=-1):
159	global music_pending_song, music_loaded, music_volume
160
161	music_volume = volume
162
163	if not music_on:
164		music_pending_song = name
165		return
166
167	if not pygame.mixer or not pygame.mixer.music:
168		print "Background music not available."
169		return
170	fullname = os.path.join('music', name)
171	try:
172		pygame.mixer.music.load(fullname)
173	except pygame.error, message:
174		print 'Cannot load music:', fullname
175		return
176	music_loaded = 1
177	pygame.mixer.music.play(-1)
178
179	if music_volume >= 0:
180		pygame.mixer.music.set_volume( music_volume)
181
182	music_pending_song = 0
183
184def toggle_fullscreen():
185	global fullscreen
186	if pygame.display.toggle_fullscreen():
187		fullscreen = fullscreen ^ 1
188		return 1
189	else:
190		return 0
191
192def toggle_sound():
193	global sound_on
194	sound_on = sound_on ^ 1
195
196def toggle_music():
197	global music_on
198	music_on = music_on ^ 1
199	if music_on:
200		if music_pending_song:
201			start_music( music_pending_song)
202		elif music_loaded:
203			pygame.mixer.music.unpause()
204	elif music_loaded:
205		if not music_pending_song:
206			pygame.mixer.music.pause()
207
208# A better tick function
209next_frame = pygame.time.get_ticks()
210def my_tick( frames_per_sec):
211	global next_frame
212	# Wait for the next frame
213	next_frame += 1000.0 / frames_per_sec
214	now = pygame.time.get_ticks()
215	if next_frame < now:
216		# No time to wait - just hide our mistake
217		# and keep going as fast as we can.
218		next_frame = now
219	else:
220		pygame.time.wait( int(next_frame) - now)
221
222# Load the sounds
223def load_sounds():
224	global filter_admit,wheel_turn,wheel_completed,change_color
225	global direct_marble,ping,trigger_setup,teleport,marble_release
226	global levelfinish,die,incorrect,switch,shredder,replicator
227	global extra_life,menu_scroll,menu_select
228
229	filter_admit = load_sound('filter_admit.wav', 0.8)
230	wheel_turn = load_sound('wheel_turn.wav', 0.8)
231	wheel_completed = load_sound('wheel_completed.wav', 0.7)
232	change_color = load_sound('change_color.wav', 0.8)
233	direct_marble = load_sound('direct_marble.wav', 0.6)
234	ping = load_sound('ping.wav', 0.8)
235	trigger_setup = load_sound('trigger_setup.wav')
236	teleport = load_sound('teleport.wav', 0.6)
237	marble_release = load_sound('marble_release.wav', 0.5)
238	levelfinish = load_sound('levelfinish.wav', 0.6)
239	die = load_sound('die.wav')
240	incorrect = load_sound('incorrect.wav', 0.15)
241	switch = load_sound('switch.wav')
242	shredder = load_sound('shredder.wav')
243	replicator = load_sound('replicator.wav')
244	extra_life = load_sound('extra_life.wav')
245	menu_scroll = load_sound('menu_scroll.wav', 0.8)
246	menu_select = load_sound('switch.wav')
247
248# Load the fonts for various parts of the game
249def load_fonts():
250	global launch_timer_font,active_marbles_font,popup_font,info_font
251
252	launch_timer_font = pygame.font.Font(None, timer_width - 2*timer_margin)
253	active_marbles_font = pygame.font.Font(None, marble_size)
254	popup_font = pygame.font.Font(None, 24)
255	info_font = pygame.font.Font(None, info_height)
256
257# Load all of the images for the various game classes.
258# The images are stored as class variables in the corresponding classes.
259def load_images():
260	Marble.images = []
261	for i in range(9):
262		Marble.images.append( load_image('marble-'+`i`+cbext, -1,
263			(marble_size, marble_size)))
264
265	Tile.plain_tiles = []
266	Tile.tunnels = []
267	for i in range(16):
268		tile = load_image('tile.png', (206,53,53), (tile_size,tile_size))
269		path = load_image('path-'+`i`+'.png', -1, (tile_size,tile_size))
270		tile.blit( path, (0,0))
271		Tile.plain_tiles.append( tile)
272		Tile.tunnels.append(load_image('tunnel-'+`i`+'.png',
273			-1,(tile_size,tile_size)))
274	Tile.paths = 0
275
276	Wheel.images = (
277		load_image('wheel.png',-1,(tile_size,tile_size)),
278		load_image('wheel-dark.png',-1,(tile_size, tile_size)),
279		)
280	Wheel.blank_images = (
281		load_image('blank-wheel.png',-1,(tile_size,tile_size)),
282		load_image('blank-wheel-dark.png',-1,(tile_size, tile_size)),
283		)
284	Wheel.moving_holes = (
285		load_image('moving-hole.png',-1,(marble_size,marble_size)),
286		load_image('moving-hole-dark.png',-1,(marble_size, marble_size)),
287		)
288
289	Buffer.bottom = load_image('buffer.png',-1,(tile_size,tile_size))
290	Buffer.top = load_image('buffer-top.png',-1,(tile_size,tile_size))
291
292	Painter.images = []
293	for i in range(8):
294		Painter.images.append( load_image('painter-'+`i`+cbext, -1,
295			(tile_size,tile_size)))
296
297	Filter.images = []
298	for i in range(8):
299		Filter.images.append( load_image('filter-'+`i`+cbext, -1,
300			(tile_size,tile_size)))
301
302	Director.images = (
303		load_image('director-0.png',-1,(tile_size,tile_size)),
304		load_image('director-1.png',-1,(tile_size,tile_size)),
305		load_image('director-2.png',-1,(tile_size,tile_size)),
306		load_image('director-3.png',-1,(tile_size,tile_size)),
307		)
308
309	Shredder.image = load_image('shredder.png',-1,(tile_size,tile_size))
310
311	Switch.images = []
312	for i in range(4):
313		Switch.images.append( [])
314		for j in range(4):
315			if i == j: Switch.images[i].append( None)
316			else: Switch.images[i].append( load_image(
317				'switch-'+`i`+`j`+'.png',-1,(tile_size,tile_size)))
318
319	Replicator.image = load_image('replicator.png',-1,(tile_size,tile_size))
320
321	Teleporter.image_h = load_image('teleporter-h.png',-1,(tile_size,tile_size))
322	Teleporter.image_v = load_image('teleporter-v.png',-1,(tile_size,tile_size))
323
324	Trigger.image = load_image('trigger.png',-1,(tile_size,tile_size))
325
326	Stoplight.image = load_image('stoplight.png',-1,(tile_size,tile_size))
327	Stoplight.smallmarbles = []
328	for im in Marble.images:
329		Stoplight.smallmarbles.append( pygame.transform.scale(im,
330			(stoplight_marble_size,stoplight_marble_size)))
331
332	Board.life_marble = load_image('life-marble.png', -1,
333		(life_marble_size, life_marble_size))
334	Board.launcher_background = load_image('launcher.png', None,
335		(horiz_tiles * tile_size,marble_size))
336	Board.launcher_v = load_image('launcher-v.png', None,
337		(marble_size, vert_tiles * tile_size + marble_size))
338	Board.launcher_corner = load_image('launcher-corner.png', (255,0,0),
339		((tile_size-marble_size)/2+marble_size,marble_size))
340	Board.launcher_entrance = load_image('entrance.png', -1,
341		(tile_size,marble_size))
342
343	IntroScreen.background = load_image('intro.png', None,
344		(screen_width, screen_height))
345	IntroScreen.menu_font = pygame.font.Font(
346		None, IntroScreen.menu_font_height)
347	IntroScreen.scroller_font = pygame.font.Font(
348		None, IntroScreen.scroller_font_height)
349	IntroScreen.hs_font = pygame.font.Font(
350		None, IntroScreen.hs_font_height)
351
352# Function to set the video mode
353def set_video_mode():
354	global screen
355
356	icon = pygame.image.load(os.path.join('graphics','icon.png'))
357	icon.set_colorkey(icon.get_at((0,0)), RLEACCEL)
358	pygame.display.set_icon(icon) # Needed both before and after set_mode
359	screen = pygame.display.set_mode( (screen_width, screen_height),
360		fullscreen * FULLSCREEN)
361	pygame.display.set_icon(icon) # Needed both before and after set_mode
362	pygame.display.set_caption('Pathological')
363
364# Classes for our game objects
365
366class Marble:
367	def __init__(self, color, center, direction):
368		self.color = color
369		self.rect = pygame.Rect((0,0,marble_size,marble_size))
370		self.rect.center = center
371		self.direction = direction
372
373	def update(self, board):
374		self.rect.move_ip(
375			marble_speed * dirs[self.direction][0],
376			marble_speed * dirs[self.direction][1])
377
378		board.affect_marble( self)
379
380	def undraw(self, screen, background):
381		screen.set_clip( self.rect)
382		screen.blit( background, (0,0))
383		screen.set_clip()
384
385	def draw(self, screen):
386		screen.blit( self.images[self.color], self.rect.topleft)
387
388class Tile:
389	def __init__(self, paths=0, center=None):
390		self.paths = paths
391
392		if center is None:
393			center = (0,0)
394
395		self.center = center
396		self.rect = pygame.Rect((0,0,tile_size,tile_size))
397		self.rect.center = center
398		self.drawn = 0
399
400	def draw_back(self, surface):
401		if self.drawn: return 0
402		surface.blit( self.plain_tiles[self.paths], self.rect.topleft)
403		self.drawn = 1
404		return 1
405
406	def update(self, board): pass
407
408	def draw_fore(self, surface): return 0
409
410	def click(self, board, posx, posy, tile_x, tile_y): pass
411
412	def affect_marble(self, board, marble, rpos):
413		if rpos == (tile_size/2,tile_size/2):
414			if self.paths & (1 << marble.direction): return
415
416			# Figure out the new direction
417			t = self.paths - (1 << (marble.direction^2))
418			if t == 1: marble.direction = 0
419			elif t == 2: marble.direction = 1
420			elif t == 4: marble.direction = 2
421			elif t == 8: marble.direction = 3
422			else: marble.direction = marble.direction ^ 2
423
424class Wheel(Tile):
425	def __init__(self, paths, center=None):
426		Tile.__init__(self, paths, center) # Call base class intializer
427		self.spinpos = 0
428		self.completed = 0
429		self.marbles = [ -3, -3, -3, -3 ]
430
431	def draw_back(self, surface):
432		if self.drawn: return 0
433
434		Tile.draw_back(self, surface)
435
436		if self.spinpos:
437			surface.blit( self.blank_images[self.completed], self.rect.topleft)
438			for i in range(4):
439				holecenter = holecenters[self.spinpos][i]
440				surface.blit( self.moving_holes[self.completed],
441					(holecenter[0]-marble_size/2+self.rect.left,
442					holecenter[1]-marble_size/2+self.rect.top))
443		else:
444			surface.blit( self.images[self.completed], self.rect.topleft)
445
446		for i in range(4):
447			color = self.marbles[i]
448			if color >= 0:
449				holecenter = holecenters[self.spinpos][i]
450				surface.blit( Marble.images[color],
451					(holecenter[0]-marble_size/2+self.rect.left,
452					holecenter[1]-marble_size/2+self.rect.top))
453
454		return 1
455
456	def update(self, board):
457		if self.spinpos > 0:
458			self.spinpos -= 1
459			self.drawn = 0
460
461	def click(self, board, posx, posy, tile_x, tile_y):
462		# Ignore all clicks while rotating
463		if self.spinpos: return
464
465		b1, b2, b3 = pygame.mouse.get_pressed()
466		if b3:
467			# First, make sure that no marbles are currently entering
468			for i in self.marbles:
469				if i == -1 or i == -2: return
470
471			# Start the wheel spinning
472			self.spinpos = wheel_steps - 1
473			play_sound( wheel_turn)
474
475			# Reposition the marbles
476			t = self.marbles[0]
477			self.marbles[0] = self.marbles[1]
478			self.marbles[1] = self.marbles[2]
479			self.marbles[2] = self.marbles[3]
480			self.marbles[3] = t
481
482			self.drawn = 0
483
484		elif b1:
485			# Determine which hole is being clicked
486			for i in range(4):
487				# If there is no marble here, skip it
488				if self.marbles[i] < 0: continue
489
490				holecenter = holecenters[0][i]
491				rect = pygame.Rect( 0, 0, marble_size, marble_size)
492				rect.center = holecenter
493				if rect.collidepoint( posx, posy):
494
495					# Determine the neighboring tile
496					neighbor = board.tiles[ (tile_y + dirs[i][1]) %
497						vert_tiles][ (tile_x + dirs[i][0]) % horiz_tiles]
498
499					if (
500						# Disallow marbles to go off the top of the board
501						(tile_y == 0 and i==0) or
502
503						# If there is no way out here, skip it
504						((self.paths & (1 << i)) == 0) or
505
506						# If the neighbor is a wheel that is either turning
507						# or has a marble already in the hole, disallow
508						# the ejection
509						(isinstance(neighbor, Wheel) and
510						(neighbor.spinpos or
511						neighbor.marbles[i^2] != -3))
512						):
513						play_sound( incorrect)
514					else:
515						# If the neighbor is a wheel, apply a special lock
516						if isinstance(neighbor, Wheel):
517							neighbor.marbles[i^2] = -2
518						elif len(board.marbles) >= board.live_marbles_limit:
519							# Impose the live marbles limit
520							play_sound( incorrect)
521							break
522
523						# Eject the marble
524						board.marbles.append(
525							Marble( self.marbles[i],
526							(holecenter[0]+self.rect.left,
527							holecenter[1]+self.rect.top),
528							i))
529						self.marbles[i] = -3
530						play_sound( marble_release)
531						self.drawn = 0
532
533					break
534
535	def affect_marble(self, board, marble, rpos):
536		# Watch for marbles entering
537		if rpos[0]+marble_size/2 == wheel_margin or \
538			rpos[0]-marble_size/2 == tile_size - wheel_margin or \
539			rpos[1]+marble_size/2 == wheel_margin or \
540			rpos[1]-marble_size/2 == tile_size - wheel_margin:
541			if self.spinpos or self.marbles[marble.direction^2] >= -1:
542				# Reject the marble
543				marble.direction = marble.direction ^ 2
544				play_sound( ping)
545			else:
546				self.marbles[marble.direction^2] = -1
547
548		for holecenter in holecenters[0]:
549			if rpos == holecenter:
550				# Accept the marble
551				board.marbles.remove( marble)
552				self.marbles[marble.direction^2] = marble.color
553
554				self.drawn = 0
555
556				break
557
558	def complete(self, board):
559		# Complete the wheel
560		for i in range(4): self.marbles[i] = -3
561		if self.completed: board.game.increase_score( 10)
562		else: board.game.increase_score( 50)
563		self.completed = 1
564		play_sound( wheel_completed)
565		self.drawn = 0
566
567	def maybe_complete(self, board):
568		if self.spinpos > 0: return 0
569
570		# Is there a trigger?
571		if (board.trigger is not None) and \
572			(board.trigger.marbles is not None):
573			# Compare against the trigger
574			for i in range(4):
575				if self.marbles[i] != board.trigger.marbles[i] and \
576					self.marbles[i] != 8: return 0
577			self.complete( board)
578			board.trigger.complete( board)
579			return 1
580
581		# Do we have four the same color?
582		color = 8
583		for c in self.marbles:
584			if c < 0: return 0
585			if color==8: color=c
586			elif c==8: c=color
587			elif c != color: return 0
588
589		# Is there a stoplight?
590		if (board.stoplight is not None) and \
591			(board.stoplight.current < 3):
592			# Compare against the stoplight
593			if color != 8 and \
594				color != board.stoplight.marbles[board.stoplight.current]:
595				return 0
596			else:
597				board.stoplight.complete( board)
598
599		self.complete( board)
600		return 1
601
602class Buffer(Tile):
603	def __init__(self, paths, color=-1):
604		Tile.__init__(self, paths) # Call base class intializer
605		self.marble = color
606		self.entering = None
607
608	def draw_back(self, surface):
609		if self.drawn: return 0
610
611		Tile.draw_back(self, surface)
612
613		color = self.marble
614		if color >= 0:
615			holecenter = self.rect.center
616			surface.blit( Marble.images[color],
617				(holecenter[0]-marble_size/2,
618				holecenter[1]-marble_size/2))
619		else:
620			surface.blit( self.bottom, self.rect.topleft)
621
622
623		return 1
624
625	def draw_fore(self, surface):
626		surface.blit( self.tunnels[self.paths], self.rect.topleft)
627		surface.blit( self.top, self.rect.topleft)
628		return 0
629
630	def affect_marble(self, board, marble, rpos):
631		# Watch for marbles entering
632		if (rpos[0]+marble_size == tile_size/2 and marble.direction == 1) or \
633			(rpos[0]-marble_size == tile_size/2 and marble.direction == 3) or \
634			(rpos[1]+marble_size == tile_size/2 and marble.direction == 2) or \
635			(rpos[1]-marble_size == tile_size/2 and marble.direction == 0):
636
637			if self.entering is not None:
638				# Bump the marble that is currently entering
639				newmarble = self.entering
640				newmarble.rect.center = self.rect.center
641				newmarble.direction = marble.direction
642
643				play_sound( ping)
644
645				# Let the base class affect the marble
646				Tile.affect_marble(self, board, newmarble,
647					(tile_size/2,tile_size/2))
648			elif self.marble >= 0:
649				# Bump the marble that is currently caught
650				newmarble = Marble( self.marble, self.rect.center, marble.direction)
651
652				board.marbles.append( newmarble)
653
654				play_sound( ping)
655
656				# Let the base class affect the marble
657				Tile.affect_marble(self, board, newmarble,
658					(tile_size/2,tile_size/2))
659
660				self.marble = -1
661				self.drawn = 0
662
663			# Remember which marble is on its way in
664			self.entering = marble
665
666		elif rpos == (tile_size/2, tile_size/2):
667			# Catch this marble
668			self.marble = marble.color
669			board.marbles.remove( marble)
670			self.entering = None
671			self.drawn = 0
672
673class Painter(Tile):
674	def __init__(self, paths, color, center=None):
675		Tile.__init__(self, paths, center) # Call base class intializer
676		self.color = color
677
678	def draw_fore(self, surface):
679		surface.blit( self.tunnels[self.paths], self.rect.topleft)
680		surface.blit( self.images[self.color], self.rect.topleft)
681		return 0
682
683	def affect_marble(self, board, marble, rpos):
684		Tile.affect_marble( self, board, marble, rpos)
685		if rpos == (tile_size/2, tile_size/2):
686			if marble.color != self.color:
687				# Change the color
688				marble.color = self.color
689				play_sound( change_color)
690
691class Filter(Tile):
692	def __init__(self, paths, color, center=None):
693		Tile.__init__(self, paths, center) # Call base class intializer
694		self.color = color
695
696	def draw_fore(self, surface):
697		surface.blit( self.tunnels[self.paths], self.rect.topleft)
698		surface.blit( self.images[self.color], self.rect.topleft)
699		return 0
700
701	def affect_marble(self, board, marble, rpos):
702		if rpos == (tile_size/2, tile_size/2):
703			# If the color is wrong, bounce the marble
704			if marble.color != self.color and marble.color != 8:
705				marble.direction = marble.direction ^ 2
706				play_sound( ping)
707			else:
708				Tile.affect_marble( self, board, marble, rpos)
709				play_sound( filter_admit)
710
711class Director(Tile):
712	def __init__(self, paths, direction, center=None):
713		Tile.__init__(self, paths, center) # Call base class intializer
714		self.direction = direction
715
716	def draw_fore(self, surface):
717		surface.blit( self.tunnels[self.paths], self.rect.topleft)
718		surface.blit( self.images[self.direction], self.rect.topleft)
719		return 0
720
721	def affect_marble(self, board, marble, rpos):
722		if rpos == (tile_size/2, tile_size/2):
723			marble.direction = self.direction
724			play_sound( direct_marble)
725
726class Shredder(Tile):
727	def __init__(self, paths, center=None):
728		Tile.__init__(self, paths, center) # Call base class intializer
729
730	def draw_fore(self, surface):
731		surface.blit( self.tunnels[self.paths], self.rect.topleft)
732		surface.blit( self.image, self.rect.topleft)
733		return 0
734
735	def affect_marble(self, board, marble, rpos):
736		if rpos == (tile_size/2, tile_size/2):
737			board.marbles.remove( marble)
738			play_sound( shredder)
739
740class Switch(Tile):
741	def __init__(self, paths, dir1, dir2, center=None):
742		Tile.__init__(self, paths, center) # Call base class intializer
743		self.curdir = dir1
744		self.otherdir = dir2
745		self.switched = 0
746
747	def switch(self):
748		t = self.curdir
749		self.curdir = self.otherdir
750		self.otherdir = t
751		self.switched = 1
752		play_sound( switch)
753
754	def draw_fore(self, surface):
755		surface.blit( self.tunnels[self.paths], self.rect.topleft)
756		surface.blit( self.images[self.curdir][self.otherdir],
757			self.rect.topleft)
758		rc = self.switched
759		self.switched = 0
760		return rc
761
762	def affect_marble(self, board, marble, rpos):
763		if rpos == (tile_size/2, tile_size/2):
764			marble.direction = self.curdir
765			self.switch()
766
767class Replicator(Tile):
768	def __init__(self, paths, count, center=None):
769		Tile.__init__(self, paths, center) # Call base class intializer
770		self.count = count
771		self.pending = []
772
773	def draw_fore(self, surface):
774		surface.blit( self.tunnels[self.paths], self.rect.topleft)
775		surface.blit( self.image, self.rect.topleft)
776		return 0
777
778	def update(self, board):
779		for i in self.pending[:]:
780			i[3] -= 1
781			if i[3] == 0:
782				i[3] = replicator_delay
783
784				# Make sure that the active marble limit isn't exceeded
785				if len(board.marbles) >= board.live_marbles_limit:
786					# Clear the pending list
787					self.pending = []
788					return
789
790				# Add the new marble
791				board.marbles.append(Marble(i[0],self.rect.center,i[1]))
792				play_sound( replicator)
793
794				i[2] -= 1
795				if i[2] <= 0: self.pending.remove( i)
796
797	def affect_marble(self, board, marble, rpos):
798		Tile.affect_marble( self, board, marble, rpos)
799		if rpos == (tile_size/2, tile_size/2):
800			# Add the marble to the pending list
801			self.pending.append( [marble.color,marble.direction,
802				self.count - 1, replicator_delay]);
803			play_sound( replicator)
804
805class Teleporter(Tile):
806	def __init__(self, paths, other=None, center=None):
807		Tile.__init__(self, paths, center) # Call base class intializer
808		if paths & 5: self.image = self.image_v
809		else: self.image = self.image_h
810		if other is not None: self.connect( other)
811
812	def draw_fore(self, surface):
813		surface.blit( self.tunnels[self.paths], self.rect.topleft)
814		surface.blit( self.image, self.rect.topleft)
815		return 0
816
817	def connect(self, other):
818		self.other = other
819		other.other = self
820
821	def affect_marble(self, board, marble, rpos):
822		if rpos == (tile_size/2, tile_size/2):
823			marble.rect.center = self.other.rect.center
824			play_sound( teleport)
825
826class Trigger(Tile):
827	def __init__(self, colors, center=None):
828		Tile.__init__(self, 0, center) # Call base class intializer
829		self.marbles = None
830		self._setup( colors)
831
832	def _setup(self, colors):
833		self.countdown = 0
834		self.marbles = [
835			random.choice(colors),
836			random.choice(colors),
837			random.choice(colors),
838			random.choice(colors),
839			]
840		self.drawn = 0
841
842	def update(self, board):
843		if self.countdown > 0:
844			self.countdown -= 1
845			if self.countdown == 0:
846				self._setup( board.colors)
847				play_sound( trigger_setup)
848
849	def draw_back(self, surface):
850		if self.drawn: return 0
851		Tile.draw_back(self, surface)
852		surface.blit( self.image, self.rect.topleft)
853		if self.marbles is not None:
854			for i in range(4):
855				surface.blit( Marble.images[self.marbles[i]],
856					(holecenters[0][i][0]+self.rect.left-marble_size/2,
857					 holecenters[0][i][1]+self.rect.top-marble_size/2))
858		return 1
859
860	def complete(self, board):
861		self.marbles = None
862		self.countdown = trigger_time * frames_per_sec
863		self.drawn = 0
864		board.game.increase_score( 50)
865
866class Stoplight(Tile):
867	def __init__(self, colors, center=None):
868		Tile.__init__(self, 0, center) # Call base class intializer
869		self.marbles = list(colors)
870		self.current = 0
871
872	def draw_back(self, surface):
873		if self.drawn: return 0
874		Tile.draw_back(self, surface)
875		surface.blit( self.image, self.rect.topleft)
876		for i in range(self.current,3):
877			surface.blit( self.smallmarbles[self.marbles[i]],
878				(self.rect.centerx-14,
879				 self.rect.top+3+(29*i)))
880		return 1
881
882	def complete(self, board):
883		for i in range(3):
884			if self.marbles[i] >= 0:
885				self.marbles[i] = -1
886				break
887		self.current += 1
888		self.drawn = 0
889		board.game.increase_score( 20)
890
891class Board:
892	def __init__(self, game, pos):
893		self.game = game
894		self.pos = pos
895		self.marbles = []
896		self.screen = game.screen
897		self.trigger = None
898		self.stoplight = None
899		self.launch_queue = []
900		self.board_complete = 0
901		self.paused = 0
902		self.name = "Unnamed"
903		self.live_marbles_limit = 10
904		self.launch_timeout = -1
905		self.board_timeout = -1
906		self.colors = default_colors
907		self.launched = 1
908
909		self.set_launch_timer( default_launch_timer)
910		self.set_board_timer( default_board_timer)
911
912		# Create the board array
913		self.tiles = []
914		for j in range( vert_tiles):
915			row = range( horiz_tiles)
916			self.tiles.append( row)
917
918		# Load the level
919		# For levels above game.level, use a pseudo-random
920		# level selection method.
921		if( game.level < game.numlevels):
922			self._load( game.circuit, game.level)
923		else:
924			# Compute a hash of the current level, involving
925			# a static timestamp.  This provides a consistent,
926			# backtrackable pseudo-random function.
927			hash = md5.new(`game.gamestart`+"/"+`game.level`).digest()
928			hashval = (ord(hash[0]) + (ord(hash[1]) << 8) + \
929				(ord(hash[2]) << 16) + (ord(hash[3]) << 24)) & 32767;
930			self._load( game.circuit, hashval % game.numlevels);
931
932		# Create the launch timer text object
933		self.launch_timer_text = launch_timer_font.render(
934			`self.launch_timer`, 1, (255,255,255))
935		self.launch_timer_text_rect = self.launch_timer_text.get_rect()
936		self.launch_timer_text_rect.centerx = launch_timer_pos[0]+timer_width/2+1
937		self.launch_timer_text_rect.bottom = \
938			launch_timer_pos[1] + timer_height - timer_margin
939
940		# Fill up the launch queue
941		for i in range( vert_tiles * tile_size / marble_size + 2):
942			self.launch_queue.append(random.choice(self.colors))
943
944		# Create The Background
945		self.background = pygame.Surface(screen.get_size()).convert()
946		self.background.fill((200, 200, 200)) # Color of Info Bar
947
948		# Draw the Backdrop
949		backdrop = load_image('backdrop.jpg', None,
950			(horiz_tiles * tile_size, vert_tiles * tile_size))
951		self.background.blit( backdrop, board_pos);
952
953		# Draw the launcher
954		self.background.blit( self.launcher_background,
955			(board_pos[0], board_pos[1] - marble_size))
956		self.background.blit( self.launcher_v,
957			(board_pos[0]+horiz_tiles*tile_size, board_pos[1]))
958		for i in range( horiz_tiles):
959			if self.tiles[0][i].paths & 1:
960				self.background.blit( self.launcher_entrance,
961					(board_pos[0]+tile_size*i, board_pos[1]-marble_size))
962		self.background.blit( self.launcher_corner,
963			(board_pos[0]+horiz_tiles*tile_size-(tile_size-marble_size)/2,
964			board_pos[1] - marble_size))
965
966		# Draw the board name
967		board_name = `self.game.level+1` + " - " + self.name
968		if self.game.level >= self.game.numlevels:
969			board_name += " (Random)"
970		text = info_font.render( board_name, 1, (0,0,0))
971		rect = text.get_rect()
972		rect.left = 8
973		self.background.blit( text, rect)
974
975		# Figure out the score location
976		text = "Score: 00000000"
977		self.score_pos = screen_width - 8 - \
978			info_font.render( text, 1, (0,0,0)).get_rect().width
979
980		# Figure out the board timer location
981		text = "00:00"
982		self.board_timer_pos = self.score_pos - 16 - \
983			info_font.render( text, 1, (0,0,0)).get_rect().width
984
985		# Initialize the screen
986		screen.blit(self.background, (0, 0))
987
988	def draw_back(self, dirty_rects):
989		# Draw the launch timer
990		if self.launch_timer_height is None:
991			height = timer_height
992			rect = (launch_timer_pos[0],launch_timer_pos[1],
993				timer_width,timer_height)
994			self.screen.fill((0,0,0), rect)
995			self.screen.fill((0,40,255),
996				(launch_timer_pos[0]+timer_margin,
997				launch_timer_pos[1]+timer_height-height,
998				timer_width-timer_margin*2,height))
999			dirty_rects.append( rect)
1000		else:
1001			height = timer_height*self.launch_timeout/self.launch_timeout_start
1002			if height < self.launch_timer_height:
1003				rect = (launch_timer_pos[0] + timer_margin,
1004					launch_timer_pos[1] + timer_height - self.launch_timer_height,
1005					timer_width-2*timer_margin, self.launch_timer_height - height)
1006				self.screen.fill((0,0,0), rect)
1007				dirty_rects.append( rect)
1008		self.launch_timer_height = height
1009		self.screen.blit( self.launch_timer_text, self.launch_timer_text_rect)
1010		dirty_rects.append( self.launch_timer_text_rect)
1011
1012		# Clear the info bar
1013		rect = (0,0,screen_width,info_height)
1014		self.screen.set_clip( rect)
1015		self.screen.blit( self.background, (0,0))
1016		self.screen.set_clip()
1017		dirty_rects.append( rect)
1018
1019		# Draw the score
1020		text = "Score: "+("00000000"+`self.game.score`)[-8:]
1021		text = info_font.render( text, 1, (0,0,0))
1022		rect = text.get_rect()
1023		rect.left = self.score_pos
1024		self.screen.blit( text, rect)
1025
1026		# Draw the board timer
1027		time_remaining = (self.board_timeout+frames_per_sec-1)/frames_per_sec
1028		text = `time_remaining/60`+":"+("00"+`time_remaining%60`)[-2:]
1029		text = info_font.render( text, 1, (0,0,0))
1030		rect = text.get_rect()
1031		rect.left = self.board_timer_pos
1032		self.screen.blit( text, rect)
1033
1034		# Draw the lives counter
1035		right_edge = self.board_timer_pos - 32
1036		for i in range(self.game.lives - 1):
1037			rect = self.life_marble.get_rect()
1038			rect.centery = info_height / 2
1039			rect.right = right_edge
1040			self.screen.blit( self.life_marble, rect)
1041			right_edge -= rect.width + 4
1042
1043		# Draw the live marbles
1044		num_marbles = len(self.marbles)
1045		if num_marbles > self.live_marbles_limit:
1046			num_marbles = self.live_marbles_limit
1047		text = `num_marbles`+"/"+`self.live_marbles_limit`
1048		text = active_marbles_font.render( text, 1, (40,40,40))
1049		rect = text.get_rect()
1050		rect.left = self.pos[0] + 8
1051		rect.centery = self.pos[1] - marble_size / 2
1052		rect.width += 100
1053		self.screen.set_clip( rect)
1054		self.screen.blit( self.background, (0,0))
1055		self.screen.set_clip()
1056		self.screen.blit( text, rect)
1057
1058		dirty_rects.append( rect)
1059
1060		for row in self.tiles:
1061			for tile in row:
1062				if tile.draw_back( self.background):
1063					self.screen.set_clip( tile.rect)
1064					self.screen.blit( self.background, (0,0))
1065					self.screen.set_clip()
1066					dirty_rects.append( tile.rect)
1067
1068		if self.launched:
1069			for i in range(len(self.launch_queue)):
1070				self.background.blit( Marble.images[self.launch_queue[i]],
1071					(self.pos[0] + horiz_tiles * tile_size,
1072					self.pos[1] + i * marble_size - marble_size))
1073			rect = (self.pos[0] + horiz_tiles * tile_size,
1074					self.pos[1] - marble_size, marble_size,
1075					marble_size + tile_size * vert_tiles)
1076			self.screen.set_clip( rect)
1077			self.screen.blit( self.background, (0,0))
1078			self.screen.set_clip()
1079			dirty_rects.append( rect)
1080			self.launched = 0
1081
1082	def draw_fore(self, dirty_rects):
1083		for row in self.tiles:
1084			for tile in row:
1085				if tile.draw_fore(self.screen):
1086					dirty_rects.append( tile.rect)
1087
1088	def update(self):
1089		# Create the list of dirty rectangles
1090		dirty_rects = []
1091
1092		# Erase the marbles
1093		for marble in self.marbles:
1094			marble.undraw( self.screen, self.background)
1095			dirty_rects.append( list(marble.rect))
1096
1097		# Animate the marbles
1098		for marble in self.marbles[:]:
1099			marble.update( self)
1100
1101		# Animate the tiles
1102		for row in self.tiles:
1103			for tile in row:
1104				tile.update( self)
1105				if tile.drawn == 0: dirty_rects.append( tile.rect)
1106
1107		# Complete any wheels, if appropriate
1108		try_again = 1
1109		while try_again:
1110			try_again = 0
1111			for row in self.tiles:
1112				for tile in row:
1113					if isinstance( tile, Wheel):
1114						try_again |= tile.maybe_complete( self)
1115
1116		# Check if the board is complete
1117		self.board_complete = 1
1118		for row in self.tiles:
1119			for tile in row:
1120				if isinstance( tile, Wheel):
1121					if tile.completed == 0: self.board_complete = 0
1122
1123		# Decrement the launch timer
1124		if self.launch_timeout > 0:
1125			self.launch_timeout -= 1
1126			if self.launch_timeout == 0: self.board_complete = -1
1127
1128		# Decrement the board timer
1129		if self.board_timeout > 0:
1130			self.board_timeout -= 1
1131			if self.board_timeout == 0: self.board_complete = -2
1132
1133		# Draw the background
1134		self.draw_back( dirty_rects)
1135
1136		# Draw all of the marbles
1137		for marble in self.marbles:
1138			marble.draw( self.screen)
1139			dirty_rects.append( marble.rect)
1140
1141		# Draw the foreground
1142		self.draw_fore( dirty_rects)
1143
1144		# Flip the display
1145		pygame.display.update( dirty_rects)
1146
1147	def set_tile(self, x, y, tile):
1148		self.tiles[y][x] = tile
1149		tile.rect.left = self.pos[0] + tile_size * x
1150		tile.rect.top = self.pos[1] + tile_size * y
1151
1152		tile.x = x
1153		tile.y = y
1154
1155		# If it's a trigger, keep track of it
1156		if isinstance( tile, Trigger):
1157			self.trigger = tile
1158
1159		# If it's a stoplight, keep track of it
1160		if isinstance( tile, Stoplight):
1161			self.stoplight = tile
1162
1163	def set_launch_timer(self, passes):
1164		self.launch_timer = passes
1165		self.launch_timeout_start = (marble_size +
1166			(horiz_tiles * tile_size - marble_size) * passes) / marble_speed
1167		self.launch_timer_height = None
1168
1169	def set_board_timer(self, seconds):
1170		self.board_timer = seconds
1171		self.board_timeout_start = seconds * frames_per_sec
1172		self.board_timeout = self.board_timeout_start
1173
1174	def launch_marble(self):
1175		self.launch_queue.append(random.choice(self.colors))
1176		self.marbles.insert( 0, Marble( self.launch_queue[0],
1177			(self.pos[0]+tile_size*horiz_tiles+marble_size/2,
1178			self.pos[1]-marble_size/2), 3))
1179		del self.launch_queue[0]
1180		self.launched = 1
1181
1182		self.launch_timeout = self.launch_timeout_start
1183		self.launch_timer_height = None
1184
1185	def affect_marble(self, marble):
1186		c = marble.rect.center
1187		cx = c[0] - self.pos[0]
1188		cy = c[1] - self.pos[1]
1189
1190		# Bounce marbles off of the top
1191		if cy == marble_size/2:
1192			marble.direction = 2
1193			return
1194
1195		if cy < 0:
1196			if cx == marble_size/2:
1197				marble.direction = 1
1198				return
1199			if cx == tile_size * horiz_tiles - marble_size/2 \
1200				and marble.direction == 1:
1201				marble.direction = 3
1202				return
1203
1204			# The special case of new marbles at the top
1205			effective_cx = cx
1206			effective_cy = cy + marble_size
1207		else:
1208			effective_cx = cx + marble_size/2 * dirs[marble.direction][0]
1209			effective_cy = cy + marble_size/2 * dirs[marble.direction][1]
1210
1211		tile_x = effective_cx / tile_size
1212		tile_y = effective_cy / tile_size
1213		tile_xr = cx - tile_x * tile_size
1214		tile_yr = cy - tile_y * tile_size
1215
1216		if tile_x >= horiz_tiles: return
1217
1218		tile = self.tiles[tile_y][tile_x]
1219
1220		if cy < 0 and marble.direction != 2:
1221			# The special case of new marbles at the top
1222			if tile_xr == tile_size / 2 and (tile.paths & 1):
1223				if isinstance( tile, Wheel):
1224					if tile.spinpos > 0 or tile.marbles[0] != -3: return
1225					tile.marbles[0] = -2
1226					marble.direction = 2
1227					self.launch_marble()
1228				elif len(self.marbles) < self.live_marbles_limit:
1229					marble.direction = 2
1230					self.launch_marble()
1231		else:
1232			tile.affect_marble( self, marble, (tile_xr, tile_yr))
1233
1234	def click(self, pos):
1235		# Determine which tile the pointer is in
1236		tile_x = (pos[0] - self.pos[0]) / tile_size
1237		tile_y = (pos[1] - self.pos[1]) / tile_size
1238		tile_xr = pos[0] - self.pos[0] - tile_x * tile_size
1239		tile_yr = pos[1] - self.pos[1] - tile_y * tile_size
1240		if tile_x >= 0 and tile_x < horiz_tiles and \
1241			tile_y >= 0 and tile_y < vert_tiles:
1242			tile = self.tiles[tile_y][tile_x]
1243			tile.click( self, tile_xr, tile_yr, tile_x, tile_y)
1244
1245	def _load(self, circuit, level):
1246		fullname = os.path.join('circuits', circuit)
1247		f = open( fullname)
1248
1249		# Skip the previous levels
1250		j = 0
1251		while j < vert_tiles * level:
1252			line = f.readline()
1253			if line == '':
1254				f.close()
1255				return 0
1256			if line[0] == '|': j += 1
1257
1258		teleporters = []
1259		teleporter_names = []
1260		stoplight = default_stoplight
1261
1262		numwheels = 0
1263		boardtimer = -1
1264
1265		j = 0
1266		while j < vert_tiles:
1267			line = f.readline()
1268
1269			if line[0] != '|':
1270				if line[0:5] == 'name=':
1271					self.name = line[5:-1]
1272				elif line[0:11] == 'maxmarbles=':
1273					self.live_marbles_limit = int(line[11:-1])
1274				elif line[0:12] == 'launchtimer=':
1275					self.set_launch_timer( int(line[12:-1]))
1276				elif line[0:11] == 'boardtimer=':
1277					boardtimer = int(line[11:-1])
1278				elif line[0:7] == 'colors=':
1279					self.colors = []
1280					for c in line[7:-1]:
1281						if c >= '0' and c <= '7':
1282							self.colors.append(int(c))
1283							self.colors.append(int(c))
1284							self.colors.append(int(c))
1285						elif c == '8':
1286							# Crazy marbles are one-third as common
1287							self.colors.append(8)
1288				elif line[0:10] == 'stoplight=':
1289					stoplight = []
1290					for c in line[10:-1]:
1291						if c >= '0' and c <= '7':
1292							stoplight.append(int(c))
1293
1294				continue
1295
1296			for i in range(horiz_tiles):
1297				type = line[i*4+1]
1298				paths = line[i*4+2]
1299				if paths == ' ': pathsint = 0
1300				elif paths >= 'a': pathsint = ord(paths)-ord('a')+10
1301				elif paths >= '0' and paths <= '9': pathsint = int(paths)
1302				else: pathsint = int(paths)
1303				color = line[i*4+3]
1304				if color == ' ': colorint = 0
1305				elif color >= 'a': colorint = ord(color)-ord('a')+10
1306				elif color >= '0' and color <= '9': colorint = int(color)
1307				else: colorint = 0
1308
1309				if type == 'O':
1310					tile = Wheel( pathsint)
1311					numwheels += 1
1312				elif type == '%': tile = Trigger(self.colors)
1313				elif type == '!': tile = Stoplight(stoplight)
1314				elif type == '&': tile = Painter(pathsint, colorint)
1315				elif type == '#': tile = Filter(pathsint, colorint)
1316				elif type == '@':
1317					if color == ' ': tile = Buffer(pathsint)
1318					else: tile = Buffer(pathsint, colorint)
1319				elif type == ' ' or \
1320					(type >= '0' and type <= '8'): tile = Tile(pathsint)
1321				elif type == 'X': tile = Shredder(pathsint)
1322				elif type == '*': tile = Replicator(pathsint, colorint)
1323				elif type == '^':
1324					if color == ' ': tile = Director(pathsint, 0)
1325					elif color == '>': tile = Switch(pathsint, 0, 1)
1326					elif color == 'v': tile = Switch(pathsint, 0, 2)
1327					elif color == '<': tile = Switch(pathsint, 0, 3)
1328				elif type == '>':
1329					if color == ' ': tile = Director(pathsint, 1)
1330					elif color == '^': tile = Switch(pathsint, 1, 0)
1331					elif color == 'v': tile = Switch(pathsint, 1, 2)
1332					elif color == '<': tile = Switch(pathsint, 1, 3)
1333				elif type == 'v':
1334					if color == ' ': tile = Director(pathsint, 2)
1335					elif color == '^': tile = Switch(pathsint, 2, 0)
1336					elif color == '>': tile = Switch(pathsint, 2, 1)
1337					elif color == '<': tile = Switch(pathsint, 2, 3)
1338				elif type == '<':
1339					if color == ' ': tile = Director(pathsint, 3)
1340					elif color == '^': tile = Switch(pathsint, 3, 0)
1341					elif color == '>': tile = Switch(pathsint, 3, 1)
1342					elif color == 'v': tile = Switch(pathsint, 3, 2)
1343				elif type == '=':
1344					if color in teleporter_names:
1345						other = teleporters[teleporter_names.index(color)]
1346						tile = Teleporter( pathsint, other)
1347					else:
1348						tile = Teleporter( pathsint)
1349						teleporters.append( tile)
1350						teleporter_names.append( color)
1351
1352				self.set_tile( i, j, tile)
1353
1354				if type >= '0' and type <= '8':
1355					if color == '^': direction = 0
1356					elif color == '>': direction = 1
1357					elif color == 'v': direction = 2
1358					else: direction = 3
1359					self.marbles.append(
1360						Marble(int(type),tile.rect.center,direction))
1361
1362			j += 1
1363		if boardtimer < 0: boardtimer = default_board_timer * numwheels
1364		self.set_board_timer( boardtimer)
1365		f.close()
1366		return 1
1367
1368	# Return values for this function:
1369	# -4: User closed the application window
1370	# -3: User aborted the level
1371	# -2: Board timer expired
1372	# -1: Launch timer expired
1373	#  1: Level completed successfully
1374	#  2: User requested a skip to the next level
1375	#  3: User requested a skip to the previous level
1376	def play_level( self):
1377		# Perform the first render
1378		self.update()
1379
1380		# Play the start sound
1381		#play_sound( levelbegin)
1382
1383		# Launch the first marble
1384		self.launch_marble()
1385
1386		# Do the first update
1387		pygame.display.update()
1388
1389		# Game Loop
1390		while not self.board_complete:
1391			# Wait for the next frame
1392			my_tick( frames_per_sec)
1393
1394			# Handle Input Events
1395			for event in pygame.event.get():
1396				if event.type is QUIT:
1397					return -4
1398				elif event.type is KEYDOWN:
1399					if event.key is K_ESCAPE: return -3
1400					elif event.key == ord('n'): return 2
1401					elif event.key == ord('b'): return 3
1402					elif event.key == ord(' ') or \
1403						event.key == ord('p') or \
1404						event.key == K_PAUSE:
1405						self.paused = self.paused ^ 1
1406						if self.paused:
1407							if screenshot:
1408								pause_popup = None
1409							else:
1410								pause_popup = popup('Game Paused')
1411						else:
1412							popdown( pause_popup)
1413					elif event.key == K_F2:
1414						toggle_fullscreen()
1415					elif event.key == K_F3:
1416						toggle_music()
1417					elif event.key == K_F4:
1418						toggle_sound()
1419
1420				elif event.type is MOUSEBUTTONDOWN:
1421					if self.paused:
1422						self.paused = 0
1423						popdown( pause_popup)
1424					else: self.click( pygame.mouse.get_pos())
1425
1426			if not self.paused: self.update()
1427
1428		# Play the end sound
1429		if self.board_complete > 0:
1430			play_sound( levelfinish)
1431		else:
1432			play_sound( die)
1433
1434		return self.board_complete
1435
1436class HighScores:
1437	num_highscores = 10
1438
1439	def __init__(self, filename):
1440		self.filename = filename
1441		self.current_score = -1
1442		self.load()
1443
1444	def qualifies(self, score):
1445		self.load()
1446		return score >= self.scores[-1][0]
1447
1448	def add_score(self, score, circuit, level, name):
1449		self.load()
1450		for i in range(len(self.scores)):
1451			if score >= self.scores[i][0]:
1452				self.scores.insert( i, (score, circuit, level, name))
1453				del self.scores[self.num_highscores:]
1454				self.save()
1455				self.current_score = i
1456				return i
1457		return -1
1458
1459	def load( self):
1460		self.scores = []
1461
1462		parser = re.compile("([0-9]+) ([^ ]+) ([0-9]+) (.*)\n")
1463
1464		try:
1465			f = open( self.filename)
1466			while len(self.scores) < self.num_highscores:
1467				line = f.readline()
1468				if line == '': break
1469				match = parser.match(line)
1470				if match is not None:
1471					(score,circuit,level,name) = match.groups()
1472					self.scores.append(
1473						(int(score), circuit, int(level), name))
1474			f.close()
1475		except: pass
1476
1477		# Extend the list if it is shorter than needed
1478		while len(self.scores) < self.num_highscores:
1479			self.scores.append( (0, 'all-boards', 1, ''))
1480
1481		# Shrink the list if it is longer than allowed
1482		del self.scores[self.num_highscores:]
1483
1484	def save(self):
1485		try:
1486			f = open( self.filename, "w")
1487		except:
1488			try:
1489				f = os.popen(write_highscores, "w")
1490			except OSError, message:
1491				print "Warning: Can't save highscores:", message
1492				return
1493
1494		try:
1495			for i in self.scores:
1496				f.write( `i[0]`+' '+i[1]+' '+`i[2]`+' '+i[3]+'\n')
1497			f.close()
1498		except:
1499			print "Warning: Problem saving highscores."
1500
1501def wait_one_sec():
1502	time.sleep(1)
1503	pygame.event.get() # Clear the event queue
1504
1505def popup( text, minsize=None):
1506	maxwidth = 0
1507	objs = []
1508	while text != "":
1509		if '\n' in text:
1510			newline = text.index('\n')
1511			line = text[:newline]
1512			text = text[newline+1:]
1513		else:
1514			line = text
1515			text = ""
1516
1517		obj = popup_font.render( line, 1, (0, 0, 0))
1518		maxwidth = max( maxwidth, obj.get_rect().width)
1519		objs.append( obj)
1520
1521	linespacing = popup_font.get_ascent() - \
1522		popup_font.get_descent() + popup_font.get_linesize()
1523	# Work around an apparent pygame bug on Windows
1524	linespacing = min( linespacing, int(1.2 * popup_font.get_height()))
1525
1526	# Leave a bit more room
1527	linespacing = int(linespacing * 1.3)
1528
1529	window_width = maxwidth + 40
1530	window_height = popup_font.get_height()+linespacing*(len(objs)-1)+40
1531	if minsize is not None:
1532		window_width = max( window_width, minsize[0])
1533		window_height = max( window_height, minsize[1])
1534
1535	window = pygame.Surface((window_width, window_height))
1536	winrect = window.get_rect()
1537	window.fill((0, 0, 0))
1538	window.fill((250, 250, 250), winrect.inflate(-2,-2))
1539
1540	y = 20
1541	for obj in objs:
1542		textpos = obj.get_rect()
1543		textpos.top = y
1544		textpos.centerx = winrect.centerx
1545		window.blit( obj, textpos)
1546		y += linespacing
1547
1548	winrect.center = screen.get_rect().center
1549	winrect.top -= 40
1550
1551	backbuf = pygame.Surface(winrect.size).convert()
1552	backbuf.blit( screen, (0,0), winrect)
1553
1554	screen.blit( window, winrect)
1555	pygame.display.update()
1556
1557	return (backbuf, winrect)
1558
1559def popdown( popup_rc):
1560	if popup_rc is not None:
1561		screen.blit( popup_rc[0], popup_rc[1])
1562		pygame.display.update( popup_rc[1])
1563
1564class Game:
1565	def __init__(self, screen, circuit, highscores):
1566		self.screen = screen
1567		self.circuit = circuit
1568		self.highscores = highscores
1569
1570		# Count the number of levels
1571		fullname = os.path.join('circuits', circuit)
1572		f = open( fullname)
1573		j = 0
1574		while 1:
1575			line = f.readline()
1576			if line == '': break
1577			if line[0] == '|': j += 1
1578		f.close()
1579		self.numlevels = j / vert_tiles
1580
1581		self.level = 0
1582		self.score = 0
1583		self.lives = initial_lives
1584
1585		self.gamestart = time.time()
1586
1587	def increase_score(self, amount):
1588		# Add the amount to the score
1589		self.score += amount
1590
1591		# Award any extra lives that are due
1592		extra_lives = amount / extra_life_frequency + \
1593			(self.score % extra_life_frequency < amount % extra_life_frequency)
1594		extra_lives = min( extra_lives, max_spare_lives+1 - self.lives)
1595		if extra_lives > 0:
1596			self.lives += extra_lives
1597			play_sound( extra_life)
1598
1599	# Return values for this function:
1600	# -1: User closed the application window
1601	#  0: The game was aborted
1602	#  1: Game completed normally
1603	#  2: User achieved a highscore
1604	def play(self):
1605		# Draw the loading screen
1606		backdrop = load_image('backdrop.jpg', None,
1607			(screen_width, screen_height))
1608		screen.blit( backdrop, (0,0))
1609		pygame.display.update()
1610
1611		popup("Please wait...\n", (150, 50))
1612
1613		start_music("background.xm", ingame_music_volume)
1614
1615		self.highscores.current_score = -1
1616
1617		while 1:
1618			# Play a level
1619			board = Board( self, board_pos)
1620
1621			rc = board.play_level()
1622
1623			# Check for the user closing the window
1624			if rc == -4: return -1
1625
1626			if rc == 2:
1627				self.level += 1
1628				continue
1629
1630			if rc == 3:
1631				if self.level > 0: self.level -= 1
1632				self.score = 0
1633				self.lives = initial_lives
1634				continue
1635
1636			if rc < 0:
1637				# The board was not completed
1638
1639				if rc == -3: message = 'Level Aborted.'
1640				elif rc == -2: message = 'The board timer has expired.'
1641				else: message = 'The launch timer has expired.'
1642
1643				self.lives -= 1
1644				if self.lives > 0:
1645					rc = self.board_dialog( message+'\nClick to try again.',
1646						rc != -3)
1647				elif self.highscores.qualifies( self.score):
1648					popup("Congratulations!\n"+
1649						"You have a highscore!\n"+
1650						"Please enter your name:", (300, 180))
1651					name = get_name( self.screen, popup_font,
1652						((screen_width-250)/2,310,250,popup_font.get_height()),
1653						(255,255,255), (0,0,0))
1654					if name is None: return -1
1655
1656					self.highscores.add_score( self.score, self.circuit,
1657						self.level+1, name)
1658
1659					return 2
1660				else:
1661					rc = self.board_dialog( message +
1662						'\nGame Over.\nClick to continue\n', rc != -3)
1663					if rc == 1: return 1
1664
1665					self.score = 0
1666					self.lives = initial_lives
1667			else:
1668				# The board was completed
1669
1670				# Compute time remaining bonus
1671				time_remaining = 100 * board.board_timeout / \
1672					board.board_timeout_start
1673				time_bonus = 5 * time_remaining
1674
1675				# Compute empty holes bonus
1676				total_holes = 0
1677				empty_holes = 0
1678				for row in board.tiles:
1679					for tile in row:
1680						if isinstance( tile, Wheel):
1681							total_holes += 4
1682							for i in tile.marbles:
1683								if i < 0: empty_holes += 1
1684				empty_holes = (100 * empty_holes + total_holes/2) / total_holes
1685				holes_bonus = 2 * empty_holes
1686
1687				self.increase_score( time_bonus + holes_bonus)
1688
1689				message = 'Level Complete!\n'+ \
1690					"Bonus for " + `time_remaining` + "% time remaining: " + \
1691					`time_bonus` + "\n" + \
1692					"Bonus for " + `empty_holes` + "% holes empty: " + \
1693					`holes_bonus` + '\nClick to continue.'
1694
1695				rc = self.board_dialog( message, 1, 1)
1696				self.level += 1
1697
1698			if rc == -2: return -1
1699			if rc < 0: return 0
1700
1701	# Return values for this function:
1702	# -2: User closed the application window
1703	# -1: User pressed escape
1704	#  0: User pressed 'b' or 'n'
1705	#  1: Some other user event
1706	def board_dialog( self, message, pause=1, complete=0):
1707		popup(message)
1708		if pause: wait_one_sec()
1709
1710		# Wait for a mouse click to continue
1711		while 1:
1712			pygame.time.wait(20)
1713			for event in pygame.event.get():
1714				if event.type is QUIT:
1715					return -2
1716				elif event.type is KEYDOWN:
1717					if event.key == K_ESCAPE: return -1
1718					if event.key == ord('b'):
1719						if self.level > 0: self.level -= 1
1720						self.score = 0
1721						self.lives = initial_lives
1722						return 0
1723					elif event.key == ord('n'):
1724						if complete == 0:
1725							# Skip to the next level
1726							self.level += 1
1727						return 0
1728					elif event.key == K_F2:
1729						toggle_fullscreen()
1730						continue
1731					elif event.key == K_F3:
1732						toggle_music()
1733						continue
1734					elif event.key == K_F4:
1735						toggle_sound()
1736						continue
1737					elif event.key == K_LSHIFT or \
1738						event.key == K_RSHIFT or \
1739						event.key == K_LALT or \
1740						event.key == K_RALT or \
1741						event.key == K_LCTRL or \
1742						event.key == K_RCTRL:
1743						continue
1744					return 1
1745				elif event.type is MOUSEBUTTONDOWN:
1746					return 1
1747
1748def translate_key( key, shift_state):
1749	if shift_state:
1750		if key >= ord('a') and key <= ord('z'): key += ord('A') - ord('a')
1751		elif key == ord('1'): key = ord('!')
1752		elif key == ord('2'): key = ord('@')
1753		elif key == ord('3'): key = ord('#')
1754		elif key == ord('4'): key = ord('$')
1755		elif key == ord('5'): key = ord('%')
1756		elif key == ord('6'): key = ord('^')
1757		elif key == ord('7'): key = ord('&')
1758		elif key == ord('8'): key = ord('*')
1759		elif key == ord('9'): key = ord('(')
1760		elif key == ord('0'): key = ord(')')
1761		elif key == ord('`'): key = ord('~')
1762		elif key == ord("'"): key = ord('"')
1763		elif key == ord(";"): key = ord(':')
1764		elif key == ord("\\"): key = ord('|')
1765		elif key == ord("["): key = ord('{')
1766		elif key == ord("]"): key = ord('}')
1767		elif key == ord(","): key = ord('<')
1768		elif key == ord("."): key = ord('>')
1769		elif key == ord("/"): key = ord('?')
1770		elif key == ord("-"): key = ord('_')
1771		elif key == ord("="): key = ord('+')
1772	return key
1773
1774def get_name( screen, font, cursor_box, backcol, forecol):
1775	cursor_width = cursor_box[3] / 3
1776	cursor_pos = [cursor_box[0], cursor_box[1], cursor_width, cursor_box[3]]
1777	name = ""
1778
1779	inner_box = pygame.Rect(cursor_box)
1780	cursor_box = inner_box.inflate( 2, 2)
1781	outer_box = cursor_box.inflate( 2, 2)
1782
1783	enter_pressed = 0
1784	while not enter_pressed:
1785		screen.fill( forecol, outer_box)
1786		screen.fill( backcol, cursor_box)
1787		cursor_pos[0] = inner_box.left
1788		if name != "":
1789			obj = font.render( name, 1, forecol)
1790			screen.blit( obj, inner_box)
1791			cursor_pos[0] += obj.get_width()
1792		screen.fill( forecol, cursor_pos)
1793		pygame.display.update( (outer_box,))
1794
1795		# Keep track of the shift keys
1796		shift_state = pygame.key.get_mods() & KMOD_SHIFT
1797
1798		pygame.time.wait(20)
1799		for event in pygame.event.get():
1800			if event.type is QUIT:
1801				return None
1802			elif event.type is KEYUP:
1803				if event.key == K_LSHIFT:
1804					shift_state &= ~KMOD_LSHIFT
1805				elif event.key == K_RSHIFT:
1806					shift_state &= ~KMOD_RSHIFT
1807			elif event.type is KEYDOWN:
1808				if event.key == K_LSHIFT:
1809					shift_state |= KMOD_LSHIFT
1810				elif event.key == K_RSHIFT:
1811					shift_state |= KMOD_RSHIFT
1812				elif event.key == K_ESCAPE or event.key == K_RETURN:
1813					enter_pressed = 1
1814					break
1815				elif event.key == K_F2:
1816					toggle_fullscreen()
1817				elif event.key == K_F3:
1818					toggle_music()
1819				elif event.key == K_F4:
1820					toggle_sound()
1821				elif event.key == K_BACKSPACE:
1822					name = name[:-1]
1823				elif event.key >= 32 and event.key <= 127:
1824					key = translate_key( event.key, shift_state)
1825					name = name + chr(key)
1826
1827	return name
1828
1829class IntroScreen:
1830	menu = ("Start Game", "High Scores", "Fullscreen:", "Music:",
1831		"Sound Effects:", "Quit Game")
1832	menu_width = 240
1833	menu_pos = ((800 - menu_width)/2, 145)
1834	menu_font_height = 32
1835	menu_color = (255,255,255)
1836	menu_cursor_color = (60,60,60)
1837	menu_cursor_leftright_margin = 2
1838	menu_cursor_bottom_margin = -2
1839	menu_cursor_top_margin = 0
1840	menu_option_left = 200
1841	menu_rect = (menu_pos[0]-menu_cursor_leftright_margin,
1842		 menu_pos[1]-menu_cursor_top_margin,
1843		 menu_width + 2 * menu_cursor_leftright_margin,
1844		 menu_font_height * len(menu) +
1845			menu_cursor_top_margin + menu_cursor_bottom_margin)
1846
1847	scroller_font_height = 28
1848	scroller_rect = (10,550,780,scroller_font_height)
1849	scroller_text = \
1850		"   Copyright (C) 2003  John-Paul Gignac.   "+ \
1851		"   Soundtrack by Matthias Le Bidan.   "+ \
1852		"   Board designs contributed by Mike Brenneman and Kim Gignac.   "+ \
1853		"   To contribute your own board designs, see the website:  "+ \
1854		"http://pathological.sourceforge.net/   "+ \
1855		"   Logo by Carrie Bloomfield.   "+ \
1856		"   Other graphics based on artwork by Mike Brenneman.   "+ \
1857		"   Project motivated by Paul Prescod.   "+ \
1858		"   Thanks to all my friends who helped make this project "+ \
1859		"a success!   "+ \
1860		"   This program is free software; you can redistribute it and/or "+ \
1861		"modify it under the terms of the GNU General Public License.  "+ \
1862		"See the LICENSE file for details.   "
1863
1864	scroller_color = (60,60,60)
1865	scroller_speed = 2
1866
1867	def __init__(self, screen, highscores):
1868		self.screen = screen
1869		self.highscores = highscores
1870		self.curpage = 0
1871
1872		self.scroller_image = self.scroller_font.render(
1873			self.scroller_text, 1, self.scroller_color)
1874
1875		self.menu_cursor = 0
1876
1877	def draw_background(self):
1878		self.screen.blit( self.background, (0,0))
1879
1880	def undraw_menu(self):
1881		if self.curpage == 1:
1882			self.undraw_highscores()
1883			return
1884
1885		self.screen.set_clip( self.menu_rect)
1886		self.draw_background()
1887		self.screen.set_clip()
1888		self.dirty_rects.append( self.menu_rect)
1889
1890	def undraw_highscores(self):
1891		self.screen.set_clip( self.hs_rect)
1892		self.draw_background()
1893		self.screen.set_clip()
1894		self.dirty_rects.append( self.hs_rect)
1895
1896	def draw_menu(self):
1897		if self.curpage == 1:
1898			self.draw_highscores()
1899			return
1900
1901		self.undraw_menu()
1902
1903		self.screen.fill( self.menu_cursor_color,
1904			(self.menu_pos[0]-self.menu_cursor_leftright_margin,
1905			 self.menu_pos[1]-self.menu_cursor_top_margin +
1906			 self.menu_cursor * self.menu_font_height,
1907			 self.menu_width + 2 * self.menu_cursor_leftright_margin,
1908			 self.menu_font_height + self.menu_cursor_top_margin +
1909			 self.menu_cursor_bottom_margin))
1910
1911		y = self.menu_pos[1]
1912		for i in self.menu:
1913			menu_option = self.menu_font.render(i, 1, self.menu_color)
1914			self.screen.blit( menu_option, (self.menu_pos[0], y))
1915			y += self.menu_font_height
1916
1917		if fullscreen: offon = 'On'
1918		else: offon = 'Off'
1919		offon = self.menu_font.render( offon, 1, self.menu_color)
1920		self.screen.blit( offon,
1921			(self.menu_pos[0]+self.menu_option_left,
1922			self.menu_pos[1]+self.menu_font_height * 2))
1923
1924		if music_on: offon = 'On'
1925		else: offon = 'Off'
1926		offon = self.menu_font.render( offon, 1, self.menu_color)
1927		self.screen.blit( offon,
1928			(self.menu_pos[0]+self.menu_option_left,
1929			self.menu_pos[1]+self.menu_font_height * 3))
1930
1931		if sound_on: offon = 'On'
1932		else: offon = 'Off'
1933		offon = self.menu_font.render( offon, 1, self.menu_color)
1934		self.screen.blit( offon,
1935			(self.menu_pos[0]+self.menu_option_left,
1936			self.menu_pos[1]+self.menu_font_height * 4))
1937
1938		self.dirty_rects.append( self.menu_rect)
1939
1940	def draw_scroller(self):
1941		self.screen.set_clip( self.scroller_rect)
1942		self.draw_background()
1943		self.screen.blit( self.scroller_image,
1944			(self.scroller_rect[0] - self.scroller_pos,
1945			self.scroller_rect[1]))
1946		self.screen.set_clip()
1947		self.dirty_rects.append( self.scroller_rect)
1948
1949	def draw(self):
1950		self.dirty_rects = []
1951		self.draw_background()
1952		self.draw_menu()
1953		self.draw_scroller()
1954		pygame.display.update()
1955
1956	def go_to_main_menu(self):
1957		# Return to the main menu
1958		play_sound( menu_select)
1959		self.undraw_menu()
1960		self.curpage = 0
1961		self.draw_menu()
1962
1963	def go_to_highscores(self):
1964		# Go to the highscores page
1965		self.undraw_menu()
1966		self.curpage = 1
1967		self.draw_menu()
1968
1969	def do(self, show_highscores=0):
1970		self.scroller_pos = -self.scroller_rect[2]
1971
1972		if( show_highscores):
1973			self.dirty_rects = []
1974			self.go_to_highscores()
1975			pygame.display.update( self.dirty_rects)
1976
1977		self.draw()
1978
1979		start_music("intro.xm", intro_music_volume)
1980
1981		while 1:
1982			# Wait for the next frame
1983			my_tick( frames_per_sec)
1984
1985			self.dirty_rects = []
1986			self.draw_scroller()
1987
1988			# Advance the scroller
1989			self.scroller_pos += self.scroller_speed
1990			if self.scroller_pos >= self.scroller_image.get_rect().width:
1991				self.scroller_pos = -self.scroller_rect[2]
1992
1993			pygame.time.wait(20)
1994			for event in pygame.event.get():
1995				if event.type is QUIT:
1996					if self.curpage == 1:
1997						self.go_to_main_menu()
1998						continue
1999					return -2
2000				elif event.type is KEYDOWN:
2001					if event.key == K_F2:
2002						play_sound( menu_select)
2003						if not toggle_fullscreen(): return -3
2004						self.draw_menu()
2005					elif event.key == K_F3:
2006						play_sound( menu_select)
2007						toggle_music()
2008						self.draw_menu()
2009					elif event.key == K_F4:
2010						toggle_sound(1, self.dirty_rects)
2011						play_sound( menu_select)
2012						self.draw_menu()
2013					elif self.curpage == 1:
2014						self.go_to_main_menu()
2015					elif event.key == K_ESCAPE:
2016						return -1
2017					elif event.key == K_DOWN:
2018						self.menu_cursor += 1
2019						play_sound( menu_scroll)
2020						if self.menu_cursor == len(self.menu):
2021							self.menu_cursor = 0
2022						self.draw_menu()
2023					elif event.key == K_UP:
2024						self.menu_cursor -= 1
2025						play_sound( menu_scroll)
2026						if self.menu_cursor < 0:
2027							self.menu_cursor = len(self.menu) - 1
2028						self.draw_menu()
2029					elif event.key == K_SPACE or event.key == K_RETURN:
2030						rc = self.menu_select( self.menu_cursor)
2031						if rc < 1: return rc
2032					continue
2033				elif event.type is MOUSEBUTTONDOWN:
2034					if self.curpage == 1:
2035						self.go_to_main_menu()
2036						continue
2037
2038					pos = pygame.mouse.get_pos()
2039
2040					# Figure out which menu option is being clicked, if any
2041
2042					if pos[0] < self.menu_pos[0]: continue
2043					if pos[0] >= self.menu_pos[0] + self.menu_width: continue
2044					if pos[1] < self.menu_pos[1]: continue
2045					i = (pos[1] - self.menu_pos[1]) / self.menu_font_height
2046					if i >= len(self.menu): continue
2047
2048					rc = self.menu_select( i)
2049					if rc < 1: return rc
2050
2051			pygame.display.update( self.dirty_rects)
2052
2053	# Return values:
2054	# -3 - Toggle fullscreen requires warm restart
2055	# -1 - User selected the Quit option
2056	#  0 - User selected Begin Game
2057	#  1 - Unknown option
2058	def menu_select( self, i):
2059		if i == 0:
2060			return 0
2061		elif i == 1:
2062			play_sound( menu_select)
2063			self.go_to_highscores()
2064		elif i == 2:
2065			play_sound( menu_select)
2066			if not toggle_fullscreen(): return -3
2067			self.draw_menu()
2068		elif i == 3:
2069			play_sound( menu_select)
2070			toggle_music()
2071			self.draw_menu()
2072		elif i == 4:
2073			toggle_sound()
2074			play_sound( menu_select)
2075			self.draw_menu()
2076		elif i == 5:
2077			return -1
2078		return 1
2079
2080	hs_font_height = 24
2081	hs_width = 320
2082	hs_pos = ((800-hs_width)/2, 114)
2083	hs_margin = 8
2084	hs_column_margin = 70
2085	hs_score_width = 70
2086	hs_level_width = 24
2087	hs_heading_color = (0,0,0)
2088	hs_heading_background = (0,80,0)
2089	hs_number_color = (210,210,210)
2090	hs_body_color = (240,240,240)
2091	hs_current_color = (240,50,50)
2092	hs_rows = HighScores.num_highscores
2093	hs_rect = (hs_pos[0],hs_pos[1],hs_width,hs_font_height * 11)
2094
2095	def draw_highscores(self):
2096		self.undraw_menu()
2097
2098		tname = self.hs_font.render("Name", 1, self.hs_heading_color)
2099		tscore = self.hs_font.render("Score", 1, self.hs_heading_color)
2100		tlevel = self.hs_font.render("Level", 1, self.hs_heading_color)
2101
2102		# Compute the column positions
2103		score_right = self.hs_width
2104		score_left = score_right - tscore.get_size()[0]
2105		level_right = score_right - self.hs_score_width - \
2106			self.hs_margin
2107		level_left = level_right - tlevel.get_size()[0]
2108		name_right = level_right - self.hs_level_width - \
2109			self.hs_margin
2110		number = self.hs_font.render('10.',1,(0,0,0))
2111		number_width = number.get_size()[0]
2112		name_left = number_width + 5
2113		name_width = name_right - name_left
2114
2115		x = self.hs_pos[0]
2116		for j in range(len(self.highscores.scores)):
2117			if (j % self.hs_rows) == 0:
2118				if j > 0: x += self.hs_column_margin + self.hs_width
2119				y = self.hs_pos[1]
2120
2121				# Draw the headings
2122#				self.screen.fill( self.hs_heading_background,
2123#					(x - 30, y, self.hs_width + 30, self.hs_font_height))
2124				self.screen.blit( tname, (x + name_left, y))
2125				self.screen.blit( tlevel, (x + level_left, y))
2126				self.screen.blit( tscore, (x + score_left, y))
2127
2128			i = self.highscores.scores[j]
2129
2130			numcolor = self.hs_number_color
2131			color = self.hs_body_color
2132			if j == self.highscores.current_score:
2133				numcolor = color = self.hs_current_color
2134
2135			y += self.hs_font_height
2136			number = self.hs_font.render(`j+1`+'.',1,numcolor)
2137			self.screen.blit( number,
2138				(x + number_width - number.get_size()[0], y))
2139			if i[3] != '':
2140				name = self.hs_font.render( i[3], 1, color)
2141				if name.get_width() > name_width:
2142					name = name.subsurface( (0,0,name_width,name.get_height()))
2143				self.screen.blit( name, (x + name_left, y))
2144			level = self.hs_font.render( `i[2]`, 1, color)
2145			self.screen.blit( level, (x + level_right - level.get_width(), y))
2146			score = self.hs_font.render( `i[0]`, 1, color)
2147			self.screen.blit( score, (x + score_right - score.get_width(), y))
2148
2149		self.dirty_rects.append( self.hs_rect)
2150
2151def setup_everything():
2152	global introscreen
2153
2154	# Configure the audio settings
2155	if sys.platform[0:3] == 'win':
2156		# On Windows platforms, increase the sample rate and the buffer size
2157		pygame.mixer.pre_init(44100,-16,1,4096)
2158
2159	# Initialize the game module
2160	pygame.init()
2161
2162	if not pygame.font: print 'Warning, fonts disabled'
2163	if not pygame.mixer: print 'Warning, sound disabled'
2164
2165	set_video_mode()
2166	load_sounds()
2167	load_fonts()
2168	load_images()
2169
2170	introscreen = IntroScreen( screen, highscores)
2171
2172# Load the highscores file
2173highscores = HighScores( highscores_file)
2174
2175setup_everything()
2176
2177# Main loop
2178show_highscores = 0
2179while 1:
2180	# Display the intro screen
2181	while 1:
2182		rc = introscreen.do( show_highscores)
2183		if rc == -3:
2184			# Warm restart to toggle fullscreen
2185			fullscreen = fullscreen ^ 1
2186			setup_everything()
2187		else:
2188			break
2189
2190	if rc < 0: break   # Handle the QUIT message
2191
2192	game = Game(screen, 'all-boards', highscores)
2193
2194	show_highscores = 1
2195
2196	rc = game.play()
2197	if rc < 0: break   # Handle the QUIT message
2198	if rc == 0: show_highscores = 0
2199
2200