diff --git a/client.go b/client.go index f04e39b..1b37b0e 100644 --- a/client.go +++ b/client.go @@ -27,17 +27,14 @@ type Client struct { Server string Game server.GameParam + // max dimensions of field + Width, Height float64 + boardstate *server.Boardstate enc encoder dec decoder ws *websocket.Conn - // visualization members - width, height float64 - viewX, viewY int - StateStream chan *server.Boardstate - Die chan struct{} - Player } @@ -149,8 +146,8 @@ func (c *Client) Negotiate(clientType string, player Player) (err error) { case "spectator": } - c.width = c.Game.BoardSize.Width - c.height = c.Game.BoardSize.Height + c.Width = c.Game.BoardSize.Width + c.Height = c.Game.BoardSize.Height return nil } @@ -167,11 +164,6 @@ func (c *Client) Play() error { 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)) if err != nil { return err diff --git a/cmd/botspectate/main.go b/cmd/botspectate/main.go index eee8d6a..4f76423 100644 --- a/cmd/botspectate/main.go +++ b/cmd/botspectate/main.go @@ -8,7 +8,6 @@ import ( "time" "hackerbots.us/client" - "hackerbots.us/server" ) 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() { rand.Seed(time.Now().UnixNano()) - var gameId string + var gameID string flag.Parse() if flag.NArg() < 1 { - gameId = "debug" + gameID = "debug" } else { - gameId = flag.Arg(0) + gameID = flag.Arg(0) } c := &client.Client{ - Server: *addr, - Name: "bspect", - GameId: gameId, - ForceJSON: *forceJSON, - StateStream: make(chan *server.Boardstate), - Die: make(chan struct{}), - Player: client.Spectator{}, + Server: *addr, + Name: "bspect", + GameId: gameID, + ForceJSON: *forceJSON, } var err error err = c.Negotiate("spectator", c.Player) @@ -40,10 +36,16 @@ func main() { os.Exit(1) } + ui := client.NewSpectator(c.Width, c.Height) + c.Player = ui + go func() { 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) + } } diff --git a/cmd/gobot/main.go b/cmd/gobot/main.go index 8784d78..c663352 100644 --- a/cmd/gobot/main.go +++ b/cmd/gobot/main.go @@ -26,26 +26,24 @@ var weaponSpeed = flag.Int("wspeed", 50, "weapons speed") var addr = flag.String("addr", "ws://localhost:8666", "server hostname") var botname = flag.String("name", "gobot", "the name that other players will see") 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() { rand.Seed(time.Now().UnixNano()) - var gameId string + var gameID string flag.Parse() if flag.NArg() < 1 { - gameId = "debug" + gameID = "debug" } else { - gameId = flag.Arg(0) + gameID = flag.Arg(0) } c := &client.Client{ - Server: *addr, - Name: *botname, - GameId: gameId, - // XXX: update with missing fields - ForceJSON: *forceJSON, - StateStream: make(chan *server.Boardstate), - Die: make(chan struct{}), + Server: *addr, + Name: *botname, + GameId: gameID, + ForceJSON: *forceJSON, } sr := server.StatsRequest{ Hp: *hp, @@ -69,17 +67,33 @@ func main() { os.Exit(1) } - var err error - err = c.Negotiate("robot", c.Player) - if err != nil { + if err := c.Negotiate("robot", c.Player); err != nil { fmt.Fprintf(os.Stderr, "%s: failed to negociate: %s\n", c.Name, err) os.Exit(1) } - go func() { - if err := c.Play(); err != nil { - close(c.Die) + if *spectate { + ui := client.NewSpectator(c.Width, c.Height) + players := client.MultiPlayer{ + c.Player, + ui, } - }() - c.Visualize() + c.Player = players + + 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) + } + } } diff --git a/player.go b/player.go index a58277e..c9b360a 100644 --- a/player.go +++ b/player.go @@ -2,8 +2,7 @@ package client import "hackerbots.us/server" -// Player is the interface that is implemented when specifying non-default -// player behavior. +// Player is the interface that defines a player's behavior. // // 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. @@ -25,14 +24,36 @@ type Player interface { 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) {} - -func (s Spectator) GetStats() map[string]server.StatsRequest { - return nil +// GetStats implements GetStats for a collection of players asking each player +// for their stats, and returning the configuration of the first player. +func (mp MultiPlayer) GetStats() map[string]server.StatsRequest { + 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 { - return nil +// SetIDs passes ids to all players. +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 } diff --git a/spectator.go b/spectator.go index 24075a4..e2f85c7 100644 --- a/spectator.go +++ b/spectator.go @@ -1,33 +1,61 @@ package client import ( + "errors" "fmt" "math" - "github.com/nsf/termbox-go" - "hackerbots.us/server" + + "github.com/nsf/termbox-go" ) -var botDown rune = 'v' -var botUp rune = '^' -var botRight rune = '>' -var botLeft rune = '<' +const d = 'v' +const u = '^' +const r = '>' +const l = '<' -type size struct { - width, height int +// 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, + } } -// Recv is our implementation of receiving a server.Boardstate from the server -func (s *Client) Recv(bs *server.Boardstate) map[string]server.Instruction { +// 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{} +} + +// 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 } -func (s *Client) Visualize() { +// Spectate runs the termbox render loop. +func (s *Spectator) Spectate() error { err := termbox.Init() if err != nil { - panic(err) + return err } s.viewX, s.viewY = termbox.Size() @@ -56,6 +84,13 @@ func (s *Client) Visualize() { switch event.Ch { case 'q': return + case 'f': + termbox.SetCell( + 20, + 20, + '*', + termbox.ColorRed, termbox.ColorBlack, + ) case 'c': termbox.Clear(termbox.ColorBlack, termbox.ColorBlack) } @@ -63,12 +98,13 @@ func (s *Client) Visualize() { case termbox.EventResize: s.viewX, s.viewY = event.Width, event.Height 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) - for _, obstacle := range u.Obstacles { + 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)) @@ -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)) 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 = botRight + b = r } else { - b = botLeft + b = l } } else { if bot.Heading.Y > 0 { - b = botUp + b = u } else { - b = botDown + b = d } } 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)) y := int((p.Position.Y / s.height) * float64(s.viewY)) 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)) 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 @@ -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)) 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 = botRight + b = r } else { - b = botLeft + b = l } } else { if bot.Heading.Y > 0 { - b = botUp + b = u } else { - b = botDown + b = d } } c := termbox.ColorWhite @@ -178,15 +214,14 @@ func (s *Client) Visualize() { ) } - err := termbox.Flush() - if err != nil { - panic(err) - } + err = termbox.Flush() case <-s.Die: + err = errors.New("was told to die") return } } }() termbox.Close() + return err }