VELOCITY ARENA FACILITATOR · LAUNCH SEQUENCES

REFFacilitator resource

Launch Sequences.

A launch sequence runs once at the start of a match, before the match loop takes over. It moves the bot from its corner to the center of the field so it can begin hunting the ball. The reference sequence below starts in the bot's home corner, drives diagonally toward the center line, turns to face up the field, and drives straight to the ball — then hands off to lookForBall().

The four steps 04 STEPS
Step 01
Turn left from the corner
Pivot 45° in place to point the bot diagonally toward the center line. Brief pause to settle the wheels before driving.
Step 02
Drive diagonally toward the center line
Cruise at the team's Power setting for ~2.2 s, scaled by powerScale. Higher Power → shorter time, same distance.
Step 03
Turn right to face up the center
Pivot 45° back the other way so the bot is now pointing up the field toward the ball zone.
Step 04
Drive straight to the ball
Final ~1.2 s cruise (Power-scaled) to reach the ball zone. Border detection re-enables and lookForBall() takes over.
Why powerScale? The pause durations are tuned for a reference motor speed of 30. When a team's allocation produces a different motorPower, the bot would travel farther or shorter than intended. Computing 30 / Math.max(1, motorPower) scales each pause inversely — faster motor → shorter pause, same distance. The Math.max(1, …) guards against divide-by-zero when a stat translates to a zero motor value.
The MakeCode REFERENCE

Paste this into a MakeCode JavaScript project alongside the match-loop globals (launchActive, motorPower, currentPower, resetRequested, ballDetected, matchRunning) and the helpers (checkBorder, handleBorder, lookForBall). Call launchSequence() once when the match starts.

launch-kick-test.ts JavaScript / MakeCode
/**
 * ============================================================
 * LAUNCH + KICK TEST  (left-turn-first, your stats)
 * ------------------------------------------------------------
 * Place bot in its corner facing the goal, ball ahead & LEFT.
 * Press A  -> run launchSequence + kick
 * Press B  -> emergency stop
 * Yes icon -> ball found & kicked    No icon -> ball not reached
 * ============================================================
 */

// ── Your stats ─────────────────────────────────────────────
let SPEED = 0
let POWER = 0
let TURNING = 0
let ENDURANCE = 0

// ── Launch tunables (plug in your tuned diagonal value) ─────
let LAUNCH_OFFSET_TURN_DEG = 35
let LAUNCH_OFFSET_DRIVE_MS = 2500   // ← set this to your tuned diagonal value
let LAUNCH_SQUARE_TURN_DEG = 35     // keep equal to OFFSET_TURN_DEG
let LAUNCH_APPROACH_MS = 600    // max forward time to reach the ball

// ── Field constants ────────────────────────────────────────
let BORDER_THRESHOLD = 130
let BALL_DETECT_CM = 40

// ── State ──────────────────────────────────────────────────
let motorPower = 0
let currentPower = 0
let chargePower = 0
let FOLLOW_THROUGH_MS = 0
let minWheelSpeed = 0
let enduranceFactor = 0
let effectiveCharge = 0
let dynamicCharge = 0
let actualFollowThrough = 0
let launchActive = false
let matchRunning = false
let ballDetected = false
let strip: neopixel.Strip = null

// ============================================================
// HELPERS
// ============================================================
function motorStop() {
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
    basic.pause(10)
    CutebotPro.pwmCruiseControl(0, 0)
    basic.pause(10)
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
}

function computeStats() {
    motorPower = SPEED * 100 / 20
    currentPower = motorPower
    chargePower = Math.min(POWER * POWER, 100)
    FOLLOW_THROUGH_MS = 120 + POWER * 35
    minWheelSpeed = Math.max(20, Math.round(motorPower * 0.25))
}

function flashHeadlights() {
    CutebotPro.singleHeadlights(CutebotProRGBLight.RGBA, 255, 255, 255)
    basic.pause(30)
    CutebotPro.singleHeadlights(CutebotProRGBLight.RGBA, 0, 0, 0)
}

// Simple border check (any sensor over threshold). Disabled during launch shimmy/kick.
function checkBorder(): number {
    if (launchActive) { return 0 }
    CutebotPro.trackbitStateValue()
    let s1 = CutebotPro.trackbitgetGray(TrackbitChannel.One)
    let s2 = CutebotPro.trackbitgetGray(TrackbitChannel.Two)
    let s3 = CutebotPro.trackbitgetGray(TrackbitChannel.Three)
    let s4 = CutebotPro.trackbitgetGray(TrackbitChannel.Four)
    if (s1 > BORDER_THRESHOLD || s2 > BORDER_THRESHOLD ||
        s3 > BORDER_THRESHOLD || s4 > BORDER_THRESHOLD) { return 1 }
    return 0
}

function confirmBall(): boolean {
    motorStop()
    basic.pause(40)
    let hits = 0
    for (let i = 0; i < 3; i++) {
        if (i > 0) { basic.pause(30) }
        let dist = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        if (dist > 3 && dist < BALL_DETECT_CM) { hits += 1 }
    }
    if (hits >= 2) { flashHeadlights(); return true }
    return false
}

