Source

Squish the Bugs / roguelike / main.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
'''
Created: Aug 4, 2013
Last modified: Aug 8, 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 sum3d(a, b):
	return (a[0] + b[0], a[1] + b[1], a[2] + b[2])

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()
		proc_event(pygame.event.Event(EVENT_DRAW))
		#pygame.display.update()
		pygame.display.flip()

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

pygame.init()
#pygame.mouse.set_cursor(pygame.cursors.)
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('Roguelike v0.0.1')




on_event(lambda a: a.type == EVENT_DRAW, lambda a: surface_screen.fill(colorBG))
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 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)

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, fills_space)
tile_gray = (pygame.image.load('Plain Block.png').convert_alpha(), (50, 90), 100, True)
#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, True)
tile_grass = (pygame.image.load('Grass Block.png').convert_alpha(), (50, 90), 100, True)
tile_water = (pygame.image.load('Water Block.png').convert_alpha(), (50, 90), 100, True)
tile_rock = (pygame.image.load('Rock.png').convert_alpha(), (50, 90), 100, False)
tile_tree = (pygame.image.load('Tree Tall.png').convert_alpha(), (50, 90), 100, 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.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)


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

blocks_changed = False
blocks = [] # contains: ((x, y, z), tile)
position_to_block = {}
def draw_tile(tile, position):
	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(
					(
						position[0] * TILE_W,
						position[2] * TILE_L - position[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
				),
				(surface_screen.get_width() / 2, surface_screen.get_height() / 2)
			)
		)
	)
def draw_environment(event):
	global blocks_changed
	if blocks_changed:
		blocks.sort(key=lambda a: a[0][2] * 10000000 + a[0][1])
		for block in blocks:
			position_to_block[block[0]] = block
		blocks_changed = False
	
	for block in blocks:
		tile = block[1]
		position = block[0]
		draw_tile(tile, position)
		if tile[3]:
			### surface shadows
			if not is_filler_at(sum3d(position, (0, 1, 0))):
				if is_filler_at(sum3d(position, (1, 1, 0))):
					draw_tile(shadow_e, position)
				if is_filler_at(sum3d(position, (-1, 1, 0))):
					draw_tile(shadow_w, position)
				if is_filler_at(sum3d(position, (0, 1, 1))):
					draw_tile(shadow_s, position)
				if is_filler_at(sum3d(position, (0, 1, -1))):
					draw_tile(shadow_n, position)
				
				### corners
				if not is_filler_at(sum3d(position, (0, 1, 1))):
					if is_filler_at(sum3d(position, (1, 1, 1))) and not is_filler_at(sum3d(position, (1, 1, 0))):
						draw_tile(shadow_se, position)
					if is_filler_at(sum3d(position, (-1, 1, 1))) and not is_filler_at(sum3d(position, (-1, 1, 0))):
						draw_tile(shadow_sw, position)
				if is_filler_at(sum3d(position, (-1, 1, -1))) and not is_filler_at(sum3d(position, (0, 1, -1))) and not is_filler_at(sum3d(position, (-1, 1, 0))):
					draw_tile(shadow_nw, position)
				if is_filler_at(sum3d(position, (1, 1, -1))) and not is_filler_at(sum3d(position, (0, 1, -1))) and not is_filler_at(sum3d(position, (1, 1, 0))):
					draw_tile(shadow_ne, position)
			
			### front shadows
			if not is_filler_at(sum3d(position, (0, 0, 1))):
				if is_filler_at(sum3d(position, (-1, 0, 1))):
					draw_tile(shadow_side_w, position)
		
	#pygame.draw.rect(surface_screen, colorHud, pygame.Rect(300 - 2, 200 - 2, 4, 4)) # centered guide dot
on_event(lambda a: a.type == EVENT_DRAW, draw_environment)

#def get_blocks(predicate):
#	for block in blocks:
#		if predicate(block):
#			yield block
#def get_blocks_at(position):
#	yield from get_blocks(lambda a: a[0] == position)
def is_filler_at(position):
	"""for block in blocks:
		block_pos = block[0]
		if block_pos != position:
			continue
		tile = block[1]
		if tile[3]:
			return True
	return False"""
	if position in position_to_block:
		if position_to_block[position][1][3]:
			return True
	return False




#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(1, 1)
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)))
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))

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, 1, 0), tile_rock])
blocks.append([(2, 2, 1), tile_tree])
blocks.append([(0, 1, -1), tile_gray])
blocks.append([(-1, 2, -1), tile_gray])
blocks.append([(-1, 1, -1), tile_gray])
blocks.append([(-1, 0, -1), tile_gray])
blocks.append([(-1, -1, 0), tile_dirt])
blocks.append([(-1, -1, 1), tile_water])
blocks.append([(0, -1, 1), tile_water])
blocks.append([(2, 1, -1), tile_grass])
blocks.append([(2, 0, 0), tile_dirt])
blocks.append([(3, 0, 0), tile_dirt])
blocks.append([(1, 0, -1), tile_dirt])
blocks_changed = True


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))
	surface_screen.blit(fps_surface, (-1, -1))
	surface_screen.blit(fps_surface, (1, 1))
	surface_screen.blit(fps_surface, (-1, 1))
	surface_screen.blit(fps_surface, (1, -1))
	
	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()