Compare commits

...

22 Commits

Author SHA1 Message Date
Stephen McQuay f9984c1826
fix panic with invalid bot name 2016-07-17 09:54:36 -07:00
Stephen McQuay 174be3b0ab
added license 2016-07-13 21:40:14 -06:00
Fraser Graham 9be5f226f5 Providing the bot ID to name mapping to the player after negotiation 2016-07-13 21:28:57 -06:00
Stephen McQuay 6c67d83f49
removed StatsReq from client struct 2016-07-13 20:12:46 -06:00
Stephen McQuay c21e0c5582
Change how server address is specified
now you spell it as such:

gobot -addr wss://hackerbots.us
gobot -addr ws://localhost:8666
2016-07-13 19:46:08 -06:00
Fraser Graham 35cb8431f6 Moved definition of what robots have which stats into the player interface 2016-07-13 16:04:48 -06:00
Stephen McQuay 70adb5b713
move main packages to standard location 2016-07-13 14:48:56 -06:00
Stephen McQuay 3668c09235
fixed negotiate misspelling 2016-07-13 12:31:28 -06:00
Stephen McQuay 50be2c5b88 don't block on dealing with StateStream
some clients don't use it. we block indefinitely if we don't allow for it to
not be used.
2016-02-23 16:57:35 -08:00
Stephen McQuay 8a85c94767 draw own bot last 2015-08-31 21:59:21 -07:00
Stephen McQuay 623dfa4c48 Remove spurious logging 2015-08-31 21:55:18 -07:00
Stephen McQuay 3ef90af341 All robots are spectators 2015-08-31 21:48:25 -07:00
Stephen McQuay 855208b39d vantiy urls
I've moved remote to https://s.mcquay.me/hackerbots/ and stood up routes for go
get on the hackerbots repos. I had to update the imports for this to take
effect.
2015-04-30 23:14:17 -07:00
Stephen McQuay 84f151864b updated import paths for go tools repo
See golang.org/s/go14subrepo for background.
2014-11-11 21:28:01 -08:00
Stephen McQuay f6439cf41d cleanly close spectator if server dies 2014-05-13 00:23:14 -07:00
Stephen McQuay f06ccd2588 trying to visualize bot direction 2014-05-12 22:30:55 -07:00
Stephen McQuay bb072255cd Added splosion visualization 2014-05-12 22:11:44 -07:00
Stephen McQuay 14297191ed Update to new protocol (Update) 2014-05-11 23:13:00 -07:00
Stephen McQuay d3e7a247c8 float32 -> float64 2014-05-11 23:11:58 -07:00
Stephen McQuay 2b710ef40d Removed the MiniObstacle nonsense. 2014-05-11 23:10:59 -07:00
Stephen McQuay b074dda677 Added curses (termbox) spectator mode 2014-05-11 21:45:26 -07:00
Stephen McQuay 7d77f18470 simple spectator that says hi every message 2014-05-11 21:45:26 -07:00
9 changed files with 731 additions and 294 deletions

128
client.go
View File

