diff --git a/LICENSE b/LICENSE index fbac088..f260d25 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2021 Alexander Stephan alexanderstephan.xyz +Copyright (c) 2023 Alexander Stephan alexanderstephan.xyz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Makefile b/Makefile index 64c946e..83240e6 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,30 @@ CC = go TARGET = gobra bindir = /usr/local/bin +SOURCE_DIRS := $(shell find . -type d) +vpath %.go $(SOURCE_DIRS) -$(TARGET): *.go +$(TARGET): cmd/main.go # Fix for ncurses install: https://github.com/rthornton128/goncurses/issues/56 export CGO_CFLAGS_ALLOW=".*" export CGO_LDFLAGS_ALLOW=".*" - $(CC) get -x github.com/alexanderstephan/goncurses + $(CC) get -x github.com/rthornton128/goncurses $(CC) get -x github.com/hajimehoshi/oto - $(CC) build -x + $(CC) build -o $(TARGET) -x $< export CGO_CFLAGS_ALLOW= export CGO_LDFLAGS_ALLOW= +.PHONY: all all: $(TARGET) +.PHONY: install install: all mv $(TARGET) $(DESTDIR)$(bindir)/$(TARGET) +.PHONY: uninstall uninstall: rm -f $(DESTDIR)$(bindir)/$(TARGET) +.PHONY: clean clean: rm -f $(TARGET) diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..b7f3109 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + "gobra/internal/gameplay" + "os" + "os/signal" + "syscall" +) + +func main() { + var cfg gameplay.Config + + flag.BoolVar(&cfg.Vim, "v", false, "Enable vim bindings") + flag.BoolVar(&cfg.DebugInfo, "d", false, "Print debug info") + flag.BoolVar(&cfg.NoBounds, "n", false, "Free boundaries") + flag.BoolVar(&cfg.Sound, "s", false, "Enable sound") + + flag.Parse() + + // Setup signal handler. + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + gameplay.Run = false // TODO: Cancel context + }() + fmt.Println(cfg.Vim) + gameplay.Start(&cfg) +} diff --git a/go.mod b/go.mod index f54548a..d044557 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,13 @@ module gobra -go 1.17 +go 1.21 require ( - github.com/alexanderstephan/goncurses v0.0.0-20200808213228-94cbc32628dd github.com/hajimehoshi/oto/v2 v2.4.1 + github.com/rthornton128/goncurses v0.0.0-20231014161942-82671379df88 ) require ( github.com/ebitengine/purego v0.4.0 // indirect - github.com/rthornton128/goncurses v0.0.0-20210908011339-931b33a34c71 // indirect golang.org/x/sys v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 552c511..ec9ca8f 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,8 @@ -github.com/alexanderstephan/goncurses v0.0.0-20200808213228-94cbc32628dd h1:CL10HSMSUDGBD1Iz/KzqEc6UX13+z7d1EXgf7PrClNY= -github.com/alexanderstephan/goncurses v0.0.0-20200808213228-94cbc32628dd/go.mod h1:ceMh1H2Mi5wk1+YVlxmlVb6zTrn0YEZt9F1QfeAInA0= -github.com/ebitengine/purego v0.3.0 h1:BDv9pD98k6AuGNQf3IF41dDppGBOe0F4AofvhFtBXF4= -github.com/ebitengine/purego v0.3.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.4.0 h1:RQVuMIxQPQ5iCGEJvjQ17YOK+1tMKjVau2FUMvXH4HE= github.com/ebitengine/purego v0.4.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/hajimehoshi/oto/v2 v2.0.2 h1:kbMCXhKAZMVyhEEyIqEqmSf4xjfk3HPAf1tMcyKCsN0= -github.com/hajimehoshi/oto/v2 v2.0.2/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ= -github.com/hajimehoshi/oto/v2 v2.3.1 h1:qrLKpNus2UfD674oxckKjNJmesp9hMh7u7QCrStB3Rc= -github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= -github.com/hajimehoshi/oto/v2 v2.4.0 h1:2A8QvGJZ7nXwcfIIthaqWdzDn9Ul/er6oASiKcsfiLg= -github.com/hajimehoshi/oto/v2 v2.4.0/go.mod h1:74bRBgfJaEDpP3NyVyHIYBJE4DgzJ2IP5l/st5qcJog= github.com/hajimehoshi/oto/v2 v2.4.1 h1:iTfZSulqdmQ5Hh4tVyVzNnK3aA4SgjbDapSM0YH3Lc4= github.com/hajimehoshi/oto/v2 v2.4.1/go.mod h1:guyF8uIgSrchrKewS1E6Xyx7joUbKOi4g9W7vpcYBSc= -github.com/rthornton128/goncurses v0.0.0-20210908011339-931b33a34c71 h1:1lA/ljJAwqsNo7HHUG7M4XY9dkp6aYnnW5Tglh7bEgA= -github.com/rthornton128/goncurses v0.0.0-20210908011339-931b33a34c71/go.mod h1:AHlKFomPTwmO7H2vL8d7VNrQNQmhMi/DBhDnHRhjbCo= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/rthornton128/goncurses v0.0.0-20231014161942-82671379df88 h1:bNvZ7P4l4rvg1O3qNDRJ4FNgKASMoG+V7OIpu4SIkgA= +github.com/rthornton128/goncurses v0.0.0-20231014161942-82671379df88/go.mod h1:AHlKFomPTwmO7H2vL8d7VNrQNQmhMi/DBhDnHRhjbCo= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gobra b/gobra deleted file mode 100755 index 6e1cde5..0000000 Binary files a/gobra and /dev/null differ diff --git a/gobra.go b/gobra.go deleted file mode 100644 index a3c20a8..0000000 --- a/gobra.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - gc "github.com/alexanderstephan/goncurses" -) - -type Direction int - -const ( - North Direction = iota - East - South - West -) - -var d Direction = East - -func (d Direction) String() string { - return [...]string{"North", "East", "South", "West"}[d] -} - -// InitSnake draws snake in the screen center pointing east -func InitSnake(stdscr *gc.Window) { - screenY, screenX := stdscr.MaxYX() - for i := 0; i < startSize; i++ { - snake.PushFront(Segment{y: screenY / 2, x: screenX/2 + i}) - } -} - -func MoveSnake() { - // Delete last element of the snake - snake.Remove(snake.Back()) - - // Read coordinates of the first snake segment - headY := snake.Front().Value.(Segment).y - headX := snake.Front().Value.(Segment).x - - // Increment or decrement last position according to direction - switch d { - case North: - headY-- - case South: - headY++ - case West: - headX-- - case East: - headX++ - } - - // Insert head with new position - snake.PushFront(Segment{headY, headX}) -} - -func GrowSnake(size int) { - for i := 0; i < size; i++ { - tailY := snake.Back().Value.(Segment).y - tailX := snake.Back().Value.(Segment).x - - // Move segment in the opposite direction - switch d { - case North: - tailY++ - case South: - tailY-- - case West: - tailX++ - case East: - tailX-- - } - - // Insert segment at back with new position - snake.PushBack(Segment{y: tailY, x: tailX}) - } -} - -func RenderSnake(stdscr *gc.Window) { - // Traverse list and draw every segment to the screen depending on the snake state - currentSegment := snake.Front() - for currentSegment != nil { - if snakeActive { - stdscr.MoveAddChar(currentSegment.Value.(Segment).y, currentSegment.Value.(Segment).x, gc.Char(snakeAlive)) - } else { - stdscr.MoveAddChar(currentSegment.Value.(Segment).y, currentSegment.Value.(Segment).x, gc.Char(snakeDead)) - } - currentSegment = currentSegment.Next() - } - - // Attach head - if snakeActive { - stdscr.MoveAddChar(snake.Front().Value.(Segment).y, snake.Front().Value.(Segment).x, snakeHead) - } -} diff --git a/controls.go b/internal/gameplay/controls.go similarity index 72% rename from controls.go rename to internal/gameplay/controls.go index d22022a..6ffee58 100644 --- a/controls.go +++ b/internal/gameplay/controls.go @@ -1,7 +1,9 @@ -package main +package gameplay import ( - gc "github.com/alexanderstephan/goncurses" + "fmt" + + gc "github.com/rthornton128/goncurses" ) // Controls @@ -12,9 +14,10 @@ var ( keyRight byte ) +// HandleKeys handles keyboard input for controlling the snake and performing other actions. func HandleKeys(input *gc.Window, stdscr *gc.Window, myFood *Food) bool { // Get input from a dedicated window, otherwise stdscr would be blocked - // Define input handlers with interrupt condition + // Define input handlers with interrupt condition. switch input.GetChar() { case gc.Key(keyUp): if d != South { @@ -43,13 +46,15 @@ func HandleKeys(input *gc.Window, stdscr *gc.Window, myFood *Food) bool { } func initKeybindings(isVim bool) { - // Remap to vim like bindings + // Remap to vim like bindings. if isVim { + fmt.Println(" is Vim") keyLeft = 'h' keyDown = 'j' keyUp = 'k' keyRight = 'l' } else { + fmt.Println(" is not Vim") keyLeft = 'a' keyUp = 'w' keyDown = 's' diff --git a/gameboard.go b/internal/gameplay/gameboard.go similarity index 85% rename from gameboard.go rename to internal/gameplay/gameboard.go index 1f178eb..f29b2e8 100644 --- a/gameboard.go +++ b/internal/gameplay/gameboard.go @@ -1,14 +1,15 @@ -package main +package gameplay import ( "container/list" - "io/ioutil" + "gobra/internal/tools" "log" "math/rand" + "os" "strconv" "time" - gc "github.com/alexanderstephan/goncurses" + gc "github.com/rthornton128/goncurses" ) // Trackers @@ -19,7 +20,7 @@ var ( screen Screen - // Create a rectangle window that is a placeholder for the snake + // Create a rectangle window that is a placeholder for the snake. input *gc.Window gobraASCII = []string{ ` 888 `, @@ -69,7 +70,7 @@ func drawLogo(stdscr *gc.Window, rows, cols int) { } func initColors() { - // Set up colors + // Set up colors. if err := gc.InitPair(1, gc.C_GREEN, gc.C_BLACK); err != nil { log.Fatal("InitPair failed: ", err) } @@ -95,7 +96,7 @@ func initColors() { } } -// InitFood initializes the foods position +// InitFood initializes the foods position. func printFood(stdscr *gc.Window, newFood *Food, rows, cols int) { stdscr.ColorOn(2) stdscr.MoveAddChar(newFood.y, newFood.x, foodChar) @@ -135,7 +136,7 @@ func gameOver(menu *gc.Window, rows, cols int) { func handleCollisions(stdscr *gc.Window, myFood *Food, rows, cols int) bool { snakeFront := snake.Front().Value.(Segment) - // Detect food collision + // Detect food collision. if snakeFront.y == myFood.y && snakeFront.x == myFood.x { for !testFoodCollision(stdscr, myFood, rows, cols) { myFood.y = rand.Intn(rows) @@ -144,11 +145,11 @@ func handleCollisions(stdscr *gc.Window, myFood *Food, rows, cols int) bool { GrowSnake(growRate) - // Calculate score + // Calculate score. newTime = time.Now() - r, err := ioutil.ReadFile("/tmp/score") - check(err) + r, err := os.ReadFile("/tmp/score") + tools.Check(err) prevScore, err := strconv.Atoi(string(r)) if err != nil { @@ -160,55 +161,55 @@ func handleCollisions(stdscr *gc.Window, myFood *Food, rows, cols int) bool { if globalScore > prevScore { d := []byte(strconv.Itoa(globalScore)) highscore = true - err := ioutil.WriteFile("/tmp/score", d, 0644) - check(err) + err := os.WriteFile("/tmp/score", d, 0644) + tools.Check(err) } - // Reset timer for next food collection + // Reset timer for next food collection. startTime = time.Now() return false } - // Check if head is element of the body - // First body element is the one after the head + // Check if head is element of the body. + // First body element is the one after the head. bodyElement := snake.Front().Next() for bodyElement != nil { if (snakeFront.y == bodyElement.Value.(Segment).y) && (snakeFront.x == bodyElement.Value.(Segment).x) { snakeActive = false - // Interrupt for-loop + // Interrupt for-loop. break } - // Move to the next element + // Move to the next element. bodyElement = bodyElement.Next() } return true } -func boundaryCheck(nobounds *bool, rows int, cols int) { +func boundaryCheck(nobounds bool, rows int, cols int) { snakeFront := snake.Front().Value.(Segment) - // Detect boundaries - if !(*nobounds) { + // Detect boundaries. + if !(nobounds) { if (snakeFront.y > rows-2) || (snakeFront.y < 1) || (snakeFront.x > cols-2) || (snakeFront.x < 1) { snakeActive = false } return } if snakeFront.y > rows-2 { - // Hit bottom border + // Hit bottom border. snake.Remove(snake.Back()) snake.PushFront(Segment{1, snakeFront.x}) } else if snakeFront.y < 1 { - // Hit top border + // Hit top border. snake.Remove(snake.Back()) snake.PushFront(Segment{rows - 2, snakeFront.x}) } else if snakeFront.x > cols-2 { - // Hit right border + // Hit right border. snake.Remove(snake.Back()) snake.PushFront(Segment{snakeFront.y, 1}) } else if snakeFront.x < 1 { - // Hit left border + // Hit left border. snake.Remove(snake.Back()) snake.PushFront(Segment{snakeFront.y, cols - 2}) } @@ -236,28 +237,28 @@ func testFoodCollision(stdscr *gc.Window, myFood *Food, rows, cols int) bool { } func NewGame(stdscr *gc.Window, myFood *Food) { - // Revive the snake + // Revive the snake. snakeActive = true - // Reset direction + // Reset direction. d = East - // Empty list + // Empty list. snake = list.New() - // Trigger initial food spawn + // Trigger initial food spawn. myFood.y = 0 myFood.x = 0 - // Set up snake in original position + // Set up snake in original position. InitSnake(stdscr) - // Reset score + // Reset score. globalScore = 0 } func calcBoardSize(stdscr *gc.Window) { - // Use maximum screen width + // Use maximum screen width. screen.rows, screen.cols = stdscr.MaxYX() if screen.rows > MaxRows && screen.cols > MaxCols { screen.rows = MaxRows @@ -274,7 +275,7 @@ func SetupGameBoard() *gc.Window { log.Fatal(err) } - // End is required to preserve terminal after execution + // End is required to preserve terminal after execution. defer gc.End() // Has the terminal the capability to use color? @@ -291,7 +292,7 @@ func SetupGameBoard() *gc.Window { gc.Cursor(0) // Hide cursor gc.CBreak(true) // Disable input buffering - // Define colors + // Define colors. initColors() input, err = gc.NewWindow(0, 0, 0, 0) @@ -300,7 +301,7 @@ func SetupGameBoard() *gc.Window { } input.Refresh() - // Welcome screen with logo and controls + // Welcome screen with logo and controls. drawLogo(stdscr, screen.rows, screen.cols) return stdscr } diff --git a/internal/gameplay/gobra.go b/internal/gameplay/gobra.go new file mode 100644 index 0000000..a76f8ea --- /dev/null +++ b/internal/gameplay/gobra.go @@ -0,0 +1,234 @@ +package gameplay + +import ( + "container/list" + "gobra/internal/sound" + "gobra/internal/tools" + "os" + "strconv" + "time" + + gc "github.com/rthornton128/goncurses" +) + +var Run bool + +type Config struct { + Vim bool + DebugInfo bool + NoBounds bool + Sound bool +} + +const ( + foodChar = 'X' + snakeAlive = 'O' + snakeDead = '+' + snakeHead = '0' + startSize = 5 + growRate = 3 + scoreMulti = 20 +) + +var ( + // Objects + snake = list.New() + newFood = Food{} + + // States + snakeActive bool + highscore bool +) + +type Direction int + +const ( + North Direction = iota + East + South + West +) + +var d Direction = East + +func (d Direction) String() string { + return [...]string{"North", "East", "South", "West"}[d] +} + +// InitSnake draws snake in the screen center pointing east. +func InitSnake(stdscr *gc.Window) { + screenY, screenX := stdscr.MaxYX() + for i := 0; i < startSize; i++ { + snake.PushFront(Segment{y: screenY / 2, x: screenX/2 + i}) + } +} + +func Start(cfg *Config) { + Run = true + if _, err := os.Stat("/tmp/score"); os.IsNotExist(err) { + d := []byte("0") + err = os.WriteFile("/tmp/score", d, 0644) + tools.Check(err) + } + + if cfg.Sound { + sound.InitSound() + } + + stdscr := SetupGameBoard() + InitSnake(stdscr) // Create initial snake + initKeybindings(cfg.Vim) + input.Timeout(100) // Threshold for timeout + tools.Check(input.Keypad(true)) // Wait for keyboard input + snakeActive = true // Snake starts alive + frameCounter := 0 // Init frame count + + stdscr.Refresh() + time.Sleep(1 * time.Second) + + var scoreLength int + for Run { + // Clear screen + stdscr.Refresh() + stdscr.Erase() + + // Draw box around the screen (for collision detection) + // TODO: Do we need to redraw everything? + drawBorder(stdscr) + + // Print debug Infos + if cfg.DebugInfo { + frameCounter++ + stdscr.MovePrint(1, 1, "DEBUG:") + stdscr.MovePrint(2, 1, frameCounter) + stdscr.MovePrint(3, 1, d) + stdscr.MovePrint(4, 1, snake.Front().Value.(Segment).y) + stdscr.MovePrint(4, 4, snake.Front().Value.(Segment).x) + stdscr.MovePrint(5, 1, newFood.y) + stdscr.MovePrint(5, 4, newFood.x) + stdscr.MovePrint(6, 1, screen.rows) + stdscr.MovePrint(6, 4, screen.cols) + stdscr.MovePrint(7, 1, rune(stdscr.MoveInChar(0, 0))) + } + + // setSnakeDir returns false when the user presses q to exit -> interrupt loop + if !HandleKeys(input, stdscr, &newFood) { + break + } + + // Determine food position if not set yet + initFood(stdscr, &newFood, screen.rows, screen.cols) + + //stdscr.Refresh() + + // Display snake (alive or dead) + handleSnake(stdscr, screen.rows, screen.cols) + + // Handle collisions + if !handleCollisions(stdscr, &newFood, screen.rows, screen.cols) && cfg.Sound { + sound.Play(sound.FreqA, 250*time.Millisecond) + } + + // Check if snake hit boundaries, if desired ports the snake to the other side of the screen + boundaryCheck(cfg.NoBounds, screen.rows, screen.cols) + + // Render food symbol + printFood(stdscr, &newFood, screen.rows, screen.cols) + + // Overwrite border once again + drawBorder(stdscr) + scoreLength = len(strconv.Itoa(globalScore)) + + // Write score to border + stdscr.ColorOn(4) + stdscr.MovePrint(0, (screen.cols/2)-(scoreLength/2), globalScore) + stdscr.ColorOff(4) + stdscr.ColorOn(3) + stdscr.MoveAddChar(0, (screen.cols/2)-(scoreLength/2)-1, '|') + + if scoreLength%2 == 0 { + stdscr.MoveAddChar(0, (screen.cols/2)+(scoreLength/2), '|') + } else { + stdscr.MoveAddChar(0, (screen.cols/2)+(scoreLength/2)+1, '|') + } + + stdscr.ColorOff(3) + // Refresh changes in screen buffer + stdscr.Refresh() + // Flush characters that have changed + tools.Check(gc.Update()) + } + + gc.End() // Restore previous terminal state +} + +// MoveSnake updates the snake's position based on its current direction. +// It removes the last segment of the snake and inserts a new head segment at the front. +func MoveSnake() { + // Delete last element of the snake. + snake.Remove(snake.Back()) + + // Read coordinates of the first snake segment. + headY := snake.Front().Value.(Segment).y + headX := snake.Front().Value.(Segment).x + + // Increment or decrement last position according to direction. + switch d { + case North: + headY-- + case South: + headY++ + case West: + headX-- + case East: + headX++ + } + + // Insert head with new position. + snake.PushFront(Segment{headY, headX}) +} + +// GrowSnake increases the length of the snake by adding segments to its tail. +// It takes an integer parameter 'size' to determine how many segments to add. +func GrowSnake(size int) { + for i := 0; i < size; i++ { + tailY := snake.Back().Value.(Segment).y + tailX := snake.Back().Value.(Segment).x + + // Move segment in the opposite direction. + switch d { + case North: + tailY++ + case South: + tailY-- + case West: + tailX++ + case East: + tailX-- + } + + // Insert segment at back with new position. + snake.PushBack(Segment{y: tailY, x: tailX}) + } +} + +// RenderSnake renders the snake on the provided ncurses window (stdscr). +// It traverses the snake's linked list, drawing each segment based on the snake's state. +// The head of the snake is also drawn. +func RenderSnake(stdscr *gc.Window) { + // Traverse list and draw every segment to the screen depending on the snake state. + currentSegment := snake.Front() + for currentSegment != nil { + if snakeActive { + stdscr.MoveAddChar(currentSegment.Value.(Segment).y, currentSegment.Value.(Segment).x, gc.Char(snakeAlive)) + } else { + stdscr.MoveAddChar(currentSegment.Value.(Segment).y, currentSegment.Value.(Segment).x, gc.Char(snakeDead)) + } + currentSegment = currentSegment.Next() + } + + // Attach head. + if snakeActive { + stdscr.MoveAddChar(snake.Front().Value.(Segment).y, snake.Front().Value.(Segment).x, snakeHead) + } +} diff --git a/sound.go b/internal/sound/sound.go similarity index 86% rename from sound.go rename to internal/sound/sound.go index 921338c..543991a 100644 --- a/sound.go +++ b/internal/sound/sound.go @@ -1,4 +1,4 @@ -package main +package sound import ( "flag" @@ -10,17 +10,14 @@ import ( "github.com/hajimehoshi/oto/v2" ) -// Audio var ( + context *oto.Context sampleRate = flag.Int("samplerate", 44100, "sample rate") channelNum = flag.Int("channelnum", 3, "number of channels") bitDepthInBytes = flag.Int("bitdepthinbytes", 2, "bit depth in bytes") ) -var context *oto.Context - -// Sound frequencies -const freqA = 300 +const FreqA = 300 // sound frequency type sineWave struct { freq float64 @@ -39,6 +36,7 @@ func newSineWave(freq float64, duration time.Duration) *sineWave { } } +// Read reads data from the sineWave and fills the provided buffer. func (s *sineWave) Read(buf []byte) (int, error) { if len(s.remaining) > 0 { n := copy(buf, s.remaining) @@ -102,17 +100,16 @@ func (s *sineWave) Read(buf []byte) (int, error) { return n, nil } -// Play plays a sound at a given frequency for a duration in milliseconds -func play(freq float64, duration time.Duration) oto.Player { +// Play plays a sound at a given frequency for a duration in milliseconds. +func Play(freq float64, duration time.Duration) oto.Player { s := newSineWave(freq, duration) p := context.NewPlayer(s) p.Play() return p } -// InitSound initializes oto so sound can be played +// InitSound initializes oto so sound can be played. func InitSound() { - // Init sounds c, ready, err := oto.NewContext(*sampleRate, *channelNum, *bitDepthInBytes) if err != nil { log.Fatal(err) @@ -121,7 +118,7 @@ func InitSound() { context = c } -// PlayFoodSound plays a high pitched sound that should be played when the snake eats food +// PlayFoodSound plays a high pitched sound that should be played when the snake eats food. func PlayFoodSound() { - play(freqA, 250*time.Millisecond) + Play(FreqA, 250*time.Millisecond) } diff --git a/internal/tools/tools.go b/internal/tools/tools.go new file mode 100644 index 0000000..97a86fc --- /dev/null +++ b/internal/tools/tools.go @@ -0,0 +1,7 @@ +package tools + +func Check(e error) { + if e != nil { + panic(e) + } +} diff --git a/main.go b/main.go deleted file mode 100644 index 829a881..0000000 --- a/main.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "container/list" - "flag" - "io/ioutil" - "math/rand" - "os" - "os/signal" - "strconv" - "syscall" - "time" - - gc "github.com/alexanderstephan/goncurses" -) - -// Parameters to tweak the playing experience -const ( - foodChar = 'X' - snakeAlive = 'O' - snakeDead = '+' - snakeHead = '0' - startSize = 5 - growRate = 3 - scoreMulti = 20 -) - -var ( - run = true // Is the game running? - - // Objects - snake = list.New() - newFood = Food{} - - // States - snakeActive bool - highscore bool -) - -func check(e error) { - if e != nil { - panic(e) - } -} - -func main() { - vim := flag.Bool("v", false, "Enable vim bindings") - debugInfo := flag.Bool("d", false, "Print debug info") - noBounds := flag.Bool("n", false, "Free boundaries") - sound := flag.Bool("s", false, "Enable sound") - - flag.Parse() - - // Randomize pseudo random functions - rand.Seed(time.Now().Unix()) - - // Setup signal handler. - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sigs - run = false - }() - - if _, err := os.Stat("/tmp/score"); os.IsNotExist(err) { - d := []byte("0") - err = ioutil.WriteFile("/tmp/score", d, 0644) - check(err) - } - - stdscr := SetupGameBoard() - InitSnake(stdscr) // Create initial snake - initKeybindings(*vim) - input.Timeout(100) // Threshold for timeout - input.Keypad(true) // Wait for keyboard input - snakeActive = true // Snake starts alive - frameCounter := 0 // Init frame count - - stdscr.Refresh() - time.Sleep(1 * time.Second) - - var scoreLength int - for run { - // Clear screen - stdscr.Refresh() - stdscr.Erase() - - // Draw box around the screen (for collision detection) - // TODO: Do we need to redraw everything? - drawBorder(stdscr) - - // Print debug Infos - if *debugInfo { - frameCounter++ - stdscr.MovePrint(1, 1, "DEBUG:") - stdscr.MovePrint(2, 1, frameCounter) - stdscr.MovePrint(3, 1, d) - stdscr.MovePrint(4, 1, snake.Front().Value.(Segment).y) - stdscr.MovePrint(4, 4, snake.Front().Value.(Segment).x) - stdscr.MovePrint(5, 1, newFood.y) - stdscr.MovePrint(5, 4, newFood.x) - stdscr.MovePrint(6, 1, screen.rows) - stdscr.MovePrint(6, 4, screen.cols) - stdscr.MovePrint(7, 1, rune(stdscr.MoveInChar(0, 0))) - } - - // setSnakeDir returns false when the user presses q to exit -> interrupt loop - if !HandleKeys(input, stdscr, &newFood) { - break - } - - // Determine food position if not set yet - initFood(stdscr, &newFood, screen.rows, screen.cols) - - //stdscr.Refresh() - - // Display snake (alive or dead) - handleSnake(stdscr, screen.rows, screen.cols) - - // Handle collisions - if !handleCollisions(stdscr, &newFood, screen.rows, screen.cols) && *sound { - play(freqA, 250*time.Millisecond) - } - - // Check if snake hit boundaries, if desired ports the snake to the other side of the screen - boundaryCheck(noBounds, screen.rows, screen.cols) - - // Render food symbol - printFood(stdscr, &newFood, screen.rows, screen.cols) - - // Overwrite border once again - drawBorder(stdscr) - scoreLength = len(strconv.Itoa(globalScore)) - - // Write score to border - stdscr.ColorOn(4) - stdscr.MovePrint(0, (screen.cols/2)-(scoreLength/2), globalScore) - stdscr.ColorOff(4) - stdscr.ColorOn(3) - stdscr.MoveAddChar(0, (screen.cols/2)-(scoreLength/2)-1, '|') - - if scoreLength%2 == 0 { - stdscr.MoveAddChar(0, (screen.cols/2)+(scoreLength/2), '|') - } else { - stdscr.MoveAddChar(0, (screen.cols/2)+(scoreLength/2)+1, '|') - } - - stdscr.ColorOff(3) - // Refresh changes in screen buffer - stdscr.Refresh() - // Flush characters that have changed - gc.Update() - } - - gc.End() // Restore previous terminal state -}