Button A

Scan, Charge, Strike, Follow Through

Computes chargePower from POWER² and waits for a ball within BALL_DETECT_CM (50 cm). Once it sees the ball, pauses 300 ms (so you can step back), then fires the full kick sequence.

Button B

Cycle match time

Steps through 0 → 60 → 120 → 180 seconds of simulated match time. Higher values reduce enduranceFactor, which then shrinks effectiveCharge on the next press of A.

Button A + B together · Emergency stop. Cuts all motors immediately and shows an X on the display.
·
Single dot
Scanning — waiting for ball to enter range
○
Hollow square
Ball detected — 300 ms pause before charging
C
C
Charging — driving forward until contact
K
K
Kicked — visible only when STOP_PAUSE_MS > 0
F
F
Follow-through — extra push after the strike
OK
OK
Sequence complete — ready to run again
🔘
Press A
Compute stats from current allocation
→
🔍
Scan
Poll ultrasonic every 50 ms · show dot
→
⚽
Ball in
Distance < BALL_DETECT_CM (50 cm) · show square · pause 300 ms
→
⚡
Charge
Drive at dynamicCharge until distance ≤ CONTACT_CM (8 cm) · show C
→
💥
Strike
Keep driving for KICK_MS (250 ms) — this is what actually hits the ball
→
⏸️
Pause
If STOP_PAUSE_MS > 0 · stop · show K · pause (visual diagnostic only)
→
🚀
Follow
Resume at dynamicCharge for actualFollowThrough ms · show F
→
✅
OK
Stop · show OK
Charge + Strike
  • Runs at dynamicCharge (chargePower × enduranceFactor, floored at CHARGE_FLOOR)
  • Polls sonar every CHARGE_CHECK_MS (15 ms)
  • Charge ends when sonar ≤ 8 cm (or after CHARGE_MAX_MS safety timeout)
  • Strike = additional 250 ms of drive through the ball (so the bot actually hits it, not just touches it)
Follow-Through Phase
  • Duration = 120 + (POWER × 35) ms, capped at 500 ms
  • Drives at the same dynamicCharge (no slow-down)
  • Ends with a hard motor stop and OK on the display
  • This is the push that does most of the work on the ball after contact
on start — Stat Allocation
Loading blocks…
SPEED = 6
TURNING = 6
ENDURANCE = 4
POWER = 4
              
The two stats that matter for this test. Only POWER drives the kick (via chargePower = POWER²) and ENDURANCE controls how much that kick decays as simulatedMatchTime grows. SPEED and TURNING are declared so the four-stat budget total stays consistent with your stat card, but neither one is read by computeStats in this program. To compare power values, flash with POWER = 4, measure the ball roll, then reflash with POWER = 8 and measure again.
on button A pressed — Arm and Charge
Loading blocks…
function computeStats() {}
function waitForBall() {}
function chargeAndFollowThrough() {}
input.onButtonPressed(Button.A, function () {
    computeStats()
    waitForBall()
    basic.pause(300)
    chargeAndFollowThrough()
})

input.onButtonPressed(Button.AB, function () {
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
    basic.showString("X")
    basic.pause(400)
    basic.clearScreen()
})
              
Press A to arm and kick. Three steps run in order: computeStats() converts the stats into working motor values, waitForBall() holds the bot in scan mode until something enters range, and after a 300 ms pause (so you can step back), chargeAndFollowThrough() fires the full charge → strike → follow-through sequence. Press A + B together at any time for an emergency stop — it cuts the motors immediately and shows an X.
on button B pressed — Cycle Match Time
Loading blocks…
let simulatedMatchTime = 0
input.onButtonPressed(Button.B, function () {
    if (simulatedMatchTime == 0) {
        simulatedMatchTime = 60
        basic.showString("60s")
    } else if (simulatedMatchTime == 60) {
        simulatedMatchTime = 120
        basic.showString("120s")
    } else if (simulatedMatchTime == 120) {
        simulatedMatchTime = 180
        basic.showString("180s")
    } else {
        simulatedMatchTime = 0
        basic.showString("0s")
    }
    basic.pause(500)
    basic.clearScreen()
})
              
