From d0c3b5a117c4a0d78775d1a631de5f3ff0e244db Mon Sep 17 00:00:00 2001 From: Stephen McQuay Date: Wed, 9 Apr 2014 21:35:54 -0700 Subject: [PATCH] prep code for reuse --- client.go | 169 ++++++++++++++++++++++++++ gobot/main.go | 70 +++++------ player.go | 160 +++++++++++++++++++++++++ robot.go | 326 -------------------------------------------------- shims.go | 33 ----- 5 files changed, 365 insertions(+), 393 deletions(-) create mode 100644 client.go create mode 100644 player.go delete mode 100644 robot.go delete mode 100644 shims.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..e8c03f6 --- /dev/null +++ b/client.go @@ -0,0 +1,169 @@ +package botclient + +import ( + "encoding/gob" + "encoding/json" + "errors" + "fmt" + "log" + + "bitbucket.org/hackerbots/botserv" + "code.google.com/p/go.net/websocket" +) + +func connect(server string, port int) (*websocket.Conn, error) { + origin := "http://localhost/" + url := fmt.Sprintf("ws://%s:%d/ws/", server, port) + return websocket.Dial(url, "", origin) +} + +type Client struct { + ForceJSON bool + GameId string + Name string + Port int + Server string + StatsReq botserv.StatsRequest + Verbose bool + Player Player + Game botserv.GameParam + + boardstate botserv.Boardstate + ws *websocket.Conn +} + +type encoder interface { + Encode(v interface{}) error +} + +type decoder interface { + Decode(v interface{}) error +} + +func (c *Client) Negociate() (err error) { + if c.Verbose { + log.Printf("%s: trying to connect to game '%s'", c.Name, c.GameId) + } + c.ws, err = connect(c.Server, c.Port) + if err != nil { + return errors.New(fmt.Sprintf("connection failure: %s", err)) + } + + err = websocket.JSON.Send(c.ws, struct { + Id string `json:"id"` + }{ + c.GameId, + }) + if err != nil { + return err + } + + var idreq struct { + Type string `json:"type"` + PlayerId string `json:"id"` + } + err = websocket.JSON.Receive(c.ws, &idreq) + if err != nil || idreq.Type == "failure" { + return errors.New(fmt.Sprintf("failure: %+v", idreq)) + } + if c.Verbose { + log.Printf("%s: idreq: %+v", c.Name, idreq) + } + + err = websocket.JSON.Send(c.ws, struct { + Type string `json:"type"` + Name string `json:"name"` + Useragent string `json:"useragent"` + }{ + Name: c.Name, + Useragent: "gobot", + Type: "robot", + }) + if err != nil { + return err + } + + supportedEncs := []string{"bson", "json", "gob"} + if c.ForceJSON { + supportedEncs = []string{"json"} + } + err = websocket.JSON.Send(c.ws, supportedEncs) + if err != nil { + return errors.New(fmt.Sprintf("failure: %+v", err)) + } + + err = websocket.JSON.Receive(c.ws, &c.Game) + if c.Game.Type != "gameparam" { + return errors.New("didn't receive a good gameparam") + } + if c.Verbose { + log.Printf("%s: game parameters: %+v", c.Name, c.Game) + } + + conf := botserv.ClientConfig{ + ID: c.GameId, + Stats: map[string]botserv.StatsRequest{ + c.Name: c.StatsReq, + }, + } + + err = websocket.JSON.Send(c.ws, conf) + + var handshake struct { + Id string `json:"id"` + Success bool `json:"success"` + Type string `json:"type"` + botserv.Failure + } + websocket.JSON.Receive(c.ws, &handshake) + if !handshake.Success { + return errors.New(handshake.Reason) + } + if c.Verbose { + log.Printf("%s: handshake: %+v", c.Name, handshake) + } + + // we don't do anything useful with dstats, but could be interesting to + // pass along to the player? + dstats := struct { + Stats map[string]botserv.Stats `json:"stats"` + Type string `json:"type"` + }{} + err = websocket.JSON.Receive(c.ws, &dstats) + if err != nil { + return err + } + + return nil +} + +func (c *Client) Play() error { + log.Printf("%s: starting loop", c.Name) + + var enc encoder + var dec decoder + + if c.Game.Encoding == "json" { + enc = json.NewEncoder(c.ws) + dec = json.NewDecoder(c.ws) + } else { + enc = gob.NewEncoder(c.ws) + dec = gob.NewDecoder(c.ws) + } + + var err error + for { + err = dec.Decode(&c.boardstate) + if err != nil { + return errors.New(fmt.Sprintf("%s: Connection likely lost: %s", c.Name, err)) + } + c.Player.Recv(&c.boardstate) + + instruction := c.Player.Instruction() + err = enc.Encode(instruction) + if err != nil { + return err + } + } + return nil +} diff --git a/gobot/main.go b/gobot/main.go index fcd1679..8f1fcdc 100644 --- a/gobot/main.go +++ b/gobot/main.go @@ -2,10 +2,8 @@ package main import ( "flag" - "fmt" "log" "math/rand" - "sync" "time" "bitbucket.org/hackerbots/botclient" @@ -26,49 +24,53 @@ var weaponSpeed = flag.Int("wspeed", 50, "weapons speed") var server = flag.String("server", "localhost", "server hostname") var port = flag.Int("port", 8666, "server port") - var botname = flag.String("name", "gobot", "the name that other players will see") - -var botcount = flag.Int("bots", 1, "number of bots to spin up") - var verbose = flag.Bool("verbose", false, "run verbosly") var forceJSON = flag.Bool("json", false, "force json encoding") func main() { log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) rand.Seed(time.Now().UnixNano()) - var gameName string + var gameId string flag.Parse() if flag.NArg() < 1 { - gameName = "debug" + gameId = "debug" } else { - gameName = flag.Arg(0) + gameId = flag.Arg(0) } - var wg sync.WaitGroup - for i := 0; i < *botcount; i++ { - r := &botclient.Robot{ - Server: *server, - Port: *port, - Name: fmt.Sprintf("%s%d", *botname, i), - GameName: gameName, - // XXX: update with missing fields - StatsReq: botserv.StatsRequest{ - Hp: *hp, - Speed: *speed, - Acceleration: *acceleration, - ScannerRadius: *scannerRadius, - TurnSpeed: *turnSpeed, - FireRate: *fireRate, - WeaponRadius: *weaponRadius, - WeaponDamage: *weaponDamage, - WeaponSpeed: *weaponSpeed, - }, - Wg: &wg, - KnownObstacles: make(map[string]botserv.Obstacle), - } - wg.Add(1) - go r.Play() + c := &botclient.Client{ + Server: *server, + Port: *port, + Name: *botname, + GameId: gameId, + // XXX: update with missing fields + StatsReq: botserv.StatsRequest{ + Hp: *hp, + Speed: *speed, + Acceleration: *acceleration, + ScannerRadius: *scannerRadius, + TurnSpeed: *turnSpeed, + FireRate: *fireRate, + WeaponRadius: *weaponRadius, + WeaponDamage: *weaponDamage, + WeaponSpeed: *weaponSpeed, + }, + Verbose: *verbose, + ForceJSON: *forceJSON, + } + var err error + err = c.Negociate() + if err != nil { + log.Printf("%s: failed to negociate: %s", c.Name, err) + } + + c.Player = botclient.NewSimplePlayer( + c.Game.BoardSize.Width, + c.Game.BoardSize.Height, + ) + + if err := c.Play(); err != nil { + log.Fatal(err) } - wg.Wait() } diff --git a/player.go b/player.go new file mode 100644 index 0000000..a988cb0 --- /dev/null +++ b/player.go @@ -0,0 +1,160 @@ +package botclient + +import ( + "fmt" + "math" + "math/rand" + + "bitbucket.org/hackerbots/botserv" + "bitbucket.org/hackerbots/vector" +) + +type Player interface { + Recv(bs *botserv.Boardstate) + Instruction() map[string]botserv.Instruction +} + +type SimplePlayer struct { + me botserv.Robot + width, height float32 + knownObstacles map[string]botserv.Obstacle + nearestEnemy *botserv.OtherRobot + fireat *govector.Point2d + moveto *govector.Point2d + speed float32 + maxSpeed float32 + safeDistance float32 +} + +func NewSimplePlayer(width, height float32) *SimplePlayer { + return &SimplePlayer{ + knownObstacles: make(map[string]botserv.Obstacle), + width: width, + height: height, + maxSpeed: 100, + safeDistance: 40, + } +} + +func (p *SimplePlayer) Recv(bs *botserv.Boardstate) { + p.speed = p.maxSpeed + if len(bs.MyRobots) > 0 { + p.me = bs.MyRobots[0] + } else { + return + } + + p.recon(bs) + p.navigate() +} + +func (p *SimplePlayer) navigate() { + if p.moveto == nil { + p.moveto = p.randomDirection() + } + + togo := p.me.Position.Sub(*p.moveto).Mag() + if togo < p.safeDistance+5 { + p.moveto = p.randomDirection() + return + } + if !p.probe(p.me.Position.Add(p.me.Heading.Scale(p.safeDistance))) { + p.speed = 0 + if !p.probe(*p.moveto) { + p.moveto = p.randomDirection() + return + } + } + if p.me.Collision != nil { + p.moveto = p.randomDirection() + p.speed = 0 + return + } +} + +func (p *SimplePlayer) recon(bs *botserv.Boardstate) { + for _, o := range bs.Objects { + obj := MiniObstacle(o) + if _, ok := p.knownObstacles[obj.Id()]; !ok { + p.knownObstacles[obj.Id()] = obj.ToObstacle() + } + } + + // simplest shooting strategy ... need to do the following: + // not shoot through buildings + // shoot at where the robot will be, not where it was. + p.nearestEnemy = nil + p.fireat = nil + closest := float32(math.Inf(1)) + for _, enemy := range bs.OtherRobots { + dist := p.me.Position.Sub(enemy.Position).Mag() + if dist < closest && dist > p.safeDistance { + p.nearestEnemy = &enemy + } + } + if p.nearestEnemy != nil { + point := p.nearestEnemy.Position.Add(p.nearestEnemy.Heading.Scale(p.safeDistance)) + p.fireat = &point + } +} + +func (p *SimplePlayer) Instruction() map[string]botserv.Instruction { + return map[string]botserv.Instruction{ + p.me.Id: { + MoveTo: p.moveto, + TargetSpeed: &p.speed, + FireAt: p.fireat, + }, + } +} + +func (p *SimplePlayer) randomDirection() *govector.Point2d { + pt := govector.Vector2d{ + X: rand.Float32() * p.width, + Y: rand.Float32() * p.height, + }.ToPoint() + return &pt +} + +func (p *SimplePlayer) probe(destination govector.Point2d) bool { + // XXX: make test for this + for _, v := range p.knownObstacles { + collided, _, _ := govector.RectIntersection( + v.Bounds, + p.me.Position, + destination.Sub(p.me.Position), + ) + if collided { + return false + } + } + return true +} + +// MiniObstacle is a convenient way to encode/decode between the [4]int -> botserv.Obstacle +type MiniObstacle [4]int + +// id is used to calculate a key for use in maps +func (mo *MiniObstacle) Id() string { + return fmt.Sprintf( + "%x%x%x%x", + mo[0], + mo[1], + mo[2], + mo[3], + ) +} + +func (mo MiniObstacle) String() string { + return mo.Id() +} + +// toObstacle is where the conversion magic happens +func (mo *MiniObstacle) ToObstacle() botserv.Obstacle { + return botserv.Obstacle{ + Bounds: govector.AABB2d{ + A: govector.Point2d{float32(mo[0]), float32(mo[1])}, + B: govector.Point2d{float32(mo[2]), float32(mo[3])}, + }, + } +} diff --git a/robot.go b/robot.go deleted file mode 100644 index 6e153ae..0000000 --- a/robot.go +++ /dev/null @@ -1,326 +0,0 @@ -package botclient - -import ( - "encoding/gob" - "encoding/json" - "errors" - "fmt" - "log" - "math" - "math/rand" - "sync" - - "bitbucket.org/hackerbots/botserv" - "bitbucket.org/hackerbots/vector" - "code.google.com/p/go.net/websocket" -) - -const maxSpeed = 100 -const safeDistance = 40 -const maxSearchIterations = 20 - -func connect(server string, port int) (*websocket.Conn, error) { - origin := "http://localhost/" - url := fmt.Sprintf("ws://%s:%d/ws/", server, port) - return websocket.Dial(url, "", origin) -} - -type Robot struct { - verbose bool - forceJSON bool - Server string - Port int - ws *websocket.Conn - game botserv.GameParam - playerId string - Name string - GameName string - StatsReq botserv.StatsRequest - statsCalculated botserv.Stats - - speed float32 - moveto *govector.Point2d - fireat *govector.Point2d - boardstate botserv.Boardstate - me botserv.Robot - KnownObstacles map[string]botserv.Obstacle - nearestEnemy *botserv.OtherRobot - - // TODO: don't know if I like how this is done ... I would rather send - // a signal over a chanel - Wg *sync.WaitGroup -} - -type encoder interface { - Encode(v interface{}) error -} - -type decoder interface { - Decode(v interface{}) error -} - -func (r *Robot) negociate() (err error) { - if r.verbose { - log.Printf("%s: trying to connect to game '%s'", r.Name, r.GameName) - } - r.ws, err = connect(r.Server, r.Port) - if err != nil { - return errors.New(fmt.Sprintf("connection failure: %s", err)) - } - - err = websocket.JSON.Send(r.ws, struct { - Id string `json:"id"` - }{ - r.GameName, - }) - if err != nil { - return err - } - - var idreq struct { - Type string `json:"type"` - PlayerId string `json:"id"` - } - err = websocket.JSON.Receive(r.ws, &idreq) - if err != nil || idreq.Type == "failure" { - return errors.New(fmt.Sprintf("failure: %+v", idreq)) - } - if r.verbose { - log.Printf("%s: idreq: %+v", r.Name, idreq) - } - - err = websocket.JSON.Send(r.ws, struct { - Type string `json:"type"` - Name string `json:"name"` - Useragent string `json:"useragent"` - }{ - Name: r.Name, - Useragent: "gobot", - Type: "robot", - }) - if err != nil { - return err - } - - supportedEncs := []string{"bson", "json", "gob"} - if r.forceJSON { - supportedEncs = []string{"json"} - } - err = websocket.JSON.Send(r.ws, supportedEncs) - if err != nil { - return errors.New(fmt.Sprintf("failure: %+v", err)) - } - - err = websocket.JSON.Receive(r.ws, &r.game) - if r.game.Type != "gameparam" { - return errors.New("didn't receive a good gameparam") - } - if r.verbose { - log.Printf("%s: game parameters: %+v", r.Name, r.game) - } - - conf := botserv.ClientConfig{ - ID: r.GameName, - Stats: map[string]botserv.StatsRequest{ - r.Name: r.StatsReq, - }, - } - - err = websocket.JSON.Send(r.ws, conf) - - var handshake struct { - Id string `json:"id"` - Success bool `json:"success"` - Type string `json:"type"` - botserv.Failure - } - websocket.JSON.Receive(r.ws, &handshake) - if !handshake.Success { - return errors.New(handshake.Reason) - } - r.playerId = handshake.Id - if r.verbose { - log.Printf("%s: handshake: %+v", r.Name, handshake) - } - - dstats := struct { - Stats map[string]botserv.Stats `json:"stats"` - Type string `json:"type"` - }{} - err = websocket.JSON.Receive(r.ws, &dstats) - if err != nil { - return err - } - log.Printf("%s: recv stats", r.Name) - - // this player only ever has one robot, so we're just picking off our own - // stats - _, ok := dstats.Stats[r.Name] - if !ok { - return errors.New("my name not found in stats map") - } - r.statsCalculated = dstats.Stats[r.Name] - - return nil -} - -func (r *Robot) Play() { - defer r.Wg.Done() - var err error - err = r.negociate() - if err != nil { - log.Printf("%s: failed to negociate: %s", r.Name, err) - return - } - - log.Printf("%s: starting loop", r.Name) - - var enc encoder - var dec decoder - - if r.game.Encoding == "json" { - enc = json.NewEncoder(r.ws) - dec = json.NewDecoder(r.ws) - } else { - enc = gob.NewEncoder(r.ws) - dec = gob.NewDecoder(r.ws) - } - - for { - r.speed = float32(maxSpeed) - if r.verbose { - log.Printf("%s: about to wait on boardstate", r.Name) - } - err = dec.Decode(&r.boardstate) - if err != nil { - log.Printf("%s: Connection lost", r.Name) - return - } - if r.verbose { - log.Printf("%s: one recv boardstate", r.Name) - } - // TODO: I need a truly-verbose flag? - // if r.verbose { - // log.Printf("\n\n%#v\n\n", r.boardstate) - // } - - r.recon() - - if len(r.boardstate.MyRobots) > 0 { - r.me = r.boardstate.MyRobots[0] - } else { - continue - } - - if r.verbose { - log.Printf("%s before: %+v", r.Name, r.moveto) - } - r.navigate() - if r.verbose { - log.Printf("%s after: %+v", r.Name, r.moveto) - } - - instruction := map[string]botserv.Instruction{ - r.me.Id: { - MoveTo: r.moveto, - TargetSpeed: &r.speed, - FireAt: r.fireat, - }, - } - err = enc.Encode(instruction) - if err != nil { - log.Println(err) - return - } - } -} - -func (r *Robot) recon() { - for _, o := range r.boardstate.Objects { - obj := MiniObstacle(o) - if _, ok := r.KnownObstacles[obj.id()]; !ok { - r.KnownObstacles[obj.id()] = obj.toObstacle() - } - } - - // simplest shooting strategy ... need to do the following: - // not shoot through buildings - // shoot at where the robot will be, not where it was. - r.nearestEnemy = nil - r.fireat = nil - closest := float32(math.Inf(1)) - for _, enemy := range r.boardstate.OtherRobots { - dist := r.me.Position.Sub(enemy.Position).Mag() - if dist < closest && dist > safeDistance { - r.nearestEnemy = &enemy - } - } - if r.nearestEnemy != nil { - point := r.nearestEnemy.Position.Add(r.nearestEnemy.Heading.Scale(safeDistance)) - r.fireat = &point - } -} - -func (r *Robot) randomDirection() *govector.Point2d { - p := govector.Vector2d{ - X: rand.Float32() * r.game.BoardSize.Width, - Y: rand.Float32() * r.game.BoardSize.Height, - }.ToPoint() - return &p -} - -func (r *Robot) probe(destination govector.Point2d) bool { - // XXX: make test for this - for _, v := range r.KnownObstacles { - collided, _, _ := govector.RectIntersection( - v.Bounds, - r.me.Position, - destination.Sub(r.me.Position), - ) - if collided { - return false - } - } - return true -} - -func (r *Robot) navigate() { - - if r.moveto == nil { - if r.verbose { - log.Printf("%s: nil", r.Name) - } - r.moveto = r.randomDirection() - } - - togo := r.me.Position.Sub(*r.moveto).Mag() - if togo < safeDistance+5 { - if r.verbose { - log.Printf("%s got to destination", r.Name) - } - r.moveto = r.randomDirection() - return - } - if !r.probe(r.me.Position.Add(r.me.Heading.Scale(safeDistance))) { - if r.verbose { - log.Printf("%s going to run into something", r.Name) - } - r.speed = 0 - if !r.probe(*r.moveto) { - if r.verbose { - log.Printf("%s unsafe to move, choose new direction", r.Name) - } - r.moveto = r.randomDirection() - return - } - } - if r.me.Collision != nil { - // XXX: I am being told I am here ... - if r.verbose { - log.Printf("%s apparent collision: %#v", r.Name, r.me.Collision) - } - r.moveto = r.randomDirection() - r.speed = 0 - return - } -} diff --git a/shims.go b/shims.go deleted file mode 100644 index 090add8..0000000 --- a/shims.go +++ /dev/null @@ -1,33 +0,0 @@ -package botclient - -import ( - "fmt" - - "bitbucket.org/hackerbots/botserv" - "bitbucket.org/hackerbots/vector" -) - -type MiniObstacle [4]int - -func (mo *MiniObstacle) id() string { - return fmt.Sprintf( - "%x%x%x%x", - mo[0], - mo[1], - mo[2], - mo[3], - ) -} - -func (mo MiniObstacle) String() string { - return mo.id() -} - -func (mo *MiniObstacle) toObstacle() botserv.Obstacle { - return botserv.Obstacle{ - Bounds: govector.AABB2d{ - A: govector.Point2d{float32(mo[0]), float32(mo[1])}, - B: govector.Point2d{float32(mo[2]), float32(mo[3])}, - }, - } -}