Created
December 16, 2019 17:24
-
-
Save michaellihs/7afdd9a8088e893b84be3046bafb8807 to your computer and use it in GitHub Desktop.
Pacman in Golang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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