Navigate · Strike · Chain · Advanced Earn skill badges 0 / 6 earned
Before you start
Setting up in MakeCode
Setup steps
  1. Set up the field. Lay the Velocity Arena mat on a flat, hard surface. You'll need: your Cutebot Pro, a micro:bit, the squeaky ball, and the rubber ball. Keep both on the field — the rubber ball is used in Challenge 3 and will play a bigger role in Season 2.
  2. Open MakeCode. Go to makecode.microbit.org in a browser. Click New Project and name it SkillsArena.
  3. Add the Cutebot Pro extension. Click the Extensions tile in the block toolbox (or gear ⚙ → Extensions). Type cutebot in the search bar and click the Cutebot Pro card to add it. You only need to do this once per project. After adding it, a Cutebot Pro category will appear in the toolbox.
  4. Switch to JavaScript. Click the JavaScript slider in the top toolbar (next to the Blocks label). This switches the editor to text mode.
  5. Delete the default code. Click inside the editor, press Ctrl+A (or ⌘A on Mac) to select all, then press Delete.
  6. Paste the starter code. Click the Copy Template button below — it copies the full starter code to your clipboard in one click. Then paste into MakeCode with Ctrl+V (or ⌘V on Mac). All four stats will be 0 — that is expected. You'll fill them in during the next step.
  7. Enter your team stats. Near the top of the code, find the STUDENT ZONE A block. Replace each 0 with your team's real values. SPEED + POWER + ENDURANCE + TURNING must equal exactly 20.
  8. Flash to the micro:bit. Click Download and follow the prompts to copy the file to your micro:bit. The LED will scroll your motorPower and chargePower, then show 1 — you are ready. Re-flash every time you change the code. Stats only take effect after a fresh flash.
How the program works
Buttons
B Cycle through challenges 1 → 2 → 3 → 4 → 5 → 6 → 1…
The number appears on the LED so you know which one is selected.
A Run the selected challenge function.
An arrow flashes, the code runs, then a ✓ appears when it finishes.
A + B Emergency stop — motors cut immediately and an X appears on the LED.
Use this any time the bot goes off course.
Startup LED sequence
On every flash, the bot shows: P [motorPower] → K [chargePower] → 1
This confirms your stats loaded correctly. P is drive power, K is kick force.
If you see P0, go back and fill in your stats in Zone A.
Your job — Zone B helper functions
drive(n) Move straight forward exactly n grid squares. 1 square ≈ 27 cm.
turnRight() Rotate 90° clockwise. Facing toward y=0 → now facing toward x=8.
turnLeft() Rotate 90° counterclockwise. Facing toward y=0 → now facing toward x=0.
kick() Fire a power burst using your Power stat. Force = POWER². Then stops.

Find the empty function for your challenge in STUDENT ZONE B and fill it in using these four helpers. Flash after every change and press A to test.

Starter Code — skillsCode.ts
Paste this entire block into the MakeCode JavaScript editor. Then replace the 0s with your team stats in STUDENT ZONE A.
/**
 * ═══════════════════════════════════════════════════════════════
 *  VELOCITY ARENA — Skills Arena Base Program
 *  micro:bit + Cutebot Pro   |   8×4 ft field   |   8×4 grid
 * ═══════════════════════════════════════════════════════════════
 *
 *  HOW TO USE
 *  ───────────
 *  1. Enter your team stats in STUDENT ZONE A below.
 *     S + P + E + T must add up to exactly 20.
 *
 *  2. Write your code inside the challenge function you are
 *     working on in STUDENT ZONE B below.
 *
 *  3. Flash the program to your micro:bit.
 *
 *  4. Press B to select a challenge (the number shows on screen).
 *     Press A to run it.
 *     Press A+B at any time for an emergency stop.
 *
 *  HELPER FUNCTIONS (call these inside your challenge functions)
 *  ─────────────────────────────────────────────────────────────
 *  drive(n)       move straight forward exactly n grid squares
 *                 1 grid square = 10.75 in = ~27 cm = 1 block
 *
 *  turnRight()    rotate exactly 90° clockwise in place
 *  turnLeft()     rotate exactly 90° counter-clockwise in place
 *
 *  kick()         burst of power from your Power stat, then stop
 *                 force = chargePower = POWER²  (0–100)
 *
 *  FIELD COORDINATE SYSTEM
 *  ────────────────────────
 *  x: 0 (left wall) → 8 (right wall)       8 columns
 *  y: 0 (top edge)  → 4 (bottom edge)      4 rows
 *  Labels on mat:  x = 1–7,  y = 1–3  (interior grid lines)
 *  Left  goal zone: x = 0–1, y = 1–3
 *  Right goal zone: x = 7–8, y = 1–3
 *  Ball at center:  (4, 2)
 * ═══════════════════════════════════════════════════════════════
 */


// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//  ★  STUDENT ZONE A  ★   ENTER YOUR TEAM STATS HERE
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//
//  RULES:  each stat ≥ 1  |  POWER ≤ 10  |  S + P + E + T = 20
//
//  SPEED      how fast the bot drives
//  POWER      how hard it kicks  (chargePower = POWER²)
//  ENDURANCE  how long it stays fast in a match
//  TURNING    how sharply it steers in a match
//
let SPEED     = 0   // ← YOUR VALUE HERE
let POWER     = 0   // ← YOUR VALUE HERE
let ENDURANCE = 0   // ← YOUR VALUE HERE
let TURNING   = 0   // ← YOUR VALUE HERE
// ── CHECK: SPEED + POWER + ENDURANCE + TURNING must equal 20 ──


// ── CALCULATED VALUES  (read-only — do not change) ─────────────
let motorPower  = SPEED * 100 / 20             // drive speed %   (0–85)
let chargePower = Math.min(POWER * POWER, 100) // kick force %    (0–100)
let alignMs     = Math.max(60, 220 - TURNING * 16) // aim time ms
let DECAY_RATE  = 20 - ENDURANCE               // endurance decay rate


// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//  DRIVE TUNING
//  Every drive() call spends ~100–150 ms overcoming static friction
//  and ramping to full speed before the bot moves a meaningful distance.
//  That startup overhead is constant — independent of how far you drive.
//  startupMs compensates for it.  unitMs controls speed per square.
//
//  Why tune startupMs FIRST?
//  drive(1) = 1 × unitMs + startupMs — startupMs dominates a short run,
//  so drive(1) isolates it cleanly.  Once startupMs is fixed, unitMs
//  only scales the per-square portion and drive(6) is easy to dial in.
//
//  Tuning order:
//    1. Run drive(1) (challenge 1, press B). Short? +20 to startupMs.
//       Overshot? −20.  Repeat until it lands on exactly 1 square.
//    2. Run drive(6). Short? +20 to unitMs.  Overshot? −20.
//       You'll land around unitMs = 590–620.
//    3. Test drive(4) and drive(2) — they should now land without re-tuning.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
let unitMs    = 600   // ← tune SECOND with drive(6) — expect 590–620
let startupMs = 100   // ← tune FIRST with drive(1)  — expect 80–140


// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//  CHALLENGE SELECTOR
//  B = next challenge (1 → 2 → … → 9 → 1 → ...)
//  A = run the selected challenge
//
//  CHALLENGE 7 — POWER CONTROL LAB has a built-in power menu:
//    Press A on #7  →  enters power-select mode (LED shows "3")
//    Press B        →  cycles: 3 → 6 → 9 → your POWER → 3 …
//    Press A        →  runs the kick at the displayed power
//    Press A+B      →  emergency stop / exit power-select mode
//
//  CHALLENGE 8 — PRECISION PARKING has two variants:
//    Press A on #8  →  runs Precision Parking (variant 1, default)
//    Press B on #8  →  switches to variant 2 (LED shows "P2")
//    Press A        →  runs Precision Parking 2
//    Press B again  →  advances to challenge 9 (resets to variant 1)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
let currentChallenge = 1
let inLabSelect = false   // true while choosing power for challenge 7
let labPowerIdx = 0       // 0=P3  1=P6  2=P9  3=your POWER
let parkVariant = 1       // challenge 8: 1=precisionParking  2=precisionParking2

