This program tests how POWER and ENDURANCE shape your bot's kick. Place a ball about 40 cm in front, press A, and the bot scans, charges forward, strikes the ball, pauses briefly, follows through, and stops. Flash with POWER = 4, measure how far the ball rolls. Reflash with POWER = 8 and measure again. The ratio of ball travel is your evidence for how the chargePower = POWER² formula behaves with a bare bumper.
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.
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.
STOP_PAUSE_MS > 0BALL_DETECT_CM (50 cm) · show square · pause 300 msdynamicCharge until distance ≤ CONTACT_CM (8 cm) · show CKICK_MS (250 ms) — this is what actually hits the ballSTOP_PAUSE_MS > 0 · stop · show K · pause (visual diagnostic only)dynamicCharge for actualFollowThrough ms · show FCHARGE_FLOOR)CHARGE_CHECK_MS (15 ms)CHARGE_MAX_MS safety timeout)120 + (POWER × 35) ms, capped at 500 msOK on the display
SPEED = 6
TURNING = 6
ENDURANCE = 4
POWER = 4
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.
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()
})
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.
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()
})
currentPower and effectiveCharge significantly. Press before arming — it has no effect mid-charge.
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
}
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.
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)
}
}
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.
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()
}
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.
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.
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.
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.
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.
CHARGE_CHECK_MS).dist > 0 && dist ≤ CONTACT_CM (8 cm), exit the charge loop immediately. No second read required.KICK_MS (250 ms). This is the strike — without it, the bot would stop at the ball instead of hitting it.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.chargePower as the PWM value during both the charge and the follow-through (no separate cruise speed).actualFollowThrough = min(FOLLOW_THROUGH_MS, 500)) so the high end doesn't run away.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.
computeStats only uses POWER and ENDURANCE. Changing SPEED or TURNING in the on-start block will have no effect on this test.| Match Time | Endurance % | Effective Charge (min 15%) |
|---|
POWER = 4 first. Leave SPEED and TURNING as they are — they're declared for budget consistency but the program doesn't read them.
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.
C → strike → F → OK. Measure how far the ball rolled from where the bot first touched it.
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×)?
enduranceFactor drops). At 180 s with low ENDURANCE, the floor (20%) takes over and the kick stops scaling with POWER entirely.
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.