here is knights_on_wheels.py again. i have fixed some bugs i introduced when translating it from lite-c. there also is a simple ai now. smile on the city track it drives pretty well but on the canyon track it sometimes tips over after jumps.

Code:
# f1          = restart level1
# f2          = restart level2
# cu cr cd cl = control vehicle 1
# page down   = reset vehicle 1
# delete      = tractor beam vehicle 1
# w d s a     = control vehicle 2
# e           = reset vehicle 2
# q           = tractor beam vehicle 2

from a7_scheduler import *
import default
import sys, os.path, random

# add_folder() works relative to the python.exe but i want it relative to the script
script = sys.argv[0]
folder = os.path.dirname(script)
folder = os.path.join(folder, "knights_on_wheels")
resource = os.path.join(folder, "resource.wrs")
add_folder(folder)
add_resource(resource)
add_folder("C:/Program Files (x86)/GStudio7/projects/carlevel")



camera_distance = 128
camera_tilt = 28
suspension_min = 6
suspension_max = 12
beam_length = 480
beam_force = 50000
vehicle_mass = 16
spring = 22750
damper = 425
motor_speed = 100
motor_power = 100
motor_brake = 180
steering_power = 100
max_laps = 3
energy_start = 100
energy_lap = 50
sd_distance = sd1_distance = 320
sd_threshold = sd1_threshold = 640
sd2_distance = 200
sd2_threshold = 416

second_camera = View()
second_camera.flags |= SHOW
vehicles = []
tires = []
tire_constraints = []
keys = (72, 77, 80, 75, 17, 32, 31, 30, 83, 16)
suspensions = ("frontleft", "frontright", "rearleft", "rearright")
needle = Bitmap("zeiger.tga")
tachometer = Bitmap("tachometer.tga")
huds = []

def set_physics(entity, hull, mhull, group, mass, friction, bounciness, min_speed, linear_damping, angular_damping):
    entity.ph_settype(PH_RIGID, hull)
    entity.ph_setmass(mass, mhull)
    entity.ph_setfriction(friction)
    entity.ph_setelasticity(bounciness, min_speed)
    entity.ph_setdamping(linear_damping, angular_damping)
    entity.ph_setgroup(group)

def reset_vehicle(vehicle):
    print "reset", vehicle.name, "| driveline angle:", vehicle.nskills2[1]
    for e in vehicle.tires + [vehicle]:
        e.ph_enable(0)
        v = (e.position - vehicle.position).rotateback(vehicle.orientation)
        v = v.rotate((vehicle.nskills2[1], 0, 0)) + vehicle.nposition2
        v.z += 64
        e.position = v
        if e is not vehicle and e.index in (1, 3, 5, 7): # tires of right side  
            o = 180
        else:
            o = 0
        e.orientation = (vehicle.nskills2[1] - o, 0, 0)
        e.ph_clearvelocity()
        e.ph_enable(1)

def reset_vehicle_1():
    reset_vehicle(vehicles[0])
e.on_pgdn = reset_vehicle_1
def reset_vehicle_2():
    reset_vehicle(vehicles[1])
e.on_e = reset_vehicle_2

def toggle_vehicle_1_ai():
    vehicles[0].ai = 1 - vehicles[0].ai 
e.on_home = toggle_vehicle_1_ai
def toggle_vehicle_2_ai():
    vehicles[1].ai = 1 - vehicles[1].ai 
e.on_2 = toggle_vehicle_2_ai



#-------------------------------------------------------------------------------
def ang_diff(a1, a2):
    a1 = ang(a1)
    a2 = ang(a2)
    if abs(a1 - a2) > 180: a2 += 360 * sign(a1 - a2)
    return a1 - a2

