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
}