@ -5,36 +5,40 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"bitbucket.org/hackerbots/server"
"code.google.com/p/go.net/websocket"
"golang.org/x/net/websocket"
"hackerbots.us/server"
)
func connect(server string, port int) (*websocket.Conn, error) {
func connect(addr string) (*websocket.Conn, error) {
origin := "http://localhost/"
url := fmt.Sprintf("ws://%s:%d/ws/", server, port)
url := fmt.Sprintf("%s/ws/", addr)
return websocket.Dial(url, "", origin)
}
// Client keeps track of connection to server and has two interesting methods:
// Negociate and Play. Users of this struct will likely use most everything as
// Negotiate and Play. Users of this struct will likely use most everything as
// is while defining their own Player to specify desired game play behavior.
type Client struct {
ForceJSON bool
GameId string
Name string
Port int
Server string
StatsReq server.StatsRequest
Verbose bool
Player Player
Game server.GameParam
boardstate server.Boardstate
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
}
type encoder interface {
@ -45,12 +49,9 @@ type decoder interface {
Decode(v interface{}) error
}
// Negociate runs through the hackerbots negociation protocol.
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)
// Negotiate runs through the hackerbots negociation protocol.
func (c *Client) Negotiate(clientType string, player Player) (err error) {
c.ws, err = connect(c.Server)
if err != nil {
return errors.New(fmt.Sprintf("connection failure: %s", err))
}
@ -72,9 +73,6 @@ func (c *Client) Negociate() (err error) {
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"`
@ -83,7 +81,7 @@ func (c *Client) Negociate() (err error) {
}{
Name: c.Name,
Useragent: "gobot",
Type: "robot",
Type: clientType,
})
if err != nil {
return err
@ -102,9 +100,6 @@ func (c *Client) Negociate() (err error) {
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)
}
if c.Game.Encoding == "json" {
c.enc = json.NewEncoder(c.ws)
@ -114,39 +109,48 @@ func (c *Client) Negociate() (err error) {
c.dec = gob.NewDecoder(c.ws)
}
conf := server.ClientConfig{
ID: c.GameId,
Stats: map[string]server.StatsRequest{
c.Name: c.StatsReq,
},
switch clientType {
case "robot":
conf := server.ClientConfig{
ID: c.GameId,
Stats: player.GetStats(),
}
err = websocket.JSON.Send(c.ws, conf)
var handshake struct {
Id string `json:"id"`
Success bool `json:"success"`
Type string `json:"type"`
server.Failure
}
websocket.JSON.Receive(c.ws, &handshake)
if !handshake.Success {
return errors.New(handshake.Reason)
}
// we don't do anything useful with dstats, but could be interesting to
// pass along to the player?
dstats := struct {
Stats map[string]server.Stats `json:"stats"`
Type string `json:"type"`
}{}
err = websocket.JSON.Receive(c.ws, &dstats)
if err != nil {
return err
}
ids := make(map[string]string)
for name, entry := range dstats.Stats {
ids[entry.Id] = name
}
player.SetIDs(ids)
case "spectator":
}
err = websocket.JSON.Send(c.ws, conf)
var handshake struct {
Id string `json:"id"`
Success bool `json:"success"`
Type string `json:"type"`
server.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]server.Stats `json:"stats"`
Type string `json:"type"`
}{}
err = websocket.JSON.Receive(c.ws, &dstats)
if err != nil {
return err
}
c.width = c.Game.BoardSize.Width
c.height = c.Game.BoardSize.Height
return nil
}
@ -155,18 +159,20 @@ func (c *Client) Negociate() (err error) {
// server, and passes this along to the embedded Player. Following this it
// sends the server the Player's instruction.
func (c *Client) Play() error {
log.Printf("%s: starting loop", c.Name)
bs := &server.Boardstate{}
var err error
for {
err = c.dec.Decode(&c.boardstate)
err = c.dec.Decode(bs)
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 = c.enc.Encode(instruction)
select {
case c.StateStream <- bs:
default:
}
err = c.enc.Encode(c.Update(bs))
if err != nil {
return err
}

49
cmd/botspectate/main.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"flag"
"fmt"
"math/rand"
"os"
"time"
"hackerbots.us/client"
"hackerbots.us/server"
)
var addr = flag.String("addr", "ws://localhost:8666", "server hostname")
var forceJSON = flag.Bool("json", false, "force json encoding")
func main() {
rand.Seed(time.Now().UnixNano())
var gameId string
flag.Parse()
if flag.NArg() < 1 {
gameId = "debug"
} else {
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{},
}
var err error
err = c.Negotiate("spectator", c.Player)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: failed to negotiate: %s\n", c.Name, err)
os.Exit(1)
}
go func() {
if err := c.Play(); err != nil {
close(c.Die)
}
}()
c.Visualize()
}

85
cmd/gobot/main.go Normal file
View File

@ -0,0 +1,85 @@
package main
import (
"flag"
"fmt"
"math/rand"
"os"
"time"
"hackerbots.us/client"
"hackerbots.us/server"
)
var hp = flag.Int("hp", 50, "")
var speed = flag.Int("speed", 50, "")
var acceleration = flag.Int("acceleration", 50, "")
var scannerRadius = flag.Int("srad", 50, "scanner radius")
var turnSpeed = flag.Int("omega", 50, "turn speed")
var fireRate = flag.Int("fire-rate", 50, "scanner radius")
var weaponRadius = flag.Int("wrad", 50, "weapon radius")
var weaponDamage = flag.Int("wdamage", 50, "weapons umph")
var weaponSpeed = flag.Int("wspeed", 50, "weapons speed")
// XXX: add TurnSpeed, WeaponDamage, WeaponSpeed
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")
func main() {
rand.Seed(time.Now().UnixNano())
var gameId string
flag.Parse()
if flag.NArg() < 1 {
gameId = "debug"
} else {
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{}),
}
sr := server.StatsRequest{
Hp: *hp,
Speed: *speed,
Acceleration: *acceleration,
ScannerRadius: *scannerRadius,
TurnSpeed: *turnSpeed,
FireRate: *fireRate,
WeaponRadius: *weaponRadius,
WeaponDamage: *weaponDamage,
WeaponSpeed: *weaponSpeed,
}
switch *botType {
case "simple":
c.Player = client.NewSimplePlayer(800, 600, sr)
case "fraserbot":
c.Player = client.NewFraserbot("Fraserbot")
default:
fmt.Fprintf(os.Stderr, "must specify a known bot \n")
os.Exit(1)
}
var err error
err = c.Negotiate("robot", c.Player)
if 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)
}
}()
c.Visualize()
}

