236 lines
4.6 KiB
Go
236 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"time"
|
|
|
|
termbox "github.com/nsf/termbox-go"
|
|
)
|
|
|
|
func main() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
score, err := playSnake()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("Final score:", score)
|
|
}
|
|
|
|
type snake struct {
|
|
body []position // tail to head positions of the snake
|
|
heading direction
|
|
width, height int
|
|
cells []termbox.Cell
|
|
}
|
|
|
|
type position struct {
|
|
X int
|
|
Y int
|
|
}
|
|
|
|
type direction int
|
|
|
|
const (
|
|
North direction = iota
|
|
East
|
|
South
|
|
West
|
|
)
|
|
|
|
func (p position) next(d direction) position {
|
|
switch d {
|
|
case North:
|
|
p.Y--
|
|
case East:
|
|
p.X++
|
|
case South:
|
|
p.Y++
|
|
case West:
|
|
p.X--
|
|
}
|
|
return p
|
|
}
|
|
|
|
func playSnake() (int, error) {
|
|
err := termbox.Init()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer termbox.Close()
|
|
|
|
termbox.Clear(fg, bg)
|
|
termbox.HideCursor()
|
|
s := &snake{
|
|
// It would be more efficient to use a circular
|
|
// buffer instead of a plain slice for s.body.
|
|
body: make([]position, 0, 32),
|
|
cells: termbox.CellBuffer(),
|
|
}
|
|
s.width, s.height = termbox.Size()
|
|
s.drawBorder()
|
|
s.startSnake()
|
|
s.placeFood()
|
|
s.flush()
|
|
|
|
moveCh, errCh := s.startEventLoop()
|
|
const delay = 125 * time.Millisecond
|
|
for t := time.NewTimer(delay); ; t.Reset(delay) {
|
|
var move direction
|
|
select {
|
|
case err = <-errCh:
|
|
return len(s.body), err
|
|
case move = <-moveCh:
|
|
if !t.Stop() {
|
|
<-t.C // handles race between moveCh and t.C
|
|
}
|
|
case <-t.C:
|
|
move = s.heading
|
|
}
|
|
if s.doMove(move) {
|
|
time.Sleep(1 * time.Second)
|
|
break
|
|
}
|
|
}
|
|
|
|
return len(s.body), err
|
|
}
|
|
|
|
func (s *snake) startEventLoop() (<-chan direction, <-chan error) {
|
|
moveCh := make(chan direction)
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
defer close(errCh)
|
|
for {
|
|
switch ev := termbox.PollEvent(); ev.Type {
|
|
case termbox.EventKey:
|
|
switch ev.Ch { // WSAD and HJKL movement
|
|
case 'w', 'W', 'k', 'K':
|
|
moveCh <- North
|
|
case 'a', 'A', 'h', 'H':
|
|
moveCh <- West
|
|
case 's', 'S', 'j', 'J':
|
|
moveCh <- South
|
|
case 'd', 'D', 'l', 'L':
|
|
moveCh <- East
|
|
case 0:
|
|
switch ev.Key { // Cursor key movement
|
|
case termbox.KeyArrowUp:
|
|
moveCh <- North
|
|
case termbox.KeyArrowDown:
|
|
moveCh <- South
|
|
case termbox.KeyArrowLeft:
|
|
moveCh <- West
|
|
case termbox.KeyArrowRight:
|
|
moveCh <- East
|
|
case termbox.KeyEsc: // Quit
|
|
return
|
|
}
|
|
}
|
|
case termbox.EventResize:
|
|
// TODO
|
|
errCh <- errors.New("terminal resizing unsupported")
|
|
return
|
|
case termbox.EventError:
|
|
errCh <- ev.Err
|
|
return
|
|
case termbox.EventInterrupt:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return moveCh, errCh
|
|
}
|
|
|
|
func (s *snake) flush() {
|
|
termbox.Flush()
|
|
s.cells = termbox.CellBuffer()
|
|
}
|
|
|
|
func (s *snake) getCellRune(p position) rune {
|
|
i := p.Y*s.width + p.X
|
|
return s.cells[i].Ch
|
|
}
|
|
func (s *snake) setCell(p position, c termbox.Cell) {
|
|
i := p.Y*s.width + p.X
|
|
s.cells[i] = c
|
|
}
|
|
|
|
func (s *snake) drawBorder() {
|
|
for x := 0; x < s.width; x++ {
|
|
s.setCell(position{x, 0}, border)
|
|
s.setCell(position{x, s.height - 1}, border)
|
|
}
|
|
for y := 0; y < s.height-1; y++ {
|
|
s.setCell(position{0, y}, border)
|
|
s.setCell(position{s.width - 1, y}, border)
|
|
}
|
|
}
|
|
|
|
func (s *snake) placeFood() {
|
|
for {
|
|
// a random empty cell
|
|
x := rand.Intn(s.width-2) + 1
|
|
y := rand.Intn(s.height-2) + 1
|
|
foodp := position{x, y}
|
|
r := s.getCellRune(foodp)
|
|
if r != ' ' {
|
|
continue
|
|
}
|
|
s.setCell(foodp, food)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *snake) startSnake() {
|
|
// a random cell somewhat near the center
|
|
x := rand.Intn(s.width/2) + s.width/4
|
|
y := rand.Intn(s.height/2) + s.height/4
|
|
head := position{x, y}
|
|
s.setCell(head, snakeHead)
|
|
s.body = append(s.body[:0], head)
|
|
s.heading = direction(rand.Intn(4))
|
|
}
|
|
|
|
func (s *snake) doMove(move direction) bool {
|
|
head := s.body[len(s.body)-1]
|
|
s.setCell(head, snakeBody)
|
|
head = head.next(move)
|
|
s.heading = move
|
|
s.body = append(s.body, head)
|
|
r := s.getCellRune(head)
|
|
s.setCell(head, snakeHead)
|
|
gameOver := false
|
|
switch r {
|
|
case food.Ch:
|
|
s.placeFood()
|
|
case border.Ch, snakeBody.Ch:
|
|
gameOver = true
|
|
fallthrough
|
|
case empty.Ch:
|
|
s.setCell(s.body[0], empty)
|
|
s.body = s.body[1:]
|
|
default:
|
|
panic(r)
|
|
}
|
|
s.flush()
|
|
return gameOver
|
|
}
|
|
|
|
const (
|
|
fg = termbox.ColorWhite
|
|
bg = termbox.ColorBlack
|
|
)
|
|
|
|
// Symbols to use.
|
|
// Could use Unicode instead of simple ASCII.
|
|
var (
|
|
empty = termbox.Cell{Ch: ' ', Bg: bg, Fg: fg}
|
|
border = termbox.Cell{Ch: '+', Bg: bg, Fg: termbox.ColorBlue}
|
|
snakeBody = termbox.Cell{Ch: '#', Bg: bg, Fg: termbox.ColorGreen}
|
|
snakeHead = termbox.Cell{Ch: 'O', Bg: bg, Fg: termbox.ColorYellow | termbox.AttrBold}
|
|
food = termbox.Cell{Ch: '@', Bg: bg, Fg: termbox.ColorRed}
|
|
)
|