// Returns the kick POWER value for the currently selected lab trial.
function labPowerValue(): number {
    if (labPowerIdx === 0) { return 3 }
    if (labPowerIdx === 1) { return 6 }
    if (labPowerIdx === 2) { return 9 }
    return POWER  // 4th option restores your actual stat
}

input.onButtonPressed(Button.B, function () {
    if (inLabSelect) {
        // Challenge 7: cycle through lab power options
        labPowerIdx = (labPowerIdx + 1) % 4
        basic.showNumber(labPowerValue())
    } else if (currentChallenge === 8 && parkVariant === 1) {
        // Challenge 8: switch to variant 2
        parkVariant = 2
        basic.showString("P2")
    } else {
        // Normal challenge cycling (also exits #8 variant 2 back to variant 1)
        if (currentChallenge === 8) { parkVariant = 1 }
        currentChallenge = (currentChallenge % 9) + 1
        basic.showNumber(currentChallenge)
    }
})

input.onButtonPressed(Button.A, function () {
    // ── Challenge 7: enter power-select mode on first press ──────
    if (currentChallenge === 7 && !inLabSelect) {
        inLabSelect = true
        basic.showNumber(labPowerValue())   // show current selection
        return
    }
    // ── Challenge 7: run at selected power on second press ───────
    if (inLabSelect) {
        inLabSelect = false
        let p = labPowerValue()
        let savedCharge = chargePower
        chargePower = Math.min(p * p, 100)  // apply POWER² to lab value
        basic.showArrow(ArrowNames.East)
        powerControlLab()
        chargePower = savedCharge           // restore team's real chargePower
        CutebotPro.stopImmediately(CutebotProMotors.ALL)
        basic.showIcon(IconNames.Yes)
        return
    }
    // ── Challenge 8: run variant 1 or 2 ─────────────────────────
    if (currentChallenge === 8) {
        basic.showArrow(ArrowNames.East)
        if (parkVariant === 2) {
            precisionParking2()
        } else {
            precisionParking()
        }
        parkVariant = 1                         // reset to default after each run
        CutebotPro.stopImmediately(CutebotProMotors.ALL)
        basic.showIcon(IconNames.Yes)
        return
    }
    // ── All other challenges ─────────────────────────────────────
    basic.showArrow(ArrowNames.East)
    if      (currentChallenge === 1) { deadStraight()    }
    else if (currentChallenge === 2) { rightAngle()      }
    else if (currentChallenge === 3) { straightShot()    }
    else if (currentChallenge === 4) { cornerKickLeft()  }
    else if (currentChallenge === 5) { cornerKickRight() }
    else if (currentChallenge === 6) { twoBallCombo()    }
    else if (currentChallenge === 9) { bossLevel()        }
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
    basic.showIcon(IconNames.Yes)
})

// A + B  →  emergency stop (works at any time; also exits power-select mode)
input.onButtonPressed(Button.AB, function () {
    inLabSelect = false
    parkVariant = 1
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
    CutebotPro.pwmCruiseControl(0, 0)
    basic.showLeds(`
        # . . . #
        . # . # .
        . . # . .
        . # . # .
        # . . . #`)
})


// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//  HELPER FUNCTIONS
//  Call these inside your challenge functions below.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

/**
 * drive(n)
 * Move straight forward for n grid squares.
 * Speed comes from your SPEED stat (motorPower).
 * Total motor-on time = unitMs × n + startupMs milliseconds.
 * Tune both values in the DRIVE TUNING section above.
 *
 * Example:  drive(3) with unitMs=500, startupMs=0  →  1500 ms
 */
