diff --git a/config.go b/config.go index b176314..0d8b4ec 100644 --- a/config.go +++ b/config.go @@ -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, diff --git a/control.go b/control.go index 3b10c26..d7d60b4 100644 --- a/control.go +++ b/control.go @@ -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 diff --git a/deathmatch.go b/deathmatch.go index 12150b2..d323602 100644 --- a/deathmatch.go +++ b/deathmatch.go @@ -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 { } diff --git a/game.go b/game.go index 1b8e6e5..46aa458 100644 --- a/game.go +++ b/game.go @@ -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 { diff --git a/id.go b/id.go index 108af32..5b82e51 100644 --- a/id.go +++ b/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() { 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 diff --git a/melee.go b/melee.go index a524b1c..7d37be4 100644 --- a/melee.go +++ b/melee.go @@ -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 diff --git a/obstacle.go b/obstacle.go index 0b4410a..d6982ac 100644 --- a/obstacle.go +++ b/obstacle.go @@ -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++ { diff --git a/player.go b/player.go index 9f1bb31..71102f5 100644 --- a/player.go +++ b/player.go @@ -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() } diff --git a/projectile.go b/projectile.go index 5d94f46..6059c5d 100644 --- a/projectile.go +++ b/projectile.go @@ -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() diff --git a/protocol.go b/protocol.go index 7ba6803..6592dd6 100644 --- a/protocol.go +++ b/protocol.go @@ -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") diff --git a/robot.go b/robot.go index 8ad7c22..7359ddb 100644 --- a/robot.go +++ b/robot.go @@ -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{ diff --git a/splosion.go b/splosion.go index 7dd74d4..fbbfb46 100644 --- a/splosion.go +++ b/splosion.go @@ -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 }