c’t-Racetrack II

Since the 30th of November, 2015 the contest is over. The code is now visible in my post c’t-Racetrack. In this time I improved my program and added a semi-working collision detection (unfortunatly it does not cover every case), a helper function (will not allow you to make an invalid move) and an additional view mode (lets you only see the car’s path).

Controls

  • Mouse movement changes the new acceleration vector applied in the next step
  • Left click moves one step
  • Right click is a undo for the latest move
  • Middle click moves the frame
  • F1 toggles view mode
  • F2 takes a screenshot
  • F3 exports current moves
  • F4 resets the game
  • ‘h’ toggles helper

An invalid move paints the frame red.


# Python 2.7.7 Code
# Pygame 1.9.1 (for Python 2.7.7)
# Jonathan Frech 16th of October, 2015
#         edited 17th of October, 2015
#         edited 20th of October, 2015
#         edited 22nd of October, 2015

#         edited 15th of November, 2015
#         edited 16th of November, 2015
#         edited 18th of November, 2015

# importing needed modules
import pygame, sys, time, math, os, random, datetime
pygame.font.init()

""" CLASSES """
# dummy class for global variables
class dummy():
	pass

# frame class
class frame():
	# init frame
	def __init__(self):
		self.width, self.height = 500+1, 400+1
		self.size = [self.width, self.height]
		self.pixelpos = [(main.WIDTH-self.width)/2, (main.HEIGHT-self.height)/2]
		self.surface = pygame.Surface(self.size)
		self.lastMousePos = [0, 0]
		
		self.border = 10
		
		# basic (barriers and text)
		self.basiccolordest = [255, 255, 255]
		self.basiccolor = self.basiccolordest[:]
		
		# background
		self.backgroundcolor = [0, 0, 0]
		
		# start and goal
		self.startcolor = [255, 0, 0]
		self.goalcolor = [0, 255, 0]
		
		# position (and related)
		self.poscolor = [255, 255, 0]
		self.poscolor2 = [150, 150, 0]
		self.poscolor3 = [100, 100, 0]
		
		# velocity and acceleration
		self.velocitycolor = [0, 150, 150]
		self.accelerationcolor = [150, 0, 150]

		# color for simple viewmode
		self.simplecolor = [50, 50, 50]

		# init font
		self.font = pygame.font.SysFont(None, 20)
		
		# init paths
		self.exportpath = main.PATH + "/moves/"
		
		# set coordinates
		self.start = [120, 180]
		self.goal = [320, 220]
		self.barriers = [
			[ [200, 200], [100, 200] ],
			[ [100, 200], [100, 100] ],
			[ [100, 100], [200, 100] ],
			[ [300, 300], [300, 200] ],
			[ [300, 200], [400, 200] ],
			[ [300, 200], [300, 100] ],
			[ [300, 100], [400, 100] ],
			[ [250, 300], [250, 200] ]
		]

		# set viewmode
		self.viewmode = "extended"

		# set helper
		self.helper = False

		self.str = dummy()
		self.reset()

	# resets to start variables
	def reset(self):
		self.pos = self.start[:]
		self.velocity = [0, 0]
		self.history = []
		self.shouldMove = False
		self.won = False
		self.errors = 0

		self.str.acceleration = [None, None]
		self.str.history = []

		self.basiccolordest = [255, 255, 255]

	# moves the entity (using a given acceleration)
	def move(self, acc):
		# save pos, vel and acc
		self.history.append([self.pos[:], self.velocity[:], acc[:]])

		# increase velocity
		self.velocity[0] += acc[0]
		self.velocity[1] += acc[1]
		
		# change pos
		self.pos[0] = self.pos[0] + self.velocity[0]
		self.pos[1] = self.pos[1] + self.velocity[1]
		
		# printout (debug)
		#print "Accelerating at (" + str(acc) + ")"
		#print "Pos now at (" + str(self.pos) + ")"
		#print tuple(acc)

	# undos last move
	def applez(self):
		if len(self.history) >= 1:
			self.pos = self.history[-1][0][:]
			self.velocity = self.history[-1][1][:]
			self.history.pop(-1)

	# draws the help menu
	def infotext(self, _surface):
		# create text
		txt = [
			"c't-Racetrack",
			"",
			"Controls",
			"   * F1 toggles view mode",
			"   * F2 takes a screenshot",
			"   * F3 exports current moves",
			"   * F4 resets the game",
			"   * 'h' toggles helper",
			"",
			"Current path",
			"   * '" + main.PATH + "'",
			"",
			"Info",
			"   * Viewmode set to " + self.viewmode,
			"   * Helper " + ["deactivated", "activated"][self.helper],
			"",
			"(c) Jonathan Frech"
		]

		# figure out maximum text width
		max = 0
		for _ in txt:
			size = self.font.size(_)
			if size[0] > max:
				max = size[0]

		# draw text
		yoffset = 0
		for _ in txt:
			text = self.font.render(_, 1, self.basiccolor)
			pos = [self.pixelpos[0] - max - self.border - 10, self.pixelpos[1] + yoffset]
			_surface.blit(text, pos)

			yoffset += self.font.get_linesize()

	# check, if the connections are valid
	def checkconnection(self, p1, p2):
		for barrier in self.barriers:
			

			for dim in range(0, 2):
				if barrier[0][dim] == barrier[1][dim]:
					# x/y the same, check for x/y diff
					d = barrier[0][dim]
					if (p1[dim] <= d and p2[dim] >= d) or (p1[dim] >= d and p2[dim] <= d):
						if ( (p1[not dim] >= barrier[0][not dim] and p1[not dim] <= barrier[1][not dim]) or (p1[not dim] >= barrier[1][not dim] and p1[not dim] <= barrier[0][not dim]) ) and ( (p2[not dim] >= barrier[0][not dim] and p2[not dim] <= barrier[1][not dim]) or (p2[not dim] >= barrier[1][not dim] and p2[not dim] <= barrier[0][not dim]) ):
							return False

		if p2[0] < 0 or p2[1] < 0 or p2[0] > self.width or p2[1] > self.height:
			return False

		return True

	# calculates a color for given connection
	def getconnectioncolor(self, p1, p2):
		if self.checkconnection(p1, p2):
			return [50, 50, 50]
		else:
			return [255, 0, 0]

	# draw the frame
	def draw(self):
		if self.viewmode == "extended":
			# draw history
			for _ in self.history:
				pygame.draw.circle(self.surface, self.poscolor3, [_[0][0] + _[1][0], _[0][1] + _[1][1]], 10)
				pygame.draw.line(self.surface, self.poscolor2, _[0], [_[0][0] + _[1][0], _[0][1] + _[1][1]])
				pygame.draw.circle(self.surface, self.poscolor2, _[0], 2)
		
		if self.viewmode == "simple":
			# draw positions (connected)
			p = None
			for _ in self.history:
				pygame.draw.circle(self.surface, self.simplecolor, _[0], 2)
				if p != None:
					pygame.draw.line(self.surface, self.getconnectioncolor(p, _[0]), p, _[0])
				p = _[0]
			
			if p != None:
				pygame.draw.line(self.surface, self.getconnectioncolor(p, self.pos), p, self.pos)

		if self.viewmode == "extended":
			# draw velocity
			pygame.draw.circle(self.surface, self.poscolor3, [self.pos[0] + self.velocity[0], self.pos[1] + self.velocity[1]], 10)
			pygame.draw.line(self.surface, self.poscolor2, self.pos, [self.pos[0] + self.velocity[0], self.pos[1] + self.velocity[1]])
		
		# draw barriers
		for _ in self.barriers:
			pygame.draw.line(self.surface, self.basiccolor, _[0], _[1])
		
		# draw start, goal, current pos
		pygame.draw.circle(self.surface, self.startcolor, self.start, 5)
		pygame.draw.circle(self.surface, self.goalcolor, self.goal, 5)
		pygame.draw.circle(self.surface, self.poscolor, self.pos, 3)

		# get and convert mouse position
		p = list(pygame.mouse.get_pos())
		p1 = [p[0] - self.pixelpos[0], self.height - p[1] + self.pixelpos[1]]
		p2 = p1[:]
		
		# calculate vector and acceleration
		vec = vecConvert(self.pos, p1)
		vecl = vecLen(vec)
		if vecl != 0 and vecl > 10:
			vec0 = vecMultiply(vec, 1. / vecl)
			vecn = intpos( vecMultiply(vec0, 10) )
			p2 = vecGetPoint(self.pos, vecn)

		# draw the vector to the mouse position
		pygame.draw.line(self.surface, self.velocitycolor, self.pos, p2)
		acc = [p2[0] - self.pos[0], p2[1] - self.pos[1]]

		# nextpos is the entity's position if current acceleration vector is applied
		nextpos = [self.pos[0] + self.velocity[0] + acc[0], self.pos[1] + self.velocity[1] + acc[1]]
		if self.viewmode == "extended":
			# draw the position regarding accelearation
			pygame.draw.circle(self.surface, self.accelerationcolor, nextpos, 3)
		if self.viewmode == "simple":
			pygame.draw.line(self.surface, self.simplecolor, self.pos, nextpos)
			pygame.draw.circle(self.surface, self.simplecolor, nextpos, 3)
		
		# shouldMove is True if the user clicked
		if self.shouldMove:
			self.shouldMove = False
			self.move(acc)

		# for later displaying text
		self.str.acceleration = acc

	# draws text
	def text(self, _surface):
		self.infotext(_surface)

		# create text to display
		txt = [
			"Position (" + str(self.pos[0]) + ", " + str(self.pos[1]) + ")",
			"Velocity (" + str(self.velocity[0]) + ", " + str(self.velocity[1]) + ")",
			"Acceleration (" + str(self.str.acceleration[0]) + ", " + str(self.str.acceleration[1]) + ")",
			"",
			[["Keep on going...", "YOU WON!!!"][self.won], "There " + ["are", "is"][self.errors == 1] + " " + str(self.errors) + " error" + ["s", ""][self.errors == 1] + "..."][self.errors > 0],
			"",
			"So far you needed " + str(len(self.history)) + " move" + ("s" * (len(self.history) != 1)) + ":",
			""
		]

		# append history
		w = -1
		for _ in self.history:
			w += 1
			if w % 4 == 0:
				txt.append("")
			txt[-1] += "(" + str(_[2][0]) + ", " + str(_[2][1]) + ")" + (", " * (w < len(self.history)-1))

		if txt[-1] == "":
			txt.pop(-1)

		# append text history
		txt.append("")
		for _ in range(0, len(self.str.history)):
			if _ > len(self.str.history)-5:
				txt.append(self.str.history[_])
		
		# draw text
		yoffset = 0
		for _ in txt:
			text = self.font.render(_, 1, self.basiccolor)
			pos = [self.pixelpos[0] + self.width + self.border + 10, self.pixelpos[1] + yoffset]
			_surface.blit(text, pos)

			yoffset += self.font.get_linesize()

	# render frame
	def render(self, _surface):
		# calculate colors
		for _ in range(0, 3):
			self.basiccolor[_] += (self.basiccolordest[_] - self.basiccolor[_]) / 100.

		# fill
		self.surface.fill(self.backgroundcolor)
		
		# draw
		self.draw()

		# add text
		self.text(_surface)

		# self.border defines the white border around the frame
		pygame.draw.rect(_surface, self.basiccolor, [self.pixelpos[0]-self.border, self.pixelpos[1]-self.border, self.width+2*self.border, self.height+2*self.border])
		
		# frame is flipped due to the origin being at the bottom left
		_surface.blit(
			pygame.transform.flip(self.surface, False, True),
			self.pixelpos
		)

	# tick frame
	def tick(self):
		# get mouse position
		if pygame.mouse.get_pressed()[1]:
			p = list(pygame.mouse.get_pos())
			self.pixelpos = [p[0] - self.lastMousePos[0], p[1] - self.lastMousePos[1]]

		# check for connection errors
		self.errors = 0
		p = None
		for _ in self.history:
			if p != None:
				if not self.checkconnection(p, _[0]):
					self.errors += 1
			p = _[0]
		
		if p != None:
			if not self.checkconnection(p, self.pos):
				self.errors += 1
		
		# helper
		if self.helper:
			if self.errors > 0:
				self.applez()

		# calculate variables
		if (self.pos[0] == self.goal[0] and self.pos[1] == self.goal[1]) and (self.velocity[0] == 0 and self.velocity[1] == 0) and self.errors == 0:
			self.won = True
			self.basiccolordest = [0, 255, 0]
		else:
			self.won = False
			if self.errors == 0:
				self.basiccolordest = [255, 255, 255]
			else:
				self.basiccolordest = [255, 0, 0]
				

	# exports current history
	def exportMoves(self):
		# try / except loop for program's safety
		try:
			# craete directory
			if not os.path.exists(self.exportpath):
				os.mkdir(self.exportpath)

			# craete name
			name = "moves" + str(len(os.listdir(self.exportpath))) + ".txt"
			
			# save
			doc = open(self.exportpath + name, "w")
			for _ in self.history:
				doc.write("(" + str(_[2][0]) + ", " + str(_[2][1]) + ")\n")
			doc.close()

			# update status
			self.str.history.append("Exported moves to '" + name + "'")
		
		except:
			# update status
			self.str.lastExport = "Export failed..."

	# handle events
	def handle(self, event):
		if event.type == pygame.MOUSEBUTTONDOWN:
			# left mouse button moves
			if event.button == 1:
				self.shouldMove = True

			# middle mouse button moves the frame
			if event.button == 2:
				p = list(pygame.mouse.get_pos())
				self.lastMousePos = [p[0] - self.pixelpos[0], p[1] - self.pixelpos[1]]

			if event.button == 3:
				self.applez()

		if event.type == pygame.KEYDOWN:
			# toggle view mode
			if event.key == pygame.K_F1:
				if self.viewmode == "extended":
					self.viewmode = "simple"
				
				elif self.viewmode == "simple":
					self.viewmode = "extended"
			
			# export moves
			if event.key == pygame.K_F3:
				self.exportMoves()
			
			# reset
			if event.key == pygame.K_F4:
				self.reset()

			# toggle helper
			if event.key == pygame.K_h:
				self.helper = not self.helper

