MakeCode Blocks · Beginner Guide

Turning Test
Program

Learn how this MakeCode program drives your bot through arc patterns, tests how the TURNING stat shapes movement, and watches for the border while it works.

Open in MakeCode

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.

⚠️
Set the stats and the border threshold before running. Defaults are 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.
🔄
Button roles, same as the Endurance Test. A = run a 15-second roam test (use for the Coverage Challenge). B = cycle simulated match time (0 → 60 → 120 → 180), and shows the resulting decayed power on screen. A+B = emergency stop.

Three buttons. Different roles than before.

A

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.

B

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.

A+B

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

on start — Setting Up Variables
Loading blocks from MakeCode…
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.

1
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.
2
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.
3
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.
4
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.
5
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.
on button A pressed — Run the Arc Test
Loading blocks from MakeCode…
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.

1
computeStats → calculates motorPower, turnRatio, thisDuration, and the endurance-decayed currentPower from your stats and the current simulated match time.
2
Direction setup — reset the direction counter (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).
3
The roam loop — keep calling 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.
4
Cleanup — when the loop exits, fully stop the motors, clear the RGB strip, and show "DONE" on the LED screen.
on button B pressed — Cycle Simulated Match Time
Loading blocks from MakeCode…
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.

1
Cycle the time — bumps the simulated match time to the next stop in the cycle.
2
Refresh power — calls computeStats immediately so currentPower reflects the new time.
3
Show both numbers — flashes the time (e.g. "120s"), then the decayed wheel power (e.g. "P38"), so you can read the effect in one button press.
✅
Set time before pressing A. Press B until you see the match time you want, then A to roam at that decay level. Compare 0s vs. 180s on the same TURNING value to feel how a tired bot's arc loses its bite.
on button A+B pressed — Emergency Stop
Loading blocks from MakeCode…
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.

🛑
Use A+B whenever the bot heads somewhere it shouldn't. The X is your signal that motors are off. After a stop, press A to start a new test from the beginning.
function computeStats — Calculating Power and Arc Shape
Loading blocks from MakeCode…
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.

motorPower = SPEED × 100 ÷ 20 ← fresh power (before decay)
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
📐
thisDuration example: With TURNING = 5 and BASE_SEGMENT_MS = 300, total time = 3600ms, per segment = 720ms — capped at 600ms. With TURNING = 10 → 360ms per segment. The cap keeps any one arc segment from running too long without a sensor check.
〰️
turnRatio example: TURNING = 2 → 0.24 (gentle arc). TURNING = 10 → 0.40 (sharp). The cap from the old version is gone — instead, roamStep clamps the ratio between 0.10 and 0.45 with jitter for variation.
⬇️
Endurance decay: Same shape as before — power fades based on ENDURANCE stat and simulatedMatchTime, with a 20% floor so the bot can always crawl. The match program uses identical math, so testing here predicts late-match behavior.
🐢
Low-power tracking (new): If 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 — Drive + Monitor Sensors Simultaneously
Loading blocks from MakeCode…
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.

1
Three timers — watchStart tracks how long the segment has been running, lastBorderPoll rate-limits sensor reads, and lastMotorRefresh rate-limits the motor keep-alive command.
2
Hard threshold — if any sensor reads above 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.
3
Soft threshold (new) — if no hard hit, check a softer threshold at 75% of the hard value (or 65% when low-power). This catches the edge of the white zone before the bot is fully on it, and triggers softEvade — a lighter recovery than the full handleBorder.
4
Motor keep-alive (new) — every 200ms, resend the same wheel-speed command. This is a workaround for a Cutebot Pro driver quirk where motors can stop responding to long uninterrupted commands. The bot would otherwise stop driving partway through long segments.
💡
Two thresholds, two recoveries. Hard hit (already on white) → full 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 checkBorder — Reading the Sensors
Loading blocks from MakeCode…
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.

1
readTrackbit — 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.
2
borderSideFrom (new — takes a threshold) — adds a 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.
3
checkBorder — uses the hard BORDER_THRESHOLD and stores the result in the global borderSide so handleBorder knows which way to turn.
↑ Front of robot
tb1left
tb2mid
tb3mid
tb4right
BOT BODY
↓ Back of robot
tb1 only → 1 (left)
tb4 only → 2 (right)
tb2 or tb3 → 3 (center / head-on)
tb1 + tb4 → 4 (full head-on, new)
All clear → 0
function handleBorder — Recovering from the Edge
Loading blocks from MakeCode…
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.

