Commits

Anonymous committed bd08a30

Moved all the images into their own folder, removed all temporary commented-out
code and other unused code, added events for bug squishing and player input for
restarting the game, and added a timed win condition.

  • Participants
  • Parent commits faca17b

Comments (0)

Files changed (59)

File squishthebugs/Brown Block.png

Removed
Old image

File squishthebugs/Character Cat Girl In Water.png

Removed
Old image

File squishthebugs/Character Cat Girl.png

Removed
Old image

File squishthebugs/DEPENDENCIES.txt

-this app requires:
-
-Python 3.2: http://www.python.org/download/releases/3.2.5/
-pygame-1.9.2a0.win32-py3.2: http://pygame.org/download.shtml

File squishthebugs/Dirt Block.png

Removed
Old image

File squishthebugs/Enemy Bug Squished.png

Removed
Old image

File squishthebugs/Enemy Bug.png

Removed
Old image

File squishthebugs/Gem Blue.png

Removed
Old image

File squishthebugs/Grass Block.png

Removed
Old image

File squishthebugs/Key.png

Removed
Old image

File squishthebugs/Plain Block.png

Removed
Old image

File squishthebugs/Question.png

Removed
Old image

File squishthebugs/READ THIS FILE OR I WILL EAT YOUR PETS.txt

+This app requires:
+
+Python 3.2: http://www.python.org/download/releases/3.2.5/
+pygame-1.9.2a0.win32-py3.2: http://pygame.org/download.shtml
+
+
+
+Run by double-clicking main.py.
+If that doesn't work, try right-clicking and do 'open with' and select Python 3.2.

File squishthebugs/Rock.png

Removed
Old image

File squishthebugs/Shadow East.png

Removed
Old image

File squishthebugs/Shadow North Clipped.png

Removed
Old image

File squishthebugs/Shadow North East.png

Removed
Old image

File squishthebugs/Shadow North West.png

Removed
Old image

File squishthebugs/Shadow North.png

Removed
Old image

File squishthebugs/Shadow Side West.png

Removed
Old image

File squishthebugs/Shadow South East.png

Removed
Old image

File squishthebugs/Shadow South West.png

Removed
Old image

File squishthebugs/Shadow South.png

Removed
Old image

File squishthebugs/Shadow West.png

Removed
Old image

File squishthebugs/SpeechBubble.png

Removed
Old image

File squishthebugs/Tree Short.png

Removed
Old image

File squishthebugs/Tree Tall.png

Removed
Old image

File squishthebugs/Tree Ugly.png

Removed
Old image

File squishthebugs/Water Block.png

Removed
Old image

File squishthebugs/Wood Block.png

Removed
Old image

File squishthebugs/images/Brown Block.png

Added
New image

File squishthebugs/images/Character Cat Girl In Water.png

Added
New image

File squishthebugs/images/Character Cat Girl.png

Added
New image

File squishthebugs/images/Dirt Block.png

Added
New image

File squishthebugs/images/Enemy Bug Squished.png

Added
New image

File squishthebugs/images/Enemy Bug.png

Added
New image

File squishthebugs/images/Gem Blue.png

Added
New image

File squishthebugs/images/Grass Block.png

Added
New image

File squishthebugs/images/Key.png

Added
New image

File squishthebugs/images/Plain Block.png

Added
New image

File squishthebugs/images/Question.png

Added
New image

File squishthebugs/images/Rock.png

Added
New image

File squishthebugs/images/Shadow East.png

Added
New image

File squishthebugs/images/Shadow North Clipped.png

Added
New image

File squishthebugs/images/Shadow North East.png

Added
New image

File squishthebugs/images/Shadow North West.png

Added
New image

File squishthebugs/images/Shadow North.png

Added
New image

File squishthebugs/images/Shadow Side West.png

Added
New image

File squishthebugs/images/Shadow South East.png

Added
New image

File squishthebugs/images/Shadow South West.png

Added
New image

File squishthebugs/images/Shadow South.png

Added
New image

File squishthebugs/images/Shadow West.png

Added
New image

