package snake import ( "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 } type Direction int const ( Up Direction = iota Down Left Right ) type CellType int const ( None CellType = iota Snake Food ) 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() { go self.gameLoop() } func (self *Game) Exit() { close(self.exit) } func (self *Game) SetDirection(direction Direction) { self.setNewDir <- direction } 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 } } } } func (self *Game) initializeState() State { centerOfScreen := Position{self.width / 2, self.height / 2} snakePositions := []Position{centerOfScreen} state := State{ snakePositions: snakePositions, direction: Right, foodPosition: self.newFoodPos(snakePositions), } return state } 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(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(currentState State) (State, bool) { nextState := currentState // copy by value newHeadPos := nextState.snakePositions[0] nextState.lastTickDirection = nextState.direction switch nextState.direction { case Up: newHeadPos.y-- case Down: newHeadPos.y++ case Left: newHeadPos.x-- case Right: newHeadPos.x++ } outOfBounds := newHeadPos.x < 0 || newHeadPos.x >= self.width || newHeadPos.y < 0 || newHeadPos.y >= self.height eatingOwnTail := lo.Contains(nextState.snakePositions, newHeadPos) if outOfBounds || eatingOwnTail { return State{}, false } nextState.snakePositions = append([]Position{newHeadPos}, nextState.snakePositions...) if newHeadPos == nextState.foodPosition { nextState.foodPosition = self.newFoodPos(nextState.snakePositions) } else { nextState.snakePositions = nextState.snakePositions[:len(nextState.snakePositions)-1] } return nextState, true } func (self *Game) getCells(state State) [][]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 state.snakePositions { setCell(pos, Snake) } setCell(state.foodPosition, Food) return cells } 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 }