371 lines
9.5 KiB
Go
371 lines
9.5 KiB
Go
package botserv
|
|
|
|
import (
|
|
"log"
|
|
|
|
v "bitbucket.org/hackerbots/vector"
|
|
"code.google.com/p/go.net/websocket"
|
|
)
|
|
|
|
// GameID is essentially the name of the game we want to join
|
|
type GameID struct {
|
|
Id string `json:"id"`
|
|
}
|
|
|
|
// PlayerID is the internal hash we give to a client
|
|
type PlayerID struct {
|
|
Type string `json:"type"`
|
|
Hash string `json:"id"`
|
|
}
|
|
|
|
func NewPlayerID(id string) *PlayerID {
|
|
return &PlayerID{
|
|
Type: "idreq",
|
|
Hash: id,
|
|
}
|
|
}
|
|
|
|
// ClientID is how a player wants to be known
|
|
type ClientID struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Useragent string `json:"useragent"`
|
|
}
|
|
|
|
// ClientID.Valid is used to be sure the player connecting is of appropriate
|
|
// type.
|
|
func (c *ClientID) Valid() (bool, string) {
|
|
switch c.Type {
|
|
case "robot", "spectator":
|
|
return true, ""
|
|
}
|
|
return false, "useragent must be 'robot' or 'spectator'"
|
|
}
|
|
|
|
// ClientConfig embodies a map of stats requests
|
|
type ClientConfig struct {
|
|
ID string `json:"id"`
|
|
Stats map[string]StatsRequest `json:"stats"`
|
|
}
|
|
|
|
// ClientConfig.Valid is what determins if a player has asked for too many
|
|
// points.
|
|
func (config ClientConfig) Valid(max int) bool {
|
|
total := 0
|
|
for _, s := range config.Stats {
|
|
total += (s.Speed +
|
|
s.Hp +
|
|
s.WeaponRadius +
|
|
s.ScannerRadius +
|
|
s.Acceleration +
|
|
s.TurnSpeed +
|
|
s.FireRate +
|
|
s.WeaponDamage +
|
|
s.WeaponSpeed)
|
|
}
|
|
if total > max {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// BoardSize is the response containing the geometry of the requested game.
|
|
type BoardSize struct {
|
|
Width float32 `json:"width"`
|
|
Height float32 `json:"height"`
|
|
}
|
|
|
|
// GameParam is sent to the client to tell them of the geometry of the game
|
|
// requested, how many points they may use, and what encoding the server will
|
|
// use to communicate with the client.
|
|
type GameParam struct {
|
|
// TODO: should have information about max points in here
|
|
BoardSize BoardSize `json:"boardsize"`
|
|
MaxPoints int `json:"max_points"`
|
|
Encoding string `json:"encoding"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// Handshake is simply the response to a client to let them know if the number
|
|
// of stats they've asked for is reasonable. If false it means try again.
|
|
type Handshake struct {
|
|
ID string `json:"id"`
|
|
Success bool `json:"success"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
func NewHandshake(id string, success bool) *Handshake {
|
|
return &Handshake{
|
|
ID: id,
|
|
Success: success,
|
|
Type: "handshake",
|
|
}
|
|
}
|
|
|
|
// Message is an empty interface used to send out arbitrary JSON/gob to
|
|
// clients, both players/spectators. We might send out Boardstate or GameOver,
|
|
// hence the interface{}.
|
|
type Message interface{}
|
|
|
|
// Boardstate is the main struct calculated every tick (per player) and sent
|
|
// out to clients. It contains the appropriate subset of all data needed by
|
|
// each player/spectator.
|
|
type Boardstate struct {
|
|
MyRobots []Robot `json:"my_robots"`
|
|
OtherRobots []OtherRobot `json:"robots"`
|
|
Projectiles []Projectile `json:"projectiles"`
|
|
Splosions []Splosion `json:"splosions"`
|
|
Objects [][4]int `json:"objects"`
|
|
Type string `json:"type"`
|
|
Turn int `json:"turn"`
|
|
AllBots []BotHealth `json:"all_bots"`
|
|
Messages []string `json:"messages"`
|
|
}
|
|
|
|
func NewBoardstate() *Boardstate {
|
|
return &Boardstate{
|
|
MyRobots: []Robot{},
|
|
OtherRobots: []OtherRobot{},
|
|
Projectiles: []Projectile{},
|
|
Splosions: []Splosion{},
|
|
AllBots: []BotHealth{},
|
|
Type: "boardstate",
|
|
}
|
|
}
|
|
|
|
// Special outbound message with a []string of winners.
|
|
type GameOver struct {
|
|
Winners []string `json:"winners"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
func NewGameOver() *GameOver {
|
|
return &GameOver{
|
|
Type: "gameover",
|
|
Winners: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
// Failure is a simple stuct that is typically converted to JSON and sent out
|
|
// to the clients so they can know why something has failed.
|
|
type Failure struct {
|
|
Reason string `json:"reason"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
func NewFailure(reason string) *Failure {
|
|
return &Failure{
|
|
Reason: reason,
|
|
Type: "failure",
|
|
}
|
|
}
|
|
|
|
// Controller.AddPlayer is the HTTP -> websocket route that is used to
|
|
// negociate a connection with a player/spectator.
|
|
func (c *Controller) AddPlayer(ws *websocket.Conn) {
|
|
var gid GameID
|
|
err := websocket.JSON.Receive(ws, &gid)
|
|
if err != nil {
|
|
log.Println("problem parsing the requested game id")
|
|
return
|
|
}
|
|
|
|
game := c.Games.Get(gid.Id)
|
|
if game == nil {
|
|
var err error
|
|
game, err = NewGame(
|
|
gid.Id,
|
|
float32(c.Conf.Width),
|
|
float32(c.Conf.Height),
|
|
c.Conf.Obstacles,
|
|
c.Conf.Tick,
|
|
c.Conf.MaxPoints,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
log.Printf("problem creating game: %s", gid.Id)
|
|
websocket.JSON.Send(ws, NewFailure("game creation error"))
|
|
return
|
|
}
|
|
go game.run()
|
|
c.Games.Add(game)
|
|
}
|
|
|
|
player_id := c.Idg.Hash()
|
|
err = websocket.JSON.Send(ws, NewPlayerID(player_id))
|
|
if err != nil {
|
|
log.Printf("game %s: unable to send player_id to player %s", gid.Id, player_id)
|
|
websocket.JSON.Send(ws, NewFailure("send error"))
|
|
return
|
|
} else {
|
|
log.Printf("game %s: sent player id: %s", gid.Id, player_id)
|
|
}
|
|
|
|
var clientid ClientID
|
|
err = websocket.JSON.Receive(ws, &clientid)
|
|
if err != nil {
|
|
log.Printf("unable to parse ClientID: gid: %s, player: %s", gid.Id, player_id)
|
|
websocket.JSON.Send(ws, NewFailure("parse error"))
|
|
return
|
|
} else {
|
|
log.Printf("game %s: recieved: %+v", gid.Id, clientid)
|
|
}
|
|
if v, msg := clientid.Valid(); !v {
|
|
log.Printf("clientid is invalid: %+v", clientid)
|
|
websocket.JSON.Send(
|
|
ws,
|
|
NewFailure(msg),
|
|
)
|
|
return
|
|
}
|
|
|
|
reqEncs := []string{}
|
|
err = websocket.JSON.Receive(ws, &reqEncs)
|
|
if err != nil {
|
|
log.Printf("%s %s unable to parse requested encodings", gid.Id, player_id)
|
|
websocket.JSON.Send(ws, NewFailure("encoding recieve error"))
|
|
return
|
|
}
|
|
prefEncs := []string{
|
|
"gob",
|
|
"json",
|
|
}
|
|
|
|
var encoding string
|
|
encodingLoops:
|
|
for _, prefEnc := range prefEncs {
|
|
for _, reqEnc := range reqEncs {
|
|
if reqEnc == prefEnc {
|
|
encoding = prefEnc
|
|
log.Println("selected following encoding:", encoding)
|
|
break encodingLoops
|
|
}
|
|
}
|
|
}
|
|
if encoding == "" {
|
|
log.Printf("%s %s unable to negociate encoding", gid.Id, player_id)
|
|
websocket.JSON.Send(
|
|
ws,
|
|
NewFailure("no overlap on supported encodings; I suggest using json"),
|
|
)
|
|
return
|
|
}
|
|
|
|
gameParam := game.gameParam()
|
|
gp := struct {
|
|
GameParam
|
|
Encoding string `json:"encoding"`
|
|
}{
|
|
GameParam: *gameParam,
|
|
Encoding: encoding,
|
|
}
|
|
err = websocket.JSON.Send(ws, gp)
|
|
if err != nil {
|
|
log.Printf("%s %s game param send error", gid.Id, player_id)
|
|
websocket.JSON.Send(ws, NewFailure("game param send error"))
|
|
return
|
|
} else {
|
|
log.Printf("%s -> %s: sent %+v", gid.Id, player_id, gameParam)
|
|
}
|
|
|
|
switch clientid.Type {
|
|
case "robot":
|
|
var conf ClientConfig
|
|
for {
|
|
log.Printf("%s Waiting for client to send conf ...", player_id)
|
|
err = websocket.JSON.Receive(ws, &conf)
|
|
log.Printf("%s: conf received: %s", player_id, conf.ID)
|
|
|
|
if err != nil {
|
|
log.Printf("%s %s config parse error", gid.Id, player_id)
|
|
websocket.JSON.Send(ws, NewFailure("config parse error"))
|
|
return
|
|
}
|
|
|
|
// TODO: verify conf's type
|
|
if conf.Valid(game.maxPoints) {
|
|
log.Printf("%s -> %s: valid client config", gid.Id, player_id)
|
|
_ = websocket.JSON.Send(ws, NewHandshake(player_id, true))
|
|
break
|
|
} else {
|
|
log.Printf("%s: Config is INVALID, abort", player_id)
|
|
_ = websocket.JSON.Send(ws, NewFailure("invalid config"))
|
|
return
|
|
}
|
|
}
|
|
|
|
p := NewPlayer(player_id, ws, game.bw, encoding)
|
|
log.Printf("%s: made a player: %s", gid.Id, p.Id)
|
|
|
|
convertedStats := map[string]Stats{}
|
|
for name, stats := range conf.Stats {
|
|
dstat := DeriveStats(stats)
|
|
convertedStats[name] = dstat
|
|
r := Robot{
|
|
Stats: dstat,
|
|
Id: c.Idg.Hash(),
|
|
Name: name,
|
|
Health: 10,
|
|
Heading: v.Vector2d{1, 0},
|
|
Scanners: make([]Scanner, 0),
|
|
Delta: c.Conf.Delta,
|
|
idg: c.Idg,
|
|
}
|
|
r.Health = r.Stats.Hp
|
|
log.Printf("%s: adding robot: %s", p.Id, r.Id)
|
|
r.reset(game)
|
|
p.Robots = append(p.Robots, &r)
|
|
}
|
|
|
|
statsPayload := struct {
|
|
Stats map[string]Stats `json:"stats"`
|
|
Type string `json:"type"`
|
|
}{
|
|
Stats: convertedStats,
|
|
Type: "stats",
|
|
}
|
|
err = websocket.JSON.Send(ws, &statsPayload)
|
|
if err != nil {
|
|
log.Printf("error sending convertedStats to client: %s", err)
|
|
websocket.JSON.Send(ws, NewFailure("protocol error: convertedStats"))
|
|
return
|
|
} else {
|
|
log.Printf("%s -> %s: sent stats payload", gid.Id, p.Id)
|
|
}
|
|
|
|
log.Printf("%s, %s: about to register this player", gid.Id, p.Id)
|
|
game.register <- p
|
|
log.Printf("%s, %s: registered player", gid.Id, p.Id)
|
|
|
|
defer func() {
|
|
log.Printf("%s, %s: about to unregister this player", gid.Id, p.Id)
|
|
game.unregister <- p
|
|
log.Printf("%s, %s: unregistered player", gid.Id, p.Id)
|
|
}()
|
|
go p.Sender()
|
|
log.Printf("%s -> %s: p.sender went", gid.Id, p.Id)
|
|
p.Recv()
|
|
log.Printf(
|
|
"%s (player): %v (robot) has been disconnected from %s (game)",
|
|
p.Id,
|
|
p.Robots[0].Id,
|
|
gid.Id,
|
|
)
|
|
case "spectator":
|
|
s := NewSpectator(player_id, ws, game.bw, encoding)
|
|
log.Printf("%s, %s: about to register this spectator", gid.Id, s.Id)
|
|
game.sregister <- s
|
|
log.Printf("%s, %s: registered spectator", gid.Id, s.Id)
|
|
defer func() {
|
|
log.Printf("%s, %s: about to unregister this spectator", gid.Id, s.Id)
|
|
game.sunregister <- s
|
|
log.Printf("%s, %s: unregistered spectator", gid.Id, s.Id)
|
|
}()
|
|
go s.Sender()
|
|
log.Printf("%s -> %s: s.sender went", gid.Id, s.Id)
|
|
s.Recv()
|
|
log.Printf("game %s: spectator %+v has been disconnected from this game", gid.Id, s)
|
|
}
|
|
log.Printf("exiting AddPlayer")
|
|
}
|