Simulates how tired the bot is. Press B before pressing A to test what a late-match charge looks like. At 180 s, high decay rates reduce both currentPower and effectiveCharge significantly. Press before arming — it has no effect mid-charge.
computeStats — Turning Stats into Motor Values
Loading blocks…
let ENDURANCE = 0
let POWER = 0
let simulatedMatchTime = 0
let chargePower = 0
let FOLLOW_THROUGH_MS = 0
let DECAY_RATE = 0
let endurancePercent = 0
let enduranceFactor = 0
function computeStats () {
    chargePower = Math.min(POWER * POWER, 100)
    FOLLOW_THROUGH_MS = 120 + POWER * 35
    DECAY_RATE = 20 - ENDURANCE
    endurancePercent = 100 - DECAY_RATE * simulatedMatchTime / 40
    endurancePercent = Math.max(endurancePercent, 20)
    enduranceFactor = endurancePercent / 100
}
              
Three values come out of this function, all driven by POWER and ENDURANCE. chargePower is POWER × POWER, capped at 100 — so P = 4 produces 16, P = 8 produces 64, P = 10 produces 100. FOLLOW_THROUGH_MS is 120 + (POWER × 35) — a linear-with-intercept formula that gives every kick a 120 ms minimum push regardless of POWER. The endurance block computes enduranceFactor from simulatedMatchTime (set by Button B); at 180 s the factor can drop to 0.2 (the 20% floor). chargeAndFollowThrough multiplies chargePower by enduranceFactor when it runs, so a tired bot kicks weaker.
waitForBall — Scan Until Something Enters Range
Loading blocks…
let dist = 0
let BALL_DETECT_CM = 0
function waitForBall () {
    basic.showLeds(`
        . . . . .
        . . . . .
        . . # . .
        . . . . .
        . . . . .
        `)
    while (true) {
        dist = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        if (dist > 0 && dist < BALL_DETECT_CM) {
            basic.showLeds(`
                . . . . .
                . # # # .
                # . . . #
                # . . . #
                . # # # .
                `)
            basic.pause(250)
            basic.clearScreen()
            return
        }
        basic.pause(50)
    }
}
              
Polls the ultrasonic sensor every 50 ms. The dot on the LED means it's scanning. When something enters range (dist < BALL_DETECT_CM, default 50 cm), it shows the hollow square pattern, pauses 250 ms (so the student sees "detected"), clears the screen, and returns. Button A then waits another 300 ms before firing the charge — that combined ~550 ms gap is meant to give the student time to step back. The dist > 0 check filters out the noise reads that come back as 0 when the sensor can't get a clean ping.
chargeAndFollowThrough — The Full Charge Sequence
Loading blocks…
let chargePower = 0
let enduranceFactor = 1
let effectiveCharge = 0
let dynamicCharge = 0
let actualFollowThrough = 0
let FOLLOW_THROUGH_MS = 0
let chargeStart = 0
let dist = 0
let CHARGE_FLOOR = 0
let CHARGE_MAX_MS = 0
let CHARGE_CHECK_MS = 0
let CONTACT_CM = 0
let KICK_MS = 0
let STOP_PAUSE_MS = 0
function chargeAndFollowThrough () {
    let factor = enduranceFactor > 0 ? enduranceFactor : 1
    effectiveCharge = Math.max(Math.round(chargePower * factor), CHARGE_FLOOR)
    effectiveCharge = Math.min(effectiveCharge, 100)
    dynamicCharge = Math.max(effectiveCharge, CHARGE_FLOOR)
    actualFollowThrough = Math.min(FOLLOW_THROUGH_MS, 500)

    // PHASE 1 — CHARGE: drive forward until we reach the ball.
    basic.showString("C")
    CutebotPro.pwmCruiseControl(dynamicCharge, dynamicCharge)
    chargeStart = input.runningTime()
    while (input.runningTime() - chargeStart < CHARGE_MAX_MS) {
        dist = CutebotPro.ultrasonic(SonarUnit.Centimeters)
        if (dist > 0 && dist <= CONTACT_CM) { break; }
        basic.pause(CHARGE_CHECK_MS)
    }

    // STRIKE: keep driving through contact so the bot actually HITS the ball.
    basic.pause(KICK_MS)

    // Optional visible STOP — happens AFTER the ball is struck.
    if (STOP_PAUSE_MS > 0) {
        CutebotPro.stopImmediately(CutebotProMotors.ALL)
        basic.showString("K")
        basic.pause(STOP_PAUSE_MS)
    }

    // PHASE 2 — FOLLOW-THROUGH: a further push for the POWER-based window.
    basic.showString("F")
    CutebotPro.pwmCruiseControl(dynamicCharge, dynamicCharge)
    basic.pause(actualFollowThrough)

    // STOP — done.
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
    basic.showString("OK")
    basic.pause(500)
    basic.clearScreen()
}
              
