MakeCode Blocks · Competition Guide

Match Code
Template

The program every bot runs in Velocity Arena. You control two zones — your stats and your launch. Everything else is the competition engine: it roams toward the goal, finds the ball, dribbles it toward the net, and recovers from borders and jams automatically.

Open in MakeCode

You control two zones. Everything else is automatic.

The match code template comes pre-built. You set your character's stats (Zone A) and program your opening move (Zone B). The engine handles the rest — finding the ball, aiming it at the goal, dribbling, and recovering from border hits and jams.

⚙️ Zone A — Stat Allocation

Set your four stats

Four numbers control everything: how fast you roam, how quickly your power fades, how sharply you arc and aim, and how hard you kick the ball.

📌Every stat is at least 1. POWER tops out at 10; SPEED, ENDURANCE, TURNING can go to 17
📌All four must add up to exactly 20 — the on-screen budget bar / CHECK line is your reminder
📌Set once at the bottom of the file — they don't change mid-match
🚀 Zone B — Launch Sequence

Program your opening move

After the match starts, your launch sequence drives the bot from its corner to behind the ball, pointed at the goal, before the main loop takes over.

📌Write your moves where the code says // ← YOUR CODE HERE
📌Leave the launchActive lines alone — they pause border detection so your exact turns run uninterrupted
📌Everything below the LOCKED banner is automatic — it drives the last bit to the ball and kicks it through

Four stats. Must total 20. Fixed match time: 180 seconds.

The match always runs 180 seconds — match length is not controlled by stats. What changes is how fast your bot moves, how fast it slows down, how sharply it aims, and how hard it hits.

Speed
S
1 – 17
Endurance
E
1 – 17
Turning
T
1 – 17
Power
P
1 – 10
SPEED + ENDURANCE + TURNING + POWER = 20
The only valid total
// What each stat controls (all computed in computeStats)
motorPower    = SPEED × 100 / 20        // starting cruise speed (SPEED 6 → 30, SPEED 10 → 50)
chargePower   = min(POWER², 100)         // POWER 3 → 9, POWER 8 → 64, POWER 10 → 100 — caps at 10!
FOLLOW_THROUGH_MS = 120 + POWER × 35     // push time after contact (POWER 8 → 400ms)
turnRatio     = 0.2 + TURNING × 0.02    // arc + aim sharpness
DECAY_RATE    = 20 − ENDURANCE       // how fast power drops over the match
minWheelSpeed = max(20, round(motorPower × 0.25)) // power floor (low-power "red" mode below this)
⚠️
ENDURANCE does not change how long the match runs — it changes how fast your bot slows down. endurancePercent = 100 − DECAY_RATE × matchTime / 40, floored at 20%. ENDURANCE 4 (DECAY_RATE 16) is down to ~28% power by minute 3; ENDURANCE 17 (DECAY_RATE 3) barely fades.
⚡
POWER is the only stat capped at 10 — and it scales by the square. chargePower = min(POWER², 100). POWER 8 → 64, POWER 9 → 81, POWER 10 → 100. Above 10 is wasted (the kick is already maxed), so the cap is 10. The other three are linear, so they run all the way to 17.

One setup, one start, two safety buttons.

A

Press 1 — Set up & calibrate

From idle: runs computeStats, then calibrates the border threshold (show White floor, then Black tape) and the goal heading (G — point at the goal). Shows the build bars + a Target icon when ready.

A

Press 2 — Start the match

When "ready", starts the 180-second match: resets state, runs launchSequence(), then enters matchLoop().

B

Soft reset

During a match, sets resetRequested = true. The loop clears ball/border state and re-centers behavior without ending the match.

A+B

Emergency stop

Sets matchRunning = false, stops all motors immediately, and shows an X. Every loop checks matchRunning and bails.

🧭
The compass is calibrated at setup. The goal-heading step (G) is what lets the bot aim its dribble at the net, bias its roaming toward the opponent's half, and avoid its own goal. Make sure the micro:bit compass is calibrated (tilt to fill the screen) before the match.