""" FUNCTIONS """
# returns an integer version of given positon
def intpos(_pos):
	return [int(_pos[0]), int(_pos[1])]

# basic vector functions
def vecConvert(p1, p2):
	return [p2[0] - p1[0], p2[1] - p1[1]]
def vecLen(vec):
	return math.sqrt( (vec[0]**2) + (vec[1]**2) )
def vecMultiply(vec, n):
	return [vec[0] * n, vec[1] * n]
def vecGetPoint(vec, point):
	return [point[0] + vec[0], point[1] + vec[1]]

# saves the current surface
def saveSurface():
	try:
		if not os.path.isdir(main.SAVEPATH):
			os.mkdir(main.SAVEPATH)

		name = "img" + str(len(os.listdir(main.SAVEPATH))) + ".png"
		pygame.image.save(main.SURF, main.SAVEPATH + name)

		main.FRAME.str.history.append("Screenshot saved to '" + name + "'")
	except:
		pass

# quits the program
def quit():
	sys.exit()

""" TICK; RENDER """
# tick function
def tick():
	# handle events
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			quit()

		if event.type == pygame.KEYDOWN:
			if event.key == pygame.K_F2:
				saveSurface()

		main.FRAME.handle(event)
	
	main.FRAME.tick()

# render function
def render():
	# fill
	main.SURF.fill([0, 0, 0])
	
	main.FRAME.render(main.SURF)

	# blit and flip
	main.SCREEN.blit(main.SURF, [0, 0])
	pygame.display.flip()

