summaryrefslogtreecommitdiffstats
path: root/pkg/snake
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-12-30 11:34:01 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-12-30 12:18:59 +1100
commitaf5b3be2861518737482f474e3b5ff6c2c551720 (patch)
tree5933a1254d7ad1d76c99ec302ed316d9865b772a /pkg/snake
parent81281a49b21c5d708e2b5ed70dc5ca5a27ea6db7 (diff)
integrate snake game into lazygit
Diffstat (limited to 'pkg/snake')
-rw-r--r--pkg/snake/cmd/main.go74
-rw-r--r--pkg/snake/snake.go174
-rw-r--r--pkg/snake/snake_test.go44
3 files changed, 132 insertions, 160 deletions
diff --git a/pkg/snake/cmd/main.go b/pkg/snake/cmd/main.go
deleted file mode 100644
index 35f27405a..000000000
--- a/pkg/snake/cmd/main.go
+++ /dev/null
@@ -1,74 +0,0 @@
-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
index a6936afbc..68feec15c 100644
--- a/pkg/snake/snake.go
+++ b/pkg/snake/snake.go
@@ -1,14 +1,47 @@
package snake
import (
- "context"
- "fmt"
"math/rand"
"time"
"github.com/samber/lo"
)
+type Game struct {
+ // width/height of the board
+ width int
+ height int
+
+ // function for rendering the game. If alive is false, the cells are expected
+ // to be ignored.
+ render func(cells [][]CellType, alive bool)
+
+ // closed when the game is exited
+ exit chan (struct{})
+
+ // channel for specifying the direction the player wants the snake to go in
+ setNewDir chan (Direction)
+
+ // allows logging for debugging
+ logger func(string)
+
+ // putting this on the struct for deterministic testing
+ randIntFn func(int) int
+}
+
+type State struct {
+ // first element is the head, final element is the tail
+ snakePositions []Position
+
+ foodPosition Position
+
+ // direction of the snake
+ direction Direction
+ // direction as of the end of the last tick. We hold onto this so that
+ // the snake can't do a 180 turn inbetween ticks
+ lastTickDirection Direction
+}
+
type Position struct {
x int
y int
@@ -31,70 +64,75 @@ const (
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 {
+func NewGame(width, height int, render func(cells [][]CellType, alive bool), logger func(string)) *Game {
return &Game{
width: width,
height: height,
render: render,
randIntFn: rand.Intn,
+ exit: make(chan struct{}),
+ logger: logger,
+ setNewDir: make(chan Direction),
}
}
-func (self *Game) Start(ctx context.Context) {
- self.initializeState()
+func (self *Game) Start() {
+ go self.gameLoop()
+}
+
+func (self *Game) Exit() {
+ close(self.exit)
+}
+
+func (self *Game) SetDirection(direction Direction) {
+ self.setNewDir <- direction
+}
- go func() {
- for {
- select {
- case <-ctx.Done():
+func (self *Game) gameLoop() {
+ state := self.initializeState()
+ var alive bool
+
+ self.render(self.getCells(state), true)
+
+ ticker := time.NewTicker(time.Duration(75) * time.Millisecond)
+
+ for {
+ select {
+ case <-self.exit:
+ return
+ case dir := <-self.setNewDir:
+ state.direction = self.newDirection(state, dir)
+ case <-ticker.C:
+ state, alive = self.tick(state)
+ self.render(self.getCells(state), alive)
+ if !alive {
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() {
+func (self *Game) initializeState() State {
centerOfScreen := Position{self.width / 2, self.height / 2}
+ snakePositions := []Position{centerOfScreen}
- self.state = State{
- snakePositions: []Position{centerOfScreen},
+ state := State{
+ snakePositions: snakePositions,
direction: Right,
+ foodPosition: self.newFoodPos(snakePositions),
}
- self.state.foodPosition = self.setNewFoodPos()
+ return state
}
-// 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++ {
+func (self *Game) newFoodPos(snakePositions []Position) Position {
+ // arbitrarily setting a limit of attempts to place food
+ attemptLimit := 1000
+
+ for i := 0; i < attemptLimit; i++ {
newFoodPos := Position{self.randIntFn(self.width), self.randIntFn(self.height)}
- if !lo.Contains(self.state.snakePositions, newFoodPos) {
+ if !lo.Contains(snakePositions, newFoodPos) {
return newFoodPos
}
}
@@ -103,10 +141,13 @@ func (self *Game) setNewFoodPos() Position {
}
// returns whether the snake is alive
-func (self *Game) tick() bool {
- newHeadPos := self.state.snakePositions[0]
+func (self *Game) tick(currentState State) (State, bool) {
+ nextState := currentState // copy by value
+ newHeadPos := nextState.snakePositions[0]
+
+ nextState.lastTickDirection = nextState.direction
- switch self.state.direction {
+ switch nextState.direction {
case Up:
newHeadPos.y--
case Down:
@@ -117,30 +158,25 @@ func (self *Game) tick() bool {
newHeadPos.x++
}
- if newHeadPos.x < 0 || newHeadPos.x >= self.width || newHeadPos.y < 0 || newHeadPos.y >= self.height {
- return false
- }
+ outOfBounds := newHeadPos.x < 0 || newHeadPos.x >= self.width || newHeadPos.y < 0 || newHeadPos.y >= self.height
+ eatingOwnTail := lo.Contains(nextState.snakePositions, newHeadPos)
- if lo.Contains(self.state.snakePositions, newHeadPos) {
- return false
+ if outOfBounds || eatingOwnTail {
+ return State{}, false
}
- self.state.snakePositions = append([]Position{newHeadPos}, self.state.snakePositions...)
+ nextState.snakePositions = append([]Position{newHeadPos}, nextState.snakePositions...)
- if newHeadPos == self.state.foodPosition {
- self.state.foodPosition = self.setNewFoodPos()
+ if newHeadPos == nextState.foodPosition {
+ nextState.foodPosition = self.newFoodPos(nextState.snakePositions)
} else {
- self.state.snakePositions = self.state.snakePositions[:len(self.state.snakePositions)-1]
+ nextState.snakePositions = nextState.snakePositions[:len(nextState.snakePositions)-1]
}
- return true
-}
-
-func (self *Game) getSpeed() int {
- return len(self.state.snakePositions)
+ return nextState, true
}
-func (self *Game) getCells() [][]CellType {
+func (self *Game) getCells(state State) [][]CellType {
cells := make([][]CellType, self.height)
setCell := func(pos Position, value CellType) {
@@ -151,15 +187,23 @@ func (self *Game) getCells() [][]CellType {
cells[i] = make([]CellType, self.width)
}
- for _, pos := range self.state.snakePositions {
+ for _, pos := range state.snakePositions {
setCell(pos, Snake)
}
- setCell(self.state.foodPosition, Food)
+ setCell(state.foodPosition, Food)
return cells
}
-func (self *Game) SetDirection(direction Direction) {
- self.state.direction = direction
+func (self *Game) newDirection(state State, direction Direction) Direction {
+ // don't allow the snake to turn 180 degrees
+ if (state.lastTickDirection == Up && direction == Down) ||
+ (state.lastTickDirection == Down && direction == Up) ||
+ (state.lastTickDirection == Left && direction == Right) ||
+ (state.lastTickDirection == Right && direction == Left) {
+ return state.direction
+ }
+
+ return direction
}
diff --git a/pkg/snake/snake_test.go b/pkg/snake/snake_test.go
index 9f1906e37..7a7ed038a 100644
--- a/pkg/snake/snake_test.go
+++ b/pkg/snake/snake_test.go
@@ -14,49 +14,51 @@ func TestSnake(t *testing.T) {
}{
{
state: State{
- snakePositions: []Position{{x: 5, y: 5}},
- direction: Right,
- foodPosition: Position{x: 9, y: 9},
+ snakePositions: []Position{{x: 5, y: 5}},
+ direction: Right,
+ lastTickDirection: Right,
+ foodPosition: Position{x: 9, y: 9},
},
expectedState: State{
- snakePositions: []Position{{x: 6, y: 5}},
- direction: Right,
- foodPosition: Position{x: 9, y: 9},
+ snakePositions: []Position{{x: 6, y: 5}},
+ direction: Right,
+ lastTickDirection: 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},
+ snakePositions: []Position{{x: 5, y: 5}, {x: 4, y: 5}, {x: 4, y: 4}, {x: 5, y: 4}},
+ direction: Up,
+ lastTickDirection: 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},
+ snakePositions: []Position{{x: 5, y: 5}},
+ direction: Right,
+ lastTickDirection: 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},
+ snakePositions: []Position{{x: 6, y: 5}, {x: 5, y: 5}},
+ direction: Right,
+ lastTickDirection: Right,
+ foodPosition: Position{x: 8, y: 8},
},
expectedAlive: true,
},
}
for _, scenario := range scenarios {
- game := NewGame(10, 10, nil)
- game.state = scenario.state
+ game := NewGame(10, 10, nil, func(string) {})
game.randIntFn = func(int) int { return 8 }
- alive := game.tick()
+ state, alive := game.tick(scenario.state)
assert.Equal(t, scenario.expectedAlive, alive)
- if scenario.expectedAlive {
- assert.EqualValues(t, scenario.expectedState, game.state)
- }
+ assert.EqualValues(t, scenario.expectedState, state)
}
}