Source

Squish the Bugs / roguelike / main.py

Full commit
'''
Created: Aug 4, 2013
Last modified: Aug 7, 2013 
'''

import pygame
import math
import sys



######################################################################## engine

def get_stick(joy, axisX, axisY):
	state = ( joy.get_axis(axisX), joy.get_axis(axisY) )
	
	magSq = magnitudeSq2d(state)
	
	### The MadCatz controller reports a vector magnitude of >1 when the sticks
	### are tilted towards the corners, which seems to be because of a dead
	### zone around the outside. Normalizing the vector whenever its magnitude
	### is >1 seems to fix this.
	if magSq > 1:
		return normalize2d(state)
	
	dead_zone_radius = .2
	
	### Circular dead zone
	if magSq < dead_zone_radius ** 2:
		return (0, 0)
	
	### Scale usable input space so it goes from 0->1 instead of abruptly
	### jumping to the hardware's representation of the edge of the dead zone.
	mag = math.sqrt(magSq)
	max_mag = 1 - dead_zone_radius
	#new_mag = max_mag / mag
	new_mag = (mag - dead_zone_radius) / max_mag
	
	return mag_scale_to_2d(state, new_mag)

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 normalize2d(vec):
	return mag_scale_2d(vec, 1 / magnitude2d(vec))

def int2d(vec):
	return (int(vec[0]), int(vec[1]))

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
	#try:
	return mag_scale_2d(vec, scalar / magnitude2d(vec))
	#except:
	#	return (NaN, NaN)
	### 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
	### a direction.
	### A real programming language would allow this function to have
	### preconditions.

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)


EVENT_DRAW = 100

event_handlers = [] # contains: (predicate:function, callback:function)
def proc_event(event):
	for handler in event_handlers:
		if handler[0](event):
			handler[1](event)
			
def proc_events():
	for event in pygame.event.get():
		proc_event(event)
		
def event_loop():
	while True:
		proc_events()
		
		surface_screen.fill(colorBG)
		proc_event(pygame.event.Event(EVENT_DRAW))
		pygame.display.update()

def on_event(predicate, callback):
	### TODO: take priorty as parameter so screen flip and hud drawing can be
	### registered in any order
	
	### TODO: what about attaching events to other events?
	def remove():
		raise Exception('on_event()~~>remove() not implemented')
		### This requires a linked list or something similar
		
	event_handlers.append((predicate, callback))
	return remove









########################################################################### app

on_event(lambda a: a.type == pygame.QUIT, lambda a: sys.exit())

def draw_joy_input(event):
	#for joy in range(len(joysticks)):
	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)
			)
		#print(str(joy) + ': ' + str(joysticks[joy].get_axis(0)))
		
		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)
colorHud = pygame.Color(255, 255, 255)
colorHudOutline = pygame.Color(0, 0, 0)

pygame.init()
surface_screen = pygame.display.set_mode((600, 400)) # TODO: flags and depth
# what about fullscreen?

joysticks = [pygame.joystick.Joystick(x) for x in range(pygame.joystick.get_count())]
for joy in joysticks:
	joy.init()



TILE_W = 1
TILE_L = .8
TILE_H = .4


# tile: (surface, (reg_x, reg_y), reg_width)
tile_gray = (pygame.image.load('Plain Block.png').convert_alpha(), (50, 90), 100)
tile_ground_double = (pygame.image.load('Plain Block.png').convert_alpha(), (50, 90), 50)
tile_dirt = (pygame.image.load('Brown Block.png').convert_alpha(), (50, 90), 100)
tile_grass = (pygame.image.load('Grass Block.png').convert_alpha(), (50, 90), 100)
tile_water = (pygame.image.load('Water Block.png').convert_alpha(), (50, 90), 100)
tile_rock = (pygame.image.load('Rock.png').convert_alpha(), (50, 130), 100)
tile_tree = (pygame.image.load('Tree Tall.png').convert_alpha(), (50, 130), 100)



#camera = (0, 0, 6, 4) # (left, top, width, height)
camera = (0, 0, 5) # (centerX, centerY, height) ~~~ width depends on screen
def move_camera(delta):
	global camera
	camera = (camera[0] + delta[0], camera[1] + delta[1], camera[2])

