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 }