Merge branch 'main' of code.madriguera.me:GoEducation/badges

This commit is contained in:
Daniel Esteban 2026-04-21 21:52:00 +02:00
commit a258ee0540
8 changed files with 1058 additions and 2 deletions

View file

@ -42,8 +42,7 @@ func Badge() {
selectedScreen = 0
fallthrough
case 0:
//showLogoBin()
selectedScreen++
showLogoBin()
if quit {
break
}

View file

@ -56,6 +56,8 @@ var menuOptions = []int8{
modeGameSnake,
modeGameLife,
modeGameColors,
modeGameReflex,
modeGamePacman,
modeInfo,
}

View file

@ -55,6 +55,8 @@ var menuOptions = []int8{
modeGameSnake,
modeGameLife,
modeGameColors,
modeGameReflex,
modeGamePacman,
modeInfo,
}

View file

@ -56,6 +56,8 @@ var menuOptions = []int8{
modeGameSnake,
modeGameLife,
modeGameColors,
modeGameReflex,
modeGamePacman,
modeInfo,
}

View file

@ -25,6 +25,7 @@ var (
colorBlackLED = color.RGBA{0, 0, 0, 0}
colorText = color.RGBA{160, 160, 160, 255}
colorOrange = color.RGBA{255, 153, 51, 255}
colorYellow = color.RGBA{255, 255, 0, 255}
)
const (
@ -50,6 +51,8 @@ const (
modeGameSnake
modeGameLife
modeGameColors
modeGameReflex
modeGamePacman
)
var buttonsState []bool
@ -65,6 +68,8 @@ var options = []string{
"Snake",
"Game of Life",
"Color Game",
"Reflex",
"Pac-Man",
}
var leds ws2812.Device
@ -74,6 +79,8 @@ var accel lis3dh.Device
var defaultFont tinyfont.Fonter
var snakeGame = NewSnakeGame()
var reflexGame = NewReflexGame()
var pacmanGame = NewPacmanGame()
func main() {

View file

@ -35,6 +35,10 @@ func runMenuOption(selected int16) {
GameOfLife()
case modeGameColors:
ColorGame()
case modeGameReflex:
reflexGame.Loop()
case modeGamePacman:
pacmanGame.Loop()
case modeInfo:
Info()
}

720
pacman.go Normal file
View file

@ -0,0 +1,720 @@
package main
import (
"image/color"
"math/rand"
"strconv"
"time"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
// Maze cell types
const (
pmEmpty = 0
pmWall = 1
pmDot = 2
pmPower = 3
)
// Directions
const (
pmUp = int16(0)
pmDown = int16(1)
pmLeft = int16(2)
pmRight = int16(3)
)
// Maze dimensions and cell size.
// pmCell scales with displayHeight so the maze always fits the screen:
// gopherbadge 320x240 → pmCell=10, maze fills 320x240 (no offset)
// nicebadge 240x135 → pmCell=5, maze fills 160x120, offset (40,7)
// gobadge 160x128 → pmCell=5, maze fills 160x120, offset (0,4)
const (
pmW = 32
pmH = 24
pmCell = displayHeight / pmH
pmGhostCount = 4
pmFrightLen = 40 // frames of fright mode
// Pixel offset to center the maze on the display
pmOffsetX = (displayWidth - pmW*pmCell) / 2
pmOffsetY = (displayHeight - pmH*pmCell) / 2
// Scoring
pmPointsDot = 10
pmPointsPower = 50
pmPointsGhost = 200
// Difficulty
pmDiffEasy = 0
pmDiffNormal = 1
pmDiffHard = 2
)
// Drawing dimensions — all derived from pmCell so they scale automatically.
// Reference values shown for pmCell=10 / pmCell=5.
const (
pmBodyOff = 1 // border inset for pac/ghosts
pmBodySz = pmCell - 2*pmBodyOff // body size: 8 / 3
pmDotOff = pmCell/2 - 1 // dot center offset: 4 / 1
pmDotSz = 1 + pmCell/10 // dot size: 2 / 1
pmPowOff = pmCell / 5 // power pellet inset: 2 / 1
pmPowSz = pmCell - 2*(pmCell/5) // power pellet size: 6 / 3
// Pacman mouth cutout (open/close animation)
pmMouthOffX = pmCell * 7 / 10 // far-side mouth start: 7 / 3
pmMouthOffY = pmCell * 2 / 5 // mouth vertical offset: 4 / 2
pmMouthW = pmCell * 3 / 10 // mouth width: 3 / 1
pmMouthH = pmCell / 5 // mouth height: 2 / 1
// Ghost eyes
pmEye1X = pmCell / 5 // left eye x: 2 / 1
pmEye2X = pmCell * 6 / 10 // right eye x: 6 / 3
pmEyeY = pmCell * 3 / 10 // eye y: 3 / 1
pmEyeSz = pmCell / 5 // eye size: 2 / 1
)
// Package-level state persists across games within a power cycle.
var pmHighScore int
var pmDifficulty int
var pmPowerPositions = [4][2]int16{
{1, 3}, {30, 3}, {1, 20}, {30, 20},
}
var (
pmColorWall = color.RGBA{33, 33, 222, 255}
pmColorDot = color.RGBA{255, 255, 255, 255}
pmColorPower = color.RGBA{255, 255, 200, 255}
pmColorPac = color.RGBA{255, 255, 0, 255}
pmColorFright = color.RGBA{33, 33, 200, 255}
pmColorBg = color.RGBA{0, 0, 0, 255}
pmGhostColors = [pmGhostCount]color.RGBA{
{255, 0, 0, 255}, // Blinky (red)
{255, 184, 255, 255}, // Pinky
{0, 255, 255, 255}, // Inky (cyan)
{255, 184, 82, 255}, // Clyde (orange)
}
)
type PacGhost struct {
x, y int16
dir int16
fright int
startX, startY int16
}
type PacmanGame struct {
maze [pmH][pmW]uint8
pacX, pacY int16
dir, nextDir int16
ghosts [pmGhostCount]PacGhost
score int
dots int
totalDots int
powerRespawnCount int
status uint8
frame int
won bool
}
func NewPacmanGame() *PacmanGame {
return &PacmanGame{status: GameSplash}
}
func (g *PacmanGame) Loop() {
g.status = GameSplash
g.showSplash()
if g.status == GameQuit {
return
}
g.startGame()
drawn := false
for {
switch g.status {
case GamePlay:
g.update()
case GameOver:
if !drawn {
g.showGameOver()
drawn = true
}
getInput()
if !buttonsOldState[buttonA] && buttonsState[buttonA] {
g.status = GameSplash
g.showSplash()
if g.status == GameQuit {
return
}
g.startGame()
drawn = false
}
if goBack() {
return
}
case GameQuit:
return
}
time.Sleep(80 * time.Millisecond)
}
}
func (g *PacmanGame) showSplash() {
display.FillScreen(pmColorBg)
if displayHeight >= 160 {
tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 55, 50, "PAC-MAN", pmColorPac)
if pmHighScore > 0 {
hs := "Best: " + strconv.Itoa(pmHighScore)
w, _ := tinyfont.LineWidth(&freesans.Regular12pt7b, hs)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, (displayWidth-int16(w))/2, 85, hs, colorYellow)
}
} else {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 30, 25, "PAC-MAN", pmColorPac)
if pmHighScore > 0 {
hs := "Best: " + strconv.Itoa(pmHighScore)
w, _ := tinyfont.LineWidth(&freesans.Regular9pt7b, hs)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, (displayWidth-int16(w))/2, 42, hs, colorYellow)
}
}
selected := pmDifficulty
g.drawDifficultyMenu(selected)
for {
getInput()
if !buttonsOldState[buttonUp] && buttonsState[buttonUp] {
if selected > pmDiffEasy {
selected--
g.drawDifficultyMenu(selected)
}
}
if !buttonsOldState[buttonDown] && buttonsState[buttonDown] {
if selected < pmDiffHard {
selected++
g.drawDifficultyMenu(selected)
}
}
if !buttonsOldState[buttonA] && buttonsState[buttonA] {
pmDifficulty = selected
return
}
if goBack() {
g.status = GameQuit
return
}
time.Sleep(50 * time.Millisecond)
}
}
func (g *PacmanGame) drawDifficultyMenu(selected int) {
labels := [3]string{"Easy", "Normal", "Hard"}
if displayHeight >= 160 {
for i := 0; i < 3; i++ {
c := colorWhite
if i == selected {
c = colorYellow
}
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 80, int16(125+i*28), labels[i], c)
}
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 30, 215, "Press A to start", colorMellonGreen)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 30, 238, "[B] EXIT", colorRed)
} else {
for i := 0; i < 3; i++ {
c := colorWhite
if i == selected {
c = colorYellow
}
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 60, int16(60+i*18), labels[i], c)
}
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 110, "Press A to start", colorMellonGreen)
tinyfont.WriteLine(&display, &freesans.Bold9pt7b, 10, 126, "[B] EXIT", colorRed)
}
}
func (g *PacmanGame) showGameOver() {
if g.score > pmHighScore {
pmHighScore = g.score
}
display.FillScreen(pmColorBg)
if displayHeight >= 160 {
if g.won {
tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 40, 50, "YOU WIN!", colorMellonGreen)
} else {
tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 20, 50, "GAME OVER", colorRed)
}
scoreText := "Score: " + strconv.Itoa(g.score)
w, _ := tinyfont.LineWidth(&freesans.Bold18pt7b, scoreText)
tinyfont.WriteLine(&display, &freesans.Bold18pt7b, (displayWidth-int16(w))/2, 100, scoreText, colorYellow)
hsText := "Best: " + strconv.Itoa(pmHighScore)
w, _ = tinyfont.LineWidth(&freesans.Regular12pt7b, hsText)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, (displayWidth-int16(w))/2, 135, hsText, colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 50, 190, "Press A to retry", colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 50, 220, "Press B to exit", colorText)
} else {
if g.won {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 30, 28, "YOU WIN!", colorMellonGreen)
} else {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 28, "GAME OVER", colorRed)
}
scoreText := "Score: " + strconv.Itoa(g.score)
w, _ := tinyfont.LineWidth(&freesans.Bold12pt7b, scoreText)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, (displayWidth-int16(w))/2, 55, scoreText, colorYellow)
hsText := "Best: " + strconv.Itoa(pmHighScore)
w, _ = tinyfont.LineWidth(&freesans.Regular9pt7b, hsText)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, (displayWidth-int16(w))/2, 75, hsText, colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 20, 100, "Press A to retry", colorWhite)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 20, 115, "Press B to exit", colorText)
}
}
func (g *PacmanGame) startGame() {
g.score = 0
g.dots = 0
g.won = false
g.frame = 0
g.dir = pmLeft
g.nextDir = pmLeft
g.powerRespawnCount = 0
g.initMaze()
g.totalDots = g.dots
g.pacX = 15
g.pacY = 17
if g.maze[g.pacY][g.pacX] == pmDot {
g.dots--
}
g.maze[g.pacY][g.pacX] = pmEmpty
gx := [4]int16{14, 17, 14, 17}
gy := [4]int16{9, 9, 13, 13}
gd := [4]int16{pmLeft, pmRight, pmLeft, pmRight}
for i := 0; i < pmGhostCount; i++ {
g.ghosts[i] = PacGhost{
x: gx[i], y: gy[i],
dir: gd[i],
startX: gx[i], startY: gy[i],
}
if g.maze[gy[i]][gx[i]] == pmDot {
g.dots--
g.maze[gy[i]][gx[i]] = pmEmpty
}
}
g.drawFullMaze()
g.drawPacman()
for i := 0; i < pmGhostCount; i++ {
g.drawGhost(i)
}
g.status = GamePlay
}
func (g *PacmanGame) initMaze() {
g.dots = 0
for y := 0; y < pmH; y++ {
for x := 0; x < pmW; x++ {
g.maze[y][x] = pmDot
g.dots++
}
}
wall := func(x, y, w, h int) {
for dy := 0; dy < h; dy++ {
for dx := 0; dx < w; dx++ {
if g.maze[y+dy][x+dx] == pmDot {
g.dots--
}
g.maze[y+dy][x+dx] = pmWall
}
}
}
empty := func(x, y, w, h int) {
for dy := 0; dy < h; dy++ {
for dx := 0; dx < w; dx++ {
if g.maze[y+dy][x+dx] == pmDot {
g.dots--
}
g.maze[y+dy][x+dx] = pmEmpty
}
}
}
// Border
wall(0, 0, pmW, 1)
wall(0, pmH-1, pmW, 1)
wall(0, 0, 1, pmH)
wall(pmW-1, 0, 1, pmH)
// Top section
wall(2, 2, 4, 2)
wall(7, 2, 6, 2)
wall(14, 2, 4, 2)
wall(19, 2, 6, 2)
wall(26, 2, 4, 2)
wall(2, 5, 4, 1)
wall(7, 5, 2, 3)
wall(10, 5, 5, 1)
wall(17, 5, 5, 1)
wall(23, 5, 2, 3)
wall(26, 5, 4, 1)
wall(2, 7, 4, 1)
wall(10, 7, 5, 1)
wall(17, 7, 5, 1)
wall(26, 7, 4, 1)
wall(2, 8, 4, 2)
wall(7, 8, 2, 2)
wall(23, 8, 2, 2)
wall(26, 8, 4, 2)
// Ghost house: rows 10-12, cols 12-19
wall(12, 10, 8, 1)
wall(12, 12, 8, 1)
wall(12, 10, 1, 3)
wall(19, 10, 1, 3)
empty(13, 11, 6, 1)
empty(15, 10, 2, 1) // entrance
// Bottom section (mirrors top)
wall(2, 14, 4, 2)
wall(7, 14, 2, 2)
wall(23, 14, 2, 2)
wall(26, 14, 4, 2)
wall(2, 16, 4, 1)
wall(10, 16, 5, 1)
wall(17, 16, 5, 1)
wall(26, 16, 4, 1)
wall(7, 17, 2, 1)
wall(23, 17, 2, 1)
wall(2, 18, 4, 1)
wall(7, 18, 2, 3)
wall(10, 18, 5, 1)
wall(17, 18, 5, 1)
wall(23, 18, 2, 3)
wall(26, 18, 4, 1)
wall(2, 20, 4, 2)
wall(7, 20, 6, 2)
wall(14, 20, 4, 2)
wall(19, 20, 6, 2)
wall(26, 20, 4, 2)
// Power pellets
for _, p := range pmPowerPositions {
g.maze[p[1]][p[0]] = pmPower
}
}
func (g *PacmanGame) spawnPowerPellet() {
var eaten [4]int
n := 0
for i, p := range pmPowerPositions {
if g.maze[p[1]][p[0]] == pmEmpty {
eaten[n] = i
n++
}
}
if n == 0 {
return
}
idx := eaten[rand.Int31n(int32(n))]
p := pmPowerPositions[idx]
g.maze[p[1]][p[0]] = pmPower
g.dots++
g.totalDots++
g.drawCell(p[0], p[1])
}
func (g *PacmanGame) drawFullMaze() {
display.FillScreen(pmColorBg)
for y := int16(0); y < pmH; y++ {
for x := int16(0); x < pmW; x++ {
g.drawCell(x, y)
}
}
}
// drawCell redraws a single maze cell at grid coordinates (x, y).
// Pixel position includes pmOffsetX/Y so the maze is centered on any display.
func (g *PacmanGame) drawCell(x, y int16) {
px := int16(pmOffsetX) + x*pmCell
py := int16(pmOffsetY) + y*pmCell
switch g.maze[y][x] {
case pmWall:
display.FillRectangle(px, py, pmCell, pmCell, pmColorWall)
case pmDot:
display.FillRectangle(px, py, pmCell, pmCell, pmColorBg)
display.FillRectangle(px+pmDotOff, py+pmDotOff, pmDotSz, pmDotSz, pmColorDot)
case pmPower:
display.FillRectangle(px, py, pmCell, pmCell, pmColorBg)
display.FillRectangle(px+pmPowOff, py+pmPowOff, pmPowSz, pmPowSz, pmColorPower)
default:
display.FillRectangle(px, py, pmCell, pmCell, pmColorBg)
}
}
func (g *PacmanGame) drawPacman() {
px := int16(pmOffsetX) + g.pacX*pmCell
py := int16(pmOffsetY) + g.pacY*pmCell
display.FillRectangle(px+pmBodyOff, py+pmBodyOff, pmBodySz, pmBodySz, pmColorPac)
if g.frame%4 < 2 {
switch g.dir {
case pmRight:
display.FillRectangle(px+pmMouthOffX, py+pmMouthOffY, pmMouthW, pmMouthH, pmColorBg)
case pmLeft:
display.FillRectangle(px, py+pmMouthOffY, pmMouthW, pmMouthH, pmColorBg)
case pmUp:
display.FillRectangle(px+pmMouthOffY, py, pmMouthH, pmMouthW, pmColorBg)
case pmDown:
display.FillRectangle(px+pmMouthOffY, py+pmMouthOffX, pmMouthH, pmMouthW, pmColorBg)
}
}
}
func (g *PacmanGame) drawGhost(idx int) {
ghost := &g.ghosts[idx]
px := int16(pmOffsetX) + ghost.x*pmCell
py := int16(pmOffsetY) + ghost.y*pmCell
c := pmGhostColors[idx]
if ghost.fright > 0 {
c = pmColorFright
}
display.FillRectangle(px+pmBodyOff, py+pmBodyOff, pmBodySz, pmBodySz, c)
display.FillRectangle(px+pmEye1X, py+pmEyeY, pmEyeSz, pmEyeSz, pmColorDot)
display.FillRectangle(px+pmEye2X, py+pmEyeY, pmEyeSz, pmEyeSz, pmColorDot)
}
func (g *PacmanGame) update() {
getInput()
if goBack() {
g.status = GameQuit
return
}
if buttonsState[buttonRight] {
g.nextDir = pmRight
} else if buttonsState[buttonLeft] {
g.nextDir = pmLeft
} else if buttonsState[buttonUp] {
g.nextDir = pmUp
} else if buttonsState[buttonDown] {
g.nextDir = pmDown
}
g.frame++
if g.canMove(g.pacX, g.pacY, g.nextDir) {
g.dir = g.nextDir
}
if g.canMove(g.pacX, g.pacY, g.dir) {
oldX, oldY := g.pacX, g.pacY
g.pacX += pmDX(g.dir)
g.pacY += pmDY(g.dir)
cell := g.maze[g.pacY][g.pacX]
if cell == pmDot {
g.maze[g.pacY][g.pacX] = pmEmpty
points := pmPointsDot
if pmDifficulty == pmDiffHard {
points *= 2
}
g.score += points
g.dots--
} else if cell == pmPower {
g.maze[g.pacY][g.pacX] = pmEmpty
g.score += pmPointsPower
g.dots--
for i := range g.ghosts {
g.ghosts[i].fright = pmFrightLen
}
}
if g.powerRespawnCount < 2 && pmDifficulty != pmDiffHard {
eaten := g.totalDots - g.dots
threshold := g.totalDots / 2
if g.powerRespawnCount == 1 {
threshold = g.totalDots * 3 / 4
}
if eaten >= threshold {
g.spawnPowerPellet()
g.powerRespawnCount++
}
}
g.drawCell(oldX, oldY)
if g.dots <= 0 {
g.won = true
g.status = GameOver
return
}
for i := 0; i < pmGhostCount; i++ {
if g.ghosts[i].x == g.pacX && g.ghosts[i].y == g.pacY {
if g.ghosts[i].fright > 0 {
g.eatGhost(i)
} else {
g.status = GameOver
return
}
}
}
}
g.drawPacman()
moveGhosts := false
switch pmDifficulty {
case pmDiffEasy:
moveGhosts = g.frame%3 == 0
case pmDiffNormal:
moveGhosts = g.frame%2 == 0
case pmDiffHard:
moveGhosts = g.frame%4 != 0
}
if moveGhosts {
for i := 0; i < pmGhostCount; i++ {
oldX, oldY := g.ghosts[i].x, g.ghosts[i].y
g.moveGhost(i)
g.drawCell(oldX, oldY)
g.drawGhost(i)
if g.ghosts[i].fright > 0 {
g.ghosts[i].fright--
}
if g.ghosts[i].x == g.pacX && g.ghosts[i].y == g.pacY {
if g.ghosts[i].fright > 0 {
g.eatGhost(i)
} else {
g.status = GameOver
return
}
}
}
}
}
func (g *PacmanGame) eatGhost(idx int) {
g.score += pmPointsGhost
g.ghosts[idx].x = g.ghosts[idx].startX
g.ghosts[idx].y = g.ghosts[idx].startY
g.ghosts[idx].fright = 0
g.drawGhost(idx)
}
func (g *PacmanGame) moveGhost(idx int) {
ghost := &g.ghosts[idx]
reverse := pmReverse(ghost.dir)
type opt struct {
dir int16
x, y int16
dist int16
}
var opts [4]opt
n := 0
for d := int16(0); d < 4; d++ {
if d == reverse {
continue
}
nx := ghost.x + pmDX(d)
ny := ghost.y + pmDY(d)
if nx < 0 || nx >= pmW || ny < 0 || ny >= pmH {
continue
}
if g.maze[ny][nx] == pmWall {
continue
}
dist := pmAbs(nx-g.pacX) + pmAbs(ny-g.pacY)
opts[n] = opt{d, nx, ny, dist}
n++
}
if n == 0 {
d := reverse
nx := ghost.x + pmDX(d)
ny := ghost.y + pmDY(d)
if nx >= 0 && nx < pmW && ny >= 0 && ny < pmH && g.maze[ny][nx] != pmWall {
ghost.x = nx
ghost.y = ny
ghost.dir = d
}
return
}
if ghost.fright > 0 {
c := rand.Int31n(int32(n))
ghost.x = opts[c].x
ghost.y = opts[c].y
ghost.dir = opts[c].dir
} else {
best := 0
for i := 1; i < n; i++ {
if opts[i].dist < opts[best].dist {
best = i
}
}
if n > 1 && rand.Int31n(4) == 0 {
best = int(rand.Int31n(int32(n)))
}
ghost.x = opts[best].x
ghost.y = opts[best].y
ghost.dir = opts[best].dir
}
}
func (g *PacmanGame) canMove(x, y, dir int16) bool {
nx := x + pmDX(dir)
ny := y + pmDY(dir)
if nx < 0 || nx >= pmW || ny < 0 || ny >= pmH {
return false
}
return g.maze[ny][nx] != pmWall
}
func pmDX(dir int16) int16 {
switch dir {
case pmLeft:
return -1
case pmRight:
return 1
}
return 0
}
func pmDY(dir int16) int16 {
switch dir {
case pmUp:
return -1
case pmDown:
return 1
}
return 0
}
func pmReverse(dir int16) int16 {
switch dir {
case pmUp:
return pmDown
case pmDown:
return pmUp
case pmLeft:
return pmRight
case pmRight:
return pmLeft
}
return -1
}
func pmAbs(x int16) int16 {
if x < 0 {
return -x
}
return x
}

320
reflex.go Normal file
View file

@ -0,0 +1,320 @@
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)
}