client/spectator.go

238 lines
5.6 KiB
Go

package client
import (
"errors"
"fmt"
"math"
"hackerbots.us/server"
"github.com/nsf/termbox-go"
)
const d = 'v'
const u = '^'
const r = '>'
const l = '<'
// NewSpectator initializes and returns a *Spectator.
func NewSpectator(w, h float64) *Spectator {
return &Spectator{
StateStream: make(chan *server.Boardstate),
Die: make(chan struct{}),
width: w,
height: h,
}
}
// Spectator encodes a termbox ui.
type Spectator struct {
// max dimensions of field
width, height float64
// dimensions of the terminal window
viewX, viewY int
StateStream chan *server.Boardstate
// when closed will cause the Spectator to exit the render loop.
Die chan struct{}
// User, if populated, will return a stream of (ostensibly keyboard) events
// for use outside of the Spectator.
User chan termbox.Event
}
// SetIDs is implemented so Spectator can be used as a client.Player.
func (s Spectator) SetIDs(map[string]string) {}
// GetStats is implemented so Spectator can be used as a client.Player.
func (s Spectator) GetStats() map[string]server.StatsRequest { return nil }
// Update is implemented so Spectator can be used as a client.Player.
func (s Spectator) Update(bs *server.Boardstate) map[string]server.Instruction {
s.StateStream <- bs
return nil
}
// Spectate runs the termbox render loop.
func (s *Spectator) Spectate() error {
err := termbox.Init()
if err != nil {
return err
}
termbox.SetInputMode(termbox.InputMouse)
s.viewX, s.viewY = termbox.Size()
events := make(chan termbox.Event, 1024)
go func() {
for {
events <- termbox.PollEvent()
}
}()
termbox.HideCursor()
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
func() {
for {
select {
case event := <-events:
switch event.Type {
case termbox.EventKey:
switch event.Key {
case termbox.KeyCtrlZ, termbox.KeyCtrlC:
return
}
switch event.Ch {
case 'q':
return
case 'f':
termbox.SetCell(
20,
20,
'*',
termbox.ColorRed, termbox.ColorBlack,
)
case 'c':
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
}
case termbox.EventResize:
s.viewX, s.viewY = event.Width, event.Height
case termbox.EventError:
err = fmt.Errorf("Quitting because of termbox error:\n%v\n", event.Err)
return
}
if s.User != nil {
s.User <- event
}
case update := <-s.StateStream:
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
for _, obstacle := range update.Obstacles {
startX := int((obstacle.Bounds.A.X / s.width) * float64(s.viewX))
stopX := int((obstacle.Bounds.B.X / s.width) * float64(s.viewX))
startY := int((obstacle.Bounds.A.Y / s.height) * float64(s.viewY))
stopY := int((obstacle.Bounds.B.Y / s.height) * float64(s.viewY))
for x := startX; x < stopX; x++ {
for y := startY; y < stopY; y++ {
termbox.SetCell(
x,
s.viewY-y,
' ',
termbox.ColorBlack, termbox.ColorBlue,
)
}
}
}
for _, bot := range update.OtherRobots {
x := int((bot.Position.X / s.width) * float64(s.viewX))
y := int((bot.Position.Y / s.height) * float64(s.viewY))
var b rune
if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) {
if bot.Heading.X > 0 {
b = r
} else {
b = l
}
} else {
if bot.Heading.Y > 0 {
b = u
} else {
b = d
}
}
c := termbox.ColorRed
if bot.Health <= 0 {
c = termbox.ColorBlack
}
termbox.SetCell(
x,
s.viewY-y,
b,
c, termbox.ColorBlack,
)
}
for _, p := range update.Projectiles {
x := int((p.Position.X / s.width) * float64(s.viewX))
y := int((p.Position.Y / s.height) * float64(s.viewY))
termbox.SetCell(
x,
s.viewY-y,
'·',
termbox.ColorWhite|termbox.AttrBold, termbox.ColorBlack,
)
}
for _, splosion := range update.Splosions {
startX := int(((splosion.Position.X - float64(splosion.Radius)) / s.width) * float64(s.viewX))
startY := int(((splosion.Position.Y - float64(splosion.Radius)) / s.height) * float64(s.viewY))
stopX := int(((splosion.Position.X+float64(splosion.Radius))/s.width)*float64(s.viewX)) + 1
stopY := int(((splosion.Position.Y+float64(splosion.Radius))/s.height)*float64(s.viewY)) + 1
for x := startX; x < stopX; x++ {
for y := startY; y < stopY; y++ {
realX := float64(x) * s.width / float64(s.viewX)
realY := float64(y) * s.height / float64(s.viewY)
dX := realX - splosion.Position.X
dY := realY - splosion.Position.Y
curRad := math.Sqrt(dX*dX + dY*dY)
if curRad < float64(splosion.Radius) {
termbox.SetCell(
x,
s.viewY-y,
'·',
termbox.ColorYellow|termbox.AttrBold, termbox.ColorRed,
)
}
}
}
}
for _, bot := range update.MyRobots {
x := int((bot.Position.X / s.width) * float64(s.viewX))
y := int((bot.Position.Y / s.height) * float64(s.viewY))
var b rune
if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) {
if bot.Heading.X > 0 {
b = r
} else {
b = l
}
} else {
if bot.Heading.Y > 0 {
b = u
} else {
b = d
}
}
c := termbox.ColorWhite
if bot.Health <= 0 {
c = termbox.ColorBlack
}
termbox.SetCell(
x,
s.viewY-y,
b,
c|termbox.AttrBold, termbox.ColorBlack,
)
}
err = termbox.Flush()
case <-s.Die:
err = errors.New("was told to die")
return
}
}
}()
termbox.Close()
return err
}