The whole kick is one function with four beats: charge, strike, optional pause, follow-through. The first lines compute dynamicCharge — the actual PWM value sent to the motors — by multiplying chargePower by enduranceFactor and clamping the result between CHARGE_FLOOR (20) and 100. actualFollowThrough caps the push duration at 500 ms so no kick runs longer than that no matter how high POWER goes.

Phase 1 — Charge: drive forward at dynamicCharge while the sonar keeps watch. As soon as the reading drops to CONTACT_CM (8 cm) the loop exits. A CHARGE_MAX_MS timeout (1.2 s) catches the case where the ball rolled away before the charge started.

Strike: the bot keeps driving for another KICK_MS (250 ms) after the loop exits. Without this beat, the bot would stop just short of the ball and only the follow-through would touch it — defeating the whole purpose of testing impact. The strike is what actually hits.

Pause (optional): if STOP_PAUSE_MS > 0, the bot stops, shows K, and pauses. This is a visual diagnostic — useful when teaching the sequence in class. Set STOP_PAUSE_MS = 0 when measuring ball travel so the kick stays continuous and the data is clean.

Phase 2 — Follow-through: resume driving at dynamicCharge for actualFollowThrough ms. This is the push that does most of the work on the ball after impact. Higher POWER → longer follow-through (120 + POWER × 35), so the kick lasts longer and pushes the ball farther.

The bot decides it has "reached" the ball based on a single ultrasonic read against CONTACT_CM. The check runs every CHARGE_CHECK_MS (15 ms) during the charge loop. After contact, the bot keeps driving for KICK_MS (250 ms) — that's the strike — before pausing or following through. This is a deliberately simple model: no multi-read confirmation, no noise filtering. The ball is foam and the contact distance is generous enough (8 cm) that false positives are rare.

1
Inside the charge loop, poll the sonar every 15 ms (CHARGE_CHECK_MS).
2
When dist > 0 && dist ≤ CONTACT_CM (8 cm), exit the charge loop immediately. No second read required.
3
After exit, keep driving for KICK_MS (250 ms). This is the strike — without it, the bot would stop at the ball instead of hitting it.
⏱️
Safety: if the loop runs longer than CHARGE_MAX_MS (1.2 s) without seeing the ball, the charge ends anyway and the sequence continues. This catches the case where the ball was placed but rolled away before the press of A.
Charge Power
chargePower = min(POWER², 100)
POWER is squared. POWER = 4 → 16, POWER = 8 → 64, POWER = 10 → 100. Every point you add is worth more than the last one. This program uses chargePower as the PWM value during both the charge and the follow-through (no separate cruise speed).
Follow-Through Duration
FOLLOW_THROUGH_MS = 120 + POWER × 35 ms
Linear with a 120 ms intercept — so every kick has a baseline push regardless of POWER. POWER = 4 → 260 ms. POWER = 8 → 400 ms. POWER = 10 → 470 ms. Then capped at 500 ms (actualFollowThrough = min(FOLLOW_THROUGH_MS, 500)) so the high end doesn't run away.
Endurance Decay
decayRate = 20 − ENDURANCE
endurancePercent = max(100 − decayRate × time / 40, 20)
enduranceFactor = endurancePercent / 100
Lower ENDURANCE = higher decayRate = faster power loss over time. The floor is 20% — the bot never goes completely dead. Inside chargeAndFollowThrough, the actual PWM is dynamicCharge = max(chargePower × enduranceFactor, CHARGE_FLOOR) — so a tired bot still meets the 20% floor.

