First pass at documentations.

This commit is contained in:
Stephen McQuay 2014-04-14 00:26:41 -07:00
parent 0cf0957d73
commit faacfe7fd9
12 changed files with 172 additions and 49 deletions

View File

@ -11,6 +11,9 @@ import (
"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 {
Tick int `json:"tick"` // ms
Timescale float32 `json:"timescale"`
@ -23,8 +26,8 @@ type Config struct {
}
const (
TICK = 60
TIMESCALE = 1.0
TICK = 60 // ms, this is how often physics is updated
TIMESCALE = 1.0 // this tweaks the temporal duration of a TICK
WIDTH = 800
HEIGHT = 550
OBSTACLES = 5
@ -32,6 +35,10 @@ const (
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) {
c := Config{
Tick: TICK,

View File

@ -13,13 +13,18 @@ import (
"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)
// ServeHTTP is JsonHandler's http.Handler implementation.
func (h JsonHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
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 {
Idg *IdGenerator
Conf Config
@ -28,6 +33,9 @@ type Controller struct {
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 {
idg := NewIdGenerator()
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() {
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) {
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) {
log.Println("games list requested")
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) {
// TODO: wrap this up in something similar to the JsonHandler to verify the
// 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) {
// TODO: wrap this up in something similar to the JsonHandler to verify the
// 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) {
key, err := c.getGameId(req.URL.Path)
if err != nil {
@ -228,6 +247,8 @@ func (c *Controller) StopGame(w http.ResponseWriter, req *http.Request) {
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) {
if c.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")
}
// 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) {
log.Println("version requested")
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) {
var err error
trimmed := strings.Trim(path, "/")
@ -273,6 +299,7 @@ func (c *Controller) getGameId(path string) (string, error) {
}
// MapLock is simply a map and a RWMutex
// TODO: obviate the need for this in Controller.Run
type MapLock struct {
M map[string]*Game
sync.RWMutex

View File

@ -4,6 +4,8 @@ import (
"log"
)
// deathmatch is a game type that resets when there is one bot standing. There
// is an obvious winner each round.
type deathmatch struct {
}

31
game.go
View File

@ -13,16 +13,21 @@ import (
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
@ -33,25 +38,31 @@ type BotStats struct {
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
players map[*Player]bool
projectiles map[*Projectile]bool
splosions map[*Splosion]bool
obstacles []Obstacle
obstacle_count int
register chan *player
unregister chan *player
register chan *Player
unregister chan *Player
turn int
players_remaining int
width, height float32
@ -68,12 +79,14 @@ type Game struct {
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},
@ -85,13 +98,13 @@ func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, m
go bw.Run()
g := &Game{
id: id,
register: make(chan *player, maxPlayer),
unregister: make(chan *player, maxPlayer),
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),
players: make(map[*Player]bool),
turn: 0,
width: width,
height: height,
@ -119,6 +132,7 @@ func NewGame(id string, width, height float32, obstacles, tick, maxPoints int, m
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)
@ -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) {
// Ensure that the robots are always sent in a consistent order
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() {
ticker := time.NewTicker(time.Duration(g.tick_duration) * time.Millisecond)
for {
@ -323,6 +340,8 @@ func (g *Game) run() {
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 {

5
id.go
View File

@ -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() {
var i int64
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 {
h := md5.New()
ns := time.Now().UnixNano() + <-id.id

View File

@ -4,6 +4,8 @@ import (
"log"
)
// melee is a game type that allows dead players to respond after a particular
// number of ms.
type melee struct {
respawn map[*Robot]float64
respawn_timer float64

View File

@ -7,6 +7,7 @@ import (
v "bitbucket.org/hackerbots/vector"
)
// Obstacle is the implementation of the generic building type in the game.
type Obstacle struct {
Bounds v.AABB2d `json:"bounds"`
Hp int `json:"-"`
@ -28,6 +29,8 @@ func (o Obstacle) minify() [4]int {
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 {
out := [][4]int{}
for i := range o {
@ -36,6 +39,8 @@ func MinifyObstacles(o []Obstacle) [][4]int {
return out
}
// GenerateObstacles returns a slice of (count) obstacles within a region
// bounded by width, height.
func GenerateObstacles(count int, width, height float32) []Obstacle {
out := []Obstacle{}
for i := 0; i < count; i++ {

View File

@ -19,39 +19,46 @@ type decoder interface {
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
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)
sc.bw.AddRx <- n
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)
sc.bw.AddTx <- n
return n, err
}
func (sc *streamCounter) Close() error {
// Close is for cleanup
func (sc *StreamCount) Close() error {
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
dec decoder
counter *streamCounter
counter *StreamCount
send chan Message
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 dec decoder
comptroller := &streamCounter{
comptroller := &StreamCount{
ws: ws,
bw: bw,
}
@ -62,7 +69,7 @@ func NewProtoTalker(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, enco
enc = gob.NewEncoder(comptroller)
dec = gob.NewDecoder(comptroller)
}
return &protoTalker{
return &ProtoTalker{
send: make(chan Message, 16),
enc: enc,
dec: dec,
@ -71,8 +78,10 @@ func NewProtoTalker(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, enco
}
}
func (pt *protoTalker) sender() {
log.Printf("%s: %T sender launched", pt.Id, pt.enc)
// Sender is the single implementation for data output to clients, both players
// and spectators.
func (pt *ProtoTalker) Sender() {
log.Printf("%s: %T Sender launched", pt.Id, pt.enc)
for things := range pt.send {
err := pt.enc.Encode(things)
if err != nil {
@ -81,24 +90,28 @@ func (pt *protoTalker) sender() {
}
}
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
Instruction Instruction
protoTalker
ProtoTalker
}
func NewPlayer(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *player {
return &player{
func NewPlayer(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *Player {
return &Player{
Robots: []*Robot{},
protoTalker: *NewProtoTalker(id, ws, bw, encoding),
ProtoTalker: *NewProtoTalker(id, ws, bw, encoding),
}
}
func (p *player) recv() {
log.Println("starting recv")
// Player.Recv is the function responsible for parsing out player instructions
// and sending them to the game.
func (p *Player) Recv() {
log.Println("starting Recv")
for {
var msgs map[string]Instruction
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()
}
// Spectator merely sends out game state, does not receive meaningful
// instructions from spectators.
type Spectator struct {
protoTalker
ProtoTalker
}
func NewSpectator(id string, ws *websocket.Conn, bw *bandwidth.Bandwidth, encoding string) *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 {
var msgs interface{}
err := s.dec.Decode(&msgs)
@ -196,6 +214,6 @@ func (s *Spectator) recv() {
// break
// }
}
log.Printf("%s: recv close", s.Id)
log.Printf("%s: Recv close", s.Id)
s.counter.Close()
}

View File

@ -6,6 +6,7 @@ import (
v "bitbucket.org/hackerbots/vector"
)
// Projectile are the things robots can shoot at eachother.
type Projectile struct {
Id string `json:"id"`
Position v.Point2d `json:"position"`
@ -17,6 +18,9 @@ type Projectile struct {
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) {
vec := p.MoveTo.Sub(p.Position)
v_norm := vec.Normalize()

View File

@ -7,16 +7,15 @@ import (
"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 {
Id string `json:"id"`
}
// > identify
// PlayerID is the internal hash we give to a client
type PlayerID struct {
Type string `json:"type"`
Hash string `json:"id"`
Failure
}
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 string `json:"type"`
Name string `json:"name"`
Useragent string `json:"useragent"`
}
// ClientID.Valid is used to be sure the player connecting is of appropriate
// type.
func (c *ClientID) Valid() (bool, string) {
switch c.Type {
case "robot", "spectator":
@ -41,11 +42,14 @@ func (c *ClientID) Valid() (bool, string) {
return false, "useragent must be 'robot' or 'spectator'"
}
// ClientConfig embodies a map of stats requests
type ClientConfig struct {
ID string `json:"id"`
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 {
total := 0
for _, s := range config.Stats {
@ -65,11 +69,15 @@ func (config ClientConfig) Valid(max int) bool {
return true
}
// BoardSize is the response containing the geometry of the requested game.
type BoardSize struct {
Width float32 `json:"width"`
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 {
// TODO: should have information about max points in here
BoardSize BoardSize `json:"boardsize"`
@ -78,8 +86,8 @@ type GameParam struct {
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 {
ID string `json:"id"`
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 {
MyRobots []Robot `json:"my_robots"`
OtherRobots []OtherRobot `json:"robots"`
@ -120,6 +133,7 @@ func NewBoardstate() *Boardstate {
}
}
// Special outbound message with a []string of winners.
type GameOver struct {
Winners []string `json:"winners"`
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 {
Reason string `json:"reason"`
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) {
var gid GameID
err := websocket.JSON.Receive(ws, &gid)
@ -324,9 +342,9 @@ encodingLoops:
game.unregister <- p
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)
p.recv()
p.Recv()
log.Printf(
"%s (player): %v (robot) has been disconnected from %s (game)",
p.Id,
@ -343,9 +361,9 @@ encodingLoops:
game.sunregister <- s
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)
s.recv()
s.Recv()
log.Printf("game %s: spectator %+v has been disconnected from this game", gid.Id, s)
}
log.Printf("exiting AddPlayer")

View File

@ -8,6 +8,8 @@ import (
v "bitbucket.org/hackerbots/vector"
)
// Robot contains everything the game needs to know to simulate robot behavior.
// Players have a []Robot
type Robot struct {
Id string `json:"id"`
Name string `json:"name"`
@ -35,6 +37,7 @@ type Robot struct {
idg *IdGenerator
}
// Collision is basically a Point2d.
type Collision struct {
v.Point2d
Type string `json:"type"`
@ -50,6 +53,7 @@ type OtherRobot struct {
Health int `json:"health"`
}
// GetTruncatedDetails pares down our info into an OtherRobot.
func (r Robot) GetTruncatedDetails() OtherRobot {
return OtherRobot{
Id: r.Id,
@ -60,6 +64,7 @@ func (r Robot) GetTruncatedDetails() OtherRobot {
}
}
// RobotSorter implements sort.Interface for OtherRobot
type RobotSorter struct {
Robots []OtherRobot
}
@ -76,7 +81,7 @@ func (s RobotSorter) Less(i, j int) bool {
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 {
Robots []BotHealth
}
@ -93,6 +98,7 @@ func (s AllRobotSorter) Less(i, j int) bool {
return s.Robots[i].RobotId < s.Robots[j].RobotId
}
// Stats is the point allocation for a Robot.
type Stats struct {
Hp int `json:"hp"`
Speed float32 `json:"speed"`
@ -105,8 +111,9 @@ type Stats struct {
WeaponSpeed float32 `json:"weapon_speed"`
}
// We request stats using an integer between 1 and 100, the
// integer values map to sensible min-max ranges
// StatsRequest is the struct used in comunication with the player. We request
// stats using an integer between 1 and 100, the integer values map to sensible
// min-max ranges
type StatsRequest struct {
Hp int `json:"hp"`
Speed int `json:"speed"`
@ -119,6 +126,7 @@ type StatsRequest struct {
WeaponSpeed int `json:"weapon_speed"`
}
// DeriveStats maps the 0-100 values to sensible in-game min-max values.
func DeriveStats(request StatsRequest) Stats {
s := Stats{}
@ -162,6 +170,7 @@ func DeriveStats(request StatsRequest) Stats {
return s
}
// Instruction is the struct a player sends each turn.
type Instruction struct {
Message *string `json:"message,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
}
// Tick is the Robot's chance to udpate itself.
func (r *Robot) Tick(g *Game) {
r.Collision = nil
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) {
r.Scanners = r.Scanners[:0]
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 {
// Throttle the fire rate
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) {
for {
start_pos := v.Point2d{

View File

@ -4,6 +4,7 @@ import (
v "bitbucket.org/hackerbots/vector"
)
// Splosion embodies an explosion.
type Splosion struct {
Id string `json:"id"`
Position v.Point2d `json:"position"`
@ -11,10 +12,12 @@ type Splosion struct {
Lifespan int `json:"-"`
}
// Tick decrements the lifespan of said Splosion.
func (s *Splosion) Tick() {
s.Lifespan--
}
// Alive determines if this Splosion is still relevant.
func (s *Splosion) Alive() bool {
return s.Lifespan > 0
}