function drive(units: number): void {
    CutebotPro.pwmCruiseControl(motorPower, motorPower)
    basic.pause(unitMs * units + startupMs)
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
    basic.pause(100)
}

/**
 * turnRight()
 * Rotate the bot 90° clockwise (viewed from above).
 *
 * Facing toward y=0 (-y)  →  now facing toward x=8 (+x)
 * Facing toward x=8 (+x)  →  now facing toward y=4 (+y)
 * Facing toward y=4 (+y)  →  now facing toward x=0 (-x)
 * Facing toward x=0 (-x)  →  now facing toward y=0 (-y)
 */
function turnRight(): void {
    CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, 90)
    basic.pause(150)
}

/**
 * turnLeft()
 * Rotate the bot 90° counterclockwise (viewed from above).
 *
 * Facing toward y=0 (-y)  →  now facing toward x=0 (-x)
 * Facing toward x=0 (-x)  →  now facing toward y=4 (+y)
 * Facing toward y=4 (+y)  →  now facing toward x=8 (+x)
 * Facing toward x=8 (+x)  →  now facing toward y=0 (-y)
 */
function turnLeft(): void {
    CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, 90)
    basic.pause(150)
}

/**
 * kick()
 * Fire a power burst toward whatever the bot is facing.
 * Force comes from your POWER stat:  chargePower = POWER²
 *
 * Example:  POWER = 7  →  chargePower = 49%
 *           POWER = 10 →  chargePower = 100%  (max)
 */
function kick(): void {
    CutebotPro.pwmCruiseControl(chargePower, chargePower)
    basic.pause(400)
    CutebotPro.stopImmediately(CutebotProMotors.ALL)
}


// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//  ★  STUDENT ZONE B  ★   WRITE YOUR CHALLENGE FUNCTIONS BELOW
//
//  Each function is called when you press A with that challenge
//  selected. Fill in the body using drive(), turnLeft(),
//  turnRight(), and kick().
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


// ── Challenge 1 — DEAD STRAIGHT ──────────────────────────────────
//  Start:  (1, 0)  facing right (+x direction)
//  Target: (7, 0)
//  Task:   Drive 6 grid squares in a straight line.  No turns.
//
function deadStraight(): void {
    // YOUR CODE HERE
    // Hint: call drive(?) — how many squares to the target?

}


// ── Challenge 2 — RIGHT ANGLE ─────────────────────────────────────
//  Start:  (1, 0)  facing down (+y direction)
//  Target: (6, 3)
//  Task:   Drive 3 squares → turn right 90° → drive 5 squares.
//
function rightAngle(): void {
    // YOUR CODE HERE
    // Hint: drive(?) → turnRight() → drive(?)

}


// ── Challenge 3 — STRAIGHT SHOT ──────────────────────────────────
//  Start:  (6, 2)  facing LEFT (toward x = 0)
//  Ball:   (4, 2)  — already lined up with the bot
//  Goal:   Left goal zone  (x = 0–1, y = 1–3)
//  Task:   Drive to the ball, then kick it into the left goal.
//          No turns needed — the shot is already aligned.
//
function straightShot(): void {
    // YOUR CODE HERE
    // Hint: drive(?) to reach the ball, then kick()

}


// ── Challenge 4 — CORNER KICK: LEFT TURN ──────────────────────────
//  Start:  (3, 4)  facing (3, 0)
//  Ball:   (2, 2)
//  Goal:   Left goal zone  (x = 0–1, y = 1–3)
//  Leg 1 is fixed: drive(2) — Δy = 4−2 = 2 squares.
//  Leg 2 is the variable: stop SHORT of the ball and let
//  kick() close the gap. Try drive(0.5), drive(0.7), etc.
//  Too close → overshoots past x=0. Too far → falls short of x=1.
//
function cornerKickLeft(): void {
    // YOUR CODE HERE
    // drive(2) → turnLeft() → drive(???) → kick()

}


// ── Challenge 5 — CORNER KICK: RIGHT SIDE ────────────────────────
//  Start:  (4, 0)  facing down (+y direction)
//  Ball:   (6, 2)
//  Goal:   Right goal zone  (x = 7–8, y = 1–3)
//  Task:   Navigate to the ball from an angle, then kick right.
//
function cornerKickRight(): void {
    // YOUR CODE HERE
    // Hint: drive(?) → turnRight() → drive(?) → kick()

}


// ── Challenge 6 — TWO-BALL COMBO ─────────────────────────────────
//  Start:  (4, 0)  facing south (+y direction)
//  Ball A: (2, 2)  → left  goal zone (x = 0–1, y = 1–3)
//  Ball B: (6, 2)  → right goal zone (x = 7–8, y = 1–3)
//  Task:   In one program, kick Ball A into the left goal and
//          Ball B into the right goal. No hints — plan your
//          own route and decide which ball you kick first.
//
function twoBallCombo(): void {
    // YOUR CODE HERE
    // Plan on paper first. Ball A at (2,2), Ball B at (6,2).
    // Hint: there is no single correct route — choose your order,
    // draw the path, count every square, count every turn.

}


// ── Challenge 7 — POWER CONTROL LAB ──────────────────────────────
//  Start:  (3, 2)  facing west (toward x = 0)
//  Ball:   (2, 2)  — reset before every run
//  Task:   Run the same drive(1) + kick() three times using
//          POWER = 3, POWER = 6, and POWER = 9.
//          Record ball distance each time. Is it linear?
//
function powerControlLab(): void {
    // YOUR CODE HERE
    // Hint: drive(1) to the ball, then kick()
    // Change POWER in STUDENT ZONE A and re-flash between runs.

}


// ── Challenge 8 — PRECISION PARKING ──────────────────────────────
//  Start:  (0, 0)  facing east (+x direction)
//  Target: taped box at (5, 3), final facing = WEST
//  Task:   Navigate to the box by any route. Stop inside it
//          facing exactly west. Sketch your path first —
//          mark your facing direction after every call.
//
function precisionParking(): void {
    // YOUR CODE HERE
    // No hint sequence — your path is your design.
    // Remember: to end up facing WEST you must plan the
    // approach direction and your final turn carefully.

}


// ── Challenge 8 variant 2 — PRECISION PARKING 2 ───────────────────
//  Same concept as Precision Parking — different start, target, or
//  constraint. Define your own variant here.
//
function precisionParking2(): void {
    // YOUR CODE HERE

}


// ── Challenge 9 — BOSS LEVEL ─────────────────────────────────────
//  Start:  (4, 0)  facing south (+y direction)
//  Ball:   (4, 2)  — center of field
//  Cone:   (4, 1)  — directly in the way; CANNOT pass through
//  Task:   Route around the cone, reach the ball, kick it into
//          either goal, then drive to the parking zone at
//          x = 0–1, y = 0. Score: 1 pt per phase (max 3).
//
function bossLevel(): void {
    // YOUR CODE HERE
    // Write your plan in 4 phases before you code:
    //   Phase 1 — detour around cone at (4,1)
    //   Phase 2 — navigate to ball at (4,2)
    //   Phase 3 — kick into left or right goal
    //   Phase 4 — drive to parking zone (x=0–1, y=0)

}

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//  STARTUP
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CutebotPro.stopImmediately(CutebotProMotors.ALL)
CutebotPro.pwmCruiseControl(0, 0)
basic.pause(100)

// Show a quick stat summary: motorPower then chargePower
basic.showString("P" + motorPower)
basic.pause(400)
basic.showString("K" + chargePower)
basic.pause(400)

// Show which challenge is loaded
basic.showNumber(currentChallenge)
Skill
10:00
remaining