badges/reflex.go

321 lines
9.4 KiB
Go
Raw Normal View History

2026-04-21 19:51:05 +00:00
package main
import (
"image/color"
"math/rand"
"strconv"
"time"
"tinygo.org/x/tinydraw"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
const (
reflexStateWait = iota
reflexStateReady
reflexStateGo
reflexStateResult
)
type ReflexGame struct {
state int
direction int // 0=Up 1=Down 2=Left 3=Right
score int
round int
startTime time.Time
reactionMs int64
bestTime int64
splashShown bool
}
func NewReflexGame() *ReflexGame {
return &ReflexGame{
state: reflexStateWait,
bestTime: 999999,
}
}
func (r *ReflexGame) Loop() {
r.state = reflexStateWait
r.score = 0
r.round = 0
r.bestTime = 999999
r.splashShown = false
for {
getInput()
switch r.state {
case reflexStateWait:
r.showInstructions()
if !buttonsOldState[buttonA] && buttonsState[buttonA] {
r.state = reflexStateReady
time.Sleep(300 * time.Millisecond)
}
if goBack() {
return
}
case reflexStateReady:
r.showGetReady()
delay := time.Duration(1000+rand.Int31n(2000)) * time.Millisecond
deadline := time.Now().Add(delay)
for time.Now().Before(deadline) {
getInput()
if goBack() {
return
}
time.Sleep(50 * time.Millisecond)
}
r.state = reflexStateGo
case reflexStateGo:
r.round++
r.direction = int(rand.Int31n(4))
r.startTime = time.Now()
r.showTarget()
pressed := false
for !pressed {
getInput()
if goBack() {
return
}
switch r.direction {
case 0:
pressed = buttonsState[buttonUp]
case 1:
pressed = buttonsState[buttonDown]
case 2:
pressed = buttonsState[buttonLeft]
case 3:
pressed = buttonsState[buttonRight]
}
time.Sleep(10 * time.Millisecond)
}
r.reactionMs = time.Since(r.startTime).Milliseconds()
if r.reactionMs < r.bestTime {
r.bestTime = r.reactionMs
}
if points := int(1000 - r.reactionMs); points > 0 {
r.score += points
}
r.state = reflexStateResult
case reflexStateResult:
r.showResult()
r.feedbackLEDs()
for i := 0; i < 40; i++ {
getInput()
if goBack() {
r.clearLEDs()
return
}
time.Sleep(50 * time.Millisecond)
}
r.clearLEDs()
if r.round >= 5 {
r.showFinalScore()
for {
getInput()
if goBack() {
return
}
if !buttonsOldState[buttonA] && buttonsState[buttonA] {
break
}
time.Sleep(50 * time.Millisecond)
}
r.state = reflexStateWait
r.score = 0
r.round = 0
r.bestTime = 999999
r.splashShown = false
} else {
r.state = reflexStateReady
}
}
time.Sleep(50 * time.Millisecond)
}
}
func (r *ReflexGame) showInstructions() {
if r.splashShown {
return
}
display.FillScreen(colorBlack)
if displayHeight >= 160 {
tinyfont.WriteLine(&display, &freesans.Bold18pt7b, 40, 50, "REFLEX", colorYellow)
tinyfont.WriteLine(&display, &freesans.Bold18pt7b, 40, 80, "TESTER", colorYellow)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 20, 130, "Press matching", colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 20, 155, "direction button", colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 20, 180, "when arrow appears!", colorWhite)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 60, 215, "[A] START", colorMellonGreen)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 60, 235, "[B] BACK", colorRed)
} else {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 25, "REFLEX TESTER", colorYellow)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 55, "Press matching", colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 70, "direction button", colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 85, "when arrow appears!", colorWhite)
tinyfont.WriteLine(&display, &freesans.Bold9pt7b, 10, 110, "[A] START", colorMellonGreen)
tinyfont.WriteLine(&display, &freesans.Bold9pt7b, 10, 125, "[B] BACK", colorRed)
}
r.splashShown = true
}
func (r *ReflexGame) showGetReady() {
display.FillScreen(colorBlack)
if displayHeight >= 160 {
tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 20, displayHeight/2+15, "GET READY!", colorOrange)
} else {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 20, displayHeight/2+8, "GET READY!", colorOrange)
}
}
func (r *ReflexGame) showTarget() {
display.FillScreen(colorBlack)
roundText := "Round " + strconv.Itoa(r.round) + "/5"
if displayHeight >= 160 {
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 20, roundText, colorText)
} else {
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 5, 14, roundText, colorText)
}
cx := int16(displayWidth / 2)
cy := int16(displayHeight / 2)
switch r.direction {
case 0:
r.drawArrow(cx, cy, colorPurple, 0)
case 1:
r.drawArrow(cx, cy, colorMellonGreen, 1)
case 2:
r.drawArrow(cx, cy, colorOrange, 2)
case 3:
r.drawArrow(cx, cy, colorRed, 3)
}
}
func (r *ReflexGame) showResult() {
display.FillScreen(colorBlack)
timeText := strconv.FormatInt(r.reactionMs, 10) + " ms"
var feedbackText string
var feedbackColor color.RGBA
switch {
case r.reactionMs < 250:
feedbackText = "AMAZING!"
feedbackColor = colorMellonGreen
case r.reactionMs < 400:
feedbackText = "GREAT!"
feedbackColor = colorMellonGreen
case r.reactionMs < 550:
feedbackText = "GOOD"
feedbackColor = colorOrange
case r.reactionMs < 750:
feedbackText = "OK"
feedbackColor = colorOrange
default:
feedbackText = "SLOW"
feedbackColor = colorRed
}
scoreText := "Score: " + strconv.Itoa(r.score)
if displayHeight >= 160 {
w, _ := tinyfont.LineWidth(&freesans.Bold24pt7b, timeText)
tinyfont.WriteLine(&display, &freesans.Bold24pt7b, (displayWidth-int16(w))/2, 80, timeText, colorYellow)
w, _ = tinyfont.LineWidth(&freesans.Bold18pt7b, feedbackText)
tinyfont.WriteLine(&display, &freesans.Bold18pt7b, (displayWidth-int16(w))/2, 125, feedbackText, feedbackColor)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 80, 165, scoreText, colorWhite)
} else {
w, _ := tinyfont.LineWidth(&freesans.Bold12pt7b, timeText)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, (displayWidth-int16(w))/2, 45, timeText, colorYellow)
w, _ = tinyfont.LineWidth(&freesans.Bold9pt7b, feedbackText)
tinyfont.WriteLine(&display, &freesans.Bold9pt7b, (displayWidth-int16(w))/2, 70, feedbackText, feedbackColor)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 95, scoreText, colorWhite)
}
}
func (r *ReflexGame) showFinalScore() {
display.FillScreen(colorBlack)
scoreText := "Final: " + strconv.Itoa(r.score)
bestText := "Best: " + strconv.FormatInt(r.bestTime, 10) + " ms"
avgTime := int64(5000-r.score) / 5
var rating string
switch {
case avgTime < 300:
rating = "LIGHTNING FAST!"
case avgTime < 450:
rating = "SUPER QUICK!"
case avgTime < 600:
rating = "PRETTY GOOD!"
default:
rating = "KEEP PRACTICING!"
}
if displayHeight >= 160 {
tinyfont.WriteLine(&display, &freesans.Bold18pt7b, 40, 50, "GAME OVER!", colorYellow)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 40, 100, scoreText, colorWhite)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 40, 130, bestText, colorMellonGreen)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 20, 180, rating, colorOrange)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 40, displayHeight-15, "Press A to retry", colorText)
} else {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 25, "GAME OVER!", colorYellow)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 55, scoreText, colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 70, bestText, colorMellonGreen)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 90, rating, colorOrange)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, displayHeight-15, "Press A to retry", colorText)
}
}
// drawArrow draws a direction arrow centered at (cx, cy).
// dir: 0=Up, 1=Down, 2=Left, 3=Right
func (r *ReflexGame) drawArrow(cx, cy int16, c color.RGBA, dir int) {
half := int16(displayHeight) / 4 // distance from center to tip/tail
headW := int16(displayHeight) / 8 // half-width of arrowhead base
shaftW := int16(displayHeight) / 20 // half-width of shaft
switch dir {
case 0: // Up: tip at top, shaft extends down
tinydraw.FilledTriangle(&display, cx, cy-half, cx-headW, cy, cx+headW, cy, c)
display.FillRectangle(cx-shaftW, cy, 2*shaftW, half, c)
case 1: // Down: tip at bottom, shaft extends up
display.FillRectangle(cx-shaftW, cy-half, 2*shaftW, half, c)
tinydraw.FilledTriangle(&display, cx, cy+half, cx-headW, cy, cx+headW, cy, c)
case 2: // Left: tip at left, shaft extends right
tinydraw.FilledTriangle(&display, cx-half, cy, cx, cy-headW, cx, cy+headW, c)
display.FillRectangle(cx, cy-shaftW, half, 2*shaftW, c)
case 3: // Right: tip at right, shaft extends left
display.FillRectangle(cx-half, cy-shaftW, half, 2*shaftW, c)
tinydraw.FilledTriangle(&display, cx+half, cy, cx, cy-headW, cx, cy+headW, c)
}
}
func (r *ReflexGame) feedbackLEDs() {
ledColors := make([]color.RGBA, numLEDs)
var c color.RGBA
switch {
case r.reactionMs < 250:
c = color.RGBA{0, 255, 0, 255}
case r.reactionMs < 400:
c = color.RGBA{0, 200, 0, 255}
case r.reactionMs < 550:
c = color.RGBA{150, 200, 0, 255}
case r.reactionMs < 750:
c = colorOrange
default:
c = colorRed
}
for i := range ledColors {
ledColors[i] = c
}
leds.WriteColors(ledColors)
}
func (r *ReflexGame) clearLEDs() {
ledColors := make([]color.RGBA, numLEDs)
for i := range ledColors {
ledColors[i] = colorBlackLED
}
leds.WriteColors(ledColors)
}