client/robot.go

327 lines
6.9 KiB
Go

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
}
}