make term ui optional and orthogonal to client and player implementations #1

Open
sm wants to merge 3 commits from viz into master
5 changed files with 149 additions and 85 deletions

View File

@ -27,17 +27,14 @@ type Client struct {
Server string Server string
Game server.GameParam Game server.GameParam
// max dimensions of field
Width, Height float64
boardstate *server.Boardstate boardstate *server.Boardstate
enc encoder enc encoder
dec decoder dec decoder
ws *websocket.Conn ws *websocket.Conn
// visualization members
width, height float64
viewX, viewY int
StateStream chan *server.Boardstate
Die chan struct{}
Player Player
} }
@ -149,8 +146,8 @@ func (c *Client) Negotiate(clientType string, player Player) (err error) {
case "spectator": case "spectator":
} }
c.width = c.Game.BoardSize.Width c.Width = c.Game.BoardSize.Width
c.height = c.Game.BoardSize.Height c.Height = c.Game.BoardSize.Height
return nil return nil
} }
@ -167,11 +164,6 @@ func (c *Client) Play() error {
return errors.New(fmt.Sprintf("%s: Connection likely lost: %s", c.Name, err)) return errors.New(fmt.Sprintf("%s: Connection likely lost: %s", c.Name, err))
} }
select {
case c.StateStream <- bs:
default:
}
err = c.enc.Encode(c.Update(bs)) err = c.enc.Encode(c.Update(bs))
if err != nil { if err != nil {
return err return err

View File

@ -8,7 +8,6 @@ import (
"time" "time"
"hackerbots.us/client" "hackerbots.us/client"
"hackerbots.us/server"
) )
var addr = flag.String("addr", "ws://localhost:8666", "server hostname") var addr = flag.String("addr", "ws://localhost:8666", "server hostname")
@ -16,22 +15,19 @@ var forceJSON = flag.Bool("json", false, "force json encoding")
func main() { func main() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
var gameId string var gameID string
flag.Parse() flag.Parse()
if flag.NArg() < 1 { if flag.NArg() < 1 {
gameId = "debug" gameID = "debug"
} else { } else {
gameId = flag.Arg(0) gameID = flag.Arg(0)
} }
c := &client.Client{ c := &client.Client{
Server: *addr, Server: *addr,
Name: "bspect", Name: "bspect",
GameId: gameId, GameId: gameID,
ForceJSON: *forceJSON, ForceJSON: *forceJSON,
StateStream: make(chan *server.Boardstate),
Die: make(chan struct{}),
Player: client.Spectator{},
} }
var err error var err error
err = c.Negotiate("spectator", c.Player) err = c.Negotiate("spectator", c.Player)
@ -40,10 +36,16 @@ func main() {
os.Exit(1) os.Exit(1)
} }
ui := client.NewSpectator(c.Width, c.Height)
c.Player = ui
go func() { go func() {
if err := c.Play(); err != nil { if err := c.Play(); err != nil {
close(c.Die) close(ui.Die)
} }
}() }()
c.Visualize() if err := ui.Spectate(); err != nil {
fmt.Fprintf(os.Stderr, "problem during visualization: %+v\n", err)
os.Exit(1)
}
} }

View File