File squishthebugs/images/SpeechBubble.png

Added
New image

File squishthebugs/images/Tree Short.png

Added
New image

File squishthebugs/images/Tree Tall.png

Added
New image

File squishthebugs/images/Tree Ugly.png

Added
New image

File squishthebugs/images/Water Block.png

Added
New image

File squishthebugs/images/Wood Block.png

Added
New image

File squishthebugs/main.py

 '''
 Created: Aug 4, 2013
-Last modified: Aug 12, 2013
+Last modified: Aug 13, 2013
 @author Flaise Saffron
 '''
 
 import math
 import sys
 import random
+import os
 
 
 ######################################################################## engine
 
+### Not used in this app anymore but still a useful library function
 def get_stick(joy, axisX, axisY):
 	state = ( joy.get_axis(axisX), joy.get_axis(axisY) )
 	
 	
 	return mag_scale_to_2d(state, new_mag)
 
+### Tampering with the pygame Vector classes to add the necessary functionality
+### didn't seem worth it; these few functions are all that are needed anyway
 def sum2d(a, b):
 	return (a[0] + b[0], a[1] + b[1])
+
 def dif2d(a, b):
 	return (a[0] - b[0], a[1] - b[1])
 
 
 def magnitudeSq2d(vec):
 	return vec[0] ** 2 + vec[1] ** 2
+
 def magnitude2d(vec):
 	return math.sqrt(magnitudeSq2d(vec))
 
 
 def mag_scale_2d(vec, scalar):
 	return (vec[0] * scalar, vec[1] * scalar)
+
 def mag_scale_to_2d(vec, scalar):
 	if not math.isfinite(scalar):
-		raise Exception() # because this was probably not intended
+		raise Exception() ### because this was probably not intended
 	return mag_scale_2d(vec, scalar / magnitude2d(vec))
 	### Just let it throw when the vector has a magnitude of zero; there's no
 	### real way to klodge that into a usable number because it would require
 
 def flipY2d(vec):
 	return (vec[0], -vec[1])
+
 def flip2d(vec):
 	return (-vec[0], -vec[1])
 
 def scale2d(vec, scalar):
 	return (vec[0] * scalar, vec[1] * scalar)
+
 def basic_product_2d(a, b):
 	return (a[0] * b[0], a[1] * b[1])
+
 def negate2d(vec):
 	return (-vec[0], -vec[1])
 
 
 EVENT_DRAW = 100
 
-event_handlers = [] # contains: (predicate:function, callback:function)
+event_handlers = [] ### contains: (predicate:function, callback:function)
 next_event_handlers = []
 def proc_event(event):
 	global event_handlers, next_event_handlers
 	while True:
 		proc_events()
 		proc_event(pygame.event.Event(EVENT_DRAW))
-		#pygame.display.update()
 		pygame.display.flip()
 
 def on_event(predicate, callback):
 	stop = on_event(predicate, lambda a: (callback(a), stop()))
 
 
+def render_text(text, position, anchor, dest_surface, font, color_fill, color_outline):
+	surface = font.render(text, True, color_outline)
+	position = sum2d(position, negate2d(basic_product_2d(surface.get_size(), anchor)))
+	
+	dest_surface.blit(surface, sum2d(position, (2, 0)))
+	dest_surface.blit(surface, sum2d(position, (-2, 0)))
+	dest_surface.blit(surface, sum2d(position, (0, 2)))
+	dest_surface.blit(surface, sum2d(position, (0, -2)))
+	
+	surface = font.render(text, True, color_fill)
+	dest_surface.blit(surface, position)
+	
+	return pygame.Rect(position, surface.get_size())
+
+
+
 ########################################################################### app
 
 pygame.init()
-#pygame.mouse.set_visible(False)
-surface_screen = pygame.display.set_mode(
-	#(0, 0),
-	#(600, 400),
-	(1024, 768),
-	#(1280, 720),
-	#(1440, 900),
-	#(1600, 1200),
-	#pygame.FULLSCREEN# |
-	#| pygame.DOUBLEBUF
-	#| pygame.HWSURFACE
-)
-pygame.display.set_caption('Squish the Bugs v0.0.8')
+surface_screen = pygame.display.set_mode((1024, 768))
+pygame.display.set_caption('Squish the Bugs v0.0.9')
 