134
fraserbot.go Normal file
View File

@ -0,0 +1,134 @@
package client
import (
"fmt"
"log"
"math/rand"
"hackerbots.us/server"
"hackerbots.us/vector"
)
// Fraserbot is a bad ass motherfucker, that will fuck SHIT UUUUUP
type Fraserbot struct {
knownObstacles map[string]server.Obstacle
nearestEnemy *server.OtherRobot
fireat *vector.Point2d
moveto *vector.Point2d
name string
botIDs map[string]string
}
// NewFraserbot simply returns a populated, usable *Fraserbot
func NewFraserbot(name string) *Fraserbot {
return &Fraserbot{
knownObstacles: make(map[string]server.Obstacle),
name: name,
}
}
// GetStats returns a map with an entry for each robot the player will control
// containing the desired stats for that robot
func (p *Fraserbot) GetStats() map[string]server.StatsRequest {
s := make(map[string]server.StatsRequest)
s[fmt.Sprintf("%v_MAIN", p.name)] = server.StatsRequest{
Hp: 100,
Speed: 10,
Acceleration: 10,
ScannerRadius: 10,
TurnSpeed: 10,
FireRate: 30,
WeaponRadius: 20,
WeaponDamage: 30,
WeaponSpeed: 30,
}
s[fmt.Sprintf("%v_Jr", p.name)] = server.StatsRequest{
Hp: 10,
Speed: 100,
Acceleration: 10,
ScannerRadius: 60,
TurnSpeed: 48,
FireRate: 1,
WeaponRadius: 10,
WeaponDamage: 10,
WeaponSpeed: 1,
}
return s
}
// SetIDs provides the mapping of names to ID's for each bot
func (p *Fraserbot) SetIDs(ids map[string]string) {
p.botIDs = ids
log.Println(ids)
}
// Update is our implementation of recieving and processing a server.Boardstate
// from the server
func (p *Fraserbot) Update(bs *server.Boardstate) map[string]server.Instruction {
instructions := make(map[string]server.Instruction)
for _, bot := range bs.MyRobots {
me := bot
// We're just starting out
if p.moveto == nil {
p.moveto = &me.Position
}
speed := float64(200)
// If we're close to where we want to go then pick a new
// place to go, we done good
if me.Position.Sub(*p.moveto).Mag() < 30 {
p.moveto = p.randomDirection(me.Position, 400)
}
if me.ProbeResult != nil && me.ProbeResult.Type == "obstacle" {
speed = -50
p.moveto = p.randomDirection(me.Position, 600)
}
if len(me.Scanners) > 0 {
// Find the bots that are not mine
var index int
found := false
for i, entry := range me.Scanners {
_, ok := p.botIDs[entry.Id]
if !ok {
index = i
found = true
break
}
}
if found {
// log.Println(me.Scanners[0])
for _, bot := range bs.OtherRobots {
if bot.Id == me.Scanners[index].Id {
p.fireat = &bot.Position
log.Printf("%v Found Enemy: %v\n", me.Name, bot.Name)
}
}
}
}
instructions[bot.Id] = server.Instruction{
MoveTo: p.moveto,
TargetSpeed: &speed,
FireAt: p.fireat,
Probe: p.moveto,
}
}
return instructions
}
// randomDirection is a spot within 200 of the current position
func (p *Fraserbot) randomDirection(pos vector.Point2d, dist float64) *vector.Point2d {
pt := vector.Vector2d{
X: (rand.Float64() * dist) - (dist / 2) + pos.X,
Y: (rand.Float64() * dist) - (dist / 2) + pos.Y,
}.ToPoint()
return &pt
}

View File

