RosettaCodeData/Task/Snake/Go/snake.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}
)