+joysticks = [pygame.joystick.Joystick(x) for x in range(pygame.joystick.get_count())]
+for joy in joysticks:
+	joy.init()
+
+color_BG = pygame.Color(220, 220, 255)
+color_HUD = pygame.Color(255, 240, 240)
+color_HUD_outline = pygame.Color(60, 30, 30)
 
 class AppEvent:
-	INPUT_DIRECTION, BLOCK_MOVED = range(1000, 1002)
+	(
+		INPUT_DIRECTION, ### kwargs: direction
+		BLOCK_MOVED, ### kwargs: block
+		BUG_SQUISHED, ### kwargs: block
+		RESTART ### void
+	) = range(1000, 1004)
+	
 class Direction:
 	NORTH, EAST, SOUTH, WEST = range(4)
 
 	global camera, previous_hat
 	if event.value[0] != previous_hat[0]:
 		if event.value[0] == 1:
-			#proc_event(pygame.event.Event(AppEvent.EAST))
-			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION))
+			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION, direction=Direction.EAST))
 		elif event.value[0] == -1:
-			#proc_event(pygame.event.Event(AppEvent.WEST))
-			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION))
+			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION, direction=Direction.WEST))
 	if event.value[1] != previous_hat[1]:
 		if event.value[1] == 1:
-			#proc_event(pygame.event.Event(AppEvent.NORTH))
-			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION))
+			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION, direction=Direction.NORTH))
 		elif event.value[1] == -1:
-			#proc_event(pygame.event.Event(AppEvent.SOUTH))
-			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION))
+			proc_event(pygame.event.Event(AppEvent.INPUT_DIRECTION, direction=Direction.SOUTH))
 	previous_hat = event.value
 on_event(lambda a: a.type == pygame.JOYHATMOTION, lambda a: hat_to_direction4(a))
 
 
-on_event(lambda a: a.type == EVENT_DRAW, lambda a: surface_screen.fill(colorBG))
+on_event(lambda a: a.type == EVENT_DRAW, lambda a: surface_screen.fill(color_BG))
 on_event(lambda a: a.type == pygame.QUIT, lambda a: sys.exit())
 on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_ESCAPE, lambda a: sys.exit())
 
