X-Git-Url: http://nitlanguage.org diff --git a/lib/bucketed_game.nit b/lib/bucketed_game.nit index 9b53417..2e8e3d3 100644 --- a/lib/bucketed_game.nit +++ b/lib/bucketed_game.nit @@ -14,45 +14,62 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Provides basic game logic utilities using buckets to coordinate and -# optimize actions on game turn ends. Supports both action at each -# end of turn as well as actions on some end of turns. +# Game framework with an emphasis on efficient event coordination # -# Allows for fast support of a large number of entities with rare actions, -# such as a forest with many individual trees. -module bucketed_game +# Provides basic game logic entities to manage a game where the logic is executed by turns: +# `Game`, `GameTurn`, `GameEvent`, `Turnable`. +# Also offers a bucket system to plan future events at a known number of turns in the future: +# `Bucketable` and the services `act_next` and `act_in`. +# +# Efficiently support large number of entities with rare or sparse actions, +# such as a forest with many individual trees growing slowly. +module bucketed_game is serialize + +import serialization +import counter # Something acting on the game -class Turnable[G: Game] +abstract class Turnable[G: Game] + + # Execute `turn` for this instance. fun do_turn(turn: GameTurn[G]) is abstract end # Something acting on the game from time to time -class Bucketable[G: Game] +abstract class Bucketable[G: Game] super Turnable[G] - private var act_at: Int = 0 + + private var act_at: nullable Int = null + + # Cancel the previously registered acting turn + # + # Once called, `self.do_turn` will not be invoked until `GameTurn::act_next` + # or `GameTurn::act_in` are called again. + fun cancel_act do act_at = null end -# Optiomized organization of `Bucketable` instances +# Optimized organization of `Bucketable` instances class Buckets[G: Game] super Turnable[G] - type BUCKET: HashSet[Bucketable[G]] - private var buckets: Array[BUCKET] + # Bucket type used in this implementation. + type BUCKET: HashSet[Bucketable[G]] private var next_bucket: nullable BUCKET = null private var current_bucket_key: Int = -1 - init - do - var n_buckets = 100 - buckets = new Array[BUCKET].with_capacity(n_buckets) + # Number of `buckets`, default at 100 + # + # Must be set prior to using any other methods of this class. + var n_buckets = 100 - for b in [0 .. n_buckets [do - buckets[b] = new HashSet[Bucketable[G]] - end - end + private var buckets: Array[BUCKET] = + [for b in n_buckets.times do new HashSet[Bucketable[G]]] is lazy + + # Stats on delays asked when adding an event with `act_in` and `act_next` + private var delays = new Counter[Int] + # Add the Bucketable event `e` at `at_tick`. fun add_at(e: Bucketable[G], at_tick: Int) do var at_key = key_for_tick(at_tick) @@ -76,24 +93,49 @@ class Buckets[G: Game] current_bucket_key = key_for_tick(turn.tick) var current_bucket = buckets[current_bucket_key] - next_bucket = new HashSet[Bucketable[G]] + var next_bucket = new HashSet[Bucketable[G]] + buckets[current_bucket_key] = next_bucket + self.next_bucket = next_bucket for e in current_bucket do - if e.act_at == turn.tick then - e.do_turn(turn) - else if e.act_at > turn.tick and - key_for_tick(e.act_at) == current_bucket_key - then - next_bucket.as(not null).add(e) + var act_at = e.act_at + if act_at != null then + if turn.tick == act_at then + e.do_turn(turn) + else if act_at > turn.tick and + key_for_tick(act_at) == current_bucket_key + then + next_bucket.add(e) + end end end + end + + # Get some statistics on both the current held events and historic expired events + fun stats: String + do + var entries = 0 + var instances = new HashSet[Bucketable[G]] + var max = 0 + var min = 100000 + for bucket in buckets do + var len = bucket.length + entries += len + instances.add_all bucket + min = min.min(len) + max = max.max(len) + end + var avg = entries.to_f / buckets.length.to_f - buckets[current_bucket_key] = next_bucket.as(not null) + return "{buckets.length} buckets; uniq/tot:{instances.length}/{entries}, avg:{avg.to_precision(1)}, min:{min}, max:{max}\n" + + "history:{delays.sum}, avg:{delays.avg}, min:{delays[delays.min.as(not null)]}, max:{delays[delays.max.as(not null)]}" end end # Game related event -class GameEvent +interface GameEvent + + # Apply `self` to `game` logic. fun apply( game : ThinGame ) is abstract end @@ -104,39 +146,51 @@ end # Game logic on the client class ThinGame - var tick: Int protected writable = 0 + + # Game tick when `self` should act. + # + # Default is 0. + var tick: Int = 0 is protected writable end # Game turn on the client class ThinGameTurn[G: ThinGame] - var tick: Int protected writable = 0 - var events: List[GameEvent] protected writable = new List[GameEvent] + # Game tick when `self` should act. + var tick: Int is protected writable - init (t: Int) do tick = t + # Game events occurred for `self`. + var events = new Array[GameEvent] is protected writable end # Game turn on the full logic class GameTurn[G: Game] super ThinGameTurn[G] + + # Game that `self` belongs to. var game: G - init (g: G) - do - super(g.tick) - game = g + # Create a new game turn for `game`. + init (game: G) is old_style_init do + super(game.tick) + self.game = game end + # Insert the Bucketable event `e` to be executed at next tick. fun act_next(e: Bucketable[G]) do game.buckets.add_at(e, tick + 1) + game.buckets.delays.inc(1) end + # Insert the Bucketable event `e` to be executed at tick `t`. fun act_in(e: Bucketable[G], t: Int) do game.buckets.add_at(e, tick + t) + game.buckets.delays.inc(t) end + # Add and `apply` a game `event`. fun add_event( event : GameEvent ) do event.apply( game ) @@ -147,12 +201,22 @@ end # Full game logic class Game super ThinGame + + # Game type used in this implementation. type G: Game + # Bucket list in this game. var buckets: Buckets[G] = new Buckets[G] - init do end + # Last turn executed in this game + # Can be used to consult the latest events (by the display for example), + # but cannot be used to add new Events. + var last_turn: nullable ThinGameTurn[G] = null + # Execute and return a new GameTurn. + # + # This method calls `do_pre_turn` before executing the GameTurn + # and `do_post_turn` after. fun do_turn: GameTurn[G] do var turn = new GameTurn[G](self) @@ -161,11 +225,22 @@ class Game buckets.do_turn(turn) do_post_turn(turn) + last_turn = turn + tick += 1 return turn end + # Execute something before executing the current GameTurn. + # + # Should be redefined by clients to add a pre-turn behavior. + # See `Game::do_turn`. fun do_pre_turn(turn: GameTurn[G]) do end + + # Execute something after executing the current GameTurn. + # + # Should be redefined by clients to add a post-turn behavior. + # See `Game::do_turn`. fun do_post_turn(turn: GameTurn[G]) do end end