@schedule
def vehicle():
    my = e.my
    my.index = len(vehicles)
    vehicles.append(my)
    print "vehicle:", my.name, my.index
    
    my.winner = False
    my.ai = False
    
    energy = energy_start
    lap = 0
    elapsed_time = 0
    speed = 0
    last_node = 0
    distance = 0
    message_time = 0
    
    if my.index == 0:
        camera = second_camera
        group = 2
        o = 0
    else:
        camera = e.camera
        group = 4
        o = 4
    
    set_physics(my, PH_BOX, PH_BOX, group, vehicle_mass, 40, 25, 10, 20, 20)
    
    my.path_set("driveline")
    # count nodes
    nodes = 1
    while my.path_getnode(nodes):
        nodes += 1
    print "nodes:", nodes
    
    yield(1) # wait for initialization of all entities
    front_left = tire_constraints[0+o]
    front_right = tire_constraints[1+o]
    rear_left = tire_constraints[2+o]
    rear_right = tire_constraints[3+o]
    my.tires = tires[0+o:4+o]
    opponent = vehicles[1 - my.index]

    global huds
    if len(huds) == 2: # delete previously created panels after loading a level
        for p in huds: p.remove()
        huds = []
    hud = Panel()
    hud.pos_x = e.screen_size.x - 160 - e.screen_size.x / 2 * my.index
    hud.pos_y = 10
    hud.bmap = tachometer
    hud.setneedle(0, 76, 76, needle, 70, 70, 130, 200, 0, lambda: speed)
    hud.flags |= SHOW
    huds.append(hud)
    
    while 1:
        
        # shortcut detection
        if lap < max_laps:
            elapsed_time += e.time_step / 16
            node = my.path_scan(my.position, my.orientation, (360, 0, sd_distance))
            if node:
                my.nposition1, my.nskills1 = my.path_getnode(node)
                my.nposition2, my.nskills2 = my.path_getnode(node * (last_node == 0) + last_node)
                # crossed finish line
                if my.nskills1[2] == 1 and my.nskills2[2] == 2:
                    lap += 1
                    energy = min(energy + energy_lap, 100)
                    if lap == max_laps:
                        if opponent.winner == False:
                            my.winner = True
                # crossed finish line in reverse or shortcut
                if (my.nskills1[2] == 2 and my.nskills2[2] == 1)\
                or (distance != 0 and distance < (my.nskills1[0] - my.nskills2[0] - sd_threshold)):
                    message_time = elapsed_time + 3
                    reset_vehicle(my)
                else:
                    last_node = node
                distance = 0
            else: # outside of the track
                d = my.position.dist(last_position) 
                if d > 0.1: distance += d
            last_position = my.position
                
        # hud
        message = ""
        if my.winner: message = "winner!"
        elif message_time > elapsed_time: message = "invalid shortcut!"
        t = (my.index + 1,
             elapsed_time / 60,
             elapsed_time % 60,
             fraction(elapsed_time) * 100,
             speed * 1.2,
             clamp(lap + 1, 1, max_laps),
             energy,
             message)
        hud_string = "player %d\ntime: %02d:%02d:%02d\nspeed: %dkm/h\nlap: %d\nenergy: %d%%\n\n%s" % t
        hud_string_x = e.screen_size.x / 2 + 20 - e.screen_size.x / 2 * my.index
        draw_text(hud_string, hud_string_x, 20, (0,0,0))
        hud.pos_x = e.screen_size.x - 160 - e.screen_size.x / 2 * my.index
        
        # controls
        if my.ai:
            # very simple and stupid ai
            key_up = key_right = key_down = key_left = 0
            
            n = my.path_getnode(cycle(last_node + 3, 1, nodes))
            if n:
                p, s = n
                draw_point3d(p, (0,255,0), 100, 20)
                a = (p - my.position).to_angle().pan            
                d = ang_diff(my.pan, a)
                key_right = clamp(d * 0.01, -1, 1)
            else:
                reset_vehicle(my)
            
            n = my.path_getnode(cycle(last_node + 9, 1, nodes))
            if n:
                p, s = n
                draw_point3d(p, (0,0,255), 100, 20)
                a = s[1]
                d = abs(ang_diff(my.pan, a))
                target_speed = max(20, 100 - d)
                if speed < target_speed:
                    key_up = 1
                if speed - 40 > target_speed:
                    key_down = 1
                c_trace((my.x, my.y, my.z + 100), (my.x, my.y, my.z - 100), IGNORE_ME)
                if e.trace_hit:
                    draw_line3d(e.target, None, 100)
                    draw_line3d(e.target + e.normal * 100, (0,0,255), 100)
                    draw_line3d(e.target + e.normal * 100, (0,0,255), 100)
                    dot = e.normal * Vector(0, 0, 1).rotate(my.orientation)
                    if dot < 0.95: # brake to prevent tipping over
                        key_up = 0
                        key_down = 1
                    if dot < 0: # tipping over couldn't be prevented
                        reset_vehicle(my)
        else:
            # manual control
            key_up = key_pressed(keys[0 + 4 * my.index])
            key_right = key_pressed(keys[1 + 4 * my.index]) 
            key_down = key_pressed(keys[2 + 4 * my.index])
            key_left = key_pressed(keys[3 + 4 * my.index])
            key_tractor = key_pressed(keys[8 + my.index])
        
        motor = Vector()
        motor.x = motor_speed * key_up * (not key_down)
        motor.y = motor_power * key_up * (not key_down) + motor_brake * key_down
        rear_left.setmotor(nullvector, motor, nullvector)
        rear_right.setmotor(nullvector, motor, nullvector)
        
        steering = Vector()
        steering.y = steering_power
        p1 = front_left.getposition()
        steering.x = (30 * (key_right - key_left)) - p1.x * min(e.time_step, 1)
        front_left.setmotor(steering, motor, nullvector)
        p2 = front_right.getposition()
        steering.x = (30 * (key_right - key_left)) - p2.x * min(e.time_step, 1)
        front_right.setmotor(steering, motor, nullvector)
        
        # suspension
        my.animate("steering", clamp((p1.x + p2.x) / 2 * (100 / 60.0) + 50, 0, 100), 0)
        
        # camera
        if not (default.def_camera and my.index == 1):
            offset = Vector(my.pan, -camera_tilt, 0).for_angle() * -camera_distance
            camera.position += ((my.position + offset) - camera.position) * min(0.4 * e.time_step, 1) 
            if abs(my.pan - camera.pan) > 180: # always take the shorter way around
                camera.pan += 360 * sign(my.pan - camera.pan)
            camera.pan += (my.pan - camera.pan) * min(0.4 * e.time_step, 1)
            camera.tilt = -camera_tilt * 0.5
        
        # tractor beam
        if key_tractor and energy > 0:
            energy = max(energy - e.time_step, 0)
            p1 = my.vec_for_vertex(1646)
            p2 = p1 + my.orientation.for_angle() * beam_length 
            c_trace(p1, p2, IGNORE_ME|IGNORE_FLAG2)
            if e.trace_hit:
                p2 = e.target
            if e.you is opponent:
                e.you.ph_addforceglobal(-(my.orientation.for_angle() * beam_force), p2)
            for i in range(0, int(p1.dist(p2)), 2):
                p1 += my.orientation.for_angle() * 2 + Vector(random.random()*0.8-0.4, random.random()*0.8-0.4, random.random()*0.8-0.4)
                draw_point3d(p1, (255*(my.index==0), 0, 255*(my.index==1)), 100, 2)
        
        # get speed
        v = my.ph_getvelocity(nullvector)
        speed = (v.length() / 32) * 60 * 60 / 1000.0
        
        yield(1)



#-------------------------------------------------------------------------------
@schedule
def tire():
    my = e.my
    my.index = len(tires)
    tires.append(my)
    print "tire:", my.name, my.index
    
    if my.index < 4:
        group = 2
        vehicle = vehicles[0]
    else:
        group = 4
        vehicle = vehicles[1]
    
    set_physics(my, PH_SPHERE, PH_SPHERE, group, 30, 80, 50, 10, 20, 20)
    
    if my.index in (0, 1, 4, 5): # front tires
        steering_limit_l = -30
        steering_limit_r = 30
    else:
        steering_limit_l = 0
        steering_limit_r = 0
    
    c = Constraint(PH_WHEEL, vehicle, my)
    c.setparams(my.position, (0, 0, 1), (1, 0, 0), (steering_limit_l, steering_limit_r, 0), nullvector, (spring, damper, 0))
    tire_constraints.append(c)
    
    while 1:
        # suspension
        z = (my.position - vehicle.position).rotateback(vehicle.orientation).z
        s = (100.0 / (suspension_max - suspension_min)) * (z - suspension_min) - 30
        vehicle.animate(suspensions[my.index % 4], clamp(s, 0, 100), ANM_ADD)
        yield(1)



#-------------------------------------------------------------------------------
@schedule
def water():
    while 1:
        e.my.v += 0.8 * e.time_step
        yield(1)



#-------------------------------------------------------------------------------
@schedule
def main():
    print "main!"
    e.fps_max = 120
    e.video_mode = 8
    e.video_screen = 2
    e.preload_mode = 3
    e.time_smooth = 0.9
    e.fog_color = 1
    e.camera.fog_end = second_camera.fog_end = 75000
    
    global sd_distance, sd_threshold, vehicles, tires, tire_constraints
    vehicles = []
    tires = []
    tire_constraints = []
    
    if not e.key_f2:
        sd_distance = sd1_distance
        sd_threshold = sd1_threshold
        level_load("canyon.wmb", globals()) # pass global namespace so that the entity actions get found
    else:
        sd_distance = sd2_distance
        sd_threshold = sd2_threshold
        level_load("city.wmb", globals()) # pass global namespace so that the entity actions get found
    
    e.camera.x = e.camera.y = e.camera.tilt = second_camera.x = second_camera.y = second_camera.tilt = 0
    e.camera.pan = e.camera.z = second_camera.pan = second_camera.z = 90
    e.sky_cube_level.material = e.mtl_unlit
    e.on_f1 = e.on_f2 = main
    ph_setgravity((0, 0, -320 * 1.4))
    ph_setcorrections(25000, 0.05)
    
    while e.key_f1 or e.key_f2: yield(1)
    while not (e.key_f1 or e.key_f2):
        e.camera.size_x = second_camera.size_x = second_camera.pos_x = e.screen_size.x / 2
        yield(1)

main()
scheduler()