720 lines
16 KiB
Go
720 lines
16 KiB
Go
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
|
|
}
|