Merge branch 'main' of code.madriguera.me:GoEducation/badges
This commit is contained in:
commit
a258ee0540
8 changed files with 1058 additions and 2 deletions
3
badge.go
3
badge.go
|
|
@ -42,8 +42,7 @@ func Badge() {
|
||||||
selectedScreen = 0
|
selectedScreen = 0
|
||||||
fallthrough
|
fallthrough
|
||||||
case 0:
|
case 0:
|
||||||
//showLogoBin()
|
showLogoBin()
|
||||||
selectedScreen++
|
|
||||||
if quit {
|
if quit {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ var menuOptions = []int8{
|
||||||
modeGameSnake,
|
modeGameSnake,
|
||||||
modeGameLife,
|
modeGameLife,
|
||||||
modeGameColors,
|
modeGameColors,
|
||||||
|
modeGameReflex,
|
||||||
|
modeGamePacman,
|
||||||
modeInfo,
|
modeInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ var menuOptions = []int8{
|
||||||
modeGameSnake,
|
modeGameSnake,
|
||||||
modeGameLife,
|
modeGameLife,
|
||||||
modeGameColors,
|
modeGameColors,
|
||||||
|
modeGameReflex,
|
||||||
|
modeGamePacman,
|
||||||
modeInfo,
|
modeInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ var menuOptions = []int8{
|
||||||
modeGameSnake,
|
modeGameSnake,
|
||||||
modeGameLife,
|
modeGameLife,
|
||||||
modeGameColors,
|
modeGameColors,
|
||||||
|
modeGameReflex,
|
||||||
|
modeGamePacman,
|
||||||
modeInfo,
|
modeInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
7
main.go
7
main.go
|
|
@ -25,6 +25,7 @@ var (
|
||||||
colorBlackLED = color.RGBA{0, 0, 0, 0}
|
colorBlackLED = color.RGBA{0, 0, 0, 0}
|
||||||
colorText = color.RGBA{160, 160, 160, 255}
|
colorText = color.RGBA{160, 160, 160, 255}
|
||||||
colorOrange = color.RGBA{255, 153, 51, 255}
|
colorOrange = color.RGBA{255, 153, 51, 255}
|
||||||
|
colorYellow = color.RGBA{255, 255, 0, 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -50,6 +51,8 @@ const (
|
||||||
modeGameSnake
|
modeGameSnake
|
||||||
modeGameLife
|
modeGameLife
|
||||||
modeGameColors
|
modeGameColors
|
||||||
|
modeGameReflex
|
||||||
|
modeGamePacman
|
||||||
)
|
)
|
||||||
|
|
||||||
var buttonsState []bool
|
var buttonsState []bool
|
||||||
|
|
@ -65,6 +68,8 @@ var options = []string{
|
||||||
"Snake",
|
"Snake",
|
||||||
"Game of Life",
|
"Game of Life",
|
||||||
"Color Game",
|
"Color Game",
|
||||||
|
"Reflex",
|
||||||
|
"Pac-Man",
|
||||||
}
|
}
|
||||||
|
|
||||||
var leds ws2812.Device
|
var leds ws2812.Device
|
||||||
|
|
@ -74,6 +79,8 @@ var accel lis3dh.Device
|
||||||
var defaultFont tinyfont.Fonter
|
var defaultFont tinyfont.Fonter
|
||||||
|
|
||||||
var snakeGame = NewSnakeGame()
|
var snakeGame = NewSnakeGame()
|
||||||
|
var reflexGame = NewReflexGame()
|
||||||
|
var pacmanGame = NewPacmanGame()
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
|
|
||||||
4
menu.go
4
menu.go
|
|
@ -35,6 +35,10 @@ func runMenuOption(selected int16) {
|
||||||
GameOfLife()
|
GameOfLife()
|
||||||
case modeGameColors:
|
case modeGameColors:
|
||||||
ColorGame()
|
ColorGame()
|
||||||
|
case modeGameReflex:
|
||||||
|
reflexGame.Loop()
|
||||||
|
case modeGamePacman:
|
||||||
|
pacmanGame.Loop()
|
||||||
case modeInfo:
|
case modeInfo:
|
||||||
Info()
|
Info()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
720
pacman.go
Normal file
720
pacman.go
Normal 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
320
reflex.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue