Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 29

Variables and loops: rehearsal

The rider steers by leaning, and for each step the rider chooses a lean by rehearsal. The lean search runs the bike's real step on a pretend motorcycle, reads where that motorcycle ends up, and throws the pretend one away. For each step, it tries twelve leans this way. Each try narrows the range the next one picks from, and the rider steers at the lean the tries close in on: the one that keeps the bike nearest the middle of the road.

Click a mark to see that try at a lean.

The picture holds step 374 of the ride, just after a bend. Each white arc is one try: a pretend motorcycle coasting down the road at one lean. The yellow line is the road's middle, and the search looks for the lean whose coast ends nearest it. The last mark is the lean the rider steers at.

Where the rehearsals start §

For each step, the rider's controls come from RiderMind.controls, from the “The rider” chapter. Its first line asks the lean search for a lean. Its second asks the throttle how hard to accelerate, for the motorcycle at that lean, built with with_lean. Both calls run rehearsals.

A coast §

Coast answers one question: if the rider held the lean and the speed from now on, where would the bike go? The answer is an Outcome:

Coast.roc
## Where a coast ends up: still on the road at this place across it, or off
## a shoulder after this many steps, counting the step that leaves.
Outcome : [EndsAt(Across), Leaves({ side : [Left, Right], after : Steps })]

The coast ends on the road at a place across it, or leaves the road at a shoulder after so many steps. One function runs it:

Coast.roc
## The coast from `motorcycle`, run a step at a time until it crosses a bound,
## gets `clear_distance` along the road, or runs `longest_coast` steps.
coast_end : Motorcycle -> CoastEnd
coast_end = |motorcycle| {
	start = motorcycle.route_place.road_point
	bounds = coast_bounds(start.across)
	var $point = start
	var $steps_run = Steps.zero
	var $motorcycle = motorcycle
	while $steps_run < longest_coast and is_coasting({ bounds, start, point: $point }) {
		$motorcycle = coasting_step($motorcycle)
		$steps_run = $steps_run.plus(Steps.one_step)
		$point = $motorcycle.route_place.road_point
	}
	outcome =
		if $point.across < bounds.left {
			Leaves({ side: Left, after: $steps_run })
		} else if $point.across > bounds.right {
			Leaves({ side: Right, after: $steps_run })
		} else {
			EndsAt($point.across)
		}
	{ outcome, steps_run: $steps_run }
}

var $point declares a variable: a name the function can set again, marked by the $ at its front. Each pass of the while loop sets $motorcycle to the bike a step on, and $point to where it now is. The bounds are the road's edges, 5 centimetres in, or wherever the bike starts if that's further out. The loop runs until the bike crosses a bound, gets 25 metres along the road, which is as far ahead as the rehearsal needs to look, or has run for 2,000 steps, which ends a slow rider circling in place. Then the last lines read where the coast ended.

The variables live and die inside coast_end. The function takes a motorcycle and returns an answer, and the same motorcycle always gives the same answer, so coast_end is as pure as every other function in the ride.

Each pass makes its step with this:

Coast.roc
## The motorcycle one step on, holding its lean and speed.
coasting_step : Motorcycle -> Motorcycle
coasting_step = |motorcycle| motorcycle.motorcycle_at_next_step(motorcycle.steady_controls())

Motorcycle.motorcycle_at_next_step is the real step, the same one that moves the real rider. steady_controls hands it the lean as it is and an acceleration of zero. The step is a pure function, so a pretend motorcycle steps exactly as the real one does.

Twelve tries §

A lean further right always ends a coast further right. So each try rules out half the leans still in play, and twelve tries narrow the choice down fast. That's a bisection: a binary search over leans.

LeanSearch.roc
## The lean for the next step, whose coast ends nearest the road's middle:
## halfway between the bounds the last try narrowed to.
best_lean : Motorcycle -> Angle.Lean
best_lean = |motorcycle| {
	var $bounds = first_bounds(motorcycle)
	for _ in 0..<lean_search_round_count {
		$bounds = bounds_after(lean_try(motorcycle, $bounds))
	}
	LeanSearch.middle_lean($bounds)
}

for _ in 0..<lean_search_round_count runs its body twelve times. 0..<12 counts from 0 up to 11, and _ stands in for the count, since the body needs only the bounds. The bounds start a degree either side of the bike's lean, so the lean it picks is always within a degree of the last one. Each try coasts the motorcycle at the lean halfway between the bounds:

LeanSearch.roc
## The motorcycle a try coasts: `motorcycle`, the one the search started
## from, with the lean halfway between `bounds`.
tried_motorcycle : Motorcycle, LeanSearch.LeanBounds -> Motorcycle
tried_motorcycle = |motorcycle, bounds|
	motorcycle.with_lean(LeanSearch.middle_lean(bounds))

lean_correction reads where that coast went, and returns which way to lean more:

LeanSearch.roc
## Which way the next try should lean, from where this try's coast went:
## off the left shoulder means more right, off the right means more left,
## and a coast that stays on the road compares its end with
## `aimed_across`. A coast that ends exactly there means lean left.
lean_correction : Coast.Outcome -> [MoreRight, MoreLeft]
lean_correction = |outcome|
	match outcome {
		Leaves({ side: Left, .. }) => MoreRight
		Leaves({ side: Right, .. }) => MoreLeft
		EndsAt(end_across) =>
			if end_across < LeanSearch.aimed_across {
				MoreRight
			} else {
				MoreLeft
			}
	}

Leaves({ side: Left, .. }) matches any coast off the left, whatever its count of steps. Off the left shoulder means lean more right, and off the right means more left. A coast that stays on the road compares where it ends with aimed_across, the road's middle. After the twelfth try, the search returns the lean halfway between the bounds it has narrowed to.

Why rehearsing is safe §

A rehearsal runs the real rules on a copy of the rider, and in Roc the copy is cheap. tried_motorcycle returns a new motorcycle, and the real one stays as it was. motorcycle_at_next_step has the thin arrow, so it reaches only its arguments. A rehearsal touches only its own copy.

Each real step of the ride runs thirteen coasts: twelve for the lean, and one more in the throttle. That last one asks whether the bike, held as it is, would leave the road at a shoulder, and brakes if so:

Throttle.roc
## Nothing, unless a coast from here leaves the road; then the brake for the
## shoulder it reaches.
shoulder_limit : Motorcycle -> ThrottleLimit
shoulder_limit = |motorcycle|
	match Coast.coast_outcome(motorcycle) {
		EndsAt(_) => NoLimit
		Leaves({ after, .. }) => AtMost(shoulder_brake(motorcycle.speed, after))
	}

So every part of the rider's step, rehearsals included, is a function from the moment before to the moment after. One part is left: where the rider looks, the next chapter's subject.