What happens from start to finish

🅰️
A · setup
stats + calibrate
→
🅰️
A · start
begin match
→
🚀
launchSequence
opening move + kick
→
MATCH LOOP — runs until matchTime ≥ 180s
🌀
Roam
goal-biased
→
🔎
Find
scan + home-in
→
🎯
Aim
center the ball
→
⚽
Dribble
toward goal
→
↩️
Recover
border / jam / drop back
→
🏁
endMatch
180s elapsed
🔄
The loop is a priority list. Each pass: if the bot has the ball, it aims and dribbles it toward the goal. Otherwise it counts roam segments — after a few, it does a stationary scan; if scans keep coming up empty it drops back to its own side and looks down the field. While driving, it also homes in on any ball it glimpses ahead.

How each section works

Zone A — Stat Allocation Your code goes here
Loading blocks from MakeCode…
let SPEED = 6
let TURNING = 2
let ENDURANCE = 4
let POWER = 8
        

The four numbers you choose. The example above is the "Sniper" build: SPEED 6 + TURNING 2 + ENDURANCE 4 + POWER 8 = 20. Replace them with your character's stats. Press A and the on-screen budget reminds you the four must total exactly 20.

1
SPEED (1–17) — starting cruise speed. motorPower = SPEED × 100 / 20. SPEED 6 = 30, SPEED 17 = 85.
2
ENDURANCE (1–17) — decay resistance. DECAY_RATE = 20 − ENDURANCE. Higher = stays fast longer. Does not change match length.
3
TURNING (1–17) — arc shape and aim speed. Higher = tighter arcs and faster, more-persistent aiming when it locks onto the ball.
4
POWER (1–10) — kick force (POWER²) and follow-through time. Capped at 10 because POWER² already hits the 100 max there.
on button A pressed — Set up, then Start
Loading blocks from MakeCode…
let matchRunning = false
let readyToLaunch = false
let goalHeading = 0
let ownGoalHeading = 0
let matchStartTime = 0
function computeStats() {}
function calibrateThreshold() {}
function calibrateGoalHeading() {}
function showStatBars() {}
function launchSequence() {}
function matchLoop() {}
input.onButtonPressed(Button.A, function () {
    if (!matchRunning && !readyToLaunch) {
        computeStats()
        calibrateThreshold()
        calibrateGoalHeading()
        ownGoalHeading = (goalHeading + 180) % 360
        readyToLaunch = true
        showStatBars()
        basic.showIcon(IconNames.Target)
    } else if (readyToLaunch) {
        readyToLaunch = false
        matchStartTime = input.runningTime()
        matchRunning = true
        launchSequence()
        matchLoop()
    }
})
        

Button A has two jobs. The first press calibrates; the second press starts the match.

1
First press (setup): computeStats() derives all working values, then calibrateThreshold() (show white floor, then black tape) finds the border cutoff and calibrateGoalHeading() records which way the goal is.
2
Stores ownGoalHeading (opposite the goal), shows the four-stat bars and a Target icon, and sets readyToLaunch = true.
3
Second press (start): records the start time, sets matchRunning = true, runs launchSequence(), then matchLoop() until 180 seconds elapse.
computeStats — Turning Stat Points Into Motor Values
Loading blocks from MakeCode…
let SPEED = 0
let ENDURANCE = 0
let TURNING = 0
let POWER = 0
let motorPower = 0
let currentPower = 0
let chargePower = 0
let FOLLOW_THROUGH_MS = 0
let totalArcTime = 0
let BASE_SEGMENT_MS = 0
let thisDuration = 0
let turnRatio = 0
let DECAY_RATE = 0
let minWheelSpeed = 0
let lowPowerThreshold = 0
function computeStats () {
    motorPower = SPEED * 100 / 20
    currentPower = motorPower
    chargePower = Math.min(POWER * POWER, 100)
    FOLLOW_THROUGH_MS = 120 + POWER * 35
    totalArcTime = BASE_SEGMENT_MS * 12
    thisDuration = Math.min(Math.round(totalArcTime / TURNING), 600)
    turnRatio = 0.2 + TURNING * 0.02
    DECAY_RATE = 20 - ENDURANCE
    minWheelSpeed = Math.max(20, Math.round(motorPower * 0.25))
    lowPowerThreshold = minWheelSpeed
}
        