1
Hard stop — hardStop reverses at −80 PWM for a brake time that grows with SPEED. A fast bot takes longer to stop, so it brakes harder.
2
Backup tuning — 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).
3
Turn angle by side — single-side hit (1 or 2): 115°. Head-on (3 or 4): 130–150° with a random nudge. Low-power: +30° wider so the bot doesn't get stuck pivoting back into the wall.
4
turnAwayFrom (new helper) — pivots in place. side 1 → right, side 2 → left, head-on → alternates based on which way it last turned (so consecutive head-ons don't oscillate).
5
Drive forward, then re-check — 300ms forward at 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.
6
Reset segment counter — sameDirectionCount = 0 so the next roamStep picks a fresh direction streak length.
🔁
3 attempts, not 1. A single recovery sometimes fails — backing up onto a different white edge, or turning into another corner. Looping three times is the difference between "bot got stuck in the corner" and "bot escaped within 2 seconds."
function roamStep — One Arc Segment (the motion model)
Loading blocks from MakeCode…
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.

1
Flip direction every 1–2 segments — track how many segments have used the same direction (sameDirectionCount). When it hits maxSameDirection, flip. This is why the bot zig-zags instead of looping cleanly in one direction.
2
Add jitter to turnRatio — 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.
3
SPEED stretches segments — 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.
4
Wheel speed math — 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.
5
RGB feedback — left turns show blue, right turns show green, and the low-power state shows red regardless of direction. The strip is a visual reading of the bot's internal state.
6
Drive, brake, recheck — call 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.
Small helpers — motorStop, hardStop, softEvade, turnAwayFrom
Loading blocks from MakeCode…
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.

🛑
motorStop — a clean motor cut. Three commands with 10ms pauses, because the driver sometimes ignores a single stop command when it's mid-PWM. Used by every other recovery function.
🚨
hardStop — reverse at −80 PWM for a brake time that grows with SPEED. A bot at SPEED 8 takes longer to stop than a bot at SPEED 4, so the brake time has to compensate or the bot overshoots into white.
↪️
turnAwayFrom — pivots in place by the given angle. Side 1 (left hit) → turn right. Side 2 (right hit) → turn left. Side 3 or 4 (head-on) → alternate based on which way it last turned, so consecutive head-ons don't oscillate.
🪶
softEvade — the lighter recovery for the soft (warning) threshold. Short backup (130–200ms) and a single turn (90–130°), no recheck loop. Used when the bot is approaching white but isn't on it yet — fast and out of the way.

What every variable means

Gold = you set it. Cyan = calculated automatically. Purple = fixed constant.

SPEED You Set
Your character's speed statDefault 6. Sets the base motor power. Also affects segment duration (longer at higher SPEED) and recovery brake time (harder at higher SPEED).
ENDURANCE You Set
Your character's endurance statDefault 4. Controls how much motor power fades over simulatedMatchTime. Low ENDURANCE = noticeable slowdown at 180s.
TURNING You Set
Your character's turning statDefault 3. The Coverage Challenge asks for T = 2, 5, and 10. Drives both the arc sharpness (via turnRatio) and the segment length (via thisDuration).
motorPower Auto
Fresh motor power before decayCalculated as SPEED × 100 ÷ 20. The starting point for power calculations. Endurance decay is applied to this to produce currentPower.
currentPower Auto
Actual motor power sent to wheels (after decay)motorPower × enduranceFactor, floored at minWheelSpeed. This is what the wheels actually do.
turnRatio Auto
Wheel speed difference that creates the arc0.2 + TURNING × 0.02, no hard cap here — roamStep clamps it to 0.10–0.45 with jitter for variation.
thisDuration Auto
Base segment duration in mstotalArcTime ÷ TURNING, capped at 600ms. roamStep adds SPEED bonus and random jitter before driving.
totalArcTime Auto
Total time budget for one arc cycleAlways BASE_SEGMENT_MS × 12 = 3,600ms in this version. Divided among segments to get thisDuration.
minWheelSpeed Auto
Floor for any single wheelmax(20, motorPower × 0.25). Each wheel is clamped to at least this — keeps the inside wheel of an arc from stalling completely.
enduranceFactor Auto
Decay multiplier (0.2–1.0)endurancePercent ÷ 100, never below 0.20. Multiplied with motorPower to get currentPower.
lowPowerActive Auto
Tired-bot flagTrue when currentPower ≤ minWheelSpeed. Triggers softer thresholds, shorter backups, wider recovery turns, and a red RGB strip instead of blue/green.
simulatedMatchTime Auto
Fake match clock for endurance testingSet by pressing Button B. 0 → 60 → 120 → 180. Drives the decay calculation in computeStats.
borderSide Auto
Which side of the bot last hit a border1 = left, 2 = right, 3 = center, 4 = both outer (head-on). Drives the turn direction in handleBorder and turnAwayFrom.
lastTurnWasLeft Auto
Which direction the last arc wentFlipped every 1–2 segments to make the bot zig-zag. Also updated by handleBorder so recovery resumes facing the right direction.
sameDirectionCount Auto
How many segments the bot has used the same directionWhen it hits maxSameDirection (1 or 2, random), flip. This is why the bot zig-zags instead of clean looping.
TEST_DURATION_MS Constant
How long each test run lastsDefault 15000 (15 seconds). Matches the Coverage Challenge — enough time to see the arc pattern develop and recover from at least one border hit.
BASE_SEGMENT_MS Constant
Base segment length before dividing by TURNINGDefault 300. Multiplied by 12 to get totalArcTime. Change this to make all segments shorter or longer.
BORDER_THRESHOLD Constant
Sensor reading cutoff for border detectionDefault 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).
BORDER_POLL_MS Constant
Sensor poll interval inside driveAndWatchDefault 12. Roughly 80 reads per second — fast enough to react before the bot drives past a white edge.

Tips for using this program

01

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.

02

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).

03

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.

04

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.

05

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.

06

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.