-def draw_joy_input(event):
-	for joyIndex, joy in enumerate(joysticks):
-		for button in range(joy.get_numbuttons()):
-			pygame.draw.rect(
-				surface_screen,
-				(colorDebugToggleOn if joy.get_button(button) else colorDebugToggleOff),
-				pygame.Rect(button * 40 + 5, joyIndex * 50 + 10, 30, 30)
-			)
-		
-		sticks = [ (0, 1), (4, 3) ]
-		for i in range(len(sticks)):
-			axes = sticks[i]
-			stick_center = ((joy.get_numbuttons() + i) * 40 + 5 + 15, joyIndex * 50 + 10 + 15)
-			pygame.draw.circle(surface_screen, colorDebug0, stick_center, 15)
-			pygame.draw.line(
-				surface_screen, colorDebug1, stick_center,
-				int2d(sum2d(stick_center, mag_scale_2d(get_stick(joy, axes[0], axes[1]), 15))),
-				3
-			)
-		
-		trigger_height = 30 * joy.get_axis(2)
-		pygame.draw.rect(
-			surface_screen,
-			colorDebug0,
-			pygame.Rect((joy.get_numbuttons() + i) * 40 + 5 + 40, joyIndex * 50 + 10, 30, 30)
-		)
-		if trigger_height > 0:
-			pygame.draw.rect(
-				surface_screen,
-				colorDebugToggleOn,
-				pygame.Rect((joy.get_numbuttons() + i) * 40 + 5 + 40, joyIndex * 50 + 10 + (30 - trigger_height), 30, trigger_height)
-			)
-		
-		### so... pygame won't detect the right-side trigger?
-		pygame.draw.rect(
-			surface_screen,
-			colorDebug0,
-			pygame.Rect((joy.get_numbuttons() + i) * 40 + 5 + 80, joyIndex * 50 + 10, 30, 30)
-		)
-		
-		for hat in range(joy.get_numhats()):
-			hat_center = ((joy.get_numbuttons() + i) * 40 + 5 + 40 + 80 + 15, joyIndex * 50 + 10 + 15)
-			pygame.draw.rect(
-				surface_screen,
-				colorDebug0,
-				pygame.Rect((joy.get_numbuttons() + i) * 40 + 5 + 40 + 80, joyIndex * 50 + 10, 30, 30),
-				3
-			)
-			pygame.draw.line(
-				surface_screen, colorDebug1, hat_center,
-				int2d(sum2d(hat_center, flipY2d(mag_scale_2d(joy.get_hat(hat), 15)))),
-				3
-			)
-#on_event(lambda a: a.type == EVENT_DRAW, draw_joy_input)
-
-colorBG = pygame.Color(255, 220, 255)
-colorDebug0 = pygame.Color(190, 170, 170)
-colorDebug1 = pygame.Color(150, 0, 0)
-colorDebugToggleOn = pygame.Color(20, 170, 20)
-colorDebugToggleOff = pygame.Color(160, 0, 0)
-color_HUD = pygame.Color(255, 255, 255)
-color_HUD_outline = pygame.Color(0, 0, 0)
-
-joysticks = [pygame.joystick.Joystick(x) for x in range(pygame.joystick.get_count())]
-for joy in joysticks:
-	joy.init()
-
-
-
 
 camera = ((0, 0, 0), 7) # ((x, y, z), view_height) ~~~ width depends on screen
 
 	camera = (dest, camera[1])
 
 