""" INIT """
# initialize program
def init():
	main.WIDTH, main.HEIGHT = 1080, 720
	main.SIZE = [main.WIDTH, main.HEIGHT]
	main.SCREEN = pygame.display.set_mode(main.SIZE)
	main.SURF = pygame.Surface(main.SIZE)
	
	main.CAPTION = "c't II"
	main.TICKS = 0

	main.PATH = os.getcwd()
	main.SAVEPATH = main.PATH + "/out/"
	
	main.FRAME = frame()
	#for _ in [(10, 0), (10, 0), (10, 0), (7, 6), (-9, 3), (-8, 5), (-4, 8), (-8, 4), (-3, -1), (6, -7), (1, -9), (-1, -9), (-3, -9), (-4, -8), (-8, -5), (-2, 3), (-1, 5), (4, 6), (3, 8)]:
	for _ in [(10, 0), (10, 0), (10, 0), (7, 6), (-9, 3), (-8, 5), (-4, 8), (-8, 4), (-3, -1), (-3, -9), (0, -9), (5, -8), (-3, -9), (0, -10), (-4, -4), (4, 8), (-2, 9), (-1, 0), (-1, 7)]:
		main.FRAME.move(list(_))

	# functions
	pygame.display.set_caption(main.CAPTION)
	#pygame.mouse.set_visible(False)

""" RUN """
# run function (uses tick() and render())
def run():
	ticksPerSecond = 60
	lastTime = time.time() * 1000000000
	nsPerTick =  1000000000.0 / float(ticksPerSecond)
	
	ticks = 0
	frames = 0
	
	lastTimer = time.time() * 1000
	delta = 0.0
	
	while True:
		now = time.time() * 1000000000
		delta += float(now - lastTime) / float(nsPerTick)
		lastTime = now
		shouldRender = False
				
		while delta >= 1:
			ticks += 1
			main.TICKS += 1
			tick()
			delta -= 1
			shouldRender = True
		
		if shouldRender:
			frames += 1
			render()
		
		if time.time() * 1000 - lastTimer >= 1000:
			lastTimer += 1000
			
			# debug
			# print("Frames: " + str(frames) + ", ticks: " + str(ticks))
			
			frames = 0
			ticks = 0

# main variable
main = dummy()
init()

# start program
run()
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s