├── .gitignore ├── Godeps ├── README.md ├── goenv ├── main.go ├── scene.go ├── scene_test.go ├── snake.go └── snake_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | gosnake 2 | go 3 | main 4 | -------------------------------------------------------------------------------- /Godeps: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/hSATAC/gosnake", 3 | "GoVersion": "go1.1.2", 4 | "Deps": [ 5 | { 6 | "ImportPath": "github.com/davecgh/go-spew/spew", 7 | "Rev": "1fe9f5ca4b46a8247ae5939097f5cfc5d8370729" 8 | }, 9 | { 10 | "ImportPath": "github.com/nsf/termbox-go", 11 | "Rev": "64181cd80c53208e7b056697d0decf94f3210ad3" 12 | }, 13 | { 14 | "ImportPath": "github.com/stretchr/testify/assert", 15 | "Rev": "4c55a02a9da3f9b9daf583332a2a82c38a4521be" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gosnake 2 | ======= 3 | [![Build Status](https://drone.io/github.com/hSATAC/gosnake/status.png)](https://drone.io/github.com/hSATAC/gosnake/latest) 4 | 5 | Snake game written in Go 6 | 7 | This is a simple project for me to practice Go. 8 | 9 | See it in action: http://asciinema.org/a/6115 10 | 11 | ### INSTALL 12 | 13 | * `go get github/hSATAC/gosnake` 14 | * run `gosnake` to play 15 | 16 | ### NOTICE 17 | 18 | You can make 100% reproducible build via [godep](https://github.com/kr/godep). 19 | 20 | * `go get github.com/kr/godep` 21 | * `godep go build` under gosnake src folder. 22 | 23 | ### TODO 24 | 25 | * Decouple Snake & Scene 26 | * Add score 27 | -------------------------------------------------------------------------------- /goenv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # source goenv 3 | 4 | GOLIB=$(pwd)/go 5 | GOVENDOR=$(pwd)/go/vendor 6 | 7 | [[ ! -e go/src ]] && mkdir -p go/src 8 | [[ ! -e go/pkg ]] && mkdir -p go/pkg 9 | [[ ! -e go/doc ]] && mkdir -p go/doc 10 | [[ ! -e go/bin ]] && mkdir -p go/bin 11 | 12 | echo "Adding $GOLIB to GOPATH..." 13 | # export GOPATH=$GOVENDOR:$GOLIB 14 | export GOPATH=$GOLIB 15 | export PATH=$GOLIB/bin:$PATH 16 | [[ -e golang ]] && export PATH=$(pwd)/golang/go/bin:$PATH 17 | echo "Done" 18 | 19 | 20 | 21 | function clean() { 22 | echo "Cleaning..." 23 | rm -rf go/pkg/ 24 | go clean -i drshine 25 | } 26 | 27 | function build_all() { 28 | go build -a -x drshine 29 | } 30 | 31 | function remote_install() { 32 | repo=$1 33 | go get -u $repo 34 | } 35 | 36 | function git_install() { 37 | installdir=$(echo $GOPATH | cut -d: -f1) 38 | repo=$1 39 | target=$installdir/src/$2 40 | branch=$3 41 | currentdir=$(pwd) 42 | 43 | if [[ -z $branch ]] ; then 44 | branch=master 45 | fi 46 | 47 | mkdir -p $(dirname $target) 48 | 49 | echo "===> Sync $repo => $target..." 50 | if [[ ! -e $target ]] ; then 51 | echo "Cloning..." 52 | git clone --branch $branch $repo $target 53 | else 54 | echo "Fetching..." 55 | GITCMD="git --work-tree $target --git-dir $target/.git" 56 | if [[ -z `$GITCMD diff` ]] ; then 57 | $GITCMD pull -q --rebase origin HEAD 58 | else 59 | $GITCMD fetch -q 60 | fi 61 | 62 | if [[ -n `$GITCMD log @{u}..` ]] ; then 63 | echo "Pushing commits..." 64 | $GITCMD push origin HEAD 65 | fi 66 | cd $target && go install && cd $currentdir 67 | fi 68 | } 69 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/davecgh/go-spew/spew" 6 | "github.com/nsf/termbox-go" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "time" 11 | ) 12 | 13 | func DrawPoint(x, y int, color Color) { 14 | // Double the width otherwise it looks weird. 15 | termbox.SetCell(x*2, y, ' ', termbox.ColorDefault, termbox.Attribute(color)) 16 | termbox.SetCell((x*2)+1, y, ' ', termbox.ColorDefault, termbox.Attribute(color)) 17 | } 18 | 19 | func ClearScene() { 20 | termbox.Flush() 21 | termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) 22 | } 23 | 24 | func SceneSize() ScreenSize { 25 | width, height := termbox.Size() 26 | size := ScreenSize{} 27 | size.width = width / 2 // Half the width because we have to to double it when drawing. 28 | size.height = height 29 | 30 | return size 31 | } 32 | 33 | func main() { 34 | filename := os.Getenv("HOME") + "/.gosnake.log" 35 | logfile, _ := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 36 | log.SetOutput(logfile) 37 | 38 | // initialize termbox 39 | err := termbox.Init() 40 | if err != nil { 41 | fmt.Println("Could not start termbox for gosnake.") 42 | log.Printf("Cannot start gomatrix, termbox.Init() gave an error:\n%s\n", err) 43 | os.Exit(1) 44 | } 45 | termbox.HideCursor() 46 | 47 | var snake = NewSnake() 48 | var scene = NewScene(snake, SceneSize()) 49 | 50 | // go 51 | go func() { 52 | for { 53 | <-time.After(60 * time.Millisecond) 54 | stop := scene.Draw() 55 | if stop { 56 | break 57 | } 58 | } 59 | }() 60 | 61 | // make chan for tembox events and run poller to send events on chan 62 | eventChan := make(chan termbox.Event) 63 | go func() { 64 | for { 65 | event := termbox.PollEvent() 66 | eventChan <- event 67 | } 68 | }() 69 | 70 | // register signals to channel 71 | sigChan := make(chan os.Signal) 72 | signal.Notify(sigChan, os.Interrupt) 73 | signal.Notify(sigChan, os.Kill) 74 | 75 | // handle termbox events and unix signals 76 | func() { 77 | for { 78 | // select for either event or signal 79 | select { 80 | case event := <-eventChan: 81 | log.Printf("Have event: \n%s", spew.Sdump(event)) 82 | // switch on event type 83 | switch event.Type { 84 | case termbox.EventKey: // actions depend on key 85 | switch event.Key { 86 | case termbox.KeyCtrlZ, termbox.KeyCtrlC: 87 | return 88 | } 89 | 90 | switch event.Ch { 91 | case 'q': 92 | return 93 | 94 | case 'w': 95 | scene.character.Turn(SNAKE_DIRECTION_UP) 96 | case 's': 97 | scene.character.Turn(SNAKE_DIRECTION_DOWN) 98 | case 'a': 99 | scene.character.Turn(SNAKE_DIRECTION_LEFT) 100 | case 'd': 101 | scene.character.Turn(SNAKE_DIRECTION_RIGHT) 102 | } 103 | 104 | case termbox.EventResize: 105 | // TODO: Handle window resize, how? 106 | log.Println("size changed") 107 | 108 | case termbox.EventError: // quit 109 | log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err) 110 | } 111 | case signal := <-sigChan: 112 | log.Printf("Have signal: \n%s", signal) 113 | return 114 | } 115 | } 116 | }() 117 | 118 | // close up 119 | termbox.Close() 120 | log.Println("stopping gosnake") 121 | os.Exit(0) 122 | } 123 | -------------------------------------------------------------------------------- /scene.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type ( 10 | Color termbox.Attribute 11 | CharacterStatus byte 12 | ) 13 | 14 | const ( 15 | COLOR_CHARACTER Color = Color(termbox.ColorRed) 16 | COLOR_FRUIT Color = Color(termbox.ColorGreen) 17 | ) 18 | 19 | const ( 20 | CHARACTER_STATUS_MOVE CharacterStatus = iota 21 | CHARACTER_STATUS_GROW 22 | CHARACTER_STATUS_DEAD 23 | ) 24 | 25 | type Character interface { 26 | Move(screenSize ScreenSize, fruit Node) CharacterStatus 27 | Turn(direction Direction) 28 | Draw() 29 | Body() Body 30 | } 31 | 32 | type ScreenSize struct { 33 | width int 34 | height int 35 | } 36 | 37 | type Scene struct { 38 | character Character 39 | size ScreenSize 40 | fruit Node 41 | } 42 | 43 | func (scene *Scene) SetSize(width int, height int) { 44 | scene.size = ScreenSize{width: width, height: height} 45 | } 46 | 47 | func (scene *Scene) Draw() (stop bool) { 48 | switch scene.character.Move(scene.size, scene.fruit) { 49 | case CHARACTER_STATUS_GROW: 50 | scene.generateFruit() 51 | case CHARACTER_STATUS_DEAD: 52 | stop = true 53 | } 54 | 55 | ClearScene() 56 | scene.drawFruit() 57 | scene.character.Draw() 58 | 59 | return stop 60 | } 61 | 62 | func (scene *Scene) availableNodes() (availableNodes []Node) { 63 | for x := 0; x < scene.size.width; x++ { 64 | for y := 0; y < scene.size.height; y++ { 65 | node := Node{x: x, y: y} 66 | if !scene.character.Body().Contains(node) { 67 | availableNodes = append(availableNodes, node) 68 | } 69 | } 70 | } 71 | 72 | return availableNodes 73 | } 74 | 75 | func (scene *Scene) randomAvailableNode() Node { 76 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 77 | nodes := scene.availableNodes() 78 | return nodes[r.Intn(len(nodes))] 79 | } 80 | 81 | func (scene *Scene) generateFruit() { 82 | node := scene.randomAvailableNode() 83 | scene.fruit = node 84 | } 85 | 86 | func (scene *Scene) drawFruit() { 87 | DrawPoint(scene.fruit.x, scene.fruit.y, COLOR_FRUIT) 88 | } 89 | 90 | func NewScene(character Character, screenSize ScreenSize) *Scene { 91 | scene := Scene{size: screenSize, character: character} 92 | scene.generateFruit() 93 | return &scene 94 | } 95 | -------------------------------------------------------------------------------- /scene_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"github.com/davecgh/go-spew/spew" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestScene(t *testing.T) { 10 | t.SkipNow() 11 | } 12 | 13 | func TestSceneAvailableNodes(t *testing.T) { 14 | var snake = NewSnake() 15 | var scene = Scene{size: ScreenSize{2, 2}, character: snake} 16 | assert.Equal(t, scene.availableNodes(), []Node{Node{x: 0, y: 1}, Node{x: 1, y: 1}}) 17 | } 18 | 19 | func TestSceneRandomAvailableNode(t *testing.T) { 20 | var snake = NewSnake() 21 | var scene = Scene{size: ScreenSize{3, 1}, character: snake} 22 | assert.Equal(t, scene.randomAvailableNode(), Node{x: 2, y: 0}) 23 | } 24 | -------------------------------------------------------------------------------- /snake.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"github.com/davecgh/go-spew/spew" 5 | "math" 6 | ) 7 | 8 | const ( 9 | SNAKE_DIRECTION_UP Direction = iota 10 | SNAKE_DIRECTION_RIGHT 11 | SNAKE_DIRECTION_DOWN 12 | SNAKE_DIRECTION_LEFT 13 | ) 14 | 15 | type Direction byte 16 | 17 | func (direction Direction) angle() int { 18 | return int(direction) * 90 19 | } 20 | 21 | type Snake struct { 22 | direction Direction 23 | body Body 24 | } 25 | 26 | type Body []Node 27 | 28 | type Node struct { 29 | x int 30 | y int 31 | } 32 | 33 | func (body Body) Contains(node Node) bool { 34 | for _, n := range body { 35 | if node == n { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | func (snake *Snake) Move(screenSize ScreenSize, fruit Node) CharacterStatus { 43 | newHead := snake.newHead(screenSize) 44 | if snake.body.Contains(newHead) { 45 | return CHARACTER_STATUS_DEAD 46 | } else if newHead == fruit { 47 | snake.growInScreenSize(screenSize) 48 | return CHARACTER_STATUS_GROW 49 | } else { 50 | snake.moveInScreenSize(screenSize) 51 | return CHARACTER_STATUS_MOVE 52 | } 53 | } 54 | 55 | func (snake *Snake) moveInScreenSize(screenSize ScreenSize) { 56 | snake.body = snake.body[1:] 57 | head := snake.newHead(screenSize) 58 | snake.body = append(snake.body, head) 59 | } 60 | 61 | func (snake *Snake) growInScreenSize(screenSize ScreenSize) { 62 | head := snake.newHead(screenSize) 63 | snake.body = append(snake.body, head) 64 | } 65 | 66 | func (snake *Snake) newHead(screenSize ScreenSize) Node { 67 | head := snake.head() 68 | var newHead Node 69 | 70 | switch snake.direction { 71 | case SNAKE_DIRECTION_RIGHT: 72 | newHead = Node{x: head.x + 1, y: head.y} 73 | case SNAKE_DIRECTION_DOWN: 74 | newHead = Node{x: head.x, y: head.y + 1} 75 | case SNAKE_DIRECTION_LEFT: 76 | newHead = Node{x: head.x - 1, y: head.y} 77 | case SNAKE_DIRECTION_UP: 78 | newHead = Node{x: head.x, y: head.y - 1} 79 | } 80 | 81 | if screenSize.width > 0 { 82 | if newHead.x < 0 { // over left edge 83 | newHead.x = screenSize.width - 1 84 | } else if newHead.x >= screenSize.width { // over right edge 85 | newHead.x = screenSize.width - newHead.x 86 | } 87 | } 88 | 89 | if screenSize.height > 0 { 90 | if newHead.y < 0 { // over top edge 91 | newHead.y = screenSize.height - 1 92 | } else if newHead.y >= screenSize.height { // over bottom edge 93 | newHead.y = screenSize.height - newHead.y 94 | } 95 | } 96 | return newHead 97 | } 98 | 99 | func (snake *Snake) Len() int { 100 | return len(snake.body) 101 | } 102 | 103 | func (snake *Snake) head() Node { 104 | return snake.body[snake.Len()-1] 105 | } 106 | 107 | func (snake *Snake) Turn(direction Direction) { 108 | // You can't turn to opposite direction 109 | angle := float64(direction.angle() - snake.direction.angle()) 110 | if math.Abs(angle) == 180.0 { 111 | return 112 | } 113 | 114 | snake.direction = direction 115 | } 116 | 117 | func (snake *Snake) Draw() { 118 | for _, node := range snake.body { 119 | DrawPoint(node.x, node.y, COLOR_CHARACTER) 120 | } 121 | } 122 | 123 | func (snake *Snake) Body() Body { 124 | return snake.body 125 | } 126 | 127 | // TODO: init with position, direction & length 128 | func NewSnake() *Snake { 129 | snake := Snake{} 130 | 131 | // give default 132 | snake.direction = SNAKE_DIRECTION_RIGHT 133 | snake.body = Body{Node{x: 0, y: 0}, Node{x: 1, y: 0}} 134 | return &snake 135 | } 136 | -------------------------------------------------------------------------------- /snake_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"github.com/davecgh/go-spew/spew" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestSnake(t *testing.T) { 10 | var snake = NewSnake() 11 | assert.Equal(t, snake.direction, Direction(SNAKE_DIRECTION_RIGHT)) 12 | assert.Equal(t, len(snake.body), 2) 13 | } 14 | 15 | func TestSnakeLen(t *testing.T) { 16 | var snake = NewSnake() 17 | snake.body = Body{Node{x: 0, y: 0}, Node{x: 1, y: 1}} 18 | assert.Equal(t, snake.Len(), 2) 19 | } 20 | 21 | func TestSnakeMoveOutOfRightEdge(t *testing.T) { 22 | var snake = NewSnake() 23 | var screenSize = ScreenSize{3, 3} 24 | snake.moveInScreenSize(screenSize) 25 | assert.Equal(t, snake.Len(), 2) 26 | assert.Equal(t, snake.body[0], Node{x: 1, y: 0}) 27 | assert.Equal(t, snake.body[1], Node{x: 2, y: 0}) 28 | 29 | snake.moveInScreenSize(screenSize) 30 | assert.Equal(t, snake.Len(), 2) 31 | assert.Equal(t, snake.body[0], Node{x: 2, y: 0}) 32 | assert.Equal(t, snake.body[1], Node{x: 0, y: 0}) 33 | } 34 | 35 | func TestSnakeMoveOutOfTopEdge(t *testing.T) { 36 | var snake = NewSnake() 37 | var screenSize = ScreenSize{3, 3} 38 | snake.Turn(SNAKE_DIRECTION_UP) 39 | snake.moveInScreenSize(screenSize) 40 | assert.Equal(t, snake.Len(), 2) 41 | assert.Equal(t, snake.body[0], Node{x: 1, y: 0}) 42 | assert.Equal(t, snake.body[1], Node{x: 1, y: 2}) 43 | 44 | snake.moveInScreenSize(screenSize) 45 | assert.Equal(t, snake.Len(), 2) 46 | assert.Equal(t, snake.body[0], Node{x: 1, y: 2}) 47 | assert.Equal(t, snake.body[1], Node{x: 1, y: 1}) 48 | } 49 | 50 | func TestSnakeMoveOutOfBottomEdge(t *testing.T) { 51 | var snake = NewSnake() 52 | var screenSize = ScreenSize{3, 3} 53 | 54 | snake.Turn(SNAKE_DIRECTION_DOWN) 55 | snake.moveInScreenSize(screenSize) 56 | 57 | snake.moveInScreenSize(screenSize) 58 | assert.Equal(t, snake.Len(), 2) 59 | assert.Equal(t, snake.body[0], Node{x: 1, y: 1}) 60 | assert.Equal(t, snake.body[1], Node{x: 1, y: 2}) 61 | 62 | snake.moveInScreenSize(screenSize) 63 | assert.Equal(t, snake.Len(), 2) 64 | assert.Equal(t, snake.body[0], Node{x: 1, y: 2}) 65 | assert.Equal(t, snake.body[1], Node{x: 1, y: 0}) 66 | } 67 | 68 | func TestSnakeMoveOutOfLeftEdge(t *testing.T) { 69 | var snake = NewSnake() 70 | var screenSize = ScreenSize{3, 3} 71 | 72 | snake.Turn(SNAKE_DIRECTION_DOWN) 73 | snake.moveInScreenSize(screenSize) 74 | 75 | snake.Turn(SNAKE_DIRECTION_LEFT) 76 | snake.moveInScreenSize(screenSize) 77 | assert.Equal(t, snake.Len(), 2) 78 | assert.Equal(t, snake.body[0], Node{x: 1, y: 1}) 79 | assert.Equal(t, snake.body[1], Node{x: 0, y: 1}) 80 | 81 | snake.moveInScreenSize(screenSize) 82 | assert.Equal(t, snake.Len(), 2) 83 | assert.Equal(t, snake.body[0], Node{x: 0, y: 1}) 84 | assert.Equal(t, snake.body[1], Node{x: 2, y: 1}) 85 | } 86 | 87 | func TestSnakeNewHead(t *testing.T) { 88 | var snake = NewSnake() 89 | var screenSize = ScreenSize{3, 3} 90 | 91 | assert.Equal(t, snake.newHead(screenSize), Node{x: 2, y: 0}) 92 | 93 | snake.moveInScreenSize(screenSize) 94 | assert.Equal(t, snake.newHead(screenSize), Node{x: 0, y: 0}) 95 | } 96 | 97 | func TestSnakeGrow(t *testing.T) { 98 | var snake = NewSnake() 99 | var screenSize = ScreenSize{3, 3} 100 | 101 | snake.growInScreenSize(screenSize) 102 | 103 | assert.Equal(t, snake.Len(), 3) 104 | assert.Equal(t, snake.body[0], Node{x: 0, y: 0}) 105 | assert.Equal(t, snake.body[1], Node{x: 1, y: 0}) 106 | assert.Equal(t, snake.body[2], Node{x: 2, y: 0}) 107 | 108 | snake.Turn(SNAKE_DIRECTION_DOWN) 109 | snake.moveInScreenSize(screenSize) 110 | assert.Equal(t, snake.Len(), 3) 111 | assert.Equal(t, snake.body[0], Node{x: 1, y: 0}) 112 | assert.Equal(t, snake.body[1], Node{x: 2, y: 0}) 113 | assert.Equal(t, snake.body[2], Node{x: 2, y: 1}) 114 | } 115 | 116 | func TestSnakeTurn(t *testing.T) { 117 | var snake = NewSnake() 118 | snake.Turn(SNAKE_DIRECTION_UP) 119 | assert.Equal(t, snake.direction, Direction(SNAKE_DIRECTION_UP)) 120 | } 121 | 122 | func TestSnakeUTurn(t *testing.T) { 123 | var snake = NewSnake() 124 | snake.direction = SNAKE_DIRECTION_DOWN 125 | snake.Turn(SNAKE_DIRECTION_UP) 126 | assert.Equal(t, snake.direction, Direction(SNAKE_DIRECTION_DOWN)) 127 | } 128 | 129 | func TestBodyContains(t *testing.T) { 130 | var body = Body{Node{x: 0, y: 0}, Node{x: 1, y: 1}} 131 | assert.Equal(t, body.Contains(Node{x: 1, y: 1}), true) 132 | } 133 | 134 | func TestBodyContainsFails(t *testing.T) { 135 | var body = Body{Node{x: 0, y: 0}, Node{x: 1, y: 1}} 136 | assert.Equal(t, body.Contains(Node{x: 1, y: 2}), false) 137 | } 138 | --------------------------------------------------------------------------------