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) }