blocks = [] # contains: ((x, y, z), tile)
def draw_environment(event):
	global blocks
	blocks = sorted(blocks, key=lambda a: a[0][2] * 10000000 + a[0][1])
	for block in blocks:
		block_pos = block[0]
		tile = block[1]
		tile_surface = tile[0]
		tile_reg_width = tile[2]
		
		scale_world2screen = surface_screen.get_height() / (camera[2] * TILE_L)
		scale_image2tile = tile_surface.get_width() / tile_reg_width
		
		dest_w = scale_world2screen * scale_image2tile
		dest_h = dest_w * tile_surface.get_height() / tile_surface.get_width()
		
		surface_screen.blit(
			pygame.transform.smoothscale(tile_surface, int2d((dest_w, dest_h))),
			sum2d(
				
				### positioning before camera motion
				dif2d(
					scale2d(
						(
							block_pos[0] * TILE_W,
							block_pos[2] * TILE_L - block_pos[1] * TILE_H
						),
						scale_world2screen
					),
					scale2d(tile[1], scale_world2screen / tile_reg_width)
				),
				
				sum2d(
					### camera position
					scale2d(
						(TILE_W * (-camera[0]), TILE_L * (-camera[1] )), 
						scale_world2screen
					),
					### camera registration (center)
					scale2d(
						(
							#( camera[2] * surface_screen.get_height() / surface_screen.get_width()) / 2,
							(camera[2] / 2 - .5) * surface_screen.get_width() / surface_screen.get_height(),
							(camera[2] ) / 2 - .5
						),
						scale_world2screen
					)
				)
			)
		)
on_event(lambda a: a.type == EVENT_DRAW, draw_environment)





#on_event(lambda a: a.type == pygame.JOYHATMOTION, lambda a: print(a))

previous_hat = (0, 0)
def on_hat(event):
	global camera, previous_hat
	#delta = (event.value[0] - previous_hat[0], event.value[1] - previous_hat[1])
	if event.value[0] != 0 and event.value[0] != previous_hat[0]:
		camera = (camera[0] + event.value[0], camera[1], camera[2])
	if event.value[1] != 0 and event.value[1] != previous_hat[1]:
		camera = (camera[0], camera[1] - event.value[1], camera[2])
	previous_hat = event.value
on_event(lambda a: a.type == pygame.JOYHATMOTION, on_hat)

pygame.key.set_repeat() # disables key repetition
on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_UP, lambda a: move_camera((0, -1)))
on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_RIGHT, lambda a: move_camera((1, 0)))
on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_DOWN, lambda a: move_camera((0, 1)))
on_event(lambda a: a.type == pygame.KEYDOWN and a.key == pygame.K_LEFT, lambda a: move_camera((-1, 0)))

blocks.append([(1, 1, 2), tile_gray])
blocks.append([(1, 1, 1), tile_gray])
blocks.append([(1, 0, 1), tile_gray])
blocks.append([(1, 0, 2), tile_gray])
blocks.append([(0, 0, 0), tile_gray])
blocks.append([(1, 0, 0), tile_gray])
blocks.append([(3, -1, 2), tile_ground_double])
blocks.append([(5, -1, 2), tile_ground_double])
blocks.append([(3, 0, 2), tile_gray])
blocks.append([(2, 0, 1), tile_dirt])
blocks.append([(2, 0, 2), tile_dirt])
blocks.append([(2, 1, 1), tile_grass])
blocks.append([(2, 1, 2), tile_grass])
blocks.append([(1, 0, 3), tile_grass])
blocks.append([(1, 0, 4), tile_water])
blocks.append([(0, 0, 4), tile_water])
blocks.append([(1, 0, 5), tile_dirt])
blocks.append([(0, -1, 5), tile_dirt])
blocks.append([(0, 0, -2), tile_dirt])
blocks.append([(0, 0, 2), tile_dirt])
blocks.append([(0, 0, 0), tile_rock])
blocks.append([(2, 1, 1), tile_tree])
blocks.append([(0, 1, -1), tile_gray])

fps_clock = pygame.time.Clock()
gui_font = pygame.font.SysFont('Verdana', 18)
def draw_fps(event):
	fps_clock.tick()
	
	fps_string = 'FPS: ' + str(int(fps_clock.get_fps()))
	
	fps_surface = gui_font.render(fps_string, True, colorHudOutline)
	surface_screen.blit(fps_surface, (-2, 0))
	surface_screen.blit(fps_surface, (2, 0))
	surface_screen.blit(fps_surface, (0, 2))
	surface_screen.blit(fps_surface, (0, -2))
	
	fps_surface = gui_font.render(fps_string, True, colorHud)
	surface_screen.blit(fps_surface, (0, 0))
on_event(lambda a: a.type == EVENT_DRAW, draw_fps)


#################################################################### unit tests

assert mag_scale_to_2d((1, 0), 2) == (2, 0)
assert mag_scale_to_2d((-2, 0), .5) == (-.5, 0)

#print(mag_scale_to_2d((1, 1), 1))
#print((math.sqrt(2), math.sqrt(2)))
assert mag_scale_to_2d((1, 1), 1) == (1 / math.sqrt(2), 1 / math.sqrt(2))













event_loop()