-blocks = [] # contains: ((x, y, z), tile)
-position_to_blocklist = {}
+TILE_W = 1
+TILE_L = .8
+TILE_H = .4
+
+### tile: (surface, (reg_x, reg_y), reg_width, is_filler, relative_layer, hud)
+tile_gray = (pygame.image.load(os.path.join('images', 'Plain Block.png')).convert_alpha(), (50, 90), 100, True, 0, False)
+tile_dirt = (pygame.image.load(os.path.join('images', 'Dirt Block.png')).convert_alpha(), (50, 90), 100, True, 0, False)
+tile_grass = (pygame.image.load(os.path.join('images', 'Grass Block.png')).convert_alpha(), (50, 90), 100, True, 0, False)
+tile_water = (pygame.image.load(os.path.join('images', 'Water Block.png')).convert_alpha(), (50, 90), 100, True, -1, False)
+tile_rock = (pygame.image.load(os.path.join('images', 'Rock.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+tile_tree = (pygame.image.load(os.path.join('images', 'Tree Tall.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+tile_player = (pygame.image.load(os.path.join('images', 'Character Cat Girl.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+tile_player_in_water = (pygame.image.load(os.path.join('images', 'Character Cat Girl In Water.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+tile_bug = (pygame.image.load(os.path.join('images', 'Enemy Bug.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+tile_bug_dead = (pygame.image.load(os.path.join('images', 'Enemy Bug Squished.png')).convert_alpha(), (50, 90), 100, False, -1, False)
+tile_speech = (pygame.image.load(os.path.join('images', 'SpeechBubble.png')).convert_alpha(), (-35, 190), 100, False, 0, True)
+tile_wood = (pygame.image.load(os.path.join('images', 'Wood Block.png')).convert_alpha(), (50, 90), 100, True, 0, False)
+tile_question = (pygame.image.load(os.path.join('images', 'Question.png')).convert_alpha(), (-35, 190), 100, False, 1, True) ### Uses font Palatino Linotype
+tile_bush_0 = (pygame.image.load(os.path.join('images', 'Tree Short.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+tile_bush_1 = (pygame.image.load(os.path.join('images', 'Tree Ugly.png')).convert_alpha(), (50, 90), 100, False, 0, False)
+
+shadow_e = (pygame.image.load(os.path.join('images', 'Shadow East.png')), (50, 90), 100)
+shadow_w = (pygame.image.load(os.path.join('images', 'Shadow West.png')), (50, 90), 100)
+shadow_s = (pygame.image.load(os.path.join('images', 'Shadow South.png')), (50, 90), 100)
+shadow_n = (pygame.image.load(os.path.join('images', 'Shadow North.png')), (50, 90), 100)
+shadow_side_w = (pygame.image.load(os.path.join('images', 'Shadow Side West.png')), (50, 90), 100)
+shadow_se = (pygame.image.load(os.path.join('images', 'Shadow South East.png')), (50, 90), 100)
+shadow_nw = (pygame.image.load(os.path.join('images', 'Shadow North West.png')), (50, 90), 100)
+shadow_sw = (pygame.image.load(os.path.join('images', 'Shadow South West.png')), (50, 90), 100)
+shadow_ne = (pygame.image.load(os.path.join('images', 'Shadow North East.png')), (50, 90), 100)
+
+
+class BlockType:
+	PLAYER, BUG, ROCK, TREE, FILLER, BUG_DEAD, SPEECH, WATER = range(8)
+
+blocks = [] ### contains: ((x, y, z), tile, BlockType)
+position_to_blocklist = {} ### contains: {(x, y, z): [blocks]}
+
+
+def clear_blocks():
+	global blocks
+	blocks = [] ### Not sure why List doesn't have a clear method...
+	
+	position_to_blocklist.clear()
+on_event(lambda a: a.type == AppEvent.RESTART, lambda a: clear_blocks())
+
 def update_blocks():
 	blocks.sort(key=lambda a: a[0][2] * 10000 + a[0][1] + a[1][4] + (100000000 if a[1][5] else 0))
 	position_to_blocklist.clear()
 def get_block_types_at(position):
 	for block in get_blocks_at(position):
 		yield block[2]
+		
 def get_blocks_at(position):
 	if position in position_to_blocklist:
-		#yield from position_to_blocklist[position]
 		for block in position_to_blocklist[position]:
 			yield block
+			
 def is_block_type_at(position, block_type):
 	return block_type in get_block_types_at(position)
-	
+
+def make_block(position, tile, block_type):
+	if len(position) != 3: raise Exception()
+	block = [position, tile, block_type]
+	blocks.append(block)
+	update_blocks()
+	return block
+
+
+
+########## Player
+
+block_player = None
+
 def move_player(block, delta):
 	def is_obstacle_type(block_type):
 		return block_type in (BlockType.BUG, BlockType.ROCK, BlockType.TREE, BlockType.FILLER)
+	
 	def is_obstacle_at(position):
 		return len([x for x in get_block_types_at(position) if is_obstacle_type(x)]) > 0
+	
 	def on_fail():
 		bubble = make_block(block[0], tile_speech, BlockType.SPEECH)
 		on_event_once(lambda a: a.type == AppEvent.INPUT_DIRECTION, lambda a: remove_block(bubble))
 					move_block(block, sum3d(delta, (0, 1 - i, 0)))
 					return
 				elif block_type == BlockType.BUG:
-					other_block[1] = tile_bug_dead
-					other_block[2] = BlockType.BUG_DEAD
+					squish_bug(other_block)
 					move_block(block, sum3d(delta, (0, -i, 0)))
 					return
 				elif is_obstacle_type(block_type):
 				else:
 					continue
 		on_fail()
+	
+def make_player(position):
+	global block_player
+	block_player = make_block(position, tile_player, BlockType.PLAYER)
+	
+	def center_camera_on_player():
+		move_camera_to(sum3d(block_player[0], (0, -1, 0)))
+	center_camera_on_player()
+	
+	def update_tile():
+		if is_block_type_at(block_player[0], BlockType.WATER):
+			block_player[1] = tile_player_in_water
+		else:
+			block_player[1] = tile_player
+	update_tile()
+	
+	
+	on_event(lambda a: a.type == AppEvent.BLOCK_MOVED and a.block == block_player, lambda a: (
+		center_camera_on_player(),
+		update_tile()
+	))
+
+
+########## Bug
+
+num_bugs = 0
+def restart_bugs():
+	global num_bugs
+	num_bugs = 0
+on_event(lambda a: a.type == AppEvent.RESTART, lambda a: restart_bugs())
+
+def squish_bug(block):
+	global num_bugs
+	num_bugs -= 1
+	
+	block[1] = tile_bug_dead
+	block[2] = BlockType.BUG_DEAD
+	
+	proc_event(pygame.event.Event(AppEvent.BUG_SQUISHED, block=block))
 
 def can_bug_move(block, delta):
 	if len(list(get_blocks_at(sum3d(block[0], (0, 1, 0))))) > 0: # something on top, usually another bug
 			elif not is_obstacle(block_type):
 				continue
 			return False
-	
+		
 def move_bug(block, delta):
 	if can_bug_move(block, delta):
 		move_block(block, delta)
-
+		
 def make_bug(position):
+	global num_bugs
+	num_bugs += 1
+	
 	last_move = None
 	def do_ai(event):
 		nonlocal last_move
 			if reverse_move != None and can_bug_move(block, reverse_move):
 				move_bug(block, reverse_move)
 				last_move = reverse_move
-		
-		
-		
 	
 	block = make_block(position, tile_bug, BlockType.BUG)
 	on_event(lambda a: a.type == AppEvent.BLOCK_MOVED and a.block == block_player, do_ai)
 	### TODO: Remove event binding when bug-death event fires
+	
 
 
-TILE_W = 1
-TILE_L = .8
-TILE_H = .4
 
-# tile: (surface, (reg_x, reg_y), reg_width, is_filler, relative_layer, hud)
-tile_gray = (pygame.image.load('Plain Block.png').convert_alpha(), (50, 90), 100, True, 0, False)
-tile_dirt = (pygame.image.load('Dirt Block.png').convert_alpha(), (50, 90), 100, True, 0, False)
-tile_grass = (pygame.image.load('Grass Block.png').convert_alpha(), (50, 90), 100, True, 0, False)
-tile_water = (pygame.image.load('Water Block.png').convert_alpha(), (50, 90), 100, True, -1, False)
-tile_rock = (pygame.image.load('Rock.png').convert_alpha(), (50, 90), 100, False, 0, False)
-tile_tree = (pygame.image.load('Tree Tall.png').convert_alpha(), (50, 90), 100, False, 0, False)
-tile_player = (pygame.image.load('Character Cat Girl.png').convert_alpha(), (50, 90), 100, False, 0, False)
-tile_player_in_water = (pygame.image.load('Character Cat Girl In Water.png').convert_alpha(), (50, 90), 100, False, 0, False)
-tile_bug = (pygame.image.load('Enemy Bug.png').convert_alpha(), (50, 90), 100, False, 0, False)
-tile_bug_dead = (pygame.image.load('Enemy Bug Squished.png').convert_alpha(), (50, 90), 100, False, -1, False)
-tile_speech = (pygame.image.load('SpeechBubble.png').convert_alpha(), (-35, 190), 100, False, 0, True)
-tile_wood = (pygame.image.load('Wood Block.png').convert_alpha(), (50, 90), 100, True, 0, False)
-tile_question = (pygame.image.load('Question.png').convert_alpha(), (-35, 190), 100, False, 1, True) ### Uses font Palatino Linotype
-tile_bush_0 = (pygame.image.load('Tree Short.png').convert_alpha(), (50, 90), 100, False, 0, False)
-tile_bush_1 = (pygame.image.load('Tree Ugly.png').convert_alpha(), (50, 90), 100, False, 0, False)
-
-shadow_e = (pygame.image.load('Shadow East.png'), (50, 90), 100)
-shadow_w = (pygame.image.load('Shadow West.png'), (50, 90), 100)
-shadow_s = (pygame.image.load('Shadow South.png'), (50, 90), 100)
-shadow_n = (pygame.image.load('Shadow North Clipped.png'), (50, 90), 100)
-shadow_side_w = (pygame.image.load('Shadow Side West.png'), (50, 90), 100)
-shadow_se = (pygame.image.load('Shadow South East.png'), (50, 90), 100)
-shadow_nw = (pygame.image.load('Shadow North West.png'), (50, 90), 100)
-shadow_sw = (pygame.image.load('Shadow South West.png'), (50, 90), 100)
-shadow_ne = (pygame.image.load('Shadow North East.png'), (50, 90), 100)
-
-
-class BlockType:
-	PLAYER, BUG, ROCK, TREE, FILLER, BUG_DEAD, SPEECH, WATER = range(8)
-
-def make_block(position, tile, block_type):
-	if len(position) != 3: raise Exception()
-	block = [position, tile, block_type]
-	blocks.append(block)
-	update_blocks()
-	return block
-
-block_player = None
-def make_player(position):
-	global block_player
-	block_player = make_block(position, tile_player, BlockType.PLAYER)
-	
-	def center_camera_on_player():
-		move_camera_to(sum3d(block_player[0], (0, -1, 0)))
-	center_camera_on_player()
-	
-	def update_tile():
-		if is_block_type_at(block_player[0], BlockType.WATER):
-			block_player[1] = tile_player_in_water
-		else:
-			block_player[1] = tile_player
-	update_tile()
-	
-	
-	on_event(lambda a: a.type == AppEvent.BLOCK_MOVED and a.block == block_player, lambda a: (
-		center_camera_on_player(), update_tile()
-	))
-
-	
-	
 def make_grass(position):
 	make_block(position, tile_grass, BlockType.FILLER)
 def make_stone(position):
 	make_block(position, tile_dirt, BlockType.FILLER)
 
 
-scale_cache = {} # { surface: ((x, y), result_surface) }
+scale_cache = {} ### { surface: ((x, y), result_surface) }
 def scale_surface_to(surface, dest_size):
 	if not (surface in scale_cache and scale_cache[surface][0] == dest_size):
 		scale_cache[surface] = (dest_size, pygame.transform.smoothscale(surface, dest_size))
 on_event(lambda a: a.type == AppEvent.INPUT_DIRECTION and a.direction == Direction.EAST, lambda a: move_player(block_player, (1, 0, 0)))
 on_event(lambda a: a.type == AppEvent.INPUT_DIRECTION and a.direction == Direction.WEST, lambda a: move_player(block_player, (-1, 0, 0)))
 
-#on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_EQUALS, lambda a: zoom_camera(-1))
-#on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_MINUS, lambda a: zoom_camera(1))
-
-def render_text(text, position, anchor, dest_surface, font, color_fill, color_outline):
-	surface = font.render(text, True, color_outline)
-	position = sum2d(position, negate2d(basic_product_2d(surface.get_size(), anchor)))
-	
-	dest_surface.blit(surface, sum2d(position, (2, 0)))
-	dest_surface.blit(surface, sum2d(position, (-2, 0)))
-	dest_surface.blit(surface, sum2d(position, (0, 2)))
-	dest_surface.blit(surface, sum2d(position, (0, -2)))
-	
-	surface = font.render(text, True, color_fill)
-	dest_surface.blit(surface, position)
-	
-	return pygame.Rect(position, surface.get_size())
-
-
-
 def ms_to_hms_string(ms):
 	seconds = int(ms / 1000)
 	
 	return ('{0:02d}'.format(hours) + ':' if hours > 0 else '') + '{0:02d}'.format(minutes) + ':' + '{0:02d}'.format(seconds)
 
 fps_clock = pygame.time.Clock()
-gui_font = pygame.font.SysFont('Verdana', 28)
+gui_font_large = pygame.font.SysFont('Verdana', 32)
+gui_font_medium = pygame.font.SysFont('Verdana', 20)
+gui_font_small = pygame.font.SysFont('Verdana', 14)
+
 puzzle_start = pygame.time.get_ticks()
+puzzle_end = None
+
+def restart_puzzle_time():
+	global puzzle_start
+	global puzzle_end
+	puzzle_start = pygame.time.get_ticks()
+	puzzle_end = None
+on_event(lambda a: a.type == AppEvent.RESTART, lambda a: restart_puzzle_time())
+
+def get_puzzle_duration():
+	if puzzle_end != None:
+		return puzzle_end - puzzle_start
+	return pygame.time.get_ticks() - puzzle_start
+
+def end_puzzle():
+	global puzzle_end
+	puzzle_end = pygame.time.get_ticks()
+on_event(lambda a: a.type == AppEvent.BUG_SQUISHED and num_bugs == 0, lambda a: end_puzzle())
+
 def draw_hud(event):
 	fps_clock.tick()
-	fps_rect = render_text('FPS: ' + str(int(fps_clock.get_fps())), (0, 0), (0, 0), surface_screen, gui_font, color_HUD, color_HUD_outline)
-	render_text('Use the arrow keys or d-pad to move.', (surface_screen.get_width(), 0), (1, 0), surface_screen, gui_font, color_HUD, color_HUD_outline)
 	
-	puzzle_end = pygame.time.get_ticks()
-	render_text('Elapsed: ' + ms_to_hms_string(puzzle_end - puzzle_start), fps_rect.bottomleft, (0, 0), surface_screen, gui_font, color_HUD, color_HUD_outline)
+	### Keep text away from edges so it's inside the title safe zone
+	render_text('FPS: ' + str(int(fps_clock.get_fps())), sum2d((45, -45), surface_screen.get_rect().bottomleft), (0, 1), surface_screen, gui_font_small, color_HUD, color_HUD_outline)
+	
+	instruction_rect_0 = render_text('Squish all the bugs.', sum2d((-45, 45), surface_screen.get_rect().topright), (1, 0), surface_screen, gui_font_medium, color_HUD, color_HUD_outline)
+	instruction_rect_1 = render_text('Use the arrow keys or d-pad to move.', sum2d((0, 10), instruction_rect_0.bottomright), (1, 0), surface_screen, gui_font_medium, color_HUD, color_HUD_outline)
+	instruction_rect_2 = render_text('If the puzzle becomes unsolvable, press', instruction_rect_1.bottomright, (1, 0), surface_screen, gui_font_medium, color_HUD, color_HUD_outline)
+	render_text('Space or any gamepad button to start over.', instruction_rect_2.bottomright, (1, 0), surface_screen, gui_font_medium, color_HUD, color_HUD_outline)
+	
+	if num_bugs > 0:
+		render_text('Elapsed: ' + ms_to_hms_string(get_puzzle_duration()), (45, 45), (0, 0), surface_screen, gui_font_large, color_HUD, color_HUD_outline)
+	else:
+		render_text('Solved in: ' + ms_to_hms_string(get_puzzle_duration()), (45, 45), (0, 0), surface_screen, gui_font_large, color_HUD, color_HUD_outline)
 on_event(lambda a: a.type == EVENT_DRAW, draw_hud)
 
+
+
 #################################################################### unit tests
 
 assert mag_scale_to_2d((1, 0), 2) == (2, 0)
 assert mag_scale_to_2d((1, 1), 1) == (1 / math.sqrt(2), 1 / math.sqrt(2))
 
 
+
 ######################################################################### begin
 
+### obviously not the best map loading solution in a large app, but whatever
 import map_0
-map_0.make(make_player, make_bug, make_grass, make_stone, make_boulder, make_tree, make_bush_0, make_bush_1, make_wood_floor, make_water, make_dirt)
+
+def load_map():
+	clear_blocks()
+	map_0.make(make_player, make_bug, make_grass, make_stone, make_boulder, make_tree, make_bush_0, make_bush_1, make_wood_floor, make_water, make_dirt)
+load_map()
+
+on_event(lambda a: a.type == AppEvent.RESTART, lambda a: load_map())
+
+on_event(
+	lambda a: (a.type == pygame.KEYDOWN and a.key == pygame.K_SPACE) or a.type == pygame.JOYBUTTONDOWN,
+	lambda a: proc_event(pygame.event.Event(AppEvent.RESTART))
+)
 
 
 event_loop()