Converts your four stats into the numbers the engine actually uses. Runs once on the first A press, before calibration.

1
motorPower = SPEED × 100 / 20 — starting motor speed. currentPower starts here and decays during the match.
2
chargePower = min(POWER², 100) — kick force. POWER 8 → 64, POWER 10 → 100 (the cap).
3
FOLLOW_THROUGH_MS = 120 + POWER × 35 — push time after contact. POWER 8 → 400ms (the engine caps the push at 500ms).
4
thisDuration = min((BASE_SEGMENT_MS × 12) / TURNING, 600) — each roam arc's length. BASE_SEGMENT_MS is 300, so totalArcTime = 3600ms.
5
turnRatio = 0.2 + TURNING × 0.02 — arc sharpness (the roam jitters this between 0.10 and 0.45).
6
minWheelSpeed = max(20, round(motorPower × 0.25)) — the real power floor. When currentPower decays to this, the bot enters low-power ("red") mode and plays conservatively.
launchSequence — Zone B: Your Opening Move Write your launch here
Loading blocks from MakeCode…
let launchActive = false
let motorPower = 0
let currentPower = 0
let matchRunning = false
let ballDetected = false
let LAUNCH_APPROACH_MS = 0
function motorStop() {}
function checkBorder() { return 0 }
function handleBorder() {}
function driveUntilBall(ms: number): boolean { return false }
function chargeAndFollowThrough() {}
function launchSequence () {
    launchActive = true
    let powerScale = 30 / Math.max(1, motorPower)

    // ← YOUR CODE HERE — example: a diagonal onto the ball→goal axis
    CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, 35)
    basic.pause(300); motorStop(); basic.pause(150)
    CutebotPro.pwmCruiseControl(currentPower, currentPower)
    basic.pause(Math.round(2520 * powerScale))
    motorStop(); basic.pause(150)
    CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, 35)
    basic.pause(300)

    // ── LOCKED below — do not change ──
    motorStop()
    launchActive = false
    if (checkBorder() > 0) { handleBorder(); return }
    driveUntilBall(Math.round(LAUNCH_APPROACH_MS * powerScale))
    ballDetected = true
    launchActive = true
    chargeAndFollowThrough()
    launchActive = false
}
        

Your opening move. Border detection is paused (launchActive = true) so your exact turns and drives run start-to-finish. The example is the "Sniper" solution's diagonal: angle onto the ball→goal axis, drive, re-square to the goal. Below the LOCKED banner the engine drives the last bit to the ball and kicks it through — straight, with no compass curve (the launch is dead-reckoned).