@ -26,26 +26,24 @@ var weaponSpeed = flag.Int("wspeed", 50, "weapons speed")
var addr = flag.String("addr", "ws://localhost:8666", "server hostname") var addr = flag.String("addr", "ws://localhost:8666", "server hostname")
var botname = flag.String("name", "gobot", "the name that other players will see") var botname = flag.String("name", "gobot", "the name that other players will see")
var forceJSON = flag.Bool("json", false, "force json encoding") var forceJSON = flag.Bool("json", false, "force json encoding")
var botType = flag.String("bot", "simple", "which Bot") var botType = flag.String("bot", "simple", "which Bot [fraserbot, simple]")
var spectate = flag.Bool("spectate", false, "enable terminal visualizer")
func main() { func main() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
var gameId string var gameID string
flag.Parse() flag.Parse()
if flag.NArg() < 1 { if flag.NArg() < 1 {
gameId = "debug" gameID = "debug"
} else { } else {
gameId = flag.Arg(0) gameID = flag.Arg(0)
} }
c := &client.Client{ c := &client.Client{
Server: *addr, Server: *addr,
Name: *botname, Name: *botname,
GameId: gameId, GameId: gameID,
// XXX: update with missing fields ForceJSON: *forceJSON,
ForceJSON: *forceJSON,
StateStream: make(chan *server.Boardstate),
Die: make(chan struct{}),
} }
sr := server.StatsRequest{ sr := server.StatsRequest{
Hp: *hp, Hp: *hp,
@ -69,17 +67,33 @@ func main() {
os.Exit(1) os.Exit(1)
} }
var err error if err := c.Negotiate("robot", c.Player); err != nil {
err = c.Negotiate("robot", c.Player)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: failed to negociate: %s\n", c.Name, err) fmt.Fprintf(os.Stderr, "%s: failed to negociate: %s\n", c.Name, err)
os.Exit(1) os.Exit(1)
} }
go func() { if *spectate {
if err := c.Play(); err != nil { ui := client.NewSpectator(c.Width, c.Height)
close(c.Die) players := client.MultiPlayer{
c.Player,
ui,
} }
}() c.Player = players
c.Visualize()
go func() {
if err := c.Play(); err != nil {
fmt.Fprintf(os.Stderr, "problem during play: %v\n", err)
close(ui.Die)
}
}()
if err := ui.Spectate(); err != nil {
fmt.Fprintf(os.Stderr, "problem during visualization: %+v\n", err)
os.Exit(1)
}
} else {
if err := c.Play(); err != nil {
fmt.Fprintf(os.Stderr, "problem during play: %v\n", err)
os.Exit(1)
}
}
} }

View File

@ -2,8 +2,7 @@ package client
import "hackerbots.us/server" import "hackerbots.us/server"
// Player is the interface that is implemented when specifying non-default // Player is the interface that defines a player's behavior.
// player behavior.
// //
// The general case will be to implement a Player type that contains the magic // The general case will be to implement a Player type that contains the magic
// required to slay other robots quickly while staying alive for a long time. // required to slay other robots quickly while staying alive for a long time.
@ -25,14 +24,36 @@ type Player interface {
Update(bs *server.Boardstate) map[string]server.Instruction Update(bs *server.Boardstate) map[string]server.Instruction
} }
type Spectator struct{} // MultiPlayer wraps multiple players and calls funcitons in Player in
// appropriate order.
//
// Typically used to add a Spectator to a bot.
type MultiPlayer []Player
func (s Spectator) SetIDs(map[string]string) {} // GetStats implements GetStats for a collection of players asking each player
// for their stats, and returning the configuration of the first player.
func (s Spectator) GetStats() map[string]server.StatsRequest { func (mp MultiPlayer) GetStats() map[string]server.StatsRequest {
return nil var s map[string]server.StatsRequest
for i := len(mp) - 1; i >= 0; i-- {
s = mp[i].GetStats()
}
return s
} }
func (s Spectator) Update(bs *server.Boardstate) map[string]server.Instruction { // SetIDs passes ids to all players.
return nil func (mp MultiPlayer) SetIDs(ids map[string]string) {
for _, p := range mp {
p.SetIDs(ids)
}
}
// Update implements Update for a collection of players sending board state to
// each player, and returning the instructions associated with the first
// player.
func (mp MultiPlayer) Update(bs *server.Boardstate) map[string]server.Instruction {
var inst map[string]server.Instruction
for i := len(mp) - 1; i >= 0; i-- {
inst = mp[i].Update(bs)
}
return inst
} }

View File

