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

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

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

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]

## 4 thoughts on “c’t-Racetrack”

1. alex says:

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

• Yeah, I removed the source code until the 30th of November to not spoil the challenge.

Like

2. alex says:

thanks for the fast reply. I kind of missed, that “hidden source code” also means no program ;-)

Like

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.