@ -1,76 +0,0 @@
package main
import (
"flag"
"log"
"math/rand"
"time"
"bitbucket.org/hackerbots/client"
"bitbucket.org/hackerbots/server"
)
var hp = flag.Int("hp", 50, "")
var speed = flag.Int("speed", 50, "")
var acceleration = flag.Int("acceleration", 50, "")
var scannerRadius = flag.Int("srad", 50, "scanner radius")
var turnSpeed = flag.Int("omega", 50, "turn speed")
var fireRate = flag.Int("fire-rate", 50, "scanner radius")
var weaponRadius = flag.Int("wrad", 50, "weapon radius")
var weaponDamage = flag.Int("wdamage", 50, "weapons umph")
var weaponSpeed = flag.Int("wspeed", 50, "weapons speed")
// XXX: add TurnSpeed, WeaponDamage, WeaponSpeed
var serverHostname = 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 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 gameId string
flag.Parse()
if flag.NArg() < 1 {
gameId = "debug"
} else {
gameId = flag.Arg(0)
}
c := &client.Client{
Server: *serverHostname,
Port: *port,
Name: *botname,
GameId: gameId,
// XXX: update with missing fields
StatsReq: server.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.Fatalf("%s: failed to negociate: %s", c.Name, err)
}
c.Player = client.NewSimplePlayer(
c.Game.BoardSize.Width,
c.Game.BoardSize.Height,
)
if err := c.Play(); err != nil {
log.Fatal(err)
}
}

27
license Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2016, Fraser Graham, Stephen McQuay
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of server nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

181
player.go
View File

