Stephen McQuay
6fd4138740
unfortunately there is an issue with how these are calculated, and they don't decrease as things quiet down. Well, the bad numbers are their for your consumption, and should Just Work TM when appropriate changes in bitbucket.org/smcquay/bandwith land. And they will. But I'm tired now.
386 lines
8.6 KiB
Go
386 lines
8.6 KiB
Go
package main
|
|
|
|
// delete me
|
|
|
|
import (
|
|
"log"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"bitbucket.org/smcquay/bandwidth"
|
|
)
|
|
|
|
const maxPlayer = 128
|
|
|
|
type BotHealth struct {
|
|
RobotId string `json:"robot_id"`
|
|
Health int `json:"health"`
|
|
}
|
|
|
|
type Scanner struct {
|
|
Id string `json:"id"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type MapLock struct {
|
|
m map[string]*game
|
|
sync.RWMutex
|
|
}
|
|
|
|
// get is a function that returns a game if found, and creates one if
|
|
// not found and force is true. In order to get a hash (rather than use
|
|
// the string you pass) send "" for id.
|
|
func (ml *MapLock) get(id string) *game {
|
|
ml.Lock()
|
|
g, _ := ml.m[id]
|
|
ml.Unlock()
|
|
return g
|
|
}
|
|
|
|
func (ml *MapLock) add(g *game) {
|
|
ml.Lock()
|
|
ml.m[g.id] = g
|
|
ml.Unlock()
|
|
}
|
|
|
|
type BotStats struct {
|
|
Kills int
|
|
Deaths int
|
|
Suicides int
|
|
Shots int
|
|
DirectHits int
|
|
Hits int
|
|
Wins int
|
|
}
|
|
|
|
type PlayerStats struct {
|
|
BotStats map[string]*BotStats
|
|
Wins int
|
|
}
|
|
|
|
type GameStats struct {
|
|
PlayerStats map[string]*PlayerStats
|
|
sync.RWMutex
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type GameMode interface {
|
|
setup(g *game)
|
|
tick(gg *game, payload *Boardstate)
|
|
gameOver(gg *game) (bool, *GameOver)
|
|
}
|
|
|
|
func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, mode string) (*game, error) {
|
|
bw, err := bandwidth.NewBandwidth(
|
|
[]int{1, 10, 60},
|
|
time.Duration(500)*time.Millisecond,
|
|
)
|
|
if err != nil {
|
|
log.Fatal("seriously, what the fuck")
|
|
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, maxPlayer),
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
}
|
|
|
|
func (g *game) run() {
|
|
var t0, t1 time.Time
|
|
ticker := time.NewTicker(time.Duration(conf.Tick) * time.Millisecond)
|
|
for {
|
|
select {
|
|
case <-g.kill:
|
|
log.Printf("game %s: received kill signal, dying gracefully", g.id)
|
|
close(g.bw.Quit)
|
|
games.Lock()
|
|
for player := range g.players {
|
|
close(player.send)
|
|
}
|
|
delete(games.m, g.id)
|
|
games.Unlock()
|
|
return
|
|
case p := <-g.register:
|
|
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:
|
|
delete(g.players, p)
|
|
close(p.send)
|
|
case s := <-g.sregister:
|
|
g.spectators[s] = true
|
|
case s := <-g.sunregister:
|
|
delete(g.spectators, s)
|
|
close(s.send)
|
|
case <-ticker.C:
|
|
t0 = time.Now()
|
|
payload := NewBoardstate()
|
|
|
|
g.turn++
|
|
payload.Turn = g.turn
|
|
if *verbose {
|
|
log.Printf("\033[2JTurn: %v", g.turn)
|
|
log.Printf("Players: %v", len(g.players))
|
|
log.Printf("Projectiles: %v", len(g.projectiles))
|
|
log.Printf("Explosions: %v", len(g.splosions))
|
|
}
|
|
|
|
// UPDATE GAME STATE
|
|
if end, data := g.mode.gameOver(g); end {
|
|
g.sendGameOver(data)
|
|
}
|
|
|
|
g.tick(payload)
|
|
g.mode.tick(g, payload)
|
|
|
|
t1 = time.Now()
|
|
if *verbose {
|
|
log.Printf("Turn Processes %v\n", t1.Sub(t0))
|
|
}
|
|
|
|
// SEND THE UPDATE TO EACH PLAYER
|
|
g.sendUpdate(payload)
|
|
|
|
t1 = time.Now()
|
|
if *verbose {
|
|
log.Printf("Sent Payload %v\n", t1.Sub(t0))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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",
|
|
}
|
|
}
|