// Drive forward until the ball is confirmed ahead, or maxMs elapses.
function driveUntilBall(maxMs: number): boolean {
    let t0 = input.runningTime()
    CutebotPro.pwmCruiseControl(currentPower, currentPower)
    while (input.runningTime() - t0 < maxMs) {
        if (!matchRunning) { return false }
        if (checkBorder() > 0) { motorStop(); return false }
        let d = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        if (d > 4 && d < BALL_DETECT_CM) {
            motorStop()
            if (confirmBall()) { return true }
            CutebotPro.pwmCruiseControl(currentPower, currentPower)   // false alarm, resume
        }
        basic.pause(5)
    }
    motorStop()
    return false
}

// ============================================================
// CHARGE + FOLLOW THROUGH  (both fixes applied)
// ============================================================
function chargeAndFollowThrough() {
    flashHeadlights()
    strip.showColor(neopixel.colors(NeoPixelColors.Yellow))

    // Fix #2: guard against enduranceFactor = 0 so the kick fires full power.
    let factor = enduranceFactor > 0 ? enduranceFactor : 1
    effectiveCharge = Math.min(Math.max(Math.round(chargePower * factor), 25), 100)
    dynamicCharge = Math.max(effectiveCharge, 30)
    actualFollowThrough = Math.min(FOLLOW_THROUGH_MS, 500)

    while (ballDetected) {
        if (!matchRunning) { motorStop(); return }
        if (checkBorder() > 0) { motorStop(); ballDetected = false; return }

        CutebotPro.pwmCruiseControl(dynamicCharge, dynamicCharge)
        let chargeStart = input.runningTime()
        while (input.runningTime() - chargeStart < actualFollowThrough) {
            if (!matchRunning) { motorStop(); return }
            if (checkBorder() > 0) { motorStop(); ballDetected = false; return }
            basic.pause(5)
        }

        let f = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        if (f > 5 && f < BALL_DETECT_CM) {
            ballDetected = true            // still in contact — keep driving through
        } else {
            // Fix #1: COMMIT a follow-through drive after the ball leaves the
            // sensor, instead of braking the instant contact is lost.
            let ftStart = input.runningTime()
            CutebotPro.pwmCruiseControl(dynamicCharge, dynamicCharge)
            while (input.runningTime() - ftStart < actualFollowThrough) {
                if (!matchRunning) { motorStop(); return }
                if (checkBorder() > 0) { motorStop(); ballDetected = false; return }
                basic.pause(5)
            }
            motorStop()
            ballDetected = false
        }
    }
}

// ============================================================
// LAUNCH SEQUENCE  (left turn first, dead reckoned)
// ============================================================
function launchSequence() {
    matchRunning = true
    ballDetected = false
    enduranceFactor = 1
    launchActive = true
    let powerScale = 30 / Math.max(1, motorPower)

    // Step 1: angle LEFT toward the center line
    CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, LAUNCH_OFFSET_TURN_DEG)
    basic.pause(300); motorStop(); basic.pause(120)

    // Step 2: diagonal onto the ball→goal axis
    CutebotPro.pwmCruiseControl(currentPower, currentPower)
    basic.pause(Math.round(LAUNCH_OFFSET_DRIVE_MS * powerScale))
    motorStop(); basic.pause(120)

    // Step 3: re-square RIGHT to face straight down-field
    CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, LAUNCH_SQUARE_TURN_DEG)
    basic.pause(300); motorStop(); basic.pause(120)

    launchActive = false
    basic.pause(30)
    if (!matchRunning) { motorStop(); return }

    // Step 4: roll forward until the ball is captured
    if (driveUntilBall(Math.round(LAUNCH_APPROACH_MS * powerScale))) {
        ballDetected = true
        // Step 5: kick straight through into the goal
        if (matchRunning && confirmBall()) {
            launchActive = true            // ignore goal-line border during the kick
            chargeAndFollowThrough()
            launchActive = false
        }
        basic.showIcon(IconNames.Yes)      // reached & kicked
    } else {
        basic.showIcon(IconNames.No)       // never reached the ball
    }

    motorStop()
    matchRunning = false
    basic.pause(800)
    basic.clearScreen()
}

// ============================================================
// BUTTONS
// ============================================================
input.onButtonPressed(Button.A, function () {
    computeStats()
    basic.showIcon(IconNames.Target)
    basic.pause(500)
    basic.clearScreen()
    launchSequence()
})

input.onButtonPressed(Button.B, function () {
    matchRunning = false
    ballDetected = false
    motorStop()
    basic.showIcon(IconNames.No)
    basic.pause(400)
    basic.clearScreen()
})

// ============================================================
// INIT
// ============================================================
strip = neopixel.create(DigitalPin.P15, 8, NeoPixelMode.RGB)
strip.setBrightness(255)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
CutebotPro.pwmCruiseControl(0, 0)
basic.pause(100)
basic.showIcon(IconNames.SmallSquare)
Tuning notes FOR FACILITATORS

When teams change their stat allocation, the launch sequence should still land the bot in roughly the same spot. All the tunables live as named constants at the top of the file — change them there, not buried in the function body. Common issues: