summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-12-30 11:34:01 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-12-30 12:18:59 +1100
commitaf5b3be2861518737482f474e3b5ff6c2c551720 (patch)
tree5933a1254d7ad1d76c99ec302ed316d9865b772a
parent81281a49b21c5d708e2b5ed70dc5ca5a27ea6db7 (diff)
integrate snake game into lazygit
-rw-r--r--pkg/cheatsheet/generate.go2
-rw-r--r--pkg/cheatsheet/generate_test.go29
-rw-r--r--pkg/gui/context/context.go3
-rw-r--r--pkg/gui/context_config.go20
-rw-r--r--pkg/gui/controllers.go6
-rw-r--r--pkg/gui/controllers/snake_controller.go68
-rw-r--r--pkg/gui/controllers/submodules_controller.go9
-rw-r--r--pkg/gui/gui.go3
-rw-r--r--pkg/gui/snake.go56
-rw-r--r--pkg/gui/views.go7
-rw-r--r--pkg/i18n/english.go6
-rw-r--r--pkg/snake/cmd/main.go74
-rw-r--r--pkg/snake/snake.go174
-rw-r--r--pkg/snake/snake_test.go44
14 files changed, 340 insertions, 161 deletions
diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go
index 437db154d..d68cfbc80 100644
--- a/pkg/cheatsheet/generate.go
+++ b/pkg/cheatsheet/generate.go
@@ -131,7 +131,7 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
return false
}
- return (binding.Description != "" || binding.Alternative != "")
+ return (binding.Description != "" || binding.Alternative != "") && binding.Key != nil
})
bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header {
diff --git a/pkg/cheatsheet/generate_test.go b/pkg/cheatsheet/generate_test.go
index 44c49a461..ba77d3b15 100644
--- a/pkg/cheatsheet/generate_test.go
+++ b/pkg/cheatsheet/generate_test.go
@@ -27,6 +27,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
},
expected: []*bindingSection{
@@ -36,6 +37,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
},
},
@@ -47,6 +49,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "",
Description: "quit",
+ Key: 'a',
},
},
expected: []*bindingSection{
@@ -56,6 +59,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "",
Description: "quit",
+ Key: 'a',
},
},
},
@@ -67,14 +71,17 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
+ Key: 'a',
},
{
ViewName: "submodules",
Description: "drop submodule",
+ Key: 'a',
},
},
expected: []*bindingSection{
@@ -84,10 +91,12 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
+ Key: 'a',
},
},
},
@@ -97,6 +106,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "submodules",
Description: "drop submodule",
+ Key: 'a',
},
},
},
@@ -108,19 +118,23 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "scroll",
+ Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "revert commit",
+ Key: 'a',
},
},
expected: []*bindingSection{
@@ -130,6 +144,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "scroll",
+ Key: 'a',
Tag: "navigation",
},
},
@@ -140,6 +155,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "commits",
Description: "revert commit",
+ Key: 'a',
},
},
},
@@ -149,10 +165,12 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
+ Key: 'a',
},
},
},
@@ -164,28 +182,34 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "scroll",
+ Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "revert commit",
+ Key: 'a',
},
{
ViewName: "commits",
Description: "scroll",
+ Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "page up",
+ Key: 'a',
Tag: "navigation",
},
},
@@ -196,11 +220,13 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "scroll",
+ Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "page up",
+ Key: 'a',
Tag: "navigation",
},
},
@@ -211,6 +237,7 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "commits",
Description: "revert commit",
+ Key: 'a',
},
},
},
@@ -220,10 +247,12 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
+ Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
+ Key: 'a',
},
},
},
diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go
index 131eecf0a..5a88b4a26 100644
--- a/pkg/gui/context/context.go
+++ b/pkg/gui/context/context.go
@@ -7,6 +7,7 @@ import (
const (
GLOBAL_CONTEXT_KEY types.ContextKey = "global"
STATUS_CONTEXT_KEY types.ContextKey = "status"
+ SNAKE_CONTEXT_KEY types.ContextKey = "snake"
FILES_CONTEXT_KEY types.ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
@@ -74,6 +75,7 @@ var AllContextKeys = []types.ContextKey{
type ContextTree struct {
Global types.Context
Status types.Context
+ Snake types.Context
Files *WorkingTreeContext
Menu *MenuContext
Branches *BranchesContext
@@ -112,6 +114,7 @@ func (self *ContextTree) Flatten() []types.Context {
return []types.Context{
self.Global,
self.Status,
+ self.Snake,
self.Submodules,
self.Files,
self.SubCommits,
diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go
index 7bd0ca7f8..063690ff5 100644
--- a/pkg/gui/context_config.go
+++ b/pkg/gui/context_config.go
@@ -32,6 +32,26 @@ func (gui *Gui) contextTree() *context.ContextTree {
OnRenderToMain: gui.statusRenderToMain,
},
),
+ Snake: context.NewSimpleContext(
+ context.NewBaseContext(context.NewBaseContextOpts{
+ Kind: types.SIDE_CONTEXT,
+ View: gui.Views.Snake,
+ WindowName: "files",
+ Key: context.SNAKE_CONTEXT_KEY,
+ Focusable: true,
+ }),
+ context.ContextCallbackOpts{
+ OnFocus: func(opts types.OnFocusOpts) error {
+ gui.startSnake()
+ return nil
+ },
+ OnFocusLost: func(opts types.OnFocusLostOpts) error {
+ gui.snakeGame.Exit()
+ gui.moveToTopOfWindow(gui.State.Contexts.Submodules)
+ return nil
+ },
+ },
+ ),
Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(),
Menu: gui.menuListContext(),
diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go
index 974080e67..d7f6647c7 100644
--- a/pkg/gui/controllers.go
+++ b/pkg/gui/controllers.go
@@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands"
+ "github.com/jesseduffield/lazygit/pkg/snake"
)
func (gui *Gui) resetControllers() {
@@ -130,6 +131,7 @@ func (gui *Gui) resetControllers() {
stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false)
stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true)
patchBuildingController := controllers.NewPatchBuildingController(common)
+ snakeController := controllers.NewSnakeController(common, func() *snake.Game { return gui.snakeGame })
setSubCommits := func(commits []*models.Commit) { gui.State.Model.SubCommits = commits }
@@ -248,6 +250,10 @@ func (gui *Gui) resetControllers() {
contextLinesController,
)
+ controllers.AttachControllers(gui.State.Contexts.Snake,
+ snakeController,
+ )
+
// this must come last so that we've got our click handlers defined against the context
listControllerFactory := controllers.NewListControllerFactory(gui.c)
for _, context := range gui.getListContexts() {
diff --git a/pkg/gui/controllers/snake_controller.go b/pkg/gui/controllers/snake_controller.go
new file mode 100644
index 000000000..4217878e3
--- /dev/null
+++ b/pkg/gui/controllers/snake_controller.go
@@ -0,0 +1,68 @@
+package controllers
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/gui/types"
+ "github.com/jesseduffield/lazygit/pkg/snake"
+)
+
+type SnakeController struct {
+ baseController
+ *controllerCommon
+
+ getGame func() *snake.Game
+}
+
+var _ types.IController = &SnakeController{}
+
+func NewSnakeController(
+ common *controllerCommon,
+ getGame func() *snake.Game,
+) *SnakeController {
+ return &SnakeController{
+ baseController: baseController{},
+ controllerCommon: common,
+ getGame: getGame,
+ }
+}
+
+func (self *SnakeController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
+ bindings := []*types.Binding{
+ {
+ Key: opts.GetKey(opts.Config.Universal.NextItem),
+ Handler: self.SetDirection(snake.Down),
+ },
+ {
+ Key: opts.GetKey(opts.Config.Universal.PrevItem),
+ Handler: self.SetDirection(snake.Up),
+ },
+ {
+ Key: opts.GetKey(opts.Config.Universal.PrevBlock),
+ Handler: self.SetDirection(snake.Left),
+ },
+ {
+ Key: opts.GetKey(opts.Config.Universal.NextBlock),
+ Handler: self.SetDirection(snake.Right),
+ },
+ {
+ Key: opts.GetKey(opts.Config.Universal.Return),
+ Handler: self.Escape,
+ },
+ }
+
+ return bindings
+}
+
+func (self *SnakeController) Context() types.Context {
+ return self.contexts.Snake
+}
+
+func (self *SnakeController) SetDirection(direction snake.Direction) func() error {
+ return func() error {
+ self.getGame().SetDirection(direction)
+ return nil
+ }
+}
+
+func (self *SnakeController) Escape() error {
+ return self.c.PushContext(self.contexts.Submodules)
+}
diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go
index 5bd7ce088..e6a6f8d17 100644
--- a/pkg/gui/controllers/submodules_controller.go
+++ b/pkg/gui/controllers/submodules_controller.go
@@ -69,6 +69,11 @@ func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*
Description: self.c.Tr.LcViewBulkSubmoduleOptions,
OpensMenu: true,
},
+ {
+ Key: nil,
+ Handler: self.easterEgg,
+ Description: self.c.Tr.EasterEgg,
+ },
}
}
@@ -219,6 +224,10 @@ func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) erro
})
}
+func (self *SubmodulesController) easterEgg() error {
+ return self.c.PushContext(self.contexts.Snake)
+}
+
func (self *SubmodulesController) checkSelected(callback func(*models.SubmoduleConfig) error) func() error {
return func() error {
submodule := self.context().GetSelected()
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 1431f6c16..80b18661b 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -33,6 +33,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/integration/components"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
+ "github.com/jesseduffield/lazygit/pkg/snake"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
@@ -154,6 +155,8 @@ type Gui struct {
c *types.HelperCommon
helpers *helpers.Helpers
+
+ snakeGame *snake.Game
}
// we keep track of some stuff from one render to the next to see if certain
diff --git a/pkg/gui/snake.go b/pkg/gui/snake.go
new file mode 100644
index 000000000..9d82275f8
--- /dev/null
+++ b/pkg/gui/snake.go
@@ -0,0 +1,56 @@
+package gui
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/jesseduffield/lazygit/pkg/gui/style"
+ "github.com/jesseduffield/lazygit/pkg/snake"
+)
+
+func (gui *Gui) startSnake() {
+ view := gui.Views.Snake
+
+ game := snake.NewGame(view.Width(), view.Height(), gui.renderSnakeGame, gui.c.LogAction)
+ gui.snakeGame = game
+ game.Start()
+}
+
+func (gui *Gui) renderSnakeGame(cells [][]snake.CellType, alive bool) {
+ view := gui.Views.Snake
+
+ if !alive {
+ _ = gui.c.ErrorMsg(gui.Tr.YouDied)
+ return
+ }
+
+ output := drawSnakeGame(cells)
+
+ view.Clear()
+ fmt.Fprint(view, output)
+ gui.c.Render()
+}
+
+func drawSnakeGame(cells [][]snake.CellType) string {
+ writer := &strings.Builder{}
+
+ for i, row := range cells {
+ for _, cell := range row {
+ switch cell {
+ case snake.None:
+ writer.WriteString(" ")
+ case snake.Snake:
+ writer.WriteString("█")
+ case snake.Food:
+ writer.WriteString(style.FgMagenta.Sprint("█"))
+ }
+ }
+
+ if i < len(cells) {
+ writer.WriteString("\n")
+ }
+ }
+
+ output := writer.String()
+ return output
+}
diff --git a/pkg/gui/views.go b/pkg/gui/views.go
index 9cc3d983f..be9a2952b 100644
--- a/pkg/gui/views.go
+++ b/pkg/gui/views.go
@@ -40,6 +40,9 @@ type Views struct {
Suggestions *gocui.View
Tooltip *gocui.View
Extras *gocui.View
+
+ // for playing the easter egg snake game
+ Snake *gocui.View
}
type viewNameMapping struct {
@@ -58,6 +61,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
// first layer. Ordering within this layer does not matter because there are
// no overlapping views
{viewPtr: &gui.Views.Status, name: "status"},
+ {viewPtr: &gui.Views.Snake, name: "snake"},
{viewPtr: &gui.Views.Submodules, name: "submodules"},
{viewPtr: &gui.Views.Files, name: "files"},
{viewPtr: &gui.Views.Tags, name: "tags"},
@@ -220,5 +224,8 @@ func (gui *Gui) createAllViews() error {
gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = true
+ gui.Views.Snake.Title = gui.c.Tr.SnakeTitle
+ gui.Views.Snake.FgColor = gocui.ColorGreen
+
return nil
}
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index b90564e29..f1c081018 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -17,6 +17,8 @@ type TranslationSet struct {
BranchesTitle string
CommitsTitle string
StashTitle string
+ SnakeTitle string
+ EasterEgg string
UnstagedChanges string
StagedChanges string
MainTitle string
@@ -213,6 +215,7 @@ type TranslationSet struct {
ErrorOccurred string
NoRoom string
YouAreHere string
+ YouDied string
LcRewordNotSupported string
LcCherryPickCopy string
LcCherryPickCopyRange string
@@ -665,6 +668,8 @@ func EnglishTranslationSet() TranslationSet {
BranchesTitle: "Branches",
CommitsTitle: "Commits",
StashTitle: "Stash",
+ SnakeTitle: "Snake",
+ EasterEgg: "easter egg",
UnstagedChanges: `Unstaged Changes`,
StagedChanges: `Staged Changes`,
MainTitle: "Main",
@@ -861,6 +866,7 @@ func EnglishTranslationSet() TranslationSet {
ErrorOccurred: "An error occurred! Please create an issue at",
NoRoom: "Not enough room",
YouAreHere: "YOU ARE HERE",
+ YouDied: "YOU DIED!",
LcRewordNotSupported: "rewording commits while interactively rebasing is not currently supported",
LcCherryPickCopy: "copy commit (cherry-pick)",
LcCherryPickCopyRange: "copy commit range (cherry-pick)",
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{
- snakePositio