mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-07 13:42:01 +02:00
Merge pull request #2338 from jesseduffield/snake
This commit is contained in:
commit
a05bdc3ee4
@ -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 {
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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() {
|
||||
|
68
pkg/gui/controllers/snake_controller.go
Normal file
68
pkg/gui/controllers/snake_controller.go
Normal file
@ -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)
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
56
pkg/gui/snake.go
Normal file
56
pkg/gui/snake.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -76,26 +76,21 @@ func (gui *Gui) resetWindowContext(c types.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// moves given context's view to the top of the window and returns
|
||||
// true if the view was not already on top.
|
||||
func (gui *Gui) moveToTopOfWindow(context types.Context) bool {
|
||||
// moves given context's view to the top of the window
|
||||
func (gui *Gui) moveToTopOfWindow(context types.Context) {
|
||||
view := context.GetView()
|
||||
if view == nil {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
window := context.GetWindowName()
|
||||
|
||||
topView := gui.topViewInWindow(window)
|
||||
|
||||
if view.Name() == topView.Name() {
|
||||
return false
|
||||
} else {
|
||||
if view.Name() != topView.Name() {
|
||||
if err := gui.g.SetViewOnTopOf(view.Name(), topView.Name()); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)",
|
||||
|
209
pkg/snake/snake.go
Normal file
209
pkg/snake/snake.go
Normal file
@ -0,0 +1,209 @@
|
||||
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
|
||||
}
|
64
pkg/snake/snake_test.go
Normal file
64
pkg/snake/snake_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
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,
|
||||
lastTickDirection: Right,
|
||||
foodPosition: Position{x: 9, y: 9},
|
||||
},
|
||||
expectedState: State{
|
||||
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,
|
||||
lastTickDirection: Up,
|
||||
foodPosition: Position{x: 9, y: 9},
|
||||
},
|
||||
expectedState: State{},
|
||||
expectedAlive: false,
|
||||
},
|
||||
{
|
||||
state: State{
|
||||
snakePositions: []Position{{x: 5, y: 5}},
|
||||
direction: Right,
|
||||
lastTickDirection: Right,
|
||||
foodPosition: Position{x: 6, y: 5},
|
||||
},
|
||||
expectedState: State{
|
||||
snakePositions: []Position{{x: 6, y: 5}, {x: 5, y: 5}},
|
||||
direction: Right,
|
||||
lastTickDirection: Right,
|
||||
foodPosition: Position{x: 8, y: 8},
|
||||
},
|
||||
expectedAlive: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
game := NewGame(10, 10, nil, func(string) {})
|
||||
game.randIntFn = func(int) int { return 8 }
|
||||
state, alive := game.tick(scenario.state)
|
||||
assert.Equal(t, scenario.expectedAlive, alive)
|
||||
assert.EqualValues(t, scenario.expectedState, state)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user