356 lines
8.5 KiB
Go
356 lines
8.5 KiB
Go
package server
|
|
|
|
// 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 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, 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
|
|
|
|
// 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",
|
|
}
|
|
}
|