You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
9.8 KiB
384 lines
9.8 KiB
package server |
|
|
|
import ( |
|
"log" |
|
|
|
"golang.org/x/net/websocket" |
|
|
|
v "hackerbots.us/vector" |
|
) |
|
|
|
// 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 float64 `json:"width"` |
|
Height float64 `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"` |
|
Obstacles []Obstacle `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, |
|
float64(c.Conf.Width), |
|
float64(c.Conf.Height), |
|
c.Conf.Tick, |
|
c.Conf.MaxPoints, |
|
"", |
|
) |
|
game.defaultObstacles = c.Conf.Obstacles |
|
game.obstacleCount = c.Conf.ObstacleCount |
|
log.Printf("%t", len(game.defaultObstacles) == 0) |
|
if len(game.defaultObstacles) == 0 { |
|
game.obstacles = GenerateObstacles( |
|
game.obstacleCount, |
|
game.width, |
|
game.height, |
|
) |
|
} else { |
|
game.obstacles = c.Conf.Obstacles |
|
} |
|
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) |
|
|
|
r := Robot{ |
|
Stats: dstat, |
|
Id: c.Idg.Hash(), |
|
Name: name, |
|
Health: 10, |
|
Heading: v.Vector2d{X: 1, Y: 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) |
|
dstat.Id = r.Id |
|
convertedStats[name] = dstat |
|
} |
|
|
|
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") |
|
}
|
|
|