27 Commits
Author SHA1 Message Date
sm b597ec529e DELETE: print out user events 2016-07-17 23:51:08 -07:00
sm aaf2641d80 Added way to get user input events from Spectator 2016-07-17 23:51:08 -07:00
sm 15f1ad15f4 some go lint (Id -> ID) 2016-07-17 23:51:00 -07:00
sm 86a00042c3 Make ui optional for gobot 2016-07-17 23:50:59 -07:00
sm f1ae71ecc0 Made Spectator/term ui pluggable 2016-07-17 23:50:59 -07:00
sm f9984c1826 fix panic with invalid bot name 2016-07-17 09:54:36 -07:00
sm 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
sm 6c67d83f49 removed StatsReq from client struct 2016-07-13 20:12:46 -06:00
sm 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
sm 70adb5b713 move main packages to standard location 2016-07-13 14:48:56 -06:00
sm 3668c09235 fixed negotiate misspelling 2016-07-13 12:31:28 -06:00
sm 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
sm 8a85c94767 draw own bot last 2015-08-31 21:59:21 -07:00
sm 623dfa4c48 Remove spurious logging 2015-08-31 21:55:18 -07:00
sm 3ef90af341 All robots are spectators 2015-08-31 21:48:25 -07:00
sm 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
sm 84f151864b updated import paths for go tools repo
See golang.org/s/go14subrepo for background.
2014-11-11 21:28:01 -08:00
sm f6439cf41d cleanly close spectator if server dies 2014-05-13 00:23:14 -07:00
sm f06ccd2588 trying to visualize bot direction 2014-05-12 22:30:55 -07:00
sm bb072255cd Added splosion visualization 2014-05-12 22:11:44 -07:00
sm 14297191ed Update to new protocol (Update) 2014-05-11 23:13:00 -07:00
sm d3e7a247c8 float32 -> float64 2014-05-11 23:11:58 -07:00
sm 2b710ef40d Removed the MiniObstacle nonsense. 2014-05-11 23:10:59 -07:00
sm b074dda677 Added curses (termbox) spectator mode 2014-05-11 21:45:26 -07:00
sm 7d77f18470 simple spectator that says hi every message 2014-05-11 21:45:26 -07:00
9 changed files with 812 additions and 292 deletions
+34 -36
View File
@@ -5,36 +5,37 @@ 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
// max dimensions of field
Width, Height float64
boardstate *server.Boardstate
enc encoder
dec decoder
ws *websocket.Conn
Player
}
type encoder interface {
@@ -45,12 +46,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 +70,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 +78,7 @@ func (c *Client) Negociate() (err error) {
}{
Name: c.Name,
Useragent: "gobot",
Type: "robot",
Type: clientType,
})
if err != nil {
return err
@@ -102,9 +97,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,11 +106,11 @@ func (c *Client) Negociate() (err error) {
c.dec = gob.NewDecoder(c.ws)
}
switch clientType {
case "robot":
conf := server.ClientConfig{
ID: c.GameId,
Stats: map[string]server.StatsRequest{
c.Name: c.StatsReq,
},
Stats: player.GetStats(),
}
err = websocket.JSON.Send(c.ws, conf)
@@ -133,9 +125,6 @@ func (c *Client) Negociate() (err error) {
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?
@@ -143,10 +132,22 @@ func (c *Client) Negociate() (err error) {
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":
}
c.Width = c.Game.BoardSize.Width
c.Height = c.Game.BoardSize.Height
return nil
}
@@ -155,18 +156,15 @@ 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)
err = c.enc.Encode(c.Update(bs))
if err != nil {
return err
}
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"flag"
"fmt"
"math/rand"
"os"
"time"
"hackerbots.us/client"
)
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,
}
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)
}
ui := client.NewSpectator(c.Width, c.Height)
c.Player = ui
go func() {
if ui.User == nil {
return
}
for e := range ui.User {
fmt.Printf("%+v\n", e)
}
}()
go func() {
if err := c.Play(); err != nil {
close(ui.Die)
}
}()
if err := ui.Spectate(); err != nil {
fmt.Fprintf(os.Stderr, "problem during visualization: %+v\n", err)
os.Exit(1)
}
}
+99
View File
@@ -0,0 +1,99 @@
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 [fraserbot, simple]")
var spectate = flag.Bool("spectate", false, "enable terminal visualizer")
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,
ForceJSON: *forceJSON,
}
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)
}
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)
}
if *spectate {
ui := client.NewSpectator(c.Width, c.Height)
players := client.MultiPlayer{
c.Player,
ui,
}
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)
}
}
}
+134
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
}
-76
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
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.
+43 -155
View File
@@ -1,171 +1,59 @@
package client
import (
"fmt"
"math"
"math/rand"
import "hackerbots.us/server"
"bitbucket.org/hackerbots/server"
"bitbucket.org/hackerbots/vector"
)
// 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.
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
// MultiPlayer wraps multiple players and calls funcitons in Player in
// appropriate order.
//
// Typically used to add a Spectator to a bot.
type MultiPlayer []Player
// 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
}
// 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,
// SetIDs passes ids to all players.
func (mp MultiPlayer) SetIDs(ids map[string]string) {
for _, p := range mp {
p.SetIDs(ids)
}
}
// 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])},
},
// 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
}
+153
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
}
+237
View File
@@ -0,0 +1,237 @@
package client
import (
"errors"
"fmt"
"math"
"hackerbots.us/server"
"github.com/nsf/termbox-go"
)
const d = 'v'
const u = '^'
const r = '>'
const l = '<'
// 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,
}
}
// 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{}
// User, if populated, will return a stream of (ostensibly keyboard) events
// for use outside of the Spectator.
User chan termbox.Event
}
// 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
}
// Spectate runs the termbox render loop.
func (s *Spectator) Spectate() error {
err := termbox.Init()
if err != nil {
return err
}
termbox.SetInputMode(termbox.InputMouse)
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 'f':
termbox.SetCell(
20,
20,
'*',
termbox.ColorRed, termbox.ColorBlack,
)
case 'c':
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
}
case termbox.EventResize:
s.viewX, s.viewY = event.Width, event.Height
case termbox.EventError:
err = fmt.Errorf("Quitting because of termbox error:\n%v\n", event.Err)
return
}
if s.User != nil {
s.User <- event
}
case update := <-s.StateStream:
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
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))
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 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 = r
} else {
b = l
}
} else {
if bot.Heading.Y > 0 {
b = u
} else {
b = d
}
}
c := termbox.ColorRed
if bot.Health <= 0 {
c = termbox.ColorBlack
}
termbox.SetCell(
x,
s.viewY-y,
b,
c, termbox.ColorBlack,
)
}
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(
x,
s.viewY-y,
'·',
termbox.ColorWhite|termbox.AttrBold, termbox.ColorBlack,
)
}
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
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 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 = r
} else {
b = l
}
} else {
if bot.Heading.Y > 0 {
b = u
} else {
b = d
}
}
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()
case <-s.Die:
err = errors.New("was told to die")
return
}
}
}()
termbox.Close()
return err
}