1
Drive by distance for geometry. runBlockCnt(n) drives exactly n × 27 cm regardless of speed — best for a planned path. Time-based driving uses pwmCruiseControl + pause(ms × powerScale); powerScale evens out fast vs. slow bots.
2
Turn in place with trolleySteering(LeftInPlace / RightInPlace, degrees). Equal-and-opposite turns cancel, so a "turn out, drive, turn back" lands you re-squared to the goal.
3
Don't touch launchActive or the LOCKED block. They re-enable border safety and run the unconditional kick. If the ball isn't there, it's just a harmless drive at the goal.
📐
The launch is a geometry problem: line up bot → ball → goal. Your kick goes the way the bot is facing, so finish your launch behind the ball, pointed at the net. Measure where the ball sits, then plan turns and distances (a 3-4-5 / Pythagorean diagonal is the classic move).
matchLoop — The 180-Second Competition Engine
Loading blocks from MakeCode…
let matchRunning = false
let matchTime = 0
let matchStartTime = 0
let motorPower = 0
let SPEED = 0
let DECAY_RATE = 0
let endurancePercent = 0
let enduranceFactor = 0
let currentPower = 0
let minWheelSpeed = 0
let lowPowerThreshold = 0
let lowPowerActive = false
let ballDetected = false
let roamCounter = 0
let lostScans = 0
let TURNING = 0
let REPOSITION_AFTER_SCANS = 0
function handleReset() {}
function endMatch() {}
function tryAimThenCharge() {}
function lookForBall() {}
function repositionHome() {}
function roamStep() {}
function checkBorder() { return 0 }
function handleBorder() {}
function matchLoop () {
    while (matchRunning) {
        handleReset()
        matchTime = (input.runningTime() - matchStartTime) / 1000
        if (matchTime >= 180) { endMatch(); break }

        motorPower = SPEED * 100 / 20
        endurancePercent = 100 - DECAY_RATE * matchTime / 40
        enduranceFactor = Math.max(endurancePercent / 100, 0.2)
        currentPower = Math.max(Math.round(motorPower * enduranceFactor), minWheelSpeed)
        lowPowerActive = currentPower <= lowPowerThreshold

        if (ballDetected) {
            tryAimThenCharge()
            continue
        }

        let scanLimit = lowPowerActive ? 2 : Math.max(3, 3 + Math.round(TURNING / 2))
        if (roamCounter >= scanLimit) {
            roamCounter = 0
            lookForBall()
            if (!ballDetected) {
                lostScans += 1
                if (lostScans >= REPOSITION_AFTER_SCANS && !lowPowerActive) {
                    lostScans = 0
                    repositionHome()
                }
            }
        }
        if (ballDetected) { tryAimThenCharge(); continue }
        roamStep()
    }
}
        

The heart of the engine. Runs until matchTime ≥ 180. Each pass recalculates power from the endurance curve, then picks one action by priority.

1
Power update. endurancePercent = 100 − DECAY_RATE × matchTime / 40 (floored at 20%) scales currentPower, floored at minWheelSpeed. When it hits that floor, lowPowerActive turns on and the bot plays conservatively.
2
Have the ball? tryAimThenCharge() — center the ball, then dribble it toward the goal. (Top priority.)
3
Time to scan? After scanLimit roam segments, lookForBall() does a stationary sweep.
4
Lost too long? After a few empty scans, repositionHome() drops the bot back to its own side and turns to look down the field (skipped on low power).
5
Otherwise roam. roamStep() drives one goal-biased arc, watching for the ball and borders the whole time.
driveAndWatch — Drive a Segment, Watch Border + Ball
Loading blocks from MakeCode…
let matchRunning = false
let launchActive = false
let lastBorderPoll = 0
let lastPingTime = 0
let pingInterval = 0
let borderHandledInSeg = false
let ballDetected = false
let ballHeading = 0
let BORDER_THRESHOLD = 0
let BALL_DETECT_CM = 0
let BALL_SEE_CM = 0
function readTrackbit() {}
function borderSideFrom(t: number): number { return 0 }
function motorStop() {}
function handleBorder() {}
function confirmBall(stopped: boolean): boolean { return false }
function driveAndWatch (ms: number, lSpeed: number, rSpeed: number) {
    let watchStart = input.runningTime()
    let BORDER_POLL_MS = 6
    while (input.runningTime() - watchStart < ms) {
        if (!matchRunning) { return }
        // ── Border (every 6ms): one read serves hard + soft ──
        if (input.runningTime() - lastBorderPoll >= BORDER_POLL_MS) {
            lastBorderPoll = input.runningTime()
            readTrackbit()
            if (!launchActive && borderSideFrom(BORDER_THRESHOLD) > 0) {
                CutebotPro.pwmCruiseControl(-60, -60)
                basic.pause(70); motorStop()
                borderHandledInSeg = true
                handleBorder()
                return
            }
        }
        // ── Ball detection while driving ──
        if (input.runningTime() - lastPingTime > pingInterval) {
            lastPingTime = input.runningTime()
            let dist = CutebotPro.ultrasonic(SonarUnit.Centimeters)
            if (dist > 5 && dist < BALL_DETECT_CM) {
                motorStop()
                if (confirmBall(true)) { ballHeading = 99; ballDetected = true; return }
            } else if (dist > 5 && dist < BALL_SEE_CM) {
                // saw it at range — drive STRAIGHT at it to close the gap
                CutebotPro.pwmCruiseControl(rSpeed, rSpeed)
            }
        }
        basic.pause(5)
    }
}
        

