c’t-Racetrack

In a recent c’t-article they showed a game regarding vector mathematics, velocities, accelerations and positions.
I could not resist and wrote this program to simulate the game.
The only thing this program cannot do is to check the barriers. That you have to check for yourself.

The game

You have a start position (red), an end position (green) and some barriers (white).
Your job is it to steer your little dot (bright yellow) onto the end position.
To make it more difficult, you are not allowed to change the dot’s position directly, but rather you accelerate it on each step.
Your acceleration vector (turquoise) also is not allowed to be longer than 10 (dim yellow bubble) and at the end point your dot must have a velocity of (0, 0).

The fewer moves you need, the better your steering abilities.

[not used]

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
  • ‘c’ clears all the moves made so far
  • ‘e’ exports current moves to a .txt file
  • Space saves a screenshot

[not used]


# 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

# 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, 400
		self.width += 1;self.height += 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
		
		self.basiccolor = [0, 0, 0]

		self.backgroundcolor = [0, 0, 0]
		self.startcolor = [255, 0, 0]
		self.goalcolor = [0, 255, 0]
		self.poscolor = [255, 255, 0]
		self.poscolor2 = [150, 150, 0]
		self.poscolor3 = [100, 100, 0]
		self.velocitycolor = [0, 150, 150]
		self.accelerationcolor = [150, 0, 150]

		self.font = pygame.font.SysFont(None, 20)
		self.exportpath = os.getcwd() + "/moves/"
		
		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] ]
		]

		self.textVars = 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.textVars.acceleration = [None, None]
		self.textVars.lastExport = "-"

		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)

	# draw the frame
	def draw(self):
		# 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)

		# 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]]

		"""# try to figure out perfect acceleration
		for x in range(-200, 200):
			for y in range(-200, 200):
				if self.velocity[0] + x == 0 and self.velocity[1] + y == 0:
					if self.pos[0] + self.velocity[0] == self.goal[0] and self.pos[1] + self.velocity[1] == self.goal[1]:
						acc = [x, y]
						#print "!" """

		# for diplaying text
		self.textVars.acceleration = acc

		# draw the position regarding accelearation
		pygame.draw.circle(self.surface, self.accelerationcolor, [self.pos[0] + self.velocity[0] + acc[0], self.pos[1] + self.velocity[1] + acc[1]], 3)
		
		# shouldMove is True if the user clicked
		if self.shouldMove:
			self.shouldMove = False
			self.move(acc)

	# draws text
	def text(self, _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.textVars.acceleration[0]) + ", " + str(self.textVars.acceleration[1]) + ")",
			"",
			["Keep on going...", "YOU WON!!!"][self.won],
			"So far you needed " + str(len(self.history)) + " move" + ("s" * (len(self.history) != 1)) + ":",
			""
		]

		# list 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)

		# write out last export status
		txt.append("")
		txt.append(self.textVars.lastExport)

		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):
		if pygame.mouse.get_pressed()[1]:
			p = list(pygame.mouse.get_pos())
			self.pixelpos = [p[0] - self.lastMousePos[0], p[1] - self.lastMousePos[1]]

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

	# 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 = "move" + 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.textVars.lastExport = "Exported to '" + name + "'"
		
		except:
			# update status
			self.textVars.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:
				if len(self.history) >= 1:
					self.pos = self.history[-1][0][:]
					self.velocity = self.history[-1][1][:]
					self.history.pop(-1)

		# reset if 'c' is pressed
		if event.type == pygame.KEYDOWN:
			if event.key == pygame.K_c:
				self.reset()

			if event.key == pygame.K_e:
				self.exportMoves()


""" 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:
		path = os.getcwd() + main.SAVEPATH
		if not os.path.isdir(path):
			os.mkdir(path)

		name = path + "img" + str(len(os.listdir(path))) + ".png"
		pygame.image.save(main.SURF, 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_SPACE:
				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"
	main.TICKS = 0
	main.SAVEPATH = "/out/"
	
	main.FRAME = frame()

	# functions
	pygame.display.set_caption(main.CAPTION)

""" 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()

[Source code was hidden until 30th of November, 2015]

Advertisements

4 thoughts on “c’t-Racetrack

  1. is it just me not finding the actual game, or there something missing? You wrote something on the C’t comment pages about an online version.

    Like

  2. Pingback: c’t-Racetrack II | J-Blog

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