@ -1,33 +1,61 @@
package client package client
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"github.com/nsf/termbox-go"
"hackerbots.us/server" "hackerbots.us/server"
"github.com/nsf/termbox-go"
) )
var botDown rune = 'v' const d = 'v'
var botUp rune = '^' const u = '^'
var botRight rune = '>' const r = '>'
var botLeft rune = '<' const l = '<'
type size struct { // NewSpectator initializes and returns a *Spectator.
width, height int func NewSpectator(w, h float64) *Spectator {
return &Spectator{
StateStream: make(chan *server.Boardstate),
Die: make(chan struct{}),
width: w,
height: h,
}
} }
// Recv is our implementation of receiving a server.Boardstate from the server // Spectator encodes a termbox ui.
func (s *Client) Recv(bs *server.Boardstate) map[string]server.Instruction { 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{}
}
// 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 s.StateStream <- bs
return nil return nil
} }
func (s *Client) Visualize() { // Spectate runs the termbox render loop.
func (s *Spectator) Spectate() error {
err := termbox.Init() err := termbox.Init()
if err != nil { if err != nil {
panic(err) return err
} }
s.viewX, s.viewY = termbox.Size() s.viewX, s.viewY = termbox.Size()
@ -56,6 +84,13 @@ func (s *Client) Visualize() {
switch event.Ch { switch event.Ch {
case 'q': case 'q':
return return
case 'f':
termbox.SetCell(
20,
20,
'*',
termbox.ColorRed, termbox.ColorBlack,
)
case 'c': case 'c':
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack) termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
} }
@ -63,12 +98,13 @@ func (s *Client) Visualize() {
case termbox.EventResize: case termbox.EventResize:
s.viewX, s.viewY = event.Width, event.Height s.viewX, s.viewY = event.Width, event.Height
case termbox.EventError: case termbox.EventError:
panic(fmt.Sprintf("Quitting because of termbox error:\n%v\n", event.Err)) err = fmt.Errorf("Quitting because of termbox error:\n%v\n", event.Err)
return
} }
case u := <-s.StateStream: case update := <-s.StateStream:
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack) termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
for _, obstacle := range u.Obstacles { for _, obstacle := range update.Obstacles {
startX := int((obstacle.Bounds.A.X / s.width) * float64(s.viewX)) startX := int((obstacle.Bounds.A.X / s.width) * float64(s.viewX))
stopX := int((obstacle.Bounds.B.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)) startY := int((obstacle.Bounds.A.Y / s.height) * float64(s.viewY))
@ -85,21 +121,21 @@ func (s *Client) Visualize() {
} }
} }
for _, bot := range u.OtherRobots { for _, bot := range update.OtherRobots {
x := int((bot.Position.X / s.width) * float64(s.viewX)) x := int((bot.Position.X / s.width) * float64(s.viewX))
y := int((bot.Position.Y / s.height) * float64(s.viewY)) y := int((bot.Position.Y / s.height) * float64(s.viewY))
var b rune var b rune
if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) { if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) {
if bot.Heading.X > 0 { if bot.Heading.X > 0 {
b = botRight b = r
} else { } else {
b = botLeft b = l
} }
} else { } else {
if bot.Heading.Y > 0 { if bot.Heading.Y > 0 {
b = botUp b = u
} else { } else {
b = botDown b = d
} }
} }
c := termbox.ColorRed c := termbox.ColorRed
@ -114,7 +150,7 @@ func (s *Client) Visualize() {
) )
} }
for _, p := range u.Projectiles { for _, p := range update.Projectiles {
x := int((p.Position.X / s.width) * float64(s.viewX)) x := int((p.Position.X / s.width) * float64(s.viewX))
y := int((p.Position.Y / s.height) * float64(s.viewY)) y := int((p.Position.Y / s.height) * float64(s.viewY))
termbox.SetCell( termbox.SetCell(
@ -125,7 +161,7 @@ func (s *Client) Visualize() {
) )
} }
for _, splosion := range u.Splosions { for _, splosion := range update.Splosions {
startX := int(((splosion.Position.X - float64(splosion.Radius)) / s.width) * float64(s.viewX)) 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)) 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 stopX := int(((splosion.Position.X+float64(splosion.Radius))/s.width)*float64(s.viewX)) + 1
@ -149,21 +185,21 @@ func (s *Client) Visualize() {
} }
} }
for _, bot := range u.MyRobots { for _, bot := range update.MyRobots {
x := int((bot.Position.X / s.width) * float64(s.viewX)) x := int((bot.Position.X / s.width) * float64(s.viewX))
y := int((bot.Position.Y / s.height) * float64(s.viewY)) y := int((bot.Position.Y / s.height) * float64(s.viewY))
var b rune var b rune
if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) { if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) {
if bot.Heading.X > 0 { if bot.Heading.X > 0 {
b = botRight b = r
} else { } else {
b = botLeft b = l
} }
} else { } else {
if bot.Heading.Y > 0 { if bot.Heading.Y > 0 {
b = botUp b = u
} else { } else {
b = botDown b = d
} }
} }
c := termbox.ColorWhite c := termbox.ColorWhite
@ -178,15 +214,14 @@ func (s *Client) Visualize() {
) )
} }
err := termbox.Flush() err = termbox.Flush()
if err != nil {
panic(err)
}
case <-s.Die: case <-s.Die:
err = errors.New("was told to die")
return return
} }
} }
}() }()
termbox.Close() termbox.Close()
return err
} }