Drives at (lSpeed, rSpeed) for ms while watching for two things. It returns early if it handles a border or finds the ball.

1
Border (every 6ms). One line-sensor read. If it crosses the threshold, the bot active-brakes (-60 for 70ms), pulls the sensors back off the line, and runs handleBorder(). A second read right after each sonar ping is the failsafe for fast crossings.
2
Commit (< 40cm). Ball confirmed within BALL_DETECT_CM → stop, confirm, set ballDetected, hand off to aiming.
3
Home-in (< 45cm). A ball glimpsed within BALL_SEE_CM while driving → stop arcing and drive straight at it to close the gap. This is how the bot finds the ball during normal roaming, not just during a scan.
ℹ️
Border sensing is paused during your launch and the kick (launchActive). Everywhere else it runs on a tight 6ms cadence — border safety outranks everything, because the one thing worse than missing the ball is driving off the field.
lookForBall + sweepLeg — Stationary Step-Scan
Loading blocks from MakeCode…
let matchRunning = false
let scanPower = 0
let ballDetected = false
let ballHeading = 0
let BALL_SCAN_CM = 0
function checkBorder() { return 0 }
function handleBorder(d: number, s: boolean) {}
function confirmBallScan(): boolean { return false }
function sweepLeg (turnLeft: boolean, ms: number, foundHeading: number): number {
    let STEP_MS = 90
    let elapsed = 0
    while (elapsed < ms) {
        if (!matchRunning) { return 2 }
        if (checkBorder() > 0) { handleBorder(0, false); return 2 }
        // rotate one small step...
        let burst = Math.min(STEP_MS, ms - elapsed)
        if (turnLeft) { CutebotPro.pwmCruiseControl(0 - scanPower, scanPower) }
        else { CutebotPro.pwmCruiseControl(scanPower, 0 - scanPower) }
        basic.pause(burst); elapsed += burst
        // ...then STOP and look (a still beam actually sees the ball)
        CutebotPro.stopImmediately(CutebotProMotors.ALL)
        basic.pause(30)
        let d = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        if (d > 5 && d < BALL_SCAN_CM) {
            if (confirmBallScan()) { ballHeading = foundHeading; ballDetected = true; return 1 }
        }
    }
    return 0
}
        

When roaming hasn't turned up the ball, lookForBall() runs a step-scan: four legs (left, back, right, back) biased toward the goal side. Each leg rotates a small step, stops, then pings.