⚡ POWER is quadratic; the floor matters. Going from POWER = 5 to POWER = 6 adds 11 percentage points of charge power (36 − 25 = 11). Going from POWER = 9 to POWER = 10 adds 19 points (100 − 81 = 19). Every point you add is worth more than the last. But: without a scoop, the ball's response to higher chargePower has diminishing returns — the bare bumper can only transfer so much energy. The Scoop Build activity is where the POWER² investment starts to pay off proportionally.

To run the comparison cleanly: keep simulated match time at 0 s, set STOP_PAUSE_MS = 0 in the constants block, flash with POWER = 4, measure how far the ball rolls, then reflash with POWER = 8 and measure again. The ratio is your evidence for how the formula behaves on the physical bot.

⚠️
SPEED and TURNING are declared but not used. The program reads them so the four-stat total matches your stat card, but computeStats only uses POWER and ENDURANCE. Changing SPEED or TURNING in the on-start block will have no effect on this test.
Stat Allocation Budget: 20 / 20
SPEED 8
Not used by this test — declared for budget consistency only
ENDURANCE 7
Decay rate = 20 − ENDURANCE (0 = no decay)
POWER 5
Charge power = POWER² (capped at 100%) · Follow-through = 120 + (POWER × 35) ms
Simulated match time:
Cruise Power
—
% (after endurance)
Charge Power
—
% (after endurance)
Follow-Through
—
ms
Endurance Left
—
% of base power
Base Charge
—
% (POWER², pre-decay)
Decay Rate
—
pts / 40 s
Effective Charge Power at Each Match Time
Match Time Endurance % Effective Charge (min 15%)
Constant Value Purpose
BALL_DETECT_CM 50 Max distance (cm) for ball detection during the scan phase. Ball is placed ~40 cm in front.
CONTACT_CM 8 Distance at which the bot has "reached" the ball — exits the charge loop
KICK_MS 250 Extra drive after contact — the strike. Without this the bot stops at the ball instead of hitting it.
CHARGE_MAX_MS 1200 Safety timeout — charge loop ends after 1.2 s even if no ball is detected
CHARGE_CHECK_MS 15 Polling interval during the charge loop
CHARGE_FLOOR 20 Minimum PWM for the kick. Lower = bigger P4 vs P8 spread, but too low and the motors stall.
STOP_PAUSE_MS 300 Pause between strike and follow-through. Set to 0 for a continuous kick when measuring ball travel; non-zero is a visual diagnostic for class.
Follow-through cap 500 ms actualFollowThrough = min(FOLLOW_THROUGH_MS, 500) — high-POWER kicks don't run away
Endurance floor 20% endurancePercent never drops below 20% regardless of time or ENDURANCE value
Post-detect pause 300 ms Delay between ball detection and start of charge — student step-back time
  1. 1 Set POWER and ENDURANCE in the code. Edit the on-start block. Flash with POWER = 4 first. Leave SPEED and TURNING as they are — they're declared for budget consistency but the program doesn't read them.
  2. 2 For clean measurement: set STOP_PAUSE_MS = 0. The 300 ms default is for teaching the sequence visually in class. When measuring how far the ball rolls, you want the strike and follow-through to be continuous.
  3. 3 Place the ball ~40 cm in front of the bot. Leave clear space ahead for the charge + strike + follow-through. Tape a start line and a meter stick on the floor for ball-roll measurements.
  4. 4 Press A. The scanning dot appears. When the bot sees the ball, it shows the square, pauses 300 ms (your cue to step back), then runs: C → strike → F → OK. Measure how far the ball rolled from where the bot first touched it.
  5. 5 Reflash with POWER = 8, repeat. Run three trials at each POWER value to average out noise. Compare ball roll: how does the ratio of distances compare to the ratio of chargePower values (16 vs 64, or 4×)?
  6. B Optional — endurance lesson. Press B before A to cycle simulated match time to 60, 120, or 180 s. The same POWER value will kick weaker as time grows (because enduranceFactor drops). At 180 s with low ENDURANCE, the floor (20%) takes over and the kick stops scaling with POWER entirely.
⛔
The bot charges on detection. Once you press A and any object enters BALL_DETECT_CM (50 cm) range, the bot drives forward at full charge power and keeps driving for the strike + follow-through. Keep hands and feet clear of its path. Use Button A + B together for emergency stop.