First pass at documentations.
This commit is contained in:
parent
0cf0957d73
commit
faacfe7fd9
11
config.go
11
config.go
@ -11,6 +11,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config embodies the configuration for a game. These are populated in various
|
||||||
|
// ways (json POSTed to server, config file, constants) and are typically owned
|
||||||
|
// by a Controller.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Tick int `json:"tick"` // ms
|
Tick int `json:"tick"` // ms
|
||||||
Timescale float32 `json:"timescale"`
|
Timescale float32 `json:"timescale"`
|
||||||
@ -23,8 +26,8 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TICK = 60
|
TICK = 60 // ms, this is how often physics is updated
|
||||||
TIMESCALE = 1.0
|
TIMESCALE = 1.0 // this tweaks the temporal duration of a TICK
|
||||||
WIDTH = 800
|
WIDTH = 800
|
||||||
HEIGHT = 550
|
HEIGHT = 550
|
||||||
OBSTACLES = 5
|
OBSTACLES = 5
|
||||||
@ -32,6 +35,10 @@ const (
|
|||||||
DEFAULT_MODE = "deathmatch"
|
DEFAULT_MODE = "deathmatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoadConfig takes the location of a json file that contains values desired to
|
||||||
|
// be used in the creation of games. Priority is given to values specified at
|
||||||
|
// game cration time using control channel, followed by values in config file,
|
||||||
|
// followed by constants defined above.
|
||||||
func LoadConfig(filename string) (Config, error) {
|
func LoadConfig(filename string) (Config, error) {
|
||||||
c := Config{
|
c := Config{
|
||||||
Tick: TICK,
|
Tick: TICK,
|
||||||
|
31
control.go
31
control.go
@ -13,13 +13,18 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// JsonHandler is a function type that allows setting the Content-Type
|
||||||
|
// appropriately for views destined to serve JSON
|
||||||
type JsonHandler func(http.ResponseWriter, *http.Request)
|
type JsonHandler func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
// ServeHTTP is JsonHandler's http.Handler implementation.
|
||||||
func (h JsonHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (h JsonHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
h(w, req)
|
h(w, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Controller is the shepherd of a collection of games. The main package in
|
||||||
|
// botserv simply populates one of these and starts an http server.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
Idg *IdGenerator
|
Idg *IdGenerator
|
||||||
Conf Config
|
Conf Config
|
||||||
@ -28,6 +33,9 @@ type Controller struct {
|
|||||||
Profile string
|
Profile string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewController takes a populated Config, and some parameters to determine
|
||||||
|
// what sorts of profiling to deal with and returns a freshly populated
|
||||||
|
// Controller.
|
||||||
func NewController(conf Config, mprof, pprof string) *Controller {
|
func NewController(conf Config, mprof, pprof string) *Controller {
|
||||||
idg := NewIdGenerator()
|
idg := NewIdGenerator()
|
||||||
return &Controller{
|
return &Controller{
|
||||||
@ -41,11 +49,15 @@ func NewController(conf Config, mprof, pprof string) *Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Eventually this thing will have a select loop for dealing with game access in a more lock-free manner?
|
// TODO Eventually this thing will have a select loop for dealing with game
|
||||||
|
// access in a more lock-free manner?
|
||||||
func (c *Controller) Run() {
|
func (c *Controller) Run() {
|
||||||
c.Idg.Run()
|
c.Idg.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartGame is the http route responsible for responding to requests to start
|
||||||
|
// games under this controller. Creates a default Config object, and populates
|
||||||
|
// it according to data POSTed.
|
||||||
func (c *Controller) StartGame(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) StartGame(w http.ResponseWriter, req *http.Request) {
|
||||||
log.Println("asked to create a game")
|
log.Println("asked to create a game")
|
||||||
|
|
||||||
@ -112,6 +124,8 @@ func (c *Controller) StartGame(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListGames makes a reasonable JSON response based on the games currently
|
||||||
|
// being run.
|
||||||
func (c *Controller) ListGames(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) ListGames(w http.ResponseWriter, req *http.Request) {
|
||||||
log.Println("games list requested")
|
log.Println("games list requested")
|
||||||
c.Games.RLock()
|
c.Games.RLock()
|
||||||
@ -146,6 +160,8 @@ func (c *Controller) ListGames(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GameStats provides an control mechanism to query for the stats of a single
|
||||||
|
// game
|
||||||
func (c *Controller) GameStats(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) GameStats(w http.ResponseWriter, req *http.Request) {
|
||||||
// TODO: wrap this up in something similar to the JsonHandler to verify the
|
// TODO: wrap this up in something similar to the JsonHandler to verify the
|
||||||
// url? Look at gorilla routing?
|
// url? Look at gorilla routing?
|
||||||
@ -171,6 +187,8 @@ func (c *Controller) GameStats(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BW provides a route to query for current bandwidth utilization for a single
|
||||||
|
// game.
|
||||||
func (c *Controller) BW(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) BW(w http.ResponseWriter, req *http.Request) {
|
||||||
// TODO: wrap this up in something similar to the JsonHandler to verify the
|
// TODO: wrap this up in something similar to the JsonHandler to verify the
|
||||||
// url? Look at gorilla routing?
|
// url? Look at gorilla routing?
|
||||||
@ -198,7 +216,8 @@ func (c *Controller) BW(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopGame is the only mechanism to decrease the number of running games in a Controller
|
// StopGame is the only mechanism to decrease the number of running games in
|
||||||
|
// a Controller
|
||||||
func (c *Controller) StopGame(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) StopGame(w http.ResponseWriter, req *http.Request) {
|
||||||
key, err := c.getGameId(req.URL.Path)
|
key, err := c.getGameId(req.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -228,6 +247,8 @@ func (c *Controller) StopGame(w http.ResponseWriter, req *http.Request) {
|
|||||||
log.Printf("returning from StopGame")
|
log.Printf("returning from StopGame")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KillServer is my favorite method of all the methods in botserv: it shuts
|
||||||
|
// things down respecting profiling requests.
|
||||||
func (c *Controller) KillServer(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) KillServer(w http.ResponseWriter, req *http.Request) {
|
||||||
if c.Profile != "" {
|
if c.Profile != "" {
|
||||||
log.Print("trying to stop cpu profile")
|
log.Print("trying to stop cpu profile")
|
||||||
@ -247,6 +268,9 @@ func (c *Controller) KillServer(w http.ResponseWriter, req *http.Request) {
|
|||||||
log.Fatal("shit got fucked up")
|
log.Fatal("shit got fucked up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Index is the function for handling all traffic not officially in the API. It
|
||||||
|
// just lets people know that this is a hackerbots server running at
|
||||||
|
// a particular version.
|
||||||
func (c *Controller) Index(w http.ResponseWriter, req *http.Request) {
|
func (c *Controller) Index(w http.ResponseWriter, req *http.Request) {
|
||||||
log.Println("version requested")
|
log.Println("version requested")
|
||||||
version := struct {
|
version := struct {
|
||||||
@ -261,6 +285,8 @@ func (c *Controller) Index(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getGameId trims the gameid off of the url. This is hokey, and makes me miss
|
||||||
|
// django regex-specified routes.
|
||||||
func (c *Controller) getGameId(path string) (string, error) {
|
func (c *Controller) getGameId(path string) (string, error) {
|
||||||
var err error
|
var err error
|
||||||
trimmed := strings.Trim(path, "/")
|
trimmed := strings.Trim(path, "/")
|
||||||
@ -273,6 +299,7 @@ func (c *Controller) getGameId(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MapLock is simply a map and a RWMutex
|
// MapLock is simply a map and a RWMutex
|
||||||
|
// TODO: obviate the need for this in Controller.Run
|
||||||
type MapLock struct {
|
type MapLock struct {
|
||||||
M map[string]*Game
|
M map[string]*Game
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// deathmatch is a game type that resets when there is one bot standing. There
|
||||||
|
// is an obvious winner each round.
|
||||||
type deathmatch struct {
|
type deathmatch struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
game.go
31
game.go
@ -13,16 +13,21 @@ import (
|
|||||||
|
|
||||||
const maxPlayer = 128
|
const maxPlayer = 128
|
||||||
|
|
||||||
|
// BotHealth is sent to all players so they know how other robots are
|
||||||
|
// doing.
|
||||||
type BotHealth struct {
|
type BotHealth struct {
|
||||||
RobotId string `json:"robot_id"`
|
RobotId string `json:"robot_id"`
|
||||||
Health int `json:"health"`
|
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 {
|
type Scanner struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BotStats is stats for a single Player's Robot.
|
||||||
type BotStats struct {
|
type BotStats struct {
|
||||||
Kills int
|
Kills int
|
||||||
Deaths int
|
Deaths int
|
||||||
@ -33,25 +38,31 @@ type BotStats struct {
|
|||||||
Wins int
|
Wins int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlayerStats is what you want many of. Contains a map of BotStats and total
|
||||||
|
// wins.
|
||||||
type PlayerStats struct {
|
type PlayerStats struct {
|
||||||
BotStats map[string]*BotStats
|
BotStats map[string]*BotStats
|
||||||
Wins int
|
Wins int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GameStats is a collection of PlayerStats for all players involved.
|
||||||
type GameStats struct {
|
type GameStats struct {
|
||||||
PlayerStats map[string]*PlayerStats
|
PlayerStats map[string]*PlayerStats
|
||||||
sync.RWMutex
|
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 {
|
type Game struct {
|
||||||
id string
|
id string
|
||||||
players map[*player]bool
|
players map[*Player]bool
|
||||||
projectiles map[*Projectile]bool
|
projectiles map[*Projectile]bool
|
||||||
splosions map[*Splosion]bool
|
splosions map[*Splosion]bool
|
||||||
obstacles []Obstacle
|
obstacles []Obstacle
|
||||||
obstacle_count int
|
obstacle_count int
|
||||||
register chan *player
|
register chan *Player
|
||||||
unregister chan *player
|
unregister chan *Player
|
||||||
turn int
|
turn int
|
||||||
players_remaining int
|
players_remaining int
|
||||||
width, height float32
|
width, height float32
|
||||||
@ -68,12 +79,14 @@ type Game struct {
|
|||||||
bw *bandwidth.Bandwidth
|
bw *bandwidth.Bandwidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the interface that different gametypes should implement.
|
||||||
type GameMode interface {
|
type GameMode interface {
|
||||||
setup(g *Game)
|
setup(g *Game)
|
||||||
tick(gg *Game, payload *Boardstate)
|
tick(gg *Game, payload *Boardstate)
|
||||||
gameOver(gg *Game) (bool, *GameOver)
|
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) {
|
func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, mode string) (*Game, error) {
|
||||||
bw, err := bandwidth.NewBandwidth(
|
bw, err := bandwidth.NewBandwidth(
|
||||||
[]int{1, 10, 60},
|
[]int{1, 10, 60},
|
||||||
@ -85,13 +98,13 @@ func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, m
|
|||||||
go bw.Run()
|
go bw.Run()
|
||||||
g := &Game{
|
g := &Game{
|
||||||
id: id,
|
id: id,
|
||||||
register: make(chan *player, maxPlayer),
|
register: make(chan *Player, maxPlayer),
|
||||||
unregister: make(chan *player, maxPlayer),
|
unregister: make(chan *Player, maxPlayer),
|
||||||
projectiles: make(map[*Projectile]bool),
|
projectiles: make(map[*Projectile]bool),
|
||||||
splosions: make(map[*Splosion]bool),
|
splosions: make(map[*Splosion]bool),
|
||||||
obstacles: GenerateObstacles(obstacles, width, height),
|
obstacles: GenerateObstacles(obstacles, width, height),
|
||||||
obstacle_count: obstacles,
|
obstacle_count: obstacles,
|
||||||
players: make(map[*player]bool),
|
players: make(map[*Player]bool),
|
||||||
turn: 0,
|
turn: 0,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@ -119,6 +132,7 @@ func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, m
|
|||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tick is the method called every TICK ms.
|
||||||
func (g *Game) tick(payload *Boardstate) {
|
func (g *Game) tick(payload *Boardstate) {
|
||||||
g.players_remaining = 0
|
g.players_remaining = 0
|
||||||
payload.Objects = MinifyObstacles(g.obstacles)
|
payload.Objects = MinifyObstacles(g.obstacles)
|
||||||
@ -174,6 +188,8 @@ func (g *Game) tick(payload *Boardstate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (g *Game) sendUpdate(payload *Boardstate) {
|
||||||
// Ensure that the robots are always sent in a consistent order
|
// Ensure that the robots are always sent in a consistent order
|
||||||
sort.Sort(RobotSorter{Robots: payload.OtherRobots})
|
sort.Sort(RobotSorter{Robots: payload.OtherRobots})
|
||||||
@ -270,6 +286,7 @@ func (g *Game) sendUpdate(payload *Boardstate) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run is the method that contians the main game loop.
|
||||||
func (g *Game) run() {
|
func (g *Game) run() {
|
||||||
ticker := time.NewTicker(time.Duration(g.tick_duration) * time.Millisecond)
|
ticker := time.NewTicker(time.Duration(g.tick_duration) * time.Millisecond)
|
||||||
for {
|
for {
|
||||||
@ -323,6 +340,8 @@ func (g *Game) run() {
|
|||||||
log.Println("run done")
|
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) {
|
func (g *Game) sendGameOver(eg *GameOver) {
|
||||||
log.Printf("sending out game over message: %+v", eg)
|
log.Printf("sending out game over message: %+v", eg)
|
||||||
for p := range g.players {
|
for p := range g.players {
|
||||||
|
5
id.go
5
id.go
@ -23,6 +23,8 @@ func NewIdGenerator() *IdGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run is called (typically in a gorotine) to allow for queries to be made
|
||||||
|
// against IdGenerator.id throgh the Hash method.
|
||||||
func (idg *IdGenerator) Run() {
|
func (idg *IdGenerator) Run() {
|
||||||
var i int64
|
var i int64
|
||||||
for i = 0; ; i++ {
|
for i = 0; ; i++ {
|
||||||
@ -30,6 +32,9 @@ func (idg *IdGenerator) Run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash is the method used by a properly instantiated IdGenerator that gives
|
||||||
|
// fairly unique strings. They are currently truncated md5 hashes of the time
|
||||||
|
// plus a unique counter
|
||||||
func (id *IdGenerator) Hash() string {
|
func (id *IdGenerator) Hash() string {
|
||||||
h := md5.New()
|
h := md5.New()
|
||||||
ns := time.Now().UnixNano() + <-id.id
|
ns := time.Now().UnixNano() + <-id.id
|
||||||
|
2
melee.go
2
melee.go
@ -4,6 +4,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// melee is a game type that allows dead players to respond after a particular
|
||||||
|
// number of ms.
|
||||||
type melee struct {
|
type melee struct {
|
||||||
respawn map[*Robot]float64
|
respawn map[*Robot]float64
|
||||||
respawn_timer float64
|
respawn_timer float64
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
v "bitbucket.org/hackerbots/vector"
|
v "bitbucket.org/hackerbots/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Obstacle is the implementation of the generic building type in the game.
|
||||||
type Obstacle struct {
|
type Obstacle struct {
|
||||||
Bounds v.AABB2d `json:"bounds"`
|
Bounds v.AABB2d `json:"bounds"`
|
||||||
Hp int `json:"-"`
|
Hp int `json:"-"`
|
||||||
@ -28,6 +29,8 @@ func (o Obstacle) minify() [4]int {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MinifyObstacles is a function used to convert []osbstacles into more tightly
|
||||||
|
// packed [][4]int for smaller json payloads
|
||||||
func MinifyObstacles(o []Obstacle) [][4]int {
|
func MinifyObstacles(o []Obstacle) [][4]int {
|
||||||
out := [][4]int{}
|
out := [][4]int{}
|
||||||
for i := range o {
|
for i := range o {
|
||||||
@ -36,6 +39,8 @@ func MinifyObstacles(o []Obstacle) [][4]int {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateObstacles returns a slice of (count) obstacles within a region
|
||||||
|
// bounded by width, height.
|
||||||
func GenerateObstacles(count int, width, height float32) []Obstacle {
|
func GenerateObstacles(count int, width, height float32) []Obstacle {
|
||||||
out := []Obstacle{}
|
out := []Obstacle{}
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
|
66
player.go
66
player.go
@ -19,39 +19,46 @@ type decoder interface {
|
|||||||
Decode(v interface{}) error
|
Decode(v interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type streamCounter struct {
|
// StreamCount is the wrapper we use around reads and writes to keep track of
|
||||||
|
// bandwidths.
|
||||||
|
type StreamCount struct {
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
bw *bandwidth.Bandwidth
|
bw *bandwidth.Bandwidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *streamCounter) Read(p []byte) (n int, err error) {
|
// StreamCount.Read implements io.Reader
|
||||||
|
func (sc *StreamCount) Read(p []byte) (n int, err error) {
|
||||||
n, err = sc.ws.Read(p)
|
n, err = sc.ws.Read(p)
|
||||||
sc.bw.AddRx <- n
|
sc.bw.AddRx <- n
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *streamCounter) Write(p []byte) (n int, err error) {
|
// StreamCount.Write implements io.Writer
|
||||||
|
func (sc *StreamCount) Write(p []byte) (n int, err error) {
|
||||||
n, err = sc.ws.Write(p)
|
n, err = sc.ws.Write(p)
|
||||||
sc.bw.AddTx <- n
|
sc.bw.AddTx <- n
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *streamCounter) Close() error {
|
// Close is for cleanup
|
||||||
|
func (sc *StreamCount) Close() error {
|
||||||
return sc.ws.Close()
|
return sc.ws.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
type protoTalker struct {
|
// ProtoTalker is the simplest form of struct that talks to consumers of the
|
||||||
|
// service. There are two important methods here: Sender and Recv.
|
||||||
|
type ProtoTalker struct {
|
||||||
enc encoder
|
enc encoder
|
||||||
dec decoder
|
dec decoder
|
||||||
counter *streamCounter
|
counter *StreamCount
|
||||||
send chan Message
|
send chan Message
|
||||||
Id string
|
Id string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProtoTalker(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *protoTalker {
|
func NewProtoTalker(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *ProtoTalker {
|
||||||
var enc encoder
|
var enc encoder
|
||||||
var dec decoder
|
var dec decoder
|
||||||
comptroller := &streamCounter{
|
comptroller := &StreamCount{
|
||||||
ws: ws,
|
ws: ws,
|
||||||
bw: bw,
|
bw: bw,
|
||||||
}
|
}
|
||||||
@ -62,7 +69,7 @@ func NewProtoTalker(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, enco
|
|||||||
enc = gob.NewEncoder(comptroller)
|
enc = gob.NewEncoder(comptroller)
|
||||||
dec = gob.NewDecoder(comptroller)
|
dec = gob.NewDecoder(comptroller)
|
||||||
}
|
}
|
||||||
return &protoTalker{
|
return &ProtoTalker{
|
||||||
send: make(chan Message, 16),
|
send: make(chan Message, 16),
|
||||||
enc: enc,
|
enc: enc,
|
||||||
dec: dec,
|
dec: dec,
|
||||||
@ -71,8 +78,10 @@ func NewProtoTalker(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, enco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pt *protoTalker) sender() {
|
// Sender is the single implementation for data output to clients, both players
|
||||||
log.Printf("%s: %T sender launched", pt.Id, pt.enc)
|
// and spectators.
|
||||||
|
func (pt *ProtoTalker) Sender() {
|
||||||
|
log.Printf("%s: %T Sender launched", pt.Id, pt.enc)
|
||||||
for things := range pt.send {
|
for things := range pt.send {
|
||||||
err := pt.enc.Encode(things)
|
err := pt.enc.Encode(things)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -81,24 +90,28 @@ func (pt *protoTalker) sender() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pt.counter.Close()
|
pt.counter.Close()
|
||||||
log.Printf("%s: sender close", pt.Id)
|
log.Printf("%s: Sender close", pt.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
type player struct {
|
// player uses protoTalker's Sender method, but adds a Recv that knows how to
|
||||||
|
// deal with game play instructions from the player.
|
||||||
|
type Player struct {
|
||||||
Robots []*Robot
|
Robots []*Robot
|
||||||
Instruction Instruction
|
Instruction Instruction
|
||||||
protoTalker
|
ProtoTalker
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayer(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *player {
|
func NewPlayer(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *Player {
|
||||||
return &player{
|
return &Player{
|
||||||
Robots: []*Robot{},
|
Robots: []*Robot{},
|
||||||
protoTalker: *NewProtoTalker(id, ws, bw, encoding),
|
ProtoTalker: *NewProtoTalker(id, ws, bw, encoding),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *player) recv() {
|
// Player.Recv is the function responsible for parsing out player instructions
|
||||||
log.Println("starting recv")
|
// and sending them to the game.
|
||||||
|
func (p *Player) Recv() {
|
||||||
|
log.Println("starting Recv")
|
||||||
for {
|
for {
|
||||||
var msgs map[string]Instruction
|
var msgs map[string]Instruction
|
||||||
err := p.dec.Decode(&msgs)
|
err := p.dec.Decode(&msgs)
|
||||||
@ -165,21 +178,26 @@ func (p *player) recv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%s: recv close", p.Id)
|
log.Printf("%s: Recv close", p.Id)
|
||||||
p.counter.Close()
|
p.counter.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spectator merely sends out game state, does not receive meaningful
|
||||||
|
// instructions from spectators.
|
||||||
type Spectator struct {
|
type Spectator struct {
|
||||||
protoTalker
|
ProtoTalker
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSpectator(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *Spectator {
|
func NewSpectator(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *Spectator {
|
||||||
return &Spectator{
|
return &Spectator{
|
||||||
protoTalker: *NewProtoTalker(id, ws, bw, encoding),
|
ProtoTalker: *NewProtoTalker(id, ws, bw, encoding),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spectator) recv() {
|
// Spectator.Recv is an interesting beast. We had to add it as for whatever
|
||||||
|
// reason the server would lock up if we weren't reading the empty responses
|
||||||
|
// from spectators.
|
||||||
|
func (s *Spectator) Recv() {
|
||||||
for {
|
for {
|
||||||
var msgs interface{}
|
var msgs interface{}
|
||||||
err := s.dec.Decode(&msgs)
|
err := s.dec.Decode(&msgs)
|
||||||
@ -196,6 +214,6 @@ func (s *Spectator) recv() {
|
|||||||
// break
|
// break
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
log.Printf("%s: recv close", s.Id)
|
log.Printf("%s: Recv close", s.Id)
|
||||||
s.counter.Close()
|
s.counter.Close()
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
v "bitbucket.org/hackerbots/vector"
|
v "bitbucket.org/hackerbots/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Projectile are the things robots can shoot at eachother.
|
||||||
type Projectile struct {
|
type Projectile struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Position v.Point2d `json:"position"`
|
Position v.Point2d `json:"position"`
|
||||||
@ -17,6 +18,9 @@ type Projectile struct {
|
|||||||
Delta float32
|
Delta float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Projectile.Tick is called every game tick and moves projectiles along,
|
||||||
|
// determines when they should blow up, and how damaage is propagated to
|
||||||
|
// players.
|
||||||
func (p *Projectile) Tick(g *Game) {
|
func (p *Projectile) Tick(g *Game) {
|
||||||
vec := p.MoveTo.Sub(p.Position)
|
vec := p.MoveTo.Sub(p.Position)
|
||||||
v_norm := vec.Normalize()
|
v_norm := vec.Normalize()
|
||||||
|
42
protocol.go
42
protocol.go
@ -7,16 +7,15 @@ import (
|
|||||||
"code.google.com/p/go.net/websocket"
|
"code.google.com/p/go.net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// < the name of the game we want to join
|
// GameID is essentially the name of the game we want to join
|
||||||
type GameID struct {
|
type GameID struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// > identify
|
// PlayerID is the internal hash we give to a client
|
||||||
type PlayerID struct {
|
type PlayerID struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Hash string `json:"id"`
|
Hash string `json:"id"`
|
||||||
Failure
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayerID(id string) *PlayerID {
|
func NewPlayerID(id string) *PlayerID {
|
||||||
@ -26,13 +25,15 @@ func NewPlayerID(id string) *PlayerID {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// < [robot | spectator], name, client-type, game ID
|
// ClientID is how a player wants to be known
|
||||||
type ClientID struct {
|
type ClientID struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Useragent string `json:"useragent"`
|
Useragent string `json:"useragent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientID.Valid is used to be sure the player connecting is of appropriate
|
||||||
|
// type.
|
||||||
func (c *ClientID) Valid() (bool, string) {
|
func (c *ClientID) Valid() (bool, string) {
|
||||||
switch c.Type {
|
switch c.Type {
|
||||||
case "robot", "spectator":
|
case "robot", "spectator":
|
||||||
@ -41,11 +42,14 @@ func (c *ClientID) Valid() (bool, string) {
|
|||||||
return false, "useragent must be 'robot' or 'spectator'"
|
return false, "useragent must be 'robot' or 'spectator'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientConfig embodies a map of stats requests
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Stats map[string]StatsRequest `json:"stats"`
|
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 {
|
func (config ClientConfig) Valid(max int) bool {
|
||||||
total := 0
|
total := 0
|
||||||
for _, s := range config.Stats {
|
for _, s := range config.Stats {
|
||||||
@ -65,11 +69,15 @@ func (config ClientConfig) Valid(max int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BoardSize is the response containing the geometry of the requested game.
|
||||||
type BoardSize struct {
|
type BoardSize struct {
|
||||||
Width float32 `json:"width"`
|
Width float32 `json:"width"`
|
||||||
Height float32 `json:"height"`
|
Height float32 `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 {
|
type GameParam struct {
|
||||||
// TODO: should have information about max points in here
|
// TODO: should have information about max points in here
|
||||||
BoardSize BoardSize `json:"boardsize"`
|
BoardSize BoardSize `json:"boardsize"`
|
||||||
@ -78,8 +86,8 @@ type GameParam struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// > [OK | FULL | NOT AUTH], board size, game params
|
// 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 {
|
type Handshake struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
@ -94,9 +102,14 @@ func NewHandshake(id string, success bool) *Handshake {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message interface {
|
// 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 {
|
type Boardstate struct {
|
||||||
MyRobots []Robot `json:"my_robots"`
|
MyRobots []Robot `json:"my_robots"`
|
||||||
OtherRobots []OtherRobot `json:"robots"`
|
OtherRobots []OtherRobot `json:"robots"`
|
||||||
@ -120,6 +133,7 @@ func NewBoardstate() *Boardstate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special outbound message with a []string of winners.
|
||||||
type GameOver struct {
|
type GameOver struct {
|
||||||
Winners []string `json:"winners"`
|
Winners []string `json:"winners"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -132,6 +146,8 @@ func NewGameOver() *GameOver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
type Failure struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -144,6 +160,8 @@ func NewFailure(reason string) *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) {
|
func (c *Controller) AddPlayer(ws *websocket.Conn) {
|
||||||
var gid GameID
|
var gid GameID
|
||||||
err := websocket.JSON.Receive(ws, &gid)
|
err := websocket.JSON.Receive(ws, &gid)
|
||||||
@ -324,9 +342,9 @@ encodingLoops:
|
|||||||
game.unregister <- p
|
game.unregister <- p
|
||||||
log.Printf("%s, %s: unregistered player", gid.Id, p.Id)
|
log.Printf("%s, %s: unregistered player", gid.Id, p.Id)
|
||||||
}()
|
}()
|
||||||
go p.sender()
|
go p.Sender()
|
||||||
log.Printf("%s -> %s: p.sender went", gid.Id, p.Id)
|
log.Printf("%s -> %s: p.sender went", gid.Id, p.Id)
|
||||||
p.recv()
|
p.Recv()
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"%s (player): %v (robot) has been disconnected from %s (game)",
|
"%s (player): %v (robot) has been disconnected from %s (game)",
|
||||||
p.Id,
|
p.Id,
|
||||||
@ -343,9 +361,9 @@ encodingLoops:
|
|||||||
game.sunregister <- s
|
game.sunregister <- s
|
||||||
log.Printf("%s, %s: unregistered spectator", gid.Id, s.Id)
|
log.Printf("%s, %s: unregistered spectator", gid.Id, s.Id)
|
||||||
}()
|
}()
|
||||||
go s.sender()
|
go s.Sender()
|
||||||
log.Printf("%s -> %s: s.sender went", gid.Id, s.Id)
|
log.Printf("%s -> %s: s.sender went", gid.Id, s.Id)
|
||||||
s.recv()
|
s.Recv()
|
||||||
log.Printf("game %s: spectator %+v has been disconnected from this game", gid.Id, s)
|
log.Printf("game %s: spectator %+v has been disconnected from this game", gid.Id, s)
|
||||||
}
|
}
|
||||||
log.Printf("exiting AddPlayer")
|
log.Printf("exiting AddPlayer")
|
||||||
|
19
robot.go
19
robot.go
@ -8,6 +8,8 @@ import (
|
|||||||
v "bitbucket.org/hackerbots/vector"
|
v "bitbucket.org/hackerbots/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Robot contains everything the game needs to know to simulate robot behavior.
|
||||||
|
// Players have a []Robot
|
||||||
type Robot struct {
|
type Robot struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -35,6 +37,7 @@ type Robot struct {
|
|||||||
idg *IdGenerator
|
idg *IdGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collision is basically a Point2d.
|
||||||
type Collision struct {
|
type Collision struct {
|
||||||
v.Point2d
|
v.Point2d
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -50,6 +53,7 @@ type OtherRobot struct {
|
|||||||
Health int `json:"health"`
|
Health int `json:"health"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTruncatedDetails pares down our info into an OtherRobot.
|
||||||
func (r Robot) GetTruncatedDetails() OtherRobot {
|
func (r Robot) GetTruncatedDetails() OtherRobot {
|
||||||
return OtherRobot{
|
return OtherRobot{
|
||||||
Id: r.Id,
|
Id: r.Id,
|
||||||
@ -60,6 +64,7 @@ func (r Robot) GetTruncatedDetails() OtherRobot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RobotSorter implements sort.Interface for OtherRobot
|
||||||
type RobotSorter struct {
|
type RobotSorter struct {
|
||||||
Robots []OtherRobot
|
Robots []OtherRobot
|
||||||
}
|
}
|
||||||
@ -76,7 +81,7 @@ func (s RobotSorter) Less(i, j int) bool {
|
|||||||
return s.Robots[i].Id < s.Robots[j].Id
|
return s.Robots[i].Id < s.Robots[j].Id
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - how do I not duplicate this code???
|
// AllRobotSorter implements sort.Inteface for BotHealth
|
||||||
type AllRobotSorter struct {
|
type AllRobotSorter struct {
|
||||||
Robots []BotHealth
|
Robots []BotHealth
|
||||||
}
|
}
|
||||||
@ -93,6 +98,7 @@ func (s AllRobotSorter) Less(i, j int) bool {
|
|||||||
return s.Robots[i].RobotId < s.Robots[j].RobotId
|
return s.Robots[i].RobotId < s.Robots[j].RobotId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stats is the point allocation for a Robot.
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
Hp int `json:"hp"`
|
Hp int `json:"hp"`
|
||||||
Speed float32 `json:"speed"`
|
Speed float32 `json:"speed"`
|
||||||
@ -105,8 +111,9 @@ type Stats struct {
|
|||||||
WeaponSpeed float32 `json:"weapon_speed"`
|
WeaponSpeed float32 `json:"weapon_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// We request stats using an integer between 1 and 100, the
|
// StatsRequest is the struct used in comunication with the player. We request
|
||||||
// integer values map to sensible min-max ranges
|
// stats using an integer between 1 and 100, the integer values map to sensible
|
||||||
|
// min-max ranges
|
||||||
type StatsRequest struct {
|
type StatsRequest struct {
|
||||||
Hp int `json:"hp"`
|
Hp int `json:"hp"`
|
||||||
Speed int `json:"speed"`
|
Speed int `json:"speed"`
|
||||||
@ -119,6 +126,7 @@ type StatsRequest struct {
|
|||||||
WeaponSpeed int `json:"weapon_speed"`
|
WeaponSpeed int `json:"weapon_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeriveStats maps the 0-100 values to sensible in-game min-max values.
|
||||||
func DeriveStats(request StatsRequest) Stats {
|
func DeriveStats(request StatsRequest) Stats {
|
||||||
s := Stats{}
|
s := Stats{}
|
||||||
|
|
||||||
@ -162,6 +170,7 @@ func DeriveStats(request StatsRequest) Stats {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instruction is the struct a player sends each turn.
|
||||||
type Instruction struct {
|
type Instruction struct {
|
||||||
Message *string `json:"message,omitempty"`
|
Message *string `json:"message,omitempty"`
|
||||||
MoveTo *v.Point2d `json:"move_to,omitempty"`
|
MoveTo *v.Point2d `json:"move_to,omitempty"`
|
||||||
@ -246,6 +255,7 @@ func (r *Robot) checkCollisions(g *Game, probe v.Vector2d) (bool, *v.Point2d, *R
|
|||||||
return finalCollision, intersection, finalRobot
|
return finalCollision, intersection, finalRobot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tick is the Robot's chance to udpate itself.
|
||||||
func (r *Robot) Tick(g *Game) {
|
func (r *Robot) Tick(g *Game) {
|
||||||
r.Collision = nil
|
r.Collision = nil
|
||||||
r.Hit = false
|
r.Hit = false
|
||||||
@ -397,6 +407,7 @@ func (r *Robot) Tick(g *Game) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scan updates the robots field of view if it's in teh appropriate mode
|
||||||
func (r *Robot) scan(g *Game) {
|
func (r *Robot) scan(g *Game) {
|
||||||
r.Scanners = r.Scanners[:0]
|
r.Scanners = r.Scanners[:0]
|
||||||
for player := range g.players {
|
for player := range g.players {
|
||||||
@ -443,6 +454,7 @@ func (r *Robot) scan(g *Game) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fire is called according to player instruction. XXX: There is a race here...
|
||||||
func (r *Robot) fire(g *Game) *Projectile {
|
func (r *Robot) fire(g *Game) *Projectile {
|
||||||
// Throttle the fire rate
|
// Throttle the fire rate
|
||||||
time_since_fired := (float32(g.turn) * (r.Delta * 1000)) - (float32(r.LastFired) * (r.Delta * 1000))
|
time_since_fired := (float32(g.turn) * (r.Delta * 1000)) - (float32(r.LastFired) * (r.Delta * 1000))
|
||||||
@ -465,6 +477,7 @@ func (r *Robot) fire(g *Game) *Projectile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset is called to move a robot to a reasonable location at game start time.
|
||||||
func (r *Robot) reset(g *Game) {
|
func (r *Robot) reset(g *Game) {
|
||||||
for {
|
for {
|
||||||
start_pos := v.Point2d{
|
start_pos := v.Point2d{
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
v "bitbucket.org/hackerbots/vector"
|
v "bitbucket.org/hackerbots/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Splosion embodies an explosion.
|
||||||
type Splosion struct {
|
type Splosion struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Position v.Point2d `json:"position"`
|
Position v.Point2d `json:"position"`
|
||||||
@ -11,10 +12,12 @@ type Splosion struct {
|
|||||||
Lifespan int `json:"-"`
|
Lifespan int `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tick decrements the lifespan of said Splosion.
|
||||||
func (s *Splosion) Tick() {
|
func (s *Splosion) Tick() {
|
||||||
s.Lifespan--
|
s.Lifespan--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alive determines if this Splosion is still relevant.
|
||||||
func (s *Splosion) Alive() bool {
|
func (s *Splosion) Alive() bool {
|
||||||
return s.Lifespan > 0
|
return s.Lifespan > 0
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user