Skip to content

Instantly share code, notes, and snippets.

@michaellihs
Created December 16, 2019 17:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaellihs/7afdd9a8088e893b84be3046bafb8807 to your computer and use it in GitHub Desktop.
Save michaellihs/7afdd9a8088e893b84be3046bafb8807 to your computer and use it in GitHub Desktop.
Pacman in Golang
package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"github.com/danicat/simpleansi"
"log"
"math/rand"
"os"
"os/exec"
"strconv"
"time"
)
type sprite struct {
row int
col int
startRow int
startCol int
}
type Config struct {
Player string `json:"player"`
Ghost string `json:"ghost"`
Wall string `json:"wall"`
Dot string `json:"dot"`
Pill string `json:"pill"`
Death string `json:"death"`
Space string `json:"space"`
UseEmoji bool `json:"use_emoji"`
}
var (
configFile = flag.String("config-file", "config.json", "path to custom configuration file")
mazeFile = flag.String("maze-file", "maze01.txt", "path to a custom maze file")
)
var player sprite
var ghosts []*sprite
var cfg Config
var score int
var numDots int
var lives = 3
var maze []string
func main() {
flag.Parse()
initialise()
defer cleanup()
err := loadMaze(*mazeFile)
if err != nil {
log.Println("Failed loading maze: ", err)
return
}
err = loadConfig(*configFile)
if err != nil {
log.Println("failed to load configuration:", err)
return
}
input := make(chan string)
go func(ch chan<- string) {
for {
input, err := readInput()
if err != nil {
log.Println("error reading input:", err)
ch <- "ESC"
}
ch <- input
}
}(input)
for {
// process movement
select {
case inp := <-input:
if inp == "ESC" {
lives = 0
}
movePlayer(inp)
default:
}
moveGhosts()
// process collisions
for _, g := range ghosts {
if player.row == g.row && player.col == g.col {
lives = lives - 1
if lives != 0 {
moveCursor(player.row, player.col)
fmt.Print(cfg.Death)
moveCursor(len(maze)+2, 0)
time.Sleep(1000 * time.Millisecond) //dramatic pause before resetting player position
player.row, player.col = player.startRow, player.startCol
}
}
}
// update screen
printScreen()
// check game over
if numDots == 0 || lives == 0 {
if lives == 0 {
moveCursor(player.row, player.col)
fmt.Print(cfg.Death)
moveCursor(len(maze)+2, 0)
}
break
}
// repeat
time.Sleep(200 * time.Millisecond)
}
}
func initialise() {
cbTerm := exec.Command("stty", "cbreak", "-echo")
cbTerm.Stdin = os.Stdin
err := cbTerm.Run()
if err != nil {
log.Fatalln("unable to activate cbreak mode:", err)
}
}
func loadConfig(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
decoder := json.NewDecoder(f)
err = decoder.Decode(&cfg)
if err != nil {
return err
}
return nil
}
func cleanup() {
cookedTerm := exec.Command("stty", "-cbreak", "echo")
cookedTerm.Stdin = os.Stdin
err := cookedTerm.Run()
if err != nil {
log.Fatalln("unable to restore cooked mode:", err)
}
}
func readInput() (string, error) {
buffer := make([]byte, 100)
cnt, err := os.Stdin.Read(buffer)
if err != nil {
return "", err
}
if cnt == 1 && buffer[0] == 0x1b {
return "ESC", nil
} else if cnt >= 3 {
if buffer[0] == 0x1b && buffer[1] == '[' {
switch buffer[2] {
case 'A' :
return "UP", nil
case 'B' :
return "DOWN", nil
case 'C' :
return "RIGHT", nil
case 'D' :
return "LEFT", nil
}
}
}
return "", nil
}
func loadMaze(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
maze = append(maze, line)
}
for row, line := range maze {
for col, char := range line {
switch char {
case 'P' :
player = sprite{row, col, row, col}
case 'G':
ghosts = append(ghosts, &sprite{row, col, row, col})
case '.' :
numDots++
}
}
}
return nil
}
func printScreen() {
simpleansi.ClearScreen()
for _, line := range maze {
for _, chr := range line {
switch chr {
case '#':
fmt.Print(simpleansi.WithBlueBackground(cfg.Wall))
case '.':
fmt.Print(cfg.Dot)
default:
fmt.Print(cfg.Space)
}
}
fmt.Println()
}
moveCursor(player.row, player.col)
fmt.Print(cfg.Player)
for _, g := range ghosts {
moveCursor(g.row, g.col)
fmt.Print(cfg.Ghost)
}
moveCursor(len(maze)+1, 0)
livesRemaining := strconv.Itoa(lives) //converts lives int to a string
if cfg.UseEmoji {
livesRemaining = getLivesAsEmoji()
}
fmt.Println("Score:", score, "\tLives:", livesRemaining)
}
func getLivesAsEmoji() string{
buf := bytes.Buffer{}
for i := lives; i > 0; i-- {
buf.WriteString(cfg.Player)
}
return buf.String()
}
func moveCursor(row, col int) {
if cfg.UseEmoji {
simpleansi.MoveCursor(row, col*2)
} else {
simpleansi.MoveCursor(row, col)
}
}
func movePlayer(input string) {
player.row, player.col = makeMove(player.row, player.col, input)
switch maze[player.row][player.col] {
case '.':
numDots--
score++
maze[player.row] = maze[player.row][0:player.col] + " " + maze[player.row][player.col+1:]
}
}
func makeMove(oldRow, oldCol int, dir string) (newRow, newCol int) {
newRow, newCol = oldRow, oldCol
switch dir {
case "UP" :
newRow = oldRow - 1
if newRow < 0 {
newRow = len(maze) - 1
}
case "DOWN" :
newRow = oldRow + 1
if newRow == len(maze) -1 {
newRow = 0
}
case "LEFT" :
newCol = oldCol - 1
if newCol < 0 {
newCol = len(maze[0]) - 1
}
case "RIGHT" :
newCol = oldCol + 1
if newCol == len(maze[0]) {
newCol = 0
}
}
if maze[newRow][newCol] == '#' {
newRow, newCol = oldRow, oldCol
}
return
}
func moveGhosts() {
for _, ghost := range ghosts {
dir := drawDirection()
ghost.row, ghost.col = makeMove(ghost.row, ghost.col, dir)
}
}
func drawDirection() string {
dir := rand.Intn(4)
move := map[int]string {
0: "UP",
1: "DOWN",
2: "LEFT",
3: "RIGHT",
}
return move[dir]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment