¿Qué hace este programa?
Este programa prueba cómo tu robot vaga en arcos según tu estadística TURNING — y la lógica de vagabundeo aquí es idéntica al ciclo de roam del programa de partido. Lo que ves en el suelo durante esta prueba es lo que verás durante los partidos reales. Mientras conduce, el robot consulta continuamente los sensores en dos umbrales (impacto duro + advertencia suave), se recupera si toca el borde, y adapta su conducción a medida que ENDURANCE decae.
La idea clave: tu estadística TURNING no solo controla si giras — controla la forma del patrón que tu robot traza alrededor del ring. TURNING alto → segmentos más cortos y cerrados (se queda local). TURNING bajo → segmentos más largos y suaves (cubre más). Probarlo aquí te dice qué esperar en un partido real.
SPEED = 6, ENDURANCE = 4, TURNING = 3 — modifícalos para que coincidan con tu personaje. Además: BORDER_THRESHOLD debe coincidir con el valor que tu programa de partido calibra, o el comportamiento del borde no será consistente entre prueba y partido.
Tres botones. Roles diferentes a los anteriores.
Ejecutar la Prueba de Vagabundeo
Calcula la potencia + configuración de arco a partir de tus estadísticas, luego vaga durante 15 segundos usando el mismo modelo de movimiento que el programa de partido. Observa el patrón en el suelo y la tira RGB — azul/verde = potencia completa, rojo = estado de baja potencia.
Ciclar Tiempo + Mostrar Potencia
Cicla entre 0 → 60 → 120 → 180 → 0 segundos. Muestra el tiempo, luego la potencia de rueda decaída resultante ("P38" para potencia = 38) para que puedas ver cómo cae la potencia de un robot cansado sin ejecutar una prueba.
Parada de Emergencia
Presiona ambos botones juntos para detener los motores al instante. Limpia la tira RGB y muestra una X en la pantalla LED. Úsalo siempre que el robot vaya hacia donde no debería.
¿Cómo funciona cada sección?
let SPEED = 6
let ENDURANCE = 4
let TURNING = 3
let TEST_DURATION_MS = 15000
let BASE_SEGMENT_MS = 300
let BORDER_THRESHOLD = 130
const BORDER_POLL_MS = 12
Se ejecuta una vez cuando el micro:bit se enciende. Los valores predeterminados ya funcionan — los cambias para comparar diferentes valores de TURNING, no para "llenar desde cero". Presiona A para ejecutar con los valores actuales.
SPEED = 6, ENDURANCE = 4, TURNING = 3 — Estadísticas iniciales por defecto. Edítalas para que coincidan con tu personaje, luego vuelve a flashear para comparar. El Desafío de Cobertura te pide específicamente correr con T = 2, 5 y 10.TEST_DURATION_MS = 15000 — Cada prueba dura 15 segundos. Esto coincide con la duración del Desafío de Cobertura — suficiente tiempo para ver desarrollarse el patrón de arco y recuperarse de al menos un impacto con el borde.BASE_SEGMENT_MS = 300 — La longitud base de cada segmento de arco, antes de que TURNING lo reduzca. Con TURNING = 3, cada segmento corre unos 100ms antes de que el robot considere un cambio de dirección.BORDER_THRESHOLD = 130 — Lectura del sensor por encima de esto cuenta como impacto con el borde. Configúralo al valor que tu programa de partido calibra — debe coincidir con el programa de partido o el comportamiento del borde será inconsistente entre prueba y partido.BORDER_POLL_MS = 12 — Cada cuánto el robot lee los sensores durante un segmento. 12ms significa aproximadamente 80 lecturas por segundo — suficientemente rápido para reaccionar antes de que el robot pase el borde blanco.
input.onButtonPressed(Button.A, function () {
computeStats()
sameDirectionCount = 0
maxSameDirection = Math.randomRange(1, 2)
lastTurnWasLeft = false
running = true
testStart = input.runningTime()
while (running && input.runningTime() - testStart < TEST_DURATION_MS) {
roamStep()
}
motorStop()
running = false
strip.clear()
basic.showString("DONE")
basic.pause(500)
basic.clearScreen()
})
El bucle principal se volvió mucho más corto. Toda la lógica de dibujado de arcos se movió a una nueva función llamada roamStep — el Botón A solo la llama una y otra vez hasta que se acabe el tiempo. Esta es la misma forma que el bucle de vagabundeo del programa de partido, así que lo que ves aquí es lo que verás en los partidos.
motorPower, turnRatio, thisDuration y la currentPower decaída por endurance a partir de tus estadísticas y el tiempo simulado actual del partido.sameDirectionCount = 0), elige cuántos segmentos mantener la misma dirección antes de cambiar (maxSameDirection es 1 o 2, aleatorio), y comienza arqueándose a la derecha (lastTurnWasLeft = false).roamStep hasta que alguien presione A+B (que establece running = false) o transcurran 15 segundos. Cada roamStep conduce un segmento de arco, sondea el borde, y puede cambiar de dirección o recuperarse de un borde.
input.onButtonPressed(Button.B, function () {
if (simulatedMatchTime == 0) { simulatedMatchTime = 60 }
else if (simulatedMatchTime == 60) { simulatedMatchTime = 120 }
else if (simulatedMatchTime == 120) { simulatedMatchTime = 180 }
else { simulatedMatchTime = 0 }
computeStats()
basic.showString(simulatedMatchTime + "s")
basic.pause(300)
basic.showString("P" + currentPower)
basic.pause(400)
basic.clearScreen()
})
Cicla el tiempo simulado del partido: 0 → 60 → 120 → 180 → 0. Después de ciclar, esta versión también llama a computeStats y muestra la potencia decaída resultante en pantalla — así puedes ver cómo cae la potencia real de un robot cansado sin tener que ejecutar una prueba.
computeStats inmediatamente para que currentPower refleje el nuevo tiempo.
input.onButtonPressed(Button.AB, function () {
running = false
motorStop()
strip.clear()
basic.showLeds(`
# . . . #
. # . # .
. . # . .
. # . # .
# . . . #
`)
basic.pause(500)
basic.clearScreen()
})
Presiona ambos botones juntos para detener los motores al instante. Pone running = false para que cualquier ciclo de vagabundeo en curso salga en el siguiente chequeo de seguridad, llama a motorStop para un corte limpio de las ruedas, limpia la tira RGB, y muestra una X en la pantalla LED para confirmar la parada.
function computeStats() {
motorPower = SPEED * 100 / 20
turnRatio = 0.2 + TURNING * 0.02
totalArcTime = BASE_SEGMENT_MS * 12
thisDuration = Math.min(Math.round(totalArcTime / Math.max(1, TURNING)), 600)
DECAY_RATE = 20 - ENDURANCE
minWheelSpeed = Math.max(20, Math.round(motorPower * 0.25))
lowPowerThreshold = minWheelSpeed
endurancePercent = 100 - DECAY_RATE * simulatedMatchTime / 40
enduranceFactor = Math.max(endurancePercent / 100, 0.2)
currentPower = Math.max(Math.round(motorPower * enduranceFactor), minWheelSpeed)
lowPowerActive = currentPower <= lowPowerThreshold
}
Esta función calcula todo antes de que cualquier motor gire — la forma del arco, la potencia decaída por endurance, y una bandera de "baja potencia" que el resto del código usa para cambiar a una conducción más suave cuando el robot está cansado.
turnRatio = 0.2 + TURNING × 0.02 ← nitidez del arco, sin tope
thisDuration = totalArcTime ÷ TURNING ← limitado a 600ms
minWheelSpeed = max(20, motorPower × 0.25) ← piso
enduranceFactor = max(endurancePercent/100, 0.2) ← decaimiento, mín 20%
currentPower = max(motorPower × factor, minWheelSpeed)
lowPowerActive = currentPower ≤ lowPowerThreshold
roamStep sujeta el ratio entre 0.10 y 0.45 con variación aleatoria.currentPower cae a la velocidad mínima de rueda, lowPowerActive = true. El resto del programa lee esta bandera para usar un umbral de sensor más suave, retroceso más corto, giro de recuperación más cerrado, y la tira RGB muestra rojo en lugar de azul/verde.
function driveAndWatch(ms: number, lSpeed: number, rSpeed: number) {
if (!running) { return }
let watchStart = input.runningTime()
let lastBorderPoll = 0
let lastMotorRefresh = input.runningTime()
while (input.runningTime() - watchStart < ms) {
if (!running) { return }
if (input.runningTime() - lastBorderPoll >= BORDER_POLL_MS) {
lastBorderPoll = input.runningTime()
readTrackbit()
let hardSide = borderSideFrom(BORDER_THRESHOLD)
if (hardSide > 0) {
borderSide = hardSide
CutebotPro.pwmCruiseControl(-60, -60)
basic.pause(70)
motorStop()
borderHandledInSeg = true
handleBorder()
return
}
let softThresh = Math.round(BORDER_THRESHOLD * (lowPowerActive ? 0.65 : 0.75))
let softSide = borderSideFrom(softThresh)
if (softSide > 0) {
softEvade(softSide)
return
}
}
// Motor keep-alive cada 200ms
if (input.runningTime() - lastMotorRefresh > 200) {
lastMotorRefresh = input.runningTime()
CutebotPro.pwmCruiseControl(lSpeed, rSpeed)
}
basic.pause(5)
}
}
El nuevo driveAndWatch hace tres trabajos a la vez: sondea los sensores del borde cada 12ms, vuelve a enviar el comando del motor cada 200ms para que el controlador no se trabe, y verifica dos umbrales de sensor separados — uno duro para un impacto real con el borde, y uno suave para advertencia temprana.
watchStart rastrea cuánto tiempo ha estado corriendo el segmento, lastBorderPoll limita la tasa de lecturas de sensores, y lastMotorRefresh limita la tasa del comando keep-alive del motor.BORDER_THRESHOLD (130), el robot está sobre blanco. Frena a −60 PWM por 70ms, parada total, establece borderHandledInSeg = true, y llama a handleBorder para la secuencia de recuperación completa. Luego sale del segmento.softEvade — una recuperación más ligera que el handleBorder completo.handleBorder completo con retroceso, giro multi-intento, y reverificación. Impacto suave (a punto de estar sobre blanco) → softEvade rápido con retroceso más corto y un solo giro. Juntos, el robot rara vez se queda atascado en el borde.
function readTrackbit() {
CutebotPro.trackbitStateValue()
tb1 = CutebotPro.trackbitgetGray(TrackbitChannel.One)
tb2 = CutebotPro.trackbitgetGray(TrackbitChannel.Two)
tb3 = CutebotPro.trackbitgetGray(TrackbitChannel.Three)
tb4 = CutebotPro.trackbitgetGray(TrackbitChannel.Four)
}
// 1 = frontal izq 2 = frontal der 3 = centro 4 = ambos externos (directo) 0 = despejado
function borderSideFrom(threshold: number): number {
if (tb1 > threshold && tb4 > threshold) { return 4 }
if (tb1 > threshold) { return 1 }
if (tb4 > threshold) { return 2 }
if (tb2 > threshold || tb3 > threshold) { return 3 }
return 0
}
function checkBorder(): number {
readTrackbit()
let side = borderSideFrom(BORDER_THRESHOLD)
if (side > 0) { borderSide = side }
return side
}
La detección del borde ahora se divide en tres ayudantes. readTrackbit lee los cuatro sensores en variables compartidas. borderSideFrom clasifica qué lado del robot está sobre blanco, dado cualquier umbral. checkBorder usa el umbral duro y es lo que las otras funciones llaman.
trackbitStateValue una vez para refrescar, luego lee cada sensor (1–4) en tb1, tb2, tb3, tb4. Otras funciones leen esas variables en lugar de volver a llamar a la API del sensor.4 = directo para cuando ambos sensores externos se activan. Esto es crítico porque un impacto directo necesita una recuperación mucho más dura (giro más grande) que un impacto de un solo lado.BORDER_THRESHOLD duro y almacena el resultado en el borderSide global para que handleBorder sepa hacia dónde girar.
function handleBorder() {
if (!running) { return }
hardStop()
if (!running) { return }
speedExtra = Math.max(SPEED - 4, 0)
backupPower = lowPowerActive ? -25 : (0 - 40 - speedExtra * 5)
backupTime = lowPowerActive ? 130 : (250 + speedExtra * 20)
let attempts = 0
while (attempts < 3) {
attempts += 1
let side = borderSide
borderSide = 0
CutebotPro.pwmCruiseControl(backupPower, backupPower)
basic.pause(backupTime)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(80)
if (!running) { return }
let angle: number
if (side == 1 || side == 2) {
angle = lowPowerActive ? 150 : 115
} else {
angle = side == 4 ? 150 + Math.randomRange(0, 20) : 130
if (lowPowerActive) { angle = Math.min(angle + 30, 180) }
}
turnAwayFrom(side, angle)
if (!running) { return }
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(40)
CutebotPro.pwmCruiseControl(currentPower, currentPower)
basic.pause(300)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(40)
if (checkBorder() == 0) { break }
}
sameDirectionCount = 0
}
La recuperación ahora es un bucle de 3 intentos. Cada intento: retroceder, girar y alejarse, conducir hacia adelante brevemente, luego volver a verificar el borde. Si el robot sigue sobre blanco, el bucle intenta de nuevo — hasta 3 veces. La distancia de retroceso, el ángulo de giro y la lógica de dirección todos escalan con SPEED y el estado de baja potencia.
hardStop invierte a −80 PWM por un tiempo de frenado que crece con SPEED. Un robot rápido tarda más en detenerse, así que frena más fuerte.speedExtra = SPEED − 4. Robots rápidos retroceden más con más potencia. Robots de baja potencia retroceden solo 130ms a −25 PWM (sin desperdicio de energía cuando el robot está cansado).currentPower para alejarse del borde, luego llama a checkBorder de nuevo. Si está despejado, break del bucle. Si sigue sobre blanco, comienza el siguiente intento.sameDirectionCount = 0 para que el siguiente roamStep elija una nueva racha de dirección.
function roamStep() {
if (!running) { return }
// Cambia dirección cada 1–2 segmentos (aleatorio)
if (sameDirectionCount >= maxSameDirection) {
lastTurnWasLeft = !lastTurnWasLeft
sameDirectionCount = 0
maxSameDirection = Math.randomRange(1, 2)
} else {
sameDirectionCount += 1
}
let dynamicRatio = Math.max(0.10, Math.min(0.45,
turnRatio + Math.randomRange(-8, 8) / 100))
let speedDurationBonus = SPEED * 12
let dynamicDuration = lowPowerActive
? Math.max(Math.min(thisDuration / 2 + Math.randomRange(-50, 50), 300), 150)
: Math.max(Math.min(thisDuration + speedDurationBonus + Math.randomRange(-50, 50), 550), 150)
let lSpeed: number
let rSpeed: number
if (lastTurnWasLeft) {
lSpeed = Math.round(currentPower * (1 - dynamicRatio))
rSpeed = Math.round(currentPower * (1 + dynamicRatio))
strip.showColor(lowPowerActive ? neopixel.colors(NeoPixelColors.Red)
: neopixel.colors(NeoPixelColors.Blue))
} else {
lSpeed = Math.round(currentPower * (1 + dynamicRatio))
rSpeed = Math.round(currentPower * (1 - dynamicRatio))
strip.showColor(lowPowerActive ? neopixel.colors(NeoPixelColors.Red)
: neopixel.rgb(0, 255, 0))
}
lSpeed = Math.max(lSpeed, minWheelSpeed)
rSpeed = Math.max(rSpeed, minWheelSpeed)
borderHandledInSeg = false
CutebotPro.pwmCruiseControl(lSpeed, rSpeed)
driveAndWatch(dynamicDuration, lSpeed, rSpeed)
if (!borderHandledInSeg) {
CutebotPro.pwmCruiseControl(-40, -40)
basic.pause(40)
}
motorStop()
if (running) {
basic.pause(40)
if (checkBorder() > 0) { handleBorder() }
}
}
Aquí es donde sucede el movimiento real del robot. Cada llamada a roamStep conduce un segmento de arco: elegir una dirección (tal vez cambiar), elegir velocidades para las ruedas izquierda y derecha, conducir vigilando el borde, frenar y verificar el borde una vez más. El programa de partido usa la misma función — así que los patrones de arco que observas aquí son los patrones que el robot dibuja en partidos reales.
sameDirectionCount). Cuando llega a maxSameDirection, cambia. Por esto el robot zigzaguea en lugar de hacer bucles limpios en una dirección.dynamicRatio = turnRatio + random(−0.08, 0.08), sujeto entre 0.10 y 0.45. Dos segmentos con el mismo TURNING nunca trazan exactamente el mismo arco.speedDurationBonus = SPEED × 12ms se agrega a la duración de cada segmento. Robots de SPEED alto cubren más terreno por segmento antes de cambiar. El estado de baja potencia divide la duración en lugar — robots cansados dan pasos más cortos y suaves.currentPower × (1 − ratio) para la rueda interna, currentPower × (1 + ratio) para la externa. La rueda interna desacelera, la externa va más rápido que currentPower. Cada rueda con piso en minWheelSpeed para que la rueda interna nunca se estanque.driveAndWatch con las velocidades de rueda elegidas para la duración elegida. Si volvió sin un impacto de borde (!borderHandledInSeg), hace un frenado activo firme a −40 PWM. Luego verifica el borde una vez más — a veces el robot rueda hacia una línea durante el frenado.
function motorStop() {
CutebotPro.stopImmediately(CutebotProMotors.ALL)
basic.pause(10)
CutebotPro.pwmCruiseControl(0, 0)
basic.pause(10)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
}
function hardStop() {
let brakeTime = 160 + Math.max(SPEED - 5, 0) * 35
CutebotPro.pwmCruiseControl(-80, -80)
basic.pause(brakeTime)
CutebotPro.stopImmediately(CutebotProMotors.ALL)
}
function turnAwayFrom(side: number, angle: number) {
if (side == 1) {
CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, angle)
lastTurnWasLeft = false
} else if (side == 2) {
CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, angle)
lastTurnWasLeft = true
} else if (lastTurnWasLeft) {
CutebotPro.trolleySteering(CutebotProTurn.RightInPlace, angle)
lastTurnWasLeft = false
} else {
CutebotPro.trolleySteering(CutebotProTurn.LeftInPlace, angle)
lastTurnWasLeft = true
}
}
function softEvade(side: number) {
motorStop()
basic.pause(20)
let backMs = lowPowerActive ? 130 : 200
let backPwr = lowPowerActive ? -25 : -30
CutebotPro.pwmCruiseControl(backPwr, backPwr)
basic.pause(backMs)
motorStop()
basic.pause(40)
turnAwayFrom(side, lowPowerActive ? 130 : 90)
motorStop()
borderHandledInSeg = true
borderSide = 0
}
Cuatro funciones cortas que las funciones más grandes llaman. Cada una hace un trabajo limpiamente para que la lógica de recuperación se mantenga legible.
¿Qué significa cada variable?
Dorado = tú lo configuras. Cian = calculado automáticamente. Púrpura = constante fija.
6. Establece la potencia base del motor. También afecta la duración del segmento (más larga en SPEED alto) y el tiempo de frenado de recuperación (más duro en SPEED alto).4. Controla cuánto disminuye la potencia del motor durante simulatedMatchTime. ENDURANCE bajo = ralentización notable a 180s.3. El Desafío de Cobertura pide T = 2, 5 y 10. Maneja tanto la nitidez del arco (vía turnRatio) como la longitud del segmento (vía thisDuration).SPEED × 100 ÷ 20. El punto de partida para los cálculos de potencia. El decaimiento de endurance se aplica a esto para producir currentPower.motorPower × enduranceFactor, con piso en minWheelSpeed. Esto es lo que las ruedas realmente hacen.0.2 + TURNING × 0.02, sin tope duro aquí — roamStep lo sujeta a 0.10–0.45 con variación aleatoria.totalArcTime ÷ TURNING, limitado a 600ms. roamStep agrega bonus de SPEED y variación aleatoria antes de conducir.BASE_SEGMENT_MS × 12 = 3.600ms en esta versión. Dividido entre segmentos para obtener thisDuration.max(20, motorPower × 0.25). Cada rueda está sujeta al menos a esto — evita que la rueda interna de un arco se estanque completamente.endurancePercent ÷ 100, nunca por debajo de 0.20. Multiplicado con motorPower para obtener currentPower.currentPower ≤ minWheelSpeed. Activa umbrales más suaves, retrocesos más cortos, giros de recuperación más amplios, y una tira RGB roja en lugar de azul/verde.computeStats.handleBorder y turnAwayFrom.handleBorder para que la recuperación se reanude mirando la dirección correcta.maxSameDirection (1 o 2, aleatorio), cambia. Por esto el robot zigzaguea en lugar de hacer bucles limpios.15000 (15 segundos). Coincide con el Desafío de Cobertura — tiempo suficiente para ver desarrollarse el patrón de arco y recuperarse de al menos un impacto de borde.300. Multiplicado por 12 para obtener totalArcTime. Cámbialo para hacer todos los segmentos más cortos o más largos.130. Configúralo al valor que use tu programa de partido — deben coincidir. El umbral suave es automáticamente 75% de esto (o 65% cuando baja potencia).12. Aproximadamente 80 lecturas por segundo — suficientemente rápido para reaccionar antes de que el robot pase un borde blanco.Consejos para usar este programa
Edita tus estadísticas, vuelve a flashear
Los valores predeterminados son SPEED = 6, ENDURANCE = 4, TURNING = 3. Los predeterminados funcionan — cambia a las estadísticas de tu personaje y vuelve a flashear para comparar. Ya no hay nada "que llenar desde cero".
Ejecuta la secuencia del Desafío de Cobertura
Vuelve a flashear con TURNING = 2, luego 5, luego 10. Observa el patrón en el suelo que traza cada uno. El Desafío de Cobertura del Día 3 pide exactamente esto — TURNING alto = segmentos más cortos y cerrados (se queda local). TURNING bajo = segmentos más largos y suaves (cubre más).
Iguala el BORDER_THRESHOLD
Configura BORDER_THRESHOLD al valor que tu programa de partido calibra. Si no coinciden, el robot reaccionará al borde blanco de manera diferente en pruebas que en partidos.
Usa el Botón B antes que A
Presiona B para avanzar el tiempo simulado del partido, observa cómo cae el valor "P", luego presiona A. Comparar TURNING = 5 a 0s vs. 180s muestra cómo los arcos de un robot cansado se aplanan — y puedes predecirlo desde el número P antes de ejecutar.
Observa la tira RGB
Azul = arqueándose a la izquierda a potencia completa. Verde = arqueándose a la derecha a potencia completa. Rojo = el robot ha caído al estado de baja potencia — recuperación más suave, segmentos más cortos, giros más amplios. Si ves rojo temprano en una prueba de 15 segundos, tu asignación de ENDURANCE puede estar demasiado baja.
Mantén A+B al alcance
A+B es la parada de emergencia. Mantente cerca y presiona ambos botones juntos en el momento en que el robot vaya hacia donde no debería. La X en la pantalla LED confirma que los motores están apagados.