powerScale. Higher Power → shorter time, same distance.lookForBall() takes over.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.
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 (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)
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:
- Turn-angle drift. The
LAUNCH_OFFSET_TURN_DEG = 35andLAUNCH_SQUARE_TURN_DEG = 35pivots are calibrated for a reference Turning value. Low Turning may under-rotate; high Turning may over-rotate. Keep the two angles equal — the second turn cancels the first, so the bot ends up squared to the goal. - Diagonal drive time.
LAUNCH_OFFSET_DRIVE_MS = 2500is the team's tuned diagonal — measure on the field, adjust once.powerScale = 30 / Math.max(1, motorPower)rescales it automatically across stat allocations, so you tune the base number once and every Speed value gets the same distance. - Ball-approach timeout.
LAUNCH_APPROACH_MS = 600is the max time the bot rolls forward looking for the ball after the diagonal. If the bot keeps timing out before reaching the ball, raise the number; if it drives past, lower it. - Border detection during the kick. The kick re-arms
launchActive = trueinside theif (matchRunning && confirmBall())block socheckBorder()ignores the goal-line sensors. Without this, the bot reads the goal line as a border and brakes mid-kick — the ball never crosses. - Endurance-factor guard.
enduranceFactor > 0 ? enduranceFactor : 1inchargeAndFollowThrough()protects against multiplying charge power by zero when the endurance system hasn't initialized — keeps the kick at full power during testing. - Follow-through commit. When the ball leaves the ultrasonic sensor mid-kick, the code commits a second drive at
dynamicChargeforFOLLOW_THROUGH_MSinstead of braking. This is what pushes the ball through the goal instead of stopping the instant contact is lost.