summaryrefslogtreecommitdiffstats
path: root/pkg/snake
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-12-29 14:32:33 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-12-29 14:32:33 +1100
commit81281a49b21c5d708e2b5ed70dc5ca5a27ea6db7 (patch)
treed20dcab87b15d85f798614f93267a45b0ec2e2c8 /pkg/snake
parentff8823093ccc3a15bce45a589c468d8b4fbbb0cb (diff)
add snake game
Diffstat (limited to 'pkg/snake')
-rw-r--r--pkg/snake/cmd/main.go74
-rw-r--r--pkg/snake/snake.go165
-rw-r--r--pkg/snake/snake_test.go62
3 files changed, 301 insertions, 0 deletions
diff --git a/pkg/snake/cmd/main.go b/pkg/snake/cmd/main.go
new file mode 100644
index 000000000..35f27405a
--- /dev/null
+++ b/pkg/snake/cmd/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+
+ "github.com/jesseduffield/lazygit/pkg/snake"
+)
+
+func main() {
+ game := snake.NewGame(10, 10, render)
+ ctx := context.Background()
+ game.Start(ctx)
+
+ go func() {
+ for {
+ var input string
+ fmt.Scanln(&input)
+
+ switch input {
+ case "w":
+ game.SetDirection(snake.Up)
+ case "s":
+ game.SetDirection(snake.Down)
+ case "a":
+ game.SetDirection(snake.Left)
+ case "d":
+ game.SetDirection(snake.Right)
+ }
+ }
+ }()
+
+ time.Sleep(100 * time.Second)
+}
+
+func render(cells [][]snake.CellType, alive bool) {
+ if !alive {
+ log.Fatal("YOU DIED!")
+ }
+
+ writer := &strings.Builder{}
+
+ width := len(cells[0])
+
+ writer.WriteString(strings.Repeat("\n", 20))
+
+ writer.WriteString(strings.Repeat("█", width+2) + "\n")
+
+ for _, row := range cells {
+ writer.WriteString("█")
+
+ for _, cell := range row {
+ switch cell {
+ case snake.None:
+ writer.WriteString(" ")
+ case snake.Snake:
+ writer.WriteString("X")
+ case snake.Food:
+ writer.WriteString("o")
+ }
+ }
+
+ writer.WriteString("█")
+
+ writer.WriteString("\n")
+ }
+
+ writer.WriteString(strings.Repeat("█", width+2))
+
+ fmt.Println(writer.String())
+}
diff --git a/pkg/snake/snake.go b/pkg/snake/snake.go
new file mode 100644
index 000000000..a6936afbc
--- /dev/null
+++ b/pkg/snake/snake.go
@@ -0,0 +1,165 @@
+package snake
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "time"
+
+ "github.com/samber/lo"
+)
+
+type Position struct {
+ x int
+ y int
+}
+
+type Direction int
+
+const (
+ Up Direction = iota
+ Down
+ Left
+ Right
+)
+
+type CellType int
+
+const (
+ None CellType = iota
+ Snake
+ Food
+)
+
+type State struct {
+ // first element is the head, final element is the tail
+ snakePositions []Position
+ direction Direction
+ foodPosition Position
+}
+
+type Game struct {
+ state State
+
+ width int
+ height int
+ render func(cells [][]CellType, alive bool)
+
+ randIntFn func(int) int
+}
+
+func NewGame(width, height int, render func(cells [][]CellType, dead bool)) *Game {
+ return &Game{
+ width: width,
+ height: height,
+ render: render,
+ randIntFn: rand.Intn,
+ }
+}
+
+func (self *Game) Start(ctx context.Context) {
+ self.initializeState()
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(time.Duration(500/self.getSpeed()) * time.Millisecond):
+ fmt.Println("updating")
+
+ alive := self.tick()
+ self.render(self.getCells(), alive)
+ if !alive {
+ return
+ }
+ }
+ }
+ }()
+}
+
+func (self *Game) initializeState() {
+ centerOfScreen := Position{self.width / 2, self.height / 2}
+
+ self.state = State{
+ snakePositions: []Position{centerOfScreen},
+ direction: Right,
+ }
+
+ self.state.foodPosition = self.setNewFoodPos()
+}
+
+// assume the player never actually wins, meaning we don't get stuck in a loop
+func (self *Game) setNewFoodPos() Position {
+ for i := 0; i < 1000; i++ {
+ newFoodPos := Position{self.randIntFn(self.width), self.randIntFn(self.height)}
+
+ if !lo.Contains(self.state.snakePositions, newFoodPos) {
+ return newFoodPos
+ }
+ }
+
+ panic("SORRY, BUT I WAS TOO LAZY TO MAKE THE SNAKE GAME SMART ENOUGH TO PUT THE FOOD SOMEWHERE SENSIBLE NO MATTER WHAT, AND I ALSO WAS TOO LAZY TO ADD A WIN CONDITION")
+}
+
+// returns whether the snake is alive
+func (self *Game) tick() bool {
+ newHeadPos := self.state.snakePositions[0]
+
+ switch self.state.direction {
+ case Up:
+ newHeadPos.y--
+ case Down:
+ newHeadPos.y++
+ case Left:
+ newHeadPos.x--
+ case Right:
+ newHeadPos.x++
+ }
+
+ if newHeadPos.x < 0 || newHeadPos.x >= self.width || newHeadPos.y < 0 || newHeadPos.y >= self.height {
+ return false
+ }
+
+ if lo.Contains(self.state.snakePositions, newHeadPos) {
+ return false
+ }
+
+ self.state.snakePositions = append([]Position{newHeadPos}, self.state.snakePositions...)
+
+ if newHeadPos == self.state.foodPosition {
+ self.state.foodPosition = self.setNewFoodPos()
+ } else {
+ self.state.snakePositions = self.state.snakePositions[:len(self.state.snakePositions)-1]
+ }
+
+ return true
+}
+
+func (self *Game) getSpeed() int {
+ return len(self.state.snakePositions)
+}
+
+func (self *Game) getCells() [][]CellType {
+ cells := make([][]CellType, self.height)
+
+ setCell := func(pos Position, value CellType) {
+ cells[pos.y][pos.x] = value
+ }
+
+ for i := 0; i < self.height; i++ {
+ cells[i] = make([]CellType, self.width)
+ }
+
+ for _, pos := range self.state.snakePositions {
+ setCell(pos, Snake)
+ }
+
+ setCell(self.state.foodPosition, Food)
+
+ return cells
+}
+
+func (self *Game) SetDirection(direction Direction) {
+ self.state.direction = direction
+}
diff --git a/pkg/snake/snake_test.go b/pkg/snake/snake_test.go
new file mode 100644
index 000000000..9f1906e37
--- /dev/null
+++ b/pkg/snake/snake_test.go
@@ -0,0 +1,62 @@
+package snake
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSnake(t *testing.T) {
+ scenarios := []struct {
+ state State
+ expectedState State
+ expectedAlive bool
+ }{
+ {
+ state: State{
+ snakePositions: []Position{{x: 5, y: 5}},
+ direction: Right,
+ foodPosition: Position{x: 9, y: 9},
+ },
+ expectedState: State{
+ snakePositions: []Position{{x: 6, y: 5}},
+ direction: Right,
+ foodPosition: Position{x: 9, y: 9},
+ },
+ expectedAlive: true,
+ },
+ {
+ state: State{
+ snakePositions: []Position{{x: 5, y: 5}, {x: 4, y: 5}, {x: 4, y: 4}, {x: 5, y: 4}},
+ direction: Up,
+ foodPosition: Position{x: 9, y: 9},
+ },
+ expectedState: State{},
+ expectedAlive: false,
+ },
+ {
+ state: State{
+ snakePositions: []Position{{x: 5, y: 5}},
+ direction: Right,
+ foodPosition: Position{x: 6, y: 5},
+ },
+ expectedState: State{
+ snakePositions: []Position{{x: 6, y: 5}, {x: 5, y: 5}},
+ direction: Right,
+ foodPosition: Position{x: 8, y: 8},
+ },
+ expectedAlive: true,
+ },
+ }
+
+ for _, scenario := range scenarios {
+ game := NewGame(10, 10, nil)
+ game.state = scenario.state
+ game.randIntFn = func(int) int { return 8 }
+ alive := game.tick()
+ assert.Equal(t, scenario.expectedAlive, alive)
+ if scenario.expectedAlive {
+ assert.EqualValues(t, scenario.expectedState, game.state)
+ }
+ }
+}