What does this program do?
This program tests how your bot roams in arcs based on your TURNING stat — and the roam logic here is identical to the match program's roam loop. What you see on the floor in this test is what you'll see during real matches. While driving, the bot continuously polls the track sensors at two thresholds (hard hit + soft warning), recovers if it hits the border, and adapts its driving as ENDURANCE decays.
The key idea: your TURNING stat doesn't just control whether you turn — it controls the shape of the pattern your bot traces around the ring. Higher TURNING → sharper, shorter segments (stays local). Lower TURNING → gentler, longer segments (covers more). Testing it here tells you what to expect in a real match.
SPEED = 6, ENDURANCE = 4, TURNING = 3 — edit to match your character. Also: BORDER_THRESHOLD must match whatever value your match program calibrates to, or border behavior won't be consistent between test and match.
Three buttons. Different roles than before.
Run the Roam Test
Calculates power + arc settings from your stats, then roams for 15 seconds using the same motion model as the match program. Watch the floor pattern and the RGB strip — blue/green = full power, red = low-power state.
Cycle Match Time + Show Power
Cycles through 0 → 60 → 120 → 180 → 0 seconds. Flashes the time, then the resulting decayed wheel power ("P38" for power = 38) so you can see how a tired bot's power drops without running a test.
Emergency Stop
Press both buttons together to cut motors immediately. Clears the RGB strip and shows an X on the LED screen. Use this whenever the bot heads somewhere it shouldn't.
How each section works
let SPEED = 6
let ENDURANCE = 4
let TURNING = 3
let TEST_DURATION_MS = 15000
let BASE_SEGMENT_MS = 300
let BORDER_THRESHOLD = 130
const BORDER_POLL_MS = 12
Runs once when the micro:bit powers on. The defaults already work — you change them to compare different TURNING values, not to "fill in zero." Press A to run with the current values.
SPEED = 6, ENDURANCE = 4, TURNING = 3 — Default starting stats. Edit these to match your character, then re-flash to compare. The Coverage Challenge specifically asks you to run with T = 2, 5, and 10.TEST_DURATION_MS = 15000 — Each test runs for 15 seconds. This matches the Coverage Challenge length — long enough to see the arc pattern develop and recover from at least one border hit.BASE_SEGMENT_MS = 300 — The base length of each arc segment, before TURNING shrinks it. With TURNING = 3, each segment runs for about 100ms before the bot considers a direction flip.BORDER_THRESHOLD = 130 — Sensor reading above this counts as a border hit. Set this to whatever your match program calibrates to — it must match the match program or border behavior will be inconsistent between test and match.BORDER_POLL_MS = 12 — How often the bot reads the sensors during a segment. 12ms means roughly 80 sensor reads per second — fast enough to react before the bot drives past the white edge.
input.onButtonPressed(Button.A, function () {
computeStats()
sameDirectionCount = 0
maxSameDirection = Math.randomRange(1, 2)
lastTurnWasLeft = false
running = true
testStart = input.runningTime()
while (running && input.runningTime() - testStart < TEST_DURATION_MS) {
roamStep()
}
motorStop()
running = false
strip.clear()
basic.showString("DONE")
basic.pause(500)
basic.clearScreen()
})
The main loop became much shorter. All the arc-drawing logic moved into a new function called roamStep — Button A just calls it over and over until time runs out. This is the same shape as the match program's roam loop, so what you see here is what you'll see in matches.
motorPower, turnRatio, thisDuration, and the endurance-decayed currentPower from your stats and the current simulated match time.sameDirectionCount = 0), pick how many segments to keep the same direction before flipping (maxSameDirection is 1 or 2, random), and start arcing right (lastTurnWasLeft = false).roamStep until either someone presses A+B (which sets running = false) or 15 seconds elapse. Each roamStep drives one arc segment, polls the border, and may flip direction or recover from an edge.
input.onButtonPressed(Button.B, function () {
if (simulatedMatchTime == 0) { simulatedMatchTime = 60 }
else if (simulatedMatchTime == 60) { simulatedMatchTime = 120 }
else if (simulatedMatchTime == 120) { simulatedMatchTime = 180 }
else { simulatedMatchTime = 0 }
computeStats()
basic.showString(simulatedMatchTime + "s")
basic.pause(300)
basic.showString("P" + currentPower)
basic.pause(400)
basic.clearScreen()
})
Cycles the simulated match time: 0 → 60 → 120 → 180 → 0. After cycling, this version also calls computeStats and shows the resulting decayed power on screen — so you can see how a tired bot's actual motor power drops without having to run a test.
computeStats immediately so currentPower reflects the new time.
input.onButtonPressed(Button.AB, function () {
running = false
motorStop()
strip.clear()
basic.showLeds(`
# . . . #
. # . # .
. . # . .
. # . # .
# . . . #
`)
basic.pause(500)
basic.clearScreen()
})
Press both buttons together to cut motors immediately. Sets running = false so any in-progress roam loop exits at the next safety check, calls motorStop for a clean wheel cut, clears the RGB strip, and flashes an X on the LED screen to confirm the stop.
function computeStats() {
motorPower = SPEED * 100 / 20
turnRatio = 0.2 + TURNING * 0.02
totalArcTime = BASE_SEGMENT_MS * 12
thisDuration = Math.min(Math.round(totalArcTime / Math.max(1, TURNING)), 600)
DECAY_RATE = 20 - ENDURANCE
minWheelSpeed = Math.max(20, Math.round(motorPower * 0.25))
lowPowerThreshold = minWheelSpeed
endurancePercent = 100 - DECAY_RATE * simulatedMatchTime / 40
enduranceFactor = Math.max(endurancePercent / 100, 0.2)
currentPower = Math.max(Math.round(motorPower * enduranceFactor), minWheelSpeed)
lowPowerActive = currentPower <= lowPowerThreshold
}
This function calculates everything before any motor turns — the arc shape, the endurance-decayed power, and a "low-power" flag the rest of the code uses to swap in gentler driving when the bot is tired.
turnRatio = 0.2 + TURNING × 0.02 ← arc sharpness, no cap
thisDuration = totalArcTime ÷ TURNING ← capped at 600ms
minWheelSpeed = max(20, motorPower × 0.25) ← floor
enduranceFactor = max(endurancePercent/100, 0.2) ← decay, min 20%
currentPower = max(motorPower × factor, minWheelSpeed)
lowPowerActive = currentPower ≤ lowPowerThreshold
roamStep clamps the ratio between 0.10 and 0.45 with jitter for variation.currentPower drops to the minimum wheel speed, lowPowerActive = true. The rest of the program reads this flag to use a softer sensor threshold, shorter backup, sharper recovery turn, and the RGB strip shows red instead of blue/green.
function driveAndWatch(ms: number, lSpeed: number, rSpeed: number) {
if (!running) { return }
let watchStart = input.runningTime()
let lastBorderPoll = 0
let lastMotorRefresh = input.runningTime()
while (input.runningTime() - watchStart < ms) {
if (!running) { return }
if (input.runningTime() - lastBorderPoll >= BORDER_POLL_MS) {
lastBorderPoll = input.runningTime()
readTrackbit()
let hardSide = borderSideFrom(BORDER_THRESHOLD)
if (hardSide > 0) {
borderSide = hardSide
CutebotPro.pwmCruiseControl(-60, -60)
basic.pause(70)
motorStop()
borderHandledInSeg = true
handleBorder()
return
}
let softThresh = Math.round(BORDER_THRESHOLD * (lowPowerActive ? 0.65 : 0.75))
let softSide = borderSideFrom(softThresh)
if (softSide > 0) {
softEvade(softSide)
return
}
}
// Motor keep-alive every 200ms
if (input.runningTime() - lastMotorRefresh > 200) {
lastMotorRefresh = input.runningTime()
CutebotPro.pwmCruiseControl(lSpeed, rSpeed)
}
basic.pause(5)
}
}
The new driveAndWatch does three jobs at once: polls the border sensors every 12ms, resends the motor command every 200ms so the driver doesn't latch, and checks two separate sensor thresholds — a hard one for a real border hit, and a soft one for an early warning.
watchStart tracks how long the segment has been running, lastBorderPoll rate-limits sensor reads, and lastMotorRefresh rate-limits the motor keep-alive command.BORDER_THRESHOLD (130), the bot is on white. Brake at −60 PWM for 70ms, full stop, set borderHandledInSeg = true, and call handleBorder for the full recovery sequence. Then exit the segment.softEvade — a lighter recovery than the full handleBorder.handleBorder with backup, multi-attempt turn, and recheck. Soft hit (about to be on white) → quick softEvade with shorter backup and one turn. Together, the bot rarely gets fully stuck on the edge.
function readTrackbit() {
CutebotPro.trackbitStateValue()
tb1 = CutebotPro.trackbitgetGray(TrackbitChannel.One)
tb2 = CutebotPro.trackbitgetGray(TrackbitChannel.Two)
tb3 = CutebotPro.trackbitgetGray(TrackbitChannel.Three)
tb4 = CutebotPro.trackbitgetGray(TrackbitChannel.Four)
}
// 1 = left-front 2 = right-front 3 = center 4 = both outer (head-on) 0 = clear
function borderSideFrom(threshold: number): number {
if (tb1 > threshold && tb4 > threshold) { return 4 }
if (tb1 > threshold) { return 1 }
if (tb4 > threshold) { return 2 }
if (tb2 > threshold || tb3 > threshold) { return 3 }
return 0
}
function checkBorder(): number {
readTrackbit()
let side = borderSideFrom(BORDER_THRESHOLD)
if (side > 0) { borderSide = side }
return side
}
Border detection splits into three helpers now. readTrackbit reads all four sensors into shared variables. borderSideFrom classifies which side of the bot is on white, given any threshold. checkBorder uses the hard threshold and is what other functions call.
trackbitStateValue once to refresh, then read each sensor (1–4) into tb1, tb2, tb3, tb4. Other functions read those variables instead of re-calling the sensor API.4 = head-on case for when both outer sensors fire. This is critical because a head-on hit needs a much harder recovery (bigger turn) than a single-side hit.BORDER_THRESHOLD and stores the result in the global borderSide so handleBorder knows which way to turn.
function handleBorder() {
if (!running) { return }
hardStop()
if (!running) { return }
speedExtra = Math.max(SPEED - 4, 0)
backupPower = lowPowerActive ? -25 : (0 - 40 - speedExtra * 5)
backupTime = lowPowerActive ? 130 : (250 + speedExtra * 20)
let attempts = 0
while (attempts < 3) {
attempts += 1
let side = borderSide
borderSide = 0
CutebotPro.pwmCruiseControl(backupPower, backupPower)
basic.pause(backupTime)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(80)
if (!running) { return }
let angle: number
if (side == 1 || side == 2) {
angle = lowPowerActive ? 150 : 115
} else {
angle = side == 4 ? 150 + Math.randomRange(0, 20) : 130
if (lowPowerActive) { angle = Math.min(angle + 30, 180) }
}
turnAwayFrom(side, angle)
if (!running) { return }
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(40)
CutebotPro.pwmCruiseControl(currentPower, currentPower)
basic.pause(300)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(40)
if (checkBorder() == 0) { break }
}
sameDirectionCount = 0
}
Recovery is now a 3-attempt loop. Each attempt: back up, turn away, drive forward briefly, then re-check the border. If the bot is still on white, the loop tries again — up to 3 times. The backup distance, turn angle, and direction logic all scale with SPEED and low-power state.
hardStop reverses at −80 PWM for a brake time that grows with SPEED. A fast bot takes longer to stop, so it brakes harder.speedExtra = SPEED − 4. Fast bots back up further with more power. Low-power bots back up just 130ms at −25 PWM (no wasted energy when the bot is tired).currentPower to get clear of the edge, then call checkBorder again. If clear, break out of the loop. If still on white, the next attempt starts.sameDirectionCount = 0 so the next roamStep picks a fresh direction streak length.
function roamStep() {
if (!running) { return }
// Flip direction every 1–2 segments (random)
if (sameDirectionCount >= maxSameDirection) {
lastTurnWasLeft = !lastTurnWasLeft
sameDirectionCount = 0
maxSameDirection = Math.randomRange(1, 2)
} else {
sameDirectionCount += 1
}
let dynamicRatio = Math.max(0.10, Math.min(0.45,
turnRatio + Math.randomRange(-8, 8) / 100))
let speedDurationBonus = SPEED * 12
let dynamicDuration = lowPowerActive
? Math.max(Math.min(thisDuration / 2 + Math.randomRange(-50, 50), 300), 150)
: Math.max(Math.min(thisDuration + speedDurationBonus + Math.randomRange(-50, 50), 550), 150)
let lSpeed: number
let rSpeed: number
if (lastTurnWasLeft) {
lSpeed = Math.round(currentPower * (1 - dynamicRatio))
rSpeed = Math.round(currentPower * (1 + dynamicRatio))
strip.showColor(lowPowerActive ? neopixel.colors(NeoPixelColors.Red)
: neopixel.colors(NeoPixelColors.Blue))
} else {
lSpeed = Math.round(currentPower * (1 + dynamicRatio))
rSpeed = Math.round(currentPower * (1 - dynamicRatio))
strip.showColor(lowPowerActive ? neopixel.colors(NeoPixelColors.Red)
: neopixel.rgb(0, 255, 0))
}
lSpeed = Math.max(lSpeed, minWheelSpeed)
rSpeed = Math.max(rSpeed, minWheelSpeed)
borderHandledInSeg = false
CutebotPro.pwmCruiseControl(lSpeed, rSpeed)
driveAndWatch(dynamicDuration, lSpeed, rSpeed)
if (!borderHandledInSeg) {
CutebotPro.pwmCruiseControl(-40, -40)
basic.pause(40)
}
motorStop()
if (running) {
basic.pause(40)
if (checkBorder() > 0) { handleBorder() }
}
}
This is where the bot's actual motion happens. Each call to roamStep drives one arc segment: pick a direction (maybe flip), pick speeds for left and right wheels, drive while watching the border, brake, and check the border one more time. The match program uses the same function — so the arc patterns you observe here are the patterns the bot draws in real matches.
sameDirectionCount). When it hits maxSameDirection, flip. This is why the bot zig-zags instead of looping cleanly in one direction.dynamicRatio = turnRatio + random(−0.08, 0.08), clamped between 0.10 and 0.45. Two segments with the same TURNING never trace exactly the same arc.speedDurationBonus = SPEED × 12 ms is added to each segment's duration. High-SPEED bots cover more ground per segment before flipping. Low-power state halves the duration instead — tired bots take shorter, gentler steps.currentPower × (1 − ratio) for the inside wheel, currentPower × (1 + ratio) for the outside wheel. Inside wheel slows, outside wheel goes faster than currentPower. Each wheel floored at minWheelSpeed so the inside wheel never stalls.driveAndWatch with the chosen wheel speeds for the chosen duration. If it returned without a border hit (!borderHandledInSeg), do a firm active brake at −40 PWM. Then check the border one more time — sometimes the bot rolls onto a line during the brake.
function motorStop() {
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(10)
CutebotPro.pwmCruiseControl(0, 0)
basic.pause(10)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
}
function hardStop() {
let brakeTime = 160 + Math.max(SPEED - 5, 0) * 35
CutebotPro.pwmCruiseControl(-80, -80)
basic.pause(brakeTime)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
}
function turnAwayFrom(side: number, angle: number) {
if (side == 1) {
CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, angle)
lastTurnWasLeft = false
} else if (side == 2) {
CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, angle)
lastTurnWasLeft = true
} else if (lastTurnWasLeft) {
CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, angle)
lastTurnWasLeft = false
} else {
CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, angle)
lastTurnWasLeft = true
}
}
function softEvade(side: number) {
motorStop()
basic.pause(20)
let backMs = lowPowerActive ? 130 : 200
let backPwr = lowPowerActive ? -25 : -30
CutebotPro.pwmCruiseControl(backPwr, backPwr)
basic.pause(backMs)
motorStop()
basic.pause(40)
turnAwayFrom(side, lowPowerActive ? 130 : 90)
motorStop()
borderHandledInSeg = true
borderSide = 0
}
Four short helper functions that the bigger functions call. Each does one job cleanly so the recovery logic stays readable.
What every variable means
Gold = you set it. Cyan = calculated automatically. Purple = fixed constant.
6. Sets the base motor power. Also affects segment duration (longer at higher SPEED) and recovery brake time (harder at higher SPEED).4. Controls how much motor power fades over simulatedMatchTime. Low ENDURANCE = noticeable slowdown at 180s.3. The Coverage Challenge asks for T = 2, 5, and 10. Drives both the arc sharpness (via turnRatio) and the segment length (via thisDuration).SPEED × 100 ÷ 20. The starting point for power calculations. Endurance decay is applied to this to produce currentPower.motorPower × enduranceFactor, floored at minWheelSpeed. This is what the wheels actually do.0.2 + TURNING × 0.02, no hard cap here — roamStep clamps it to 0.10–0.45 with jitter for variation.totalArcTime ÷ TURNING, capped at 600ms. roamStep adds SPEED bonus and random jitter before driving.BASE_SEGMENT_MS × 12 = 3,600ms in this version. Divided among segments to get thisDuration.max(20, motorPower × 0.25). Each wheel is clamped to at least this — keeps the inside wheel of an arc from stalling completely.endurancePercent ÷ 100, never below 0.20. Multiplied with motorPower to get currentPower.currentPower ≤ minWheelSpeed. Triggers softer thresholds, shorter backups, wider recovery turns, and a red RGB strip instead of blue/green.computeStats.handleBorder and turnAwayFrom.handleBorder so recovery resumes facing the right direction.maxSameDirection (1 or 2, random), flip. This is why the bot zig-zags instead of clean looping.15000 (15 seconds). Matches the Coverage Challenge — enough time to see the arc pattern develop and recover from at least one border hit.300. Multiplied by 12 to get totalArcTime. Change this to make all segments shorter or longer.130. Set this to whatever your match program uses — they must match. The soft threshold is automatically 75% of this (or 65% when low-power).12. Roughly 80 reads per second — fast enough to react before the bot drives past a white edge.Tips for using this program
Edit your stats, re-flash
Defaults are SPEED = 6, ENDURANCE = 4, TURNING = 3. The defaults work — change to your character's stats and re-flash to compare. There's nothing to "fill in from zero" anymore.
Run the Coverage Challenge sequence
Re-flash with TURNING = 2, then 5, then 10. Watch the floor pattern each one traces. The Day 3 Coverage Challenge asks exactly this — high TURNING = sharper, shorter segments (stays local). Low TURNING = gentler, longer segments (covers more).
Match the BORDER_THRESHOLD
Set BORDER_THRESHOLD to whatever value your match program calibrates to. If they don't match, the bot will react to the white border differently in tests than in matches.
Use Button B before A
Press B to advance the simulated match time, watch the "P" value drop, then press A. Comparing TURNING = 5 at 0s vs. 180s shows how a tired bot's arcs flatten — and you can predict it from the P number before running.
Watch the RGB strip
Blue = arcing left at full power. Green = arcing right at full power. Red = the bot has hit low-power state — softer recovery, shorter segments, wider turns. If you see red early in a 15-second test, your ENDURANCE allocation may be too low.
Keep A+B within reach
A+B is the emergency stop. Stay close, and press both buttons together the moment the bot heads somewhere it shouldn't. The X on the LED screen confirms motors are off.