One thing I discovered in my research, is that the truncation of continuous units speed and positions onto discrete units of tile location and time has a huge effect. For example, a trackstop placed at tile "15" could have much different slowdown distance than a trackstop placed at tile "16" because the linger time is 3 ticks versus 2 ticks.
Here is a python program I wrote that models the physics and exactly reproduces all stop-distances for lowest,low, and medium trackstops. I left off in the middle of trying to figure out what ramps do, so no guarantees on usability.
import sys
import logging
import numpy
loglevel = logging.WARN
if( '-v' in sys.argv ):
loglevel = logging.INFO
if( '-vv' in sys.argv ):
loglevel = logging.DEBUG
logging.basicConfig( stream=sys.stdout, level=loglevel )
LOG = logging.getLogger()
LOG1 = logging.getLogger( 'timelog' )
LOG2 = logging.getLogger( 'delaylog' )
UNK = 999
XIDX = 0
ZIDX = 1
FORWARD = numpy.array( [1,0] )
DOWNHILL = numpy.array( [1,-1] ) ## toady said 2,2,3 was the box at one point
##DOWNHILL = FORWARD
class Track(object):
'''All values found experimentally using the runout distance measurement'''
DATA = [ # DERVIED FROM RUNOUT WITH FEATURES:
('T', 'TRACK' , 0.1), # T only, -and- tile delay log
('TT', 'TRACK_TURN' , 1.77), # T, TT
('SLL', 'TRACKSTOP_LOWEST' , 0.1), # T, SLL
('SL', 'TRACKSTOP_LOW' , 0.5), # T, SL
('SM', 'TRACKSTOP_MEDIUM' , 5), # T, SM
('SH', 'TRACKSTOP_HIGH' , 86.45),# T, SM, SH, **RD**
('SHH', 'TRACKSTOP_HIGHEST', UNK),
('RLL', 'ROLLER_LOWEST' , 0.1, 50, 131 ), # T, SM, RLL. ??accel is rough
('RL', 'ROLLER_LOW' , 0.1, 27, 200), # T, SM, RL
('RM', 'ROLLER_MEDIUM' , -13.1),# T, SL, SM, RM
('RH', 'ROLLER_HIGH' , -UNK), # T,
('RHH', 'ROLLER_HIGHEST' , -UNK), # ???
('TD', 'TRACKRAMP_DOWN' , 0.0, 49.74, None, DOWNHILL ), # ???
('TU', 'TRACKRAMP_UP' , UNK ), #
('F', 'FLOOR' , 2 ), # F only, -and- T, F
]
@staticmethod
def friction(values):
'''takes a number or list (e.g. from DATA) and returns the friction'''
if( isinstance(values,list) or isinstance(values,tuple)):
return values[0]
else:
return values
@staticmethod
def acceleration(values):
if( isinstance(values,list) or isinstance(values,tuple)):
if( len(values) > 1 ):
return values[1]
return 0
@staticmethod
def maxspeed(values):
if( isinstance(values,list) or isinstance(values,tuple)):
if( len(values) > 2 ):
return values[2]
return 0
@staticmethod
def direction(values):
if( isinstance(values,list) or isinstance(values,tuple)):
if( len(values) > 3 ):
return values[3]
return FORWARD
def __init__(self):
for data in self.DATA:
shortname, longname = data[:2]
values = data[2:]
values = Track.friction(values), Track.acceleration(values), Track.maxspeed(values), Track.direction(values)
setattr(self,shortname,values)
setattr(self,longname,values)
def sign(x):
return cmp(x,0)
class Cart(object):
position = None # x,y array measures in tiles
velocity = None # x,y array measured in microtiles/tick
def __init__(self):
# array dimensions are x,z
self.position = numpy.array( [0,0], dtype=numpy.float32 )
self.velocity = numpy.array( [0,0], dtype=numpy.float32 )
def setPosition(self, x=None, z=None):
if( x is not None ):
self.position[XIDX] = x
if( z is not None ):
self.position[ZIDX] = z
def getdistance(self):
return self.position[XIDX]
distance = property(getdistance)
def nextPosition(self):
# units of speed are microtiles per tick.
return self.position + self.velocity / 1000
def limitSpeed(self, max_speed):
'''decrease speed to at most max_speed'''
curr_speed = self.speed
if( curr_speed > max_speed ):
self.velocity = max_speed * (self.velocity / curr_speed)
LOG.debug( 'limitSpeed to %s', max_speed )
def getspeed(self):
return numpy.linalg.norm(self.velocity)
speed = property(getspeed)
def addSpeed(self, delta_speed, direction=None):
'''accellerate (add speed) in a (direction is array) or along current dir (direction is None)'''
if( direction is not None ):
direction = direction / numpy.linalg.norm(direction)
else:
direction = self.velocity / self.speed
LOG.debug( 'addSpeed %s PLUS %s going %s', self.velocity, delta_speed, direction )
self.velocity += (direction * delta_speed)
LOG.debug( 'addSpeed EQUALS %s', self.velocity )
def subSpeed(self, delta_speed ):
'''accellerate opposite current direction, never change direction'''
curr_speed = self.speed
delta_speed = min( curr_speed, abs(delta_speed))
self.addSpeed( -delta_speed )
def setDirection(self, direction):
'''preserve the current speed, but change the direction'''
direction = direction / numpy.linalg.norm(direction)
#LOG.warn( 'direction changed to %s', direction )
curr_speed = numpy.linalg.norm(self.velocity)
self.velocity = curr_speed * direction
def __str__(self):
return 'Cart[ @ %s --> %s ]' % (self.position, self.velocity)
class Race(object):
time = 0 # measured in ticks (int)
course = [] # list of tile friction values along the route.
cart = Cart()
def __init__(self, course):
self.course = course
self.delays = [0] * len(course)
self.prevtime = 0
def impulse(self):
self.cart.addSpeed( 200, direction=FORWARD )
self.cart.setPosition( x=1.5 )
def tick(self):
self.time += 1
LOG.info( 'time %s, cart %s', self.time, self.cart )
# STEP1. lookup tile values from current location.
# assume linear track along X.
location = int( self.cart.position[XIDX] )
tile = self.course[location]
friction = Track.friction( tile )
accel = Track.acceleration( tile )
maxspeed = Track.maxspeed( tile )
LOG.info( 'Track stats at %s. friction %s, accel %s, maxspeed %s', location, friction, accel, maxspeed )
# STEP2. update speed based on acceleration from ramp/roller.
if( accel ):
LOG.info( 'applying accel of %f', accel )
self.cart.addSpeed( accel )
# STEP3. limit speed to the speed limit (applicable for rollers)
### TODO.. unknown if rollers deccelerate (and how much) when above max speed
if( maxspeed ):
self.cart.limitSpeed( maxspeed )
# STEP4. update speed based on decceleration from friction.
LOG.debug( 'applying friction of %f', friction )
self.cart.subSpeed( friction )
# STEP5. calculate position based on speed.
nextpos = self.cart.nextPosition()
nextlocation = int(nextpos[XIDX])
if( nextlocation != location and nextlocation < len(self.course) ):
# update delay stats.
delay = self.time - self.prevtime
self.delays[location] = delay
LOG2.debug( 'delay %03d,%02d', location, delay )
# STEP6. check interaction effects with destination tile
nexttile = self.course[ nextlocation ]
# tracks yank the cart into the desired direction!
nextdirection = Track.direction( nexttile )
#LOG.warn( 'next direction at %s is %s', nextlocation, nextdirection )
self.cart.setDirection( nextdirection )
self.prevtime = self.time
self.cart.position = nextpos
#prevlocation, prevtime = self.prev
#if( location != prevlocation ):
# delay = self.time - prevtime
# self.prev = (location, self.time)
return ( self.cart.speed <= 0 )
def dumpCourse( self, size=30 ):
markers = [' . '] * size
frictions = [' . '] * size
accels = [' . '] * size
speeds = [' . '] * size
for x in xrange(0,min(size,len(self.course))):
if( x % 10 == 0):
markers[x] = '%5d'%x
frictions[x] = '%05s' % ( '%02.1f'% Track.friction(course[x]) )
accels[x] = '%05s' % ( '%02.1f'% Track.acceleration(course[x]) )
speeds[x] = '%05s' % ( '%02.1f'% Track.maxspeed(course[x]) )
LOG.info( ' '.join( markers ))
LOG.info( ' '.join( frictions ))
LOG.info( ' '.join( speeds ))
def run( self ):
LOG1.debug( 'Tick,Dist,Speed' )
LOG2.debug( 'Tile,Delay')
#self.dumpCourse()
self.impulse()
while( self.time < 10000 ):
self.tick()
LOG1.debug( '%03d,%0.2f,%0.1f', self.time, self.cart.position[XIDX],
self.cart.speed )
if( self.cart.distance >= len(self.course)):
LOG.info( 'Course overrun at time %d, final speed %f',
self.time, self.cart.speed )
break
if( self.cart.speed <= 0 ):
LOG.info( 'Cart stopped at final distance %d', self.cart.distance )
break
return int(self.cart.distance)
class Solver(object):
MAXVALUE = 10000
def __init__(self, course ):
'''Attempt to determine the friction/accel values of the feature repeated
at each point in testreange.'''
self.course = course
self.setVariables()
self.setTargets()
def setTargets(self, stopdist=0, tiledelays=None):
self.stopdist = stopdist
self.tiledelays = tiledelays ## tuples of (course-index,delay)
def setVariables(self, *testvariables):
self.testvariables = testvariables ## tuples of course-index, tile-index
def test(self, *values):
if( len(values) != len(self.testvariables)):
LOG.warn( 'bad test values, need %s numbers', len(self.testvariables))
return
for vv in xrange(len(values)):
cidx, vidx = self.testvariables[vv]
newtup = list(self.course[cidx])
newtup[vidx] = values[vv]
self.course[cidx] = newtup
LOG.info( 'TEST course[%s] <-- %s', cidx, newtup )
r = Race(self.course)
dist = r.run()
## positive error means "too fast"
rv = []
msgs = []
tmp = 'stop %s' % dist
if( self.stopdist ):
err = dist - self.stopdist
tmp += ' (%+d)'%(err)
msgs.append( tmp )
rv.append( err )
if( self.tiledelays ):
msgs.append( 'delays' )
for idx, delay in self.tiledelays:
err = delay - r.delays[idx]
msgs.append( '%s (%+d)'%(r.delays[idx], err) )
rv.append( err )
LOG.info( 'TEST %s -> %s', ' '.join(map(str,values)), ' '.join( msgs ))
return rv
def sweep(self, bound1, bound2, maxdecimals=1):
rounder = pow(10,maxdecimals)
r1 = int(min(bound1,bound2)*rounder)
r2 = int(max(bound1,bound2)*rounder)
LOG.warn( 'Sweep rounder %s, %s, %s', r1, r2, rounder )
for value in xrange(r1,r2+1,1):
value = value / float(rounder)
result = self.test( value )
yield value, result
def search(self, maxdecimals=1, hint=None):
value = 0
result = 1
rounder = pow(10,maxdecimals)
upper = self.MAXVALUE
lower = 0
if hint:
lower,upper = hint
for limit in xrange(0,1000):
value = (upper + lower) / 2.0
value = int(rounder*value) / float(rounder) # require
if( value == upper or value == lower ):
break
result = self.test( value ) [0]
if( self.testindex == 1):
# accel and maxspeed have opposite direcition as friction in solution finding
result = -result
if( result < 0 ):
upper = value
elif( result > 0 ):
lower = value
else:
break
if( upper == lower ):
break
if( result != 0 ):
LOG.warn( 'NO SOLUTION! target was %d, with maxdecimals %d', self.stopdist, maxdecimals )
valuetup = self.course[self.testrange[0]]
LOG.warn( 'Final value at %s is %s, last bounds (%s < x < %s)', self.testrange, valuetup, lower, upper )
#----------------------------------------------------------------------
#----------------------------------------------------------------------
#----------------------------------------------------------------------
track = Track() # singleton
import unittest
class RaceTest(unittest.TestCase):
def test_flat(self):
course = [track.T] * 500
r = Race(course)
self.assertEqual( r.run(), 201 )
def test_low(self):
course = [track.T] * 500
course[50] = track.SL
r = Race(course)
self.assertEqual( r.run(), 197 )
course[60] = track.SL
r = Race(course)
self.assertEqual( r.run(), 193 )
def test_medium(self):
course = [track.T] * 500
course[10] = track.SM
r = Race(course)
self.assertEqual( r.run(), 148 )
course = [track.T] * 500
course[20] = track.SM
r = Race(course)
self.assertEqual( r.run(), 149 )
def test_ramp(self):
course = [track.T] * 500
course[6] = track.TD
for x in range(10,25):
course[x] = track.SM
r = Race(course)
self.assertEqual( r.run(), 55 )
suite = unittest.TestLoader().loadTestsFromTestCase(RaceTest)
#unittest.TextTestRunner(verbosity=2).run( suite )
maxdecimal = 1
if( '-0' in sys.argv ):
maxdecimal = 0
if( '-1' in sys.argv ):
maxdecimal = 1
if( '-2' in sys.argv ):
maxdecimal = 2
course = [track.T]*500
TESTIDX = 6
course[TESTIDX] = track.TD
for x in range(10,25):
course[x] = track.SM
r = Solver( course )
r.setVariables( (TESTIDX,1) )
r.setTargets( stopdist=55, tiledelays=[(TESTIDX,5)] )
#for x in range(10,24):
# course[x] = track.SM
#r = Solver( course, stopdist=105, testrange=[6], testindex=0 )
#for x in range(10,23):
# course[x] = track.SM
#r = Solver( course, stopdist=155, testrange=[6], testindex=0 )
if( 'solve' in sys.argv ):
## TODO.. additional constraint to solver. linger over testindex range!
r.search( maxdecimals=15, hint=[1,80])
if( 'sweep' in sys.argv ):
for value,error in r.sweep( 0, 80, maxdecimals=0):
LOG.warn( 'sweep %s %s', value, error )
if( 'run' in sys.argv ):
#value = 39.3
value = 0
error = r.test( value )
LOG.warn( 'run %s %s', value, error )