diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-12-30 11:34:01 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-12-30 12:18:59 +1100 |
commit | af5b3be2861518737482f474e3b5ff6c2c551720 (patch) | |
tree | 5933a1254d7ad1d76c99ec302ed316d9865b772a /pkg/snake | |
parent | 81281a49b21c5d708e2b5ed70dc5ca5a27ea6db7 (diff) |
integrate snake game into lazygit
Diffstat (limited to 'pkg/snake')
-rw-r--r-- | pkg/snake/cmd/main.go | 74 | ||||
-rw-r--r-- | pkg/snake/snake.go | 174 | ||||
-rw-r--r-- | pkg/snake/snake_test.go | 44 |
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) } } |