1
Stop-and-ping. Pinging mid-rotation lets the narrow sonar beam skip right past the ball. Rotating a step then stopping to look catches it reliably.
2
On a confirmed hit the bot is now facing the ball, so closeOnBall() drives straight in until it's close enough to commit the shot.
3
Returns a code: 1 = found, 2 = aborted (border/stop), 0 = nothing this leg.
tryAimThenCharge + chargeAndFollowThrough — Aim, Then Dribble to Goal
Loading blocks from MakeCode…
let matchRunning = false
let launchActive = false
let goalHeadingValid = false
let goalHeading = 0
let ballDetected = false
let dynamicCharge = 0
let actualFollowThrough = 0
let CONTACT_CM = 0
let GOAL_AIM_TOL = 0
let BALL_DETECT_CM = 0
function stableHeadingBlocking(): number { return -1 }
function checkBorder() { return 0 }
function handleBorder() {}
function alignBeforeCharge(): boolean { return false }
function tryAimThenCharge () {
    if (alignBeforeCharge()) {
        chargeAndFollowThrough()
    } else {
        ballDetected = false
    }
}
function chargeAndFollowThrough () {
    // Goal-seeking dribble: bias one wheel so the IN-CONTACT push curves
    // toward the goal heading. Skipped during the dead-reckoned launch.
    let lPush = dynamicCharge
    let rPush = dynamicCharge
    if (!launchActive && goalHeadingValid) {
        let gh = stableHeadingBlocking()
        if (gh >= 0) {
            let ge = goalHeading - gh
            while (ge > 180) { ge -= 360 }
            while (ge < -180) { ge += 360 }
            let bias = Math.round(dynamicCharge * 0.45)
            if (ge > GOAL_AIM_TOL) { rPush = Math.max(dynamicCharge - bias, 18) }
            else if (ge < 0 - GOAL_AIM_TOL) { lPush = Math.max(dynamicCharge - bias, 18) }
        }
    }
    while (ballDetected) {
        if (!matchRunning) { return }
        if (checkBorder() > 0) { handleBorder(); ballDetected = false; return }
        let currentDist = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        // In contact -> curve toward goal; still approaching -> drive straight in
        if (currentDist > 5 && currentDist < CONTACT_CM) {
            CutebotPro.pwmCruiseControl(lPush, rPush)
        } else {
            CutebotPro.pwmCruiseControl(dynamicCharge, dynamicCharge)
        }
        basic.pause(actualFollowThrough)
        // ball still ahead? keep dribbling. lost it? stop and rescan.
        let f = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        ballDetected = (f > 5 && f < BALL_DETECT_CM)
    }
}
        

First alignBeforeCharge() centers the ball (and nudges out of the own-goal cone). Then the bot pushes the ball — and curves the push toward the goal while it's in contact.

1
Aim first. If the ball was seen off to a side, the bot rotates toward it and re-confirms before committing — at least two attempts (more with higher TURNING).
2
Goal-seeking dribble. Once in contact (< CONTACT_CM = 15cm), it reads the compass and eases off one wheel so the ball curves toward the goal heading. Already pointed at the goal → straight kick.
3
Approach straight. While the ball is still ahead but not yet touching, it drives straight at it — curving early would veer off and miss.
4
Chase, don't pivot. After each push, if the ball is still ahead it keeps driving (dribbling it on); only when the ball is truly lost does it stop and rescan. Jam guards break free if the wheels spin without the ball moving.
🥅
Own goals count. A ball crossing either goal line scores for that net's attacker. The dribble aims at your goal — which is exactly why the compass / goal-heading calibration matters. If a bot ever curves toward the wrong net, the turn direction in turnToHeading (and these two branches) is wired for the opposite chassis and needs swapping.
repositionHome — Drop Back, Then Look Down-Field
Loading blocks from MakeCode…
let matchRunning = false
let goalHeadingValid = false
let lowPowerActive = false
let ownGoalHeading = 0
let goalHeading = 0
let currentPower = 0
let REPOSITION_MS = 0
function turnToHeading(target: number, tol: number) {}
function checkBorder() { return 0 }
function motorStop() {}
function lookForBall() {}
function repositionHome () {
    if (!matchRunning || !goalHeadingValid || lowPowerActive) { return }
    // 1. Face our own side.
    turnToHeading(ownGoalHeading, 15)
    // 2. Drive home, ignoring the ball on the way (it'd be on the wrong side of us).
    let t0 = input.runningTime()
    CutebotPro.pwmCruiseControl(currentPower, currentPower)
    while (input.runningTime() - t0 < REPOSITION_MS) {
        if (checkBorder() > 0) { motorStop(); break }
        basic.pause(8)
    }
    motorStop()
    // 3. Turn to look DOWN THE FIELD (toward their goal) and scan.
    turnToHeading(goalHeading, 15)
    lookForBall()
}
        

