1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-27 12:32:37 +02:00

add snake game

This commit is contained in:
Jesse Duffield 2022-12-29 14:32:33 +11:00
parent ff8823093c
commit 81281a49b2
3 changed files with 301 additions and 0 deletions

74
pkg/snake/cmd/main.go Normal file
View File

@ -0,0 +1,74 @@
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())
}

165
pkg/snake/snake.go Normal file
View File

@ -0,0 +1,165 @@
package snake
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/samber/lo"
)
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
)
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 {
return &Game{
width: width,
height: height,
render: render,
randIntFn: rand.Intn,
}
}
func (self *Game) Start(ctx context.Context) {
self.initializeState()
go func() {
for {
select {
case <-ctx.Done():
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() {
centerOfScreen := Position{self.width / 2, self.height / 2}
self.state = State{
snakePositions: []Position{centerOfScreen},
direction: Right,
}
self.state.foodPosition = self.setNewFoodPos()
}
// 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++ {
newFoodPos := Position{self.randIntFn(self.width), self.randIntFn(self.height)}
if !lo.Contains(self.state.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() bool {
newHeadPos := self.state.snakePositions[0]
switch self.state.direction {
case Up:
newHeadPos.y--
case Down:
newHeadPos.y++
case Left:
newHeadPos.x--
case Right:
newHeadPos.x++
}
if newHeadPos.x < 0 || newHeadPos.x >= self.width || newHeadPos.y < 0 || newHeadPos.y >= self.height {
return false
}
if lo.Contains(self.state.snakePositions, newHeadPos) {
return false
}
self.state.snakePositions = append([]Position{newHeadPos}, self.state.snakePositions...)
if newHeadPos == self.state.foodPosition {
self.state.foodPosition = self.setNewFoodPos()
} else {
self.state.snakePositions = self.state.snakePositions[:len(self.state.snakePositions)-1]
}
return true
}
func (self *Game) getSpeed() int {
return len(self.state.snakePositions)
}
func (self *Game) getCells() [][]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 self.state.snakePositions {
setCell(pos, Snake)
}
setCell(self.state.foodPosition, Food)
return cells
}
func (self *Game) SetDirection(direction Direction) {
self.state.direction = direction
}

62
pkg/snake/snake_test.go Normal file
View File

@ -0,0 +1,62 @@
package snake
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSnake(t *testing.T) {
scenarios := []struct {
state State
expectedState State
expectedAlive bool
}{
{
state: State{
snakePositions: []Position{{x: 5, y: 5}},
direction: Right,
foodPosition: Position{x: 9, y: 9},
},
expectedState: State{
snakePositions: []Position{{x: 6, y: 5}},
direction: 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},
},
expectedState: State{},
expectedAlive: false,
},
{
state: State{
snakePositions: []Position{{x: 5, y: 5}},
direction: 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},
},
expectedAlive: true,
},
}
for _, scenario := range scenarios {
game := NewGame(10, 10, nil)
game.state = scenario.state
game.randIntFn = func(int) int { return 8 }
alive := game.tick()
assert.Equal(t, scenario.expectedAlive, alive)
if scenario.expectedAlive {
assert.EqualValues(t, scenario.expectedState, game.state)
}
}
}