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 )