diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-12-29 14:32:33 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-12-29 14:32:33 +1100 |
commit | 81281a49b21c5d708e2b5ed70dc5ca5a27ea6db7 (patch) | |
tree | d20dcab87b15d85f798614f93267a45b0ec2e2c8 /pkg/snake | |
parent | ff8823093ccc3a15bce45a589c468d8b4fbbb0cb (diff) |
add snake game
Diffstat (limited to 'pkg/snake')
-rw-r--r-- | pkg/snake/cmd/main.go | 74 | ||||
-rw-r--r-- | pkg/snake/snake.go | 165 | ||||
-rw-r--r-- | pkg/snake/snake_test.go | 62 |
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) + } + } +} |