@ -1,13 +1,6 @@
package client
import (
"fmt"
"math"
"math/rand"
"bitbucket.org/hackerbots/server"
"bitbucket.org/hackerbots/vector"
)
import "hackerbots.us/server"
// Player is the interface that is implemented when specifying non-default
// player behavior.
@ -15,157 +8,31 @@ import (
// 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.
type Player interface {
Recv(bs *server.Boardstate)
Instruction() map[string]server.Instruction
// GetStats returns a map with an entry for each robot the player will control
// containing the desired stats for that robot
GetStats() map[string]server.StatsRequest
// SetIDs is called from the client once the server
// has accepted the robots supplied in GetStats and validated
// their config, the data passed into SetIDs is a mapping of
// bot name to server side bot ID that is used in all bot
// dats sent from the server
SetIDs(map[string]string)
// Update is called on reciept of a board state packet and the response is
// the instructions for each robot in a map of robot id to instructions
Update(bs *server.Boardstate) map[string]server.Instruction
}
// SimplePlayer is our default player and stands as a starting point for your
// own Player implementations.
type SimplePlayer struct {
me server.Robot
width, height float32
knownObstacles map[string]server.Obstacle
nearestEnemy *server.OtherRobot
fireat *vector.Point2d
moveto *vector.Point2d
speed float32
maxSpeed float32
safeDistance float32
type Spectator struct{}
func (s Spectator) SetIDs(map[string]string) {}
func (s Spectator) GetStats() map[string]server.StatsRequest {
return nil
}
// NewSimplePlayer simply returns a populated, usable *SimplePlayer
func NewSimplePlayer(width, height float32) *SimplePlayer {
return &SimplePlayer{
knownObstacles: make(map[string]server.Obstacle),
width: width,
height: height,
maxSpeed: 100,
safeDistance: 40,
}
}
// Recv is our implementation of receiving a server.Boardstate from the server
func (p *SimplePlayer) Recv(bs *server.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 *server.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
}
}
// Instruction is our default implementation of preparing a map of information
// to be sent to server.
func (p *SimplePlayer) Instruction() map[string]server.Instruction {
return map[string]server.Instruction{
p.me.Id: {
MoveTo: p.moveto,
TargetSpeed: &p.speed,
FireAt: p.fireat,
},
}
}
func (p *SimplePlayer) randomDirection() *vector.Point2d {
pt := vector.Vector2d{
X: rand.Float32() * p.width,
Y: rand.Float32() * p.height,
}.ToPoint()
return &pt
}
func (p *SimplePlayer) probe(destination vector.Point2d) bool {
// XXX: make test for this
for _, v := range p.knownObstacles {
collided, _, _ := vector.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 -> server.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() server.Obstacle {
return server.Obstacle{
Bounds: vector.AABB2d{
A: vector.Point2d{X: float32(mo[0]), Y: float32(mo[1])},
B: vector.Point2d{X: float32(mo[2]), Y: float32(mo[3])},
},
}
func (s Spectator) Update(bs *server.Boardstate) map[string]server.Instruction {
return nil
}

153
simple_player.go Normal file
View File

@ -0,0 +1,153 @@
package client
import (
"math"
"math/rand"
"hackerbots.us/server"
"hackerbots.us/vector"
)
// SimplePlayer is our default player and stands as a starting point for your
// own Player implementations.
type SimplePlayer struct {
me server.Robot
width, height float64
knownObstacles map[string]server.Obstacle
nearestEnemy *server.OtherRobot
fireat *vector.Point2d
moveto *vector.Point2d
speed float64
maxSpeed float64
safeDistance float64
stats server.StatsRequest
}
// NewSimplePlayer simply returns a populated, usable *SimplePlayer
func NewSimplePlayer(width, height float64, stats server.StatsRequest) *SimplePlayer {
return &SimplePlayer{
knownObstacles: make(map[string]server.Obstacle),
width: width,
height: height,
maxSpeed: 100,
safeDistance: 40,
stats: stats,
}
}
func (p *SimplePlayer) SetIDs(map[string]string) {}
// GetStats returns a map with an entry for each robot the player will control
// containing the desired stats for that robot
func (p *SimplePlayer) GetStats() map[string]server.StatsRequest {
s := make(map[string]server.StatsRequest)
s["simple"] = p.stats
return s
}
// Update is our implementation of recieving and processing a server.Boardstate
// from the server
func (p *SimplePlayer) Update(bs *server.Boardstate) map[string]server.Instruction {
instructions := make(map[string]server.Instruction)
for _, bot := range bs.MyRobots {
p.me = bot
p.speed = 1000
if p.me.Health <= 0 {
continue
}
p.recon(bs)
p.navigate()
probe_point := p.me.Position.Add(p.me.Heading.Scale(p.safeDistance))
instructions[bot.Id] = server.Instruction{
MoveTo: p.moveto,
TargetSpeed: &p.speed,
FireAt: p.fireat,
Probe: &probe_point,
}
}
return instructions
}
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 *server.Boardstate) {
// XXX: need to keep track of seen objects ..
// 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 := 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
}
}
// Instruction is our default implementation of preparing a map of information
// to be sent to server.
func (p *SimplePlayer) Instruction() map[string]server.Instruction {
return map[string]server.Instruction{
p.me.Id: {
MoveTo: p.moveto,
TargetSpeed: &p.speed,
FireAt: p.fireat,
},
}
}
func (p *SimplePlayer) randomDirection() *vector.Point2d {
pt := vector.Vector2d{
X: rand.Float64() * p.width,
Y: rand.Float64() * p.height,
}.ToPoint()
return &pt
}
func (p *SimplePlayer) probe(destination vector.Point2d) bool {
// XXX: make test for this
for _, v := range p.knownObstacles {
collided, _, _ := vector.RectIntersection(
v.Bounds,
p.me.Position,
destination.Sub(p.me.Position),
)
if collided {
return false
}
}
return true
}

192
spectator.go Normal file
View File

@ -0,0 +1,192 @@
package client
import (
"fmt"
"math"
"github.com/nsf/termbox-go"
"hackerbots.us/server"
)
var botDown rune = 'v'
var botUp rune = '^'
var botRight rune = '>'
var botLeft rune = '<'
type size struct {
width, height int
}
// Recv is our implementation of receiving a server.Boardstate from the server
func (s *Client) Recv(bs *server.Boardstate) map[string]server.Instruction {
s.StateStream <- bs
return nil
}
func (s *Client) Visualize() {
err := termbox.Init()
if err != nil {
panic(err)
}
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 'c':
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
}
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))
}
case u := <-s.StateStream:
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
for _, obstacle := range u.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 u.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
} else {
b = botLeft
}
} else {
if bot.Heading.Y > 0 {
b = botUp
} else {
b = botDown
}
}
c := termbox.ColorRed
if bot.Health <= 0 {
c = termbox.ColorBlack
}
termbox.SetCell(
x,
s.viewY-y,
b,
c, termbox.ColorBlack,
)
}
for _, p := range u.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 u.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 u.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
} else {
b = botLeft
}
} else {
if bot.Heading.Y > 0 {
b = botUp
} else {
b = botDown
}
}
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()
if err != nil {
panic(err)
}
case <-s.Die:
return
}
}
}()
termbox.Close()
}