package botserv // delete me import ( "log" "sort" "sync" "time" "bitbucket.org/smcquay/bandwidth" ) const maxPlayer = 128 // BotHealth is sent to all players so they know how other robots are // doing. type BotHealth struct { RobotId string `json:"robot_id"` Health int `json:"health"` } // Scanner contains a Robot/Projectile hash and is sent to the user to // let them know which things they know about. type Scanner struct { Id string `json:"id"` Type string `json:"type"` } // BotStats is stats for a single Player's Robot. type BotStats struct { Kills int Deaths int Suicides int Shots int DirectHits int Hits int Wins int } // PlayerStats is what you want many of. Contains a map of BotStats and total // wins. type PlayerStats struct { BotStats map[string]*BotStats Wins int } // GameStats is a collection of PlayerStats for all players involved. type GameStats struct { PlayerStats map[string]*PlayerStats sync.RWMutex } // Game is the main point of interest in this application. Embodies all info // required to keep track of players, robots, stats, projectils, etc. // Currently Controllers have a map of these. type Game struct { id string players map[*Player]bool projectiles map[*Projectile]bool splosions map[*Splosion]bool obstacles []Obstacle obstacle_count int register chan *Player unregister chan *Player turn int players_remaining int width, height float32 maxPoints int spectators map[*Spectator]bool sregister chan *Spectator sunregister chan *Spectator kill chan bool repair_hp int repair_rate float32 tick_duration int stats GameStats mode GameMode bw *bandwidth.Bandwidth } // This is the interface that different gametypes should implement. type GameMode interface { setup(g *Game) tick(gg *Game, payload *Boardstate) gameOver(gg *Game) (bool, *GameOver) } // NewGame Poplulates a Game struct and starts the bandwidth calculator. func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, mode string) (*Game, error) { bw, err := bandwidth.NewBandwidth( []int{1, 10, 60}, 1*time.Second, ) if err != nil { return nil, err } go bw.Run() g := &Game{ id: id, register: make(chan *Player, maxPlayer), unregister: make(chan *Player, maxPlayer), projectiles: make(map[*Projectile]bool), splosions: make(map[*Splosion]bool), obstacles: GenerateObstacles(obstacles, width, height), obstacle_count: obstacles, players: make(map[*Player]bool), turn: 0, width: width, height: height, maxPoints: maxPoints, spectators: make(map[*Spectator]bool), sregister: make(chan *Spectator), sunregister: make(chan *Spectator), kill: make(chan bool), repair_hp: 5, repair_rate: 3.0, tick_duration: tick, players_remaining: 2, stats: GameStats{PlayerStats: make(map[string]*PlayerStats)}, bw: bw, } if mode == "melee" { g.mode = &melee{respawn: make(map[*Robot]float64)} } else { g.mode = &deathmatch{} } g.mode.setup(g) return g, nil } // tick is the method called every TICK ms. func (g *Game) tick(payload *Boardstate) { g.players_remaining = 0 payload.Objects = MinifyObstacles(g.obstacles) // Update Players for p := range g.players { living_robots := 0 for _, r := range p.Robots { if r.Health > 0 { living_robots++ r.Tick(g) } if len(r.Message) > 0 { if len(r.Message) > 100 { r.Message = r.Message[0:99] } payload.Messages = append(payload.Messages, r.Message) } payload.OtherRobots = append( payload.OtherRobots, r.GetTruncatedDetails()) payload.AllBots = append( payload.AllBots, BotHealth{RobotId: r.Id, Health: r.Health}) } if living_robots > 0 { g.players_remaining++ } } // Update Projectiles for pr := range g.projectiles { pr.Tick(g) } // We do this here, because the tick calls can alter g.projectiles for pr := range g.projectiles { payload.Projectiles = append(payload.Projectiles, *pr) } // Update Splosions for s := range g.splosions { s.Tick() if !s.Alive() { delete(g.splosions, s) } payload.Splosions = append(payload.Splosions, *s) } } // sendUpdate is what we use to determine what data goes out to each client; // performs filtering and sorting of the data. func (g *Game) sendUpdate(payload *Boardstate) { // Ensure that the robots are always sent in a consistent order sort.Sort(RobotSorter{Robots: payload.OtherRobots}) sort.Sort(AllRobotSorter{Robots: payload.AllBots}) for p := range g.players { // Copy the payload but only add the robots in scanner range player_payload := NewBoardstate() player_payload.Messages = payload.Messages player_payload.AllBots = payload.AllBots player_payload.Turn = payload.Turn for _, r := range p.Robots { player_payload.MyRobots = append(player_payload.MyRobots, *r) // player_payload.OtherRobots = append( // player_payload.OtherRobots, // r.GetTruncatedDetails()) } player_payload.Objects = [][4]int{} player_payload.Splosions = []Splosion{} player_payload.Projectiles = []Projectile{} living_robots := 0 for _, r := range p.Robots { if r.Health > 0 { living_robots++ // Filter robots by scanner for player := range g.players { for _, scan_entry := range r.Scanners { for _, r := range player.Robots { if r.Id == scan_entry.Id { player_payload.OtherRobots = append( player_payload.OtherRobots, r.GetTruncatedDetails()) } } } } // Filter projectiles for proj := range g.projectiles { if proj.Owner == r { player_payload.Projectiles = append( player_payload.Projectiles, *proj) } for _, scan_entry := range r.Scanners { if proj.Id == scan_entry.Id { player_payload.Projectiles = append( player_payload.Projectiles, *proj) } } } // Filter splosions for splo := range g.splosions { for _, scan_entry := range r.Scanners { if splo.Id == scan_entry.Id { player_payload.Splosions = append( player_payload.Splosions, *splo) } } } // Filter objects for _, ob := range g.obstacles { if ob.distance_from_point(r.Position) < float32(r.Stats.ScannerRadius)+r.ScanCounter { player_payload.Objects = append( player_payload.Objects, ob.minify()) } } } } // if living_robots == 0 { // player_payload.OtherRobots = payload.OtherRobots // player_payload.Projectiles = payload.Projectiles // player_payload.Splosions = payload.Splosions // player_payload.Objects = payload.Objects // } p.send <- player_payload } for s := range g.spectators { s.send <- payload } } // run is the method that contians the main game loop. func (g *Game) run() { ticker := time.NewTicker(time.Duration(g.tick_duration) * time.Millisecond) for { select { case <-g.kill: log.Printf("game %s: received kill signal, dying gracefully", g.id) g.bw.Quit <- true for player := range g.players { close(player.send) } return case p := <-g.register: log.Println("registering player:", p.Id) g.players[p] = true g.stats.PlayerStats[p.Id] = &PlayerStats{ BotStats: make(map[string]*BotStats), } for _, r := range p.Robots { g.stats.PlayerStats[p.Id].BotStats[r.Name] = &BotStats{} r.gameStats = g.stats.PlayerStats[p.Id].BotStats[r.Name] } case p := <-g.unregister: log.Println("unregistering player:", p.Id) delete(g.players, p) close(p.send) case s := <-g.sregister: log.Println("registering spectator:", s.Id) g.spectators[s] = true case s := <-g.sunregister: log.Println("unregistering spectator:", s.Id) delete(g.spectators, s) close(s.send) case <-ticker.C: payload := NewBoardstate() g.turn++ payload.Turn = g.turn // UPDATE GAME STATE if end, data := g.mode.gameOver(g); end { g.sendGameOver(data) } g.tick(payload) g.mode.tick(g, payload) // SEND THE UPDATE TO EACH PLAYER g.sendUpdate(payload) } } log.Println("run done") } // sendGameOver is a special method that sends a GameOver object to the clients // instead of a normal Boardstate message. func (g *Game) sendGameOver(eg *GameOver) { log.Printf("sending out game over message: %+v", eg) for p := range g.players { p.send <- eg } for s := range g.spectators { s.send <- eg } } // returns a GameParam object popuplated by info from the game. This is // used during client/server initial negociation. func (g *Game) gameParam() *GameParam { return &GameParam{ BoardSize: BoardSize{ Width: g.width, Height: g.height, }, MaxPoints: g.maxPoints, Type: "gameparam", } }