321 lines
9.4 KiB
Go
321 lines
9.4 KiB
Go
|
|
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)
|
||
|
|
}
|