When scans keep coming up empty, the bot stops loitering. It returns to its own end, then turns to face the opponent's goal — where the ball usually is — and scans from there.

1
Ignores the ball on the way home. Grabbing it mid-retreat would leave the bot between the ball and its own net — a kick that direction is an own goal.
2
Skipped on low power. A tired bot plays where it stands instead of crawling across the field.
3
Uses the compass (turnToHeading) to face its own goal, then the opponent's. Needs the goal-heading calibration to work.
readTrackbit + borderSideFrom — Read the Line Sensors
Loading blocks from MakeCode…
let tb1 = 0
let tb2 = 0
let tb3 = 0
let tb4 = 0
function readTrackbit () {
    CutebotPro.trackbitStateValue()
    tb1 = CutebotPro.trackbitgetGray(TrackbitChannel.One)
    tb2 = CutebotPro.trackbitgetGray(TrackbitChannel.Two)
    tb3 = CutebotPro.trackbitgetGray(TrackbitChannel.Three)
    tb4 = CutebotPro.trackbitgetGray(TrackbitChannel.Four)
}
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
}
        

readTrackbit() samples all four line sensors once into shared temps; borderSideFrom(threshold) reads those temps and reports which side hit the border. Splitting them means the engine takes one I2C read and reuses it for both the hard and soft thresholds.

1
1 = left-front (ch1) → turn right. 2 = right-front (ch4) → turn left.
2
3 = center (ch2/ch3) → head-on; 4 = both outer → square-on. Both trigger a bigger backup + turn in handleBorder.
3
0 = all clear. The BORDER_THRESHOLD is set at calibration (white floor vs. black tape) and adapts slightly as the floor reading drifts.
ℹ️
If the bot ever drives off the field, it's almost always the border threshold, not the recovery. Re-run the W/B calibration and watch that the threshold lands clearly between the floor (F) and tape (T) values shown on screen.
handleBorder — Brake, Back Up, Turn Away
Loading blocks from MakeCode…
let matchRunning = false
let lowPowerActive = false
let SPEED = 0
let borderSide = 0
let backupPower = 0
let backupTime = 0
let lastTurnWasLeft = false
function hardStop() {}
function turnAwayFrom(side: number, angle: number) {}
function checkBorder() { return 0 }
function lookForBall() {}
function handleBorder (depth: number = 0, scanAfter: boolean = true) {
    if (!matchRunning) { return }
    hardStop()
    let 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)
        let angle = (side == 1 || side == 2) ? 115 : 130
        turnAwayFrom(side, angle)
        CutebotPro.pwmCruiseControl(currentPower, currentPower)
        basic.pause(300)
        CutebotPro.stopImmediately(CutebotProMotors.ALL)
        if (checkBorder() == 0) { break }
    }
    if (scanAfter) { lookForBall() }
}
        

Called whenever a border is detected. Active-brakes, backs up (scaled by speed), turns away from the triggered side, nudges forward, and re-checks — up to three times until clear. On low power the moves shrink so the bot can't reverse off the opposite edge.

1
hardStop() kills momentum, then it pins its rough field position from the compass heading at the wall (used by the own-goal cone).
2
turnAwayFrom(side, angle) — sides 1/2 turn deterministically; head-on (3/4) alternates left/right so it doesn't re-hit the same corner.
3
After clearing, it runs a scan to re-find the ball. A separate jam recovery (unstick) handles wheels that spin without moving — a randomized reverse + big turn so two locked bots break apart.

Every variable and what it does

Zone A — You Set These

