package server 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 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) convertedStats[name] = dstat 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) } 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") }