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.
354 lines
8.4 KiB
354 lines
8.4 KiB
package server |
|
|
|
// delete me |
|
|
|
import ( |
|
"log" |
|
"sort" |
|
"sync" |
|
"time" |
|
|
|
"mcquay.me/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 |
|
defaultObstacles []Obstacle |
|
obstacles []Obstacle |
|
obstacleCount int |
|
register chan *Player |
|
unregister chan *Player |
|
turn int |
|
players_remaining int |
|
width, height float64 |
|
maxPoints int |
|
spectators map[*Spectator]bool |
|
sregister chan *Spectator |
|
sunregister chan *Spectator |
|
kill chan bool |
|
repair_hp int |
|
repair_rate float64 |
|
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 float64, 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), |
|
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 |
|
|
|
// 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.Obstacles = []Obstacle{} |
|
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) < float64(r.Stats.ScannerRadius)+r.ScanCounter { |
|
player_payload.Obstacles = append( |
|
player_payload.Obstacles, ob) |
|
} |
|
} |
|
} |
|
} |
|
|
|
p.send <- player_payload |
|
} |
|
for s := range g.spectators { |
|
payload.Obstacles = g.obstacles |
|
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) |
|
} |
|
} |
|
} |
|
|
|
// 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", |
|
} |
|
}
|
|
|