1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-02 09:21:40 +02:00
lazygit/pkg/snake/snake.go

210 lines
4.5 KiB
Go
Raw Normal View History

2022-12-29 05:32:33 +02:00
package snake
import (
"math/rand"
"time"
"github.com/samber/lo"
)
2022-12-30 02:34:01 +02:00
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
}
2022-12-29 05:32:33 +02:00
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
)
2022-12-30 02:34:01 +02:00
func NewGame(width, height int, render func(cells [][]CellType, alive bool), logger func(string)) *Game {
2022-12-29 05:32:33 +02:00
return &Game{
width: width,
height: height,
render: render,
randIntFn: rand.Intn,
2022-12-30 02:34:01 +02:00
exit: make(chan struct{}),
logger: logger,
setNewDir: make(chan Direction),
2022-12-29 05:32:33 +02:00
}
}
2022-12-30 02:34:01 +02:00
func (self *Game) Start() {
go self.gameLoop()
}
func (self *Game) Exit() {
close(self.exit)
}
func (self *Game) SetDirection(direction Direction) {
self.setNewDir <- direction
}
2022-12-29 05:32:33 +02:00
2022-12-30 02:34:01 +02:00
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 {
2022-12-29 05:32:33 +02:00
return
}
}
2022-12-30 02:34:01 +02:00
}
2022-12-29 05:32:33 +02:00
}
2022-12-30 02:34:01 +02:00
func (self *Game) initializeState() State {
2022-12-29 05:32:33 +02:00
centerOfScreen := Position{self.width / 2, self.height / 2}
2022-12-30 02:34:01 +02:00
snakePositions := []Position{centerOfScreen}
2022-12-29 05:32:33 +02:00
2022-12-30 02:34:01 +02:00
state := State{
snakePositions: snakePositions,
2022-12-29 05:32:33 +02:00
direction: Right,
2022-12-30 02:34:01 +02:00
foodPosition: self.newFoodPos(snakePositions),
2022-12-29 05:32:33 +02:00
}
2022-12-30 02:34:01 +02:00
return state
2022-12-29 05:32:33 +02:00
}
2022-12-30 02:34:01 +02:00
func (self *Game) newFoodPos(snakePositions []Position) Position {
// arbitrarily setting a limit of attempts to place food
attemptLimit := 1000
for i := 0; i < attemptLimit; i++ {
2022-12-29 05:32:33 +02:00
newFoodPos := Position{self.randIntFn(self.width), self.randIntFn(self.height)}
2022-12-30 02:34:01 +02:00
if !lo.Contains(snakePositions, newFoodPos) {
2022-12-29 05:32:33 +02:00
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
2022-12-30 02:34:01 +02:00
func (self *Game) tick(currentState State) (State, bool) {
nextState := currentState // copy by value
newHeadPos := nextState.snakePositions[0]
nextState.lastTickDirection = nextState.direction
2022-12-29 05:32:33 +02:00
2022-12-30 02:34:01 +02:00
switch nextState.direction {
2022-12-29 05:32:33 +02:00
case Up:
newHeadPos.y--
case Down:
newHeadPos.y++
case Left:
newHeadPos.x--
case Right:
newHeadPos.x++
}
2022-12-30 02:34:01 +02:00
outOfBounds := newHeadPos.x < 0 || newHeadPos.x >= self.width || newHeadPos.y < 0 || newHeadPos.y >= self.height
eatingOwnTail := lo.Contains(nextState.snakePositions, newHeadPos)
2022-12-29 05:32:33 +02:00
2022-12-30 02:34:01 +02:00
if outOfBounds || eatingOwnTail {
return State{}, false
2022-12-29 05:32:33 +02:00
}
2022-12-30 02:34:01 +02:00
nextState.snakePositions = append([]Position{newHeadPos}, nextState.snakePositions...)
2022-12-29 05:32:33 +02:00
2022-12-30 02:34:01 +02:00
if newHeadPos == nextState.foodPosition {
nextState.foodPosition = self.newFoodPos(nextState.snakePositions)
2022-12-29 05:32:33 +02:00
} else {
2022-12-30 02:34:01 +02:00
nextState.snakePositions = nextState.snakePositions[:len(nextState.snakePositions)-1]
2022-12-29 05:32:33 +02:00
}
2022-12-30 02:34:01 +02:00
return nextState, true
2022-12-29 05:32:33 +02:00
}
2022-12-30 02:34:01 +02:00
func (self *Game) getCells(state State) [][]CellType {
2022-12-29 05:32:33 +02:00
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)
}
2022-12-30 02:34:01 +02:00
for _, pos := range state.snakePositions {
2022-12-29 05:32:33 +02:00
setCell(pos, Snake)
}
2022-12-30 02:34:01 +02:00
setCell(state.foodPosition, Food)
2022-12-29 05:32:33 +02:00
return cells
}
2022-12-30 02:34:01 +02:00
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
2022-12-29 05:32:33 +02:00
}