SPEED
1–17
Starting motor speedmotorPower = SPEED × 100 / 20. Decays over the match via DECAY_RATE.
ENDURANCE
1–17
Decay resistanceDECAY_RATE = 20 − ENDURANCE. Higher = slower power loss. Does not change the 180-second match length.
TURNING
1–17
Arc + aim sharpnessSets turnRatio, segment length, scan cadence, and how many aim attempts before giving up on a side ball.
POWER
1–10
Kick force and follow-throughchargePower = min(POWER², 100). FOLLOW_THROUGH_MS = 120 + POWER × 35. Capped at 10 — the square already hits 100 there.

Auto-Computed — Don't Modify

motorPower / currentPower
Auto
Cruise speedmotorPower is the starting value; currentPower is it after endurance decay, floored at minWheelSpeed.
enduranceFactor
Auto
Power remaining (0.2–1.0)endurancePercent / 100, floored at 0.2. Scales currentPower and the charge.
minWheelSpeed / lowPowerActive
Auto
Power floor + tired flagmax(20, round(motorPower × 0.25)). When currentPower hits the floor, lowPowerActive turns on (the "red" conservative mode).
ballDetected / ballHeading
Auto
Ball stateballHeading: −1 left, 0 centered, 1 right, 99 found-while-driving. Drives the aim step.
goalHeading / ownGoalHeading
Auto
Compass headingsSet at calibration (G). Used for the goal-seeking dribble, goal-biased roaming, repositioning, and own-goal avoidance.
roamCounter / lostScans
Auto
Search countersroamCounter triggers a scan; lostScans triggers the drop-back when scans keep failing.
launchActive
Auto
Border-pause flagtrue during your launch and the kick. While true, border checks return 0 so the scripted move and the goal-line push aren't aborted.
matchRunning
Auto
Match active flagEvery loop checks it. A+B sets it false to stop everything.

System Constants — Tune With Care

BALL_DETECT_CM
40
Commit-to-charge rangeBall confirmed within 40 cm → stop and aim.
BALL_SEE_CM
45
Home-in range while drivingA ball glimpsed within 45 cm → drive straight at it.
BALL_SCAN_CM
50
Stationary scan rangeHow far the step-scan looks. Shorter = fewer false hits on walls / people / the opponent.
CONTACT_CM
15
Dribble-curve thresholdThe goal-seeking curve only kicks in once the ball is this close (in contact).
BORDER_THRESHOLD
calibrated
White-line cutoffSet at calibration, 55% of the way from floor to tape. Sensor reading above it = border.
BASE_SEGMENT_MS
300
Arc cycle base time× 12 = totalArcTime (3600ms), ÷ TURNING = each roam segment's length (capped at 600ms).
GOAL_AIM_TOL
18
"Aimed enough" windowWithin 18° of the goal heading, the dribble pushes straight instead of curving.
REPOSITION_AFTER_SCANS
3
Drop-back triggerEmpty scans in a row before the bot retreats to its own side and looks down-field.

Getting more out of the match code

01

Calibrate every time

Run the W/B border calibration and the G goal-heading step on the actual field each match. The bot drives off the field if the threshold is wrong, and dribbles the wrong way if the goal heading is off.

02

ENDURANCE ≠ match length

All matches run 180 seconds. ENDURANCE controls how fast your speed drops. ENDURANCE 4 is down to ~28% by minute 3; ENDURANCE 17 barely fades. High SPEED only helps if you can still use it late.

03

POWER squares, then caps

chargePower = POWER². POWER 8 → 64, POWER 10 → 100. There's no point going above 10. Spend extra points on the linear stats, which run to 17.

04

Launch is geometry

Finish your launch behind the ball, pointed at the goal. Use runBlockCnt for exact distances and a Pythagorean diagonal to cut the corner. Test it before a real match.

05

Verify the turn direction

The dribble and drop-back use the compass. If your bot ever curves toward the wrong net, swap the two branches in turnToHeading (and the dribble) for your chassis — test it once standing still.

06

Total must equal 20

Power 1–10, the rest 1–17, all four summing to exactly 20. Try builds in the simulator first — a Sniper (high POWER) plays very differently from a Speedster or a Tank.