Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 32

The truck's motion

The truck is what the rider chases. It starts 500 metres up the road, and for most of the ride the rider closes in on it, unevenly:

Step Rider, along the route Truck, along the route Lead
0 0 metres 500 metres 500 metres
2,000 1,831 metres 2,274 metres 443 metres
4,000 4,796 metres 5,051 metres 255 metres
6,000 6,790 metres 6,931 metres 141 metres

The closest the rider gets is 50 metres, at step 6,374. After that the rider slows to stop at the route's end, at step 6,746. The truck drives on past the end of the route and out of the picture. It stays ahead to the last, by design.

A tease, carried out by physics §

The truck's driver is teasing the rider: staying ahead, but letting the rider gain. That's a hand of god: a rule that bends the world on purpose, for the story. Here it's one function, tease_position. Given how far the rider has come, it says where the truck would like to be. The lead it aims for shrinks from 500 metres at the start of the route to 100 at the end.

The rest is physics. The truck speeds up while it's short of that mark and holds its speed once it's past it. It slows for the bends, and that's mostly where the rider gains. The tease only ever speeds the truck up, so near the end the rider can get closer than the 100 metres it aims for. The truck can always pull away again, because it outdoes the rider by 10%, in both its acceleration and its top speed.

Carried from step to step §

Where the truck is depends on where the rider has been, so the truck is one of the four things each moment carries. Its step, truck_progress_at_next_step, takes a TruckProgress and returns the next one. The rider moves first, and then the truck reads where the rider has got to. The reading runs one way, from the truck to the rider.

Get the types right §

The truck's state is three fields:

TruckJourney.roc, in TruckProgress
## How far along the route the truck is, how fast it is going, and whether
## its last step slowed it. Nominal, not opaque: other modules read its
## fields.
TruckProgress := {
	route_position : Distance,
	speed : Speed,
	pace : TruckJourney.Pace,
	# ...

route_position is a Distance along the whole route, the same measure as the rider's progress, so the two can be compared directly. Pace, which the brake lights read in the “The truck” chapter, says whether the truck's last step slowed it down.

A bend ahead of the truck is its own type, and so is the answer to "is there one":

TruckJourney.roc
## A bend ahead of the truck: how far off it is, and how sharply it turns.
ComingBend : { distance : Distance, turn : Angle.Turn }
TruckJourney.roc
## The next bend of the route past a point, or none: the route ends first.
NextBend : [BendAhead(ComingBend), NoBendAhead]

BendAhead carries a ComingBend: how far off the bend is, and its turn, an angle. NoBendAhead is a bare tag: the route ends before another bend, or the truck is already past the end. The only way to read a bend's distance is inside the branch where there is a bend.

The functions §

TruckJourney Route  finds the next bend on VehicleLimits  keeps to the limits Speed  measures how fast it goes in Acceleration  measures speeding up in RideInstant  steps it TruckJourney Route  finds the next bend on VehicleLimits  keeps to the limits Speed  measures how fast it goes in Acceleration  measures speeding up in RideInstant  steps it
The modules around TruckJourney

RideInstant moves the truck once a step. All it hands over is how far along the route the rider has just got to, and the route itself:

RideInstant.roc, in next
# ...
truck = TruckJourney.truck_progress_at_next_step(
	instant.truck,
	{ rider_distance: rider_place.distance_along(route), route },
	# ...

That record holds all the truck reads of the rider. The truck and the rider share VehicleLimits, the limits both keep to. The truck's 10% is its own, chase_margin in TruckJourney.

The step works out the tease, then looks for the next bend:

TruckJourney.roc
## The truck's progress one step on: where the rider is along the route,
## and the route itself, decide its pace.
truck_progress_at_next_step : TruckJourney.TruckProgress,
{
	rider_distance : Distance,
	route : Route,
} -> TruckJourney.TruckProgress
truck_progress_at_next_step = |progress, { rider_distance, route }| {
	tease = tease_position({ rider_distance, route_length: route.route_length() })
	match next_bend(route, progress.route_position) {
		BendAhead(bend) =>
			if bend.distance <= brake_distance {
				progress_braking_for(progress, bend)
			} else {
				progress_toward_tease(progress, tease)
			}
		NoBendAhead => progress_toward_tease(progress, tease)
	}
}

Here's the tease itself. The lead shrinks in step with the rider's progress along the route:

TruckJourney.roc
## The tease: where the truck aims to be along the route when the rider is
## `rider_distance` along it, so that the rider closes in on it over the whole
## route. The lead it aims for shrinks from `tease_start_lead` to
## `tease_finish_lead` in step with the rider's progress. The truck only chases
## this mark, and never slows to keep to it, so the real gap closes unevenly: the
## truck can run past the mark on a straight, and it slows for the bends.
tease_position : { rider_distance : Distance, route_length : Distance } -> Distance
tease_position = |{ rider_distance, route_length }| {
	fraction_left = 1.0 - rider_distance.ratio_to(route_length)
	extra_lead = tease_start_lead.minus(tease_finish_lead).times(fraction_left)
	rider_distance.plus(tease_finish_lead).plus(extra_lead)
}

Finding the next bend walks the route's segments with their ends, and takes the first one that ends past the truck:

TruckJourney.roc
## The next bend of `route` past `route_position`.
next_bend : Route, Distance -> NextBend
next_bend = |route, route_position|
	match route.segments_with_ends().find_first(|stretch| route_position < stretch.end) {
		Ok({ segment, end }) =>
			match segment.ending {
				Bends({ turn, .. }) => BendAhead({ distance: end.minus(route_position), turn })
				RouteEnds => NoBendAhead
			}
		Err(NotFound) => NoBendAhead
	}

Then come the two ways to take a step. Both sit below the module's block, so they're private, and both are arithmetic on Speed. Within 60 metres of a bend, the truck brakes so it reaches the bend at its cautious speed. VehicleLimits.turn_speed is the speed a turn allows, and the rider's throttle asks it too. The truck's own rule is that limit times its caution, 80%, to leave a margin:

TruckJourney.roc
## The truck takes a bend at this fraction of the speed the bend allows, to
## leave a margin.
turn_caution : Fraction
turn_caution = Fraction.of(0.8)
TruckJourney.roc
## The truck's progress one step on, slowing for `bend`, so that it reaches the
## bend at a cautious speed.
progress_braking_for : TruckJourney.TruckProgress, ComingBend -> TruckJourney.TruckProgress
progress_braking_for = |progress, bend| {
	target_speed = VehicleLimits.turn_speed(bend.turn).scaled_by(turn_caution)
	acceleration = VehicleLimits.braking_to(
		progress.speed,
		{ target: target_speed, within: bend.distance },
	)
	# The steady brake may land a little under its target; `VehicleLimits.braking_to`
	# says why. The truck alone keeps to the target here.
	speed = target_speed.max(progress.speed.accelerated_by(acceleration))
	{
		route_position: progress.route_position.plus(speed.step_length()),
		speed,
		pace: if speed < progress.speed {
			Slowing
		} else {
			NotSlowing
		},
	}
}

VehicleLimits.braking_to works out one steady brake that reaches the target speed at the bend. The brake is steady over the whole distance left, so one of the last steps before the bend can land a little under the target, and target_speed.max(...) holds the truck there. braking_to crashes on a brake that would take the speed below zero, the invariant it holds for the rider too. Farther from a bend, the truck chases the tease:

TruckJourney.roc
## The truck's progress one step on with no bend close: it speeds up while it
## is short of `tease`, and otherwise holds its speed.
progress_toward_tease : TruckJourney.TruckProgress, Distance -> TruckJourney.TruckProgress
progress_toward_tease = |progress, tease| {
	speed =
		if progress.route_position < tease {
			chase_top_speed.min(progress.speed.accelerated_by(chase_acceleration))
		} else {
			progress.speed
		}
	{
		route_position: progress.route_position.plus(speed.step_length()),
		speed,
		pace: NotSlowing,
	}
}

The truck reads only how far along the route the rider has come. The cat and the bird ahead react to where the rider is, starting with the cat.