colstrom-rtanque 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/.yardopts +4 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.ci +6 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +168 -0
  12. data/Rakefile +1 -0
  13. data/bin/rtanque +117 -0
  14. data/lib/rtanque.rb +31 -0
  15. data/lib/rtanque/arena.rb +8 -0
  16. data/lib/rtanque/bot.rb +117 -0
  17. data/lib/rtanque/bot/brain.rb +50 -0
  18. data/lib/rtanque/bot/brain_helper.rb +23 -0
  19. data/lib/rtanque/bot/command.rb +23 -0
  20. data/lib/rtanque/bot/radar.rb +54 -0
  21. data/lib/rtanque/bot/sensors.rb +37 -0
  22. data/lib/rtanque/bot/turret.rb +14 -0
  23. data/lib/rtanque/configuration.rb +47 -0
  24. data/lib/rtanque/explosion.rb +23 -0
  25. data/lib/rtanque/gui.rb +25 -0
  26. data/lib/rtanque/gui/bot.rb +71 -0
  27. data/lib/rtanque/gui/bot/health_color_calculator.rb +37 -0
  28. data/lib/rtanque/gui/draw_group.rb +30 -0
  29. data/lib/rtanque/gui/explosion.rb +25 -0
  30. data/lib/rtanque/gui/shell.rb +31 -0
  31. data/lib/rtanque/gui/window.rb +64 -0
  32. data/lib/rtanque/heading.rb +162 -0
  33. data/lib/rtanque/match.rb +73 -0
  34. data/lib/rtanque/match/tick_group.rb +50 -0
  35. data/lib/rtanque/movable.rb +51 -0
  36. data/lib/rtanque/normalized_attr.rb +69 -0
  37. data/lib/rtanque/point.rb +140 -0
  38. data/lib/rtanque/runner.rb +88 -0
  39. data/lib/rtanque/shell.rb +44 -0
  40. data/lib/rtanque/version.rb +3 -0
  41. data/resources/images/body.png +0 -0
  42. data/resources/images/bullet.png +0 -0
  43. data/resources/images/explosions/explosion2-1.png +0 -0
  44. data/resources/images/explosions/explosion2-10.png +0 -0
  45. data/resources/images/explosions/explosion2-11.png +0 -0
  46. data/resources/images/explosions/explosion2-12.png +0 -0
  47. data/resources/images/explosions/explosion2-13.png +0 -0
  48. data/resources/images/explosions/explosion2-14.png +0 -0
  49. data/resources/images/explosions/explosion2-15.png +0 -0
  50. data/resources/images/explosions/explosion2-16.png +0 -0
  51. data/resources/images/explosions/explosion2-17.png +0 -0
  52. data/resources/images/explosions/explosion2-18.png +0 -0
  53. data/resources/images/explosions/explosion2-19.png +0 -0
  54. data/resources/images/explosions/explosion2-2.png +0 -0
  55. data/resources/images/explosions/explosion2-20.png +0 -0
  56. data/resources/images/explosions/explosion2-21.png +0 -0
  57. data/resources/images/explosions/explosion2-22.png +0 -0
  58. data/resources/images/explosions/explosion2-23.png +0 -0
  59. data/resources/images/explosions/explosion2-24.png +0 -0
  60. data/resources/images/explosions/explosion2-25.png +0 -0
  61. data/resources/images/explosions/explosion2-26.png +0 -0
  62. data/resources/images/explosions/explosion2-27.png +0 -0
  63. data/resources/images/explosions/explosion2-28.png +0 -0
  64. data/resources/images/explosions/explosion2-29.png +0 -0
  65. data/resources/images/explosions/explosion2-3.png +0 -0
  66. data/resources/images/explosions/explosion2-30.png +0 -0
  67. data/resources/images/explosions/explosion2-31.png +0 -0
  68. data/resources/images/explosions/explosion2-32.png +0 -0
  69. data/resources/images/explosions/explosion2-33.png +0 -0
  70. data/resources/images/explosions/explosion2-34.png +0 -0
  71. data/resources/images/explosions/explosion2-35.png +0 -0
  72. data/resources/images/explosions/explosion2-36.png +0 -0
  73. data/resources/images/explosions/explosion2-37.png +0 -0
  74. data/resources/images/explosions/explosion2-38.png +0 -0
  75. data/resources/images/explosions/explosion2-39.png +0 -0
  76. data/resources/images/explosions/explosion2-4.png +0 -0
  77. data/resources/images/explosions/explosion2-40.png +0 -0
  78. data/resources/images/explosions/explosion2-41.png +0 -0
  79. data/resources/images/explosions/explosion2-42.png +0 -0
  80. data/resources/images/explosions/explosion2-43.png +0 -0
  81. data/resources/images/explosions/explosion2-44.png +0 -0
  82. data/resources/images/explosions/explosion2-45.png +0 -0
  83. data/resources/images/explosions/explosion2-46.png +0 -0
  84. data/resources/images/explosions/explosion2-47.png +0 -0
  85. data/resources/images/explosions/explosion2-48.png +0 -0
  86. data/resources/images/explosions/explosion2-49.png +0 -0
  87. data/resources/images/explosions/explosion2-5.png +0 -0
  88. data/resources/images/explosions/explosion2-50.png +0 -0
  89. data/resources/images/explosions/explosion2-51.png +0 -0
  90. data/resources/images/explosions/explosion2-52.png +0 -0
  91. data/resources/images/explosions/explosion2-53.png +0 -0
  92. data/resources/images/explosions/explosion2-54.png +0 -0
  93. data/resources/images/explosions/explosion2-55.png +0 -0
  94. data/resources/images/explosions/explosion2-56.png +0 -0
  95. data/resources/images/explosions/explosion2-57.png +0 -0
  96. data/resources/images/explosions/explosion2-58.png +0 -0
  97. data/resources/images/explosions/explosion2-59.png +0 -0
  98. data/resources/images/explosions/explosion2-6.png +0 -0
  99. data/resources/images/explosions/explosion2-60.png +0 -0
  100. data/resources/images/explosions/explosion2-61.png +0 -0
  101. data/resources/images/explosions/explosion2-62.png +0 -0
  102. data/resources/images/explosions/explosion2-63.png +0 -0
  103. data/resources/images/explosions/explosion2-64.png +0 -0
  104. data/resources/images/explosions/explosion2-65.png +0 -0
  105. data/resources/images/explosions/explosion2-66.png +0 -0
  106. data/resources/images/explosions/explosion2-67.png +0 -0
  107. data/resources/images/explosions/explosion2-68.png +0 -0
  108. data/resources/images/explosions/explosion2-69.png +0 -0
  109. data/resources/images/explosions/explosion2-7.png +0 -0
  110. data/resources/images/explosions/explosion2-70.png +0 -0
  111. data/resources/images/explosions/explosion2-71.png +0 -0
  112. data/resources/images/explosions/explosion2-8.png +0 -0
  113. data/resources/images/explosions/explosion2-9.png +0 -0
  114. data/resources/images/grass.png +0 -0
  115. data/resources/images/radar.png +0 -0
  116. data/resources/images/turret.png +0 -0
  117. data/rtanque.gemspec +34 -0
  118. data/sample_bots/camper.rb +79 -0
  119. data/sample_bots/keyboard.rb +50 -0
  120. data/sample_bots/seek_and_destroy.rb +53 -0
  121. data/screenshots/battle_1.png +0 -0
  122. data/screenshots/battle_2.png +0 -0
  123. data/spec/rtanque/bot_spec.rb +239 -0
  124. data/spec/rtanque/heading_spec.rb +279 -0
  125. data/spec/rtanque/match_spec.rb +46 -0
  126. data/spec/rtanque/normalized_attr_spec.rb +90 -0
  127. data/spec/rtanque/point_spec.rb +196 -0
  128. data/spec/rtanque/radar_spec.rb +87 -0
  129. data/spec/rtanque/shell_spec.rb +35 -0
  130. data/spec/rtanque_spec.rb +34 -0
  131. data/spec/spec_helper.rb +51 -0
  132. data/templates/bot.erb +17 -0
  133. metadata +315 -0
@@ -0,0 +1,50 @@
1
+ module RTanque
2
+ class Bot
3
+ # Commands the associated {RTanque::Bot}. This class should be inherited from and {NAME} and {#tick!} overridden.
4
+ #
5
+ # See {RTanque::Bot::BrainHelper} for a useful mixin
6
+ #
7
+ # Sample bots:
8
+ #
9
+ # * {file:sample_bots/seek_and_destroy.rb SeekAndDestroy}
10
+ # * {file:sample_bots/camper.rb Camper}
11
+ # * {file:sample_bots/keyboard.rb Keyboard} Special bot controlled by the keyboard
12
+ #
13
+ class Brain
14
+ # Bot's display name
15
+ # @!parse NAME = 'bot name'
16
+
17
+ # @!attribute [r] sensors
18
+ # @return [RTanque::Bot::Sensors]
19
+ # @!attribute [r] command
20
+ # @return [RTanque::Bot::Command]
21
+ attr_accessor :sensors, :command
22
+ # @return [RTanque::Arena]
23
+ attr_reader :arena
24
+
25
+ # @!visibility private
26
+ def initialize(arena)
27
+ @arena = arena
28
+ end
29
+
30
+ # @!visibility private
31
+ def tick(sensors)
32
+ self.sensors = sensors
33
+ RTanque::Bot::Command.new.tap do |empty_command|
34
+ self.command = empty_command
35
+ self.tick!
36
+ end
37
+ end
38
+
39
+ # Main logic goes here
40
+ #
41
+ # Get input from {#sensors}. See {RTanque::Bot::Sensors}
42
+ #
43
+ # Give output to {#command}. See {RTanque::Bot::Command}
44
+ # @abstract
45
+ def tick!
46
+ # Sweet bot logic
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ module RTanque
2
+ class Bot
3
+ # Some helpful constants and methods for use as mixin in {RTanque::Bot::Brain}
4
+ module BrainHelper
5
+ BOT_RADIUS = Bot::RADIUS
6
+ MAX_FIRE_POWER = Bot::MAX_FIRE_POWER
7
+ MIN_FIRE_POWER = Bot::MIN_FIRE_POWER
8
+ MAX_HEALTH = Bot::MAX_HEALTH
9
+ MAX_BOT_SPEED = Bot::MAX_SPEED
10
+ MAX_BOT_ROTATION = Configuration.bot.turn_step
11
+ MAX_TURRET_ROTATION = Configuration.turret.turn_step
12
+ MAX_RADAR_ROTATION = Configuration.radar.turn_step
13
+
14
+ # Run block every 'num_of_ticks'
15
+ # @param [Integer] num_of_ticks tick interval at which to execute block
16
+ # @yield
17
+ # @return [void]
18
+ def at_tick_interval(num_of_ticks)
19
+ yield if sensors.ticks % num_of_ticks == 0
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module RTanque
2
+ class Bot
3
+ # Command provide output from the {RTanque::Bot::Brain} about the current state of the {RTanque::Match}
4
+ #
5
+ # They are made available to {RTanque::Bot::Brain} via {RTanque::Bot::Brain#command}
6
+ #
7
+ # All values are bound. Setting an out-of-bounds value will result in it being set to the max/min allowed value.
8
+ #
9
+ # @attr_writer [Float] speed
10
+ # @attr_writer [Float, RTanque::Heading] heading
11
+ # @attr_writer [Float, RTanque::Heading] radar_heading
12
+ # @attr_writer [Float, RTanque::Heading] turret_heading
13
+ # @attr_writer [Float, nil] fire_power sets firing power. Setting to nil will stop firing. See {#fire}
14
+ #
15
+ # @param [Float] power alias to {#fire_power=}
16
+ # @!method fire(power)
17
+ Command = Struct.new(:speed, :heading, :radar_heading, :turret_heading, :fire_power) do
18
+ def fire(power = 3)
19
+ self.fire_power = power
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ module RTanque
2
+ class Bot
3
+ class Radar
4
+ include Enumerable
5
+ include Movable
6
+ extend NormalizedAttr
7
+ VISION_RANGE = Configuration.radar.vision
8
+ attr_normalized(:heading, Heading::FULL_RANGE, Configuration.radar.turn_step)
9
+
10
+ # A Reflection is the information obtained for a bot detected by {RTanque::Bot::Radar}
11
+ #
12
+ # @attr_reader [RTanque::Heading] heading
13
+ # @attr_reader [Float] distance
14
+ # @attr_reader [String] name
15
+ Reflection = Struct.new(:heading, :distance, :name) do
16
+ def self.new_from_points(from_position, to_position, &tap)
17
+ self.new(from_position.heading(to_position), from_position.distance(to_position)).tap(&tap)
18
+ end
19
+ end
20
+
21
+ def initialize(bot, heading)
22
+ @bot = bot
23
+ @heading = heading
24
+ @reflections = []
25
+ end
26
+
27
+ def position
28
+ @bot.position
29
+ end
30
+
31
+ def each(&block)
32
+ @reflections.each(&block)
33
+ end
34
+
35
+ def empty?
36
+ self.count == 0
37
+ end
38
+
39
+ def scan(bots)
40
+ @reflections.clear
41
+ bots.each do |other_bot|
42
+ if self.can_detect?(other_bot)
43
+ @reflections << Reflection.new_from_points(self.position, other_bot.position) { |reflection| reflection.name = other_bot.name }
44
+ end
45
+ end
46
+ self
47
+ end
48
+
49
+ def can_detect?(other_bot)
50
+ VISION_RANGE.include?(Heading.delta_between_points(self.position, self.heading, other_bot.position))
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ module RTanque
2
+ class Bot
3
+ # Sensors provide input to the {RTanque::Bot::Brain} about the current state of the {RTanque::Match}
4
+ #
5
+ # They are made available to {RTanque::Bot::Brain} via {RTanque::Bot::Brain#sensors}
6
+ #
7
+ # @attr_reader [Integer] ticks number of ticks, starts at 0
8
+ # @attr_reader [Float] health health of bot. if == 0, dead
9
+ # @attr_reader [Float] gun_energy energy of cannon. if < 0, cannot fire
10
+ # @attr_reader [Float] speed
11
+ # @attr_reader [RTanque::Point] position
12
+ # @attr_reader [RTanque::Heading] heading
13
+ # @attr_reader [RTanque::Heading] radar_heading
14
+ # @attr_reader [RTanque::Heading] turret_heading
15
+ # @attr_reader [Enumerator] radar enumerates all bots scanned by the radar, yielding {RTanque::Bot::Radar::Reflection}
16
+ Sensors = Struct.new(:ticks, :health, :speed, :position, :heading, :radar, :radar_heading, :turret_heading, :gun_energy) do
17
+ def initialize(*args, &block)
18
+ super(*args)
19
+ block.call(self) if block
20
+ self.freeze
21
+ end
22
+
23
+ def button_down?(button_id)
24
+ if self.class.const_defined?(:Gosu)
25
+ button_id = Gosu::Window.char_to_button_id(button_id) unless button_id.kind_of?(Integer)
26
+ @gui_window && @gui_window.button_down?(button_id)
27
+ else
28
+ false
29
+ end
30
+ end
31
+
32
+ def gui_window=(gui_window)
33
+ @gui_window = gui_window
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module RTanque
2
+ class Bot
3
+ class Turret
4
+ include Movable
5
+ extend NormalizedAttr
6
+ LENGTH = Configuration.turret.length
7
+ attr_normalized(:heading, Heading::FULL_RANGE, Configuration.turret.turn_step)
8
+
9
+ def initialize(heading)
10
+ @heading = heading
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ require 'configuration'
2
+
3
+ module RTanque
4
+ one_degree = (Math::PI / 180.0)
5
+
6
+ # @!visibility private
7
+ Configuration = ::Configuration.for('default') do
8
+ raise_brain_tick_errors true
9
+ quit_when_finished true
10
+
11
+ bot do
12
+ radius 19
13
+ health_reduction_on_exception 2
14
+ health 0..100
15
+ speed -3..3
16
+ speed_step 0.05
17
+ turn_step one_degree * 1.5
18
+ fire_power 1..5
19
+ gun_energy_max 10
20
+ gun_energy_factor 10
21
+ end
22
+ turret do
23
+ length 28
24
+ turn_step (one_degree * 2.0)
25
+ end
26
+ radar do
27
+ turn_step 0.05
28
+ vision -(one_degree * 10.0)..(one_degree * 10.0)
29
+ end
30
+ shell do
31
+ speed_factor 4.5
32
+ ratio 1.5 # used by Bot#adjust_fire_power and to calculate damage done by shell to bot
33
+ end
34
+ explosion do
35
+ life_span 70 * 1 # should be multiple of the number of frames in the explosion animation
36
+ end
37
+ gui do
38
+ update_interval 16.666666 # in milliseconds. 16.666666 == 60 FPS
39
+ fonts do
40
+ small 16
41
+ end
42
+ end
43
+ end
44
+ def Configuration.config(&block)
45
+ ::Configuration::DSL.evaluate(self, &block)
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module RTanque
2
+ class Explosion
3
+ include Movable
4
+ attr_reader :position
5
+ LIFE_SPAN = Configuration.explosion.life_span # ticks
6
+ def initialize(position)
7
+ @position = position
8
+ @ticks = 0
9
+ end
10
+
11
+ def percent_dead
12
+ @ticks / LIFE_SPAN.to_f
13
+ end
14
+
15
+ def tick
16
+ @ticks += 1
17
+ end
18
+
19
+ def dead?
20
+ @ticks > LIFE_SPAN
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module RTanque
2
+ module Gui
3
+ RESOURCE_DIR = File.expand_path('../../../resources', __FILE__)
4
+ def self.resource_path(*components)
5
+ File.join(RESOURCE_DIR, *components)
6
+ end
7
+
8
+ module ZOrder
9
+ BACKGROUND = -1
10
+ BOT_HEALTH = 5
11
+ BOT_NAME = 6
12
+ BOT_BODY = 7
13
+ BOT_TURRET = 8
14
+ BOT_RADAR = 9
15
+ SHELL = 10
16
+ EXPLOSION = 20
17
+ end
18
+ end
19
+ end
20
+
21
+ require 'rtanque/gui/draw_group'
22
+ require 'rtanque/gui/window'
23
+ require 'rtanque/gui/bot'
24
+ require 'rtanque/gui/shell'
25
+ require 'rtanque/gui/explosion'
@@ -0,0 +1,71 @@
1
+ require 'gosu'
2
+ require 'texplay'
3
+
4
+ require 'rtanque/gui/bot/health_color_calculator'
5
+
6
+ module RTanque
7
+ module Gui
8
+ class Bot
9
+ attr_reader :bot
10
+
11
+ HEALTH_BAR_HEIGHT = 3
12
+ HEALTH_BAR_WIDTH = 100
13
+
14
+ def initialize(window, bot)
15
+ @window = window
16
+ @bot = bot
17
+ @body_image = Gosu::Image.new(@window, Gui.resource_path("images/body.png"))
18
+ @turret_image = Gosu::Image.new(@window, Gui.resource_path("images/turret.png"))
19
+ @radar_image = Gosu::Image.new(@window, Gui.resource_path("images/radar.png"))
20
+ @score_bar_image = TexPlay.create_blank_image(@window, HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT)
21
+ @name_font = Gosu::Font.new(@window, Window::FONT_NAME, Window::SMALL_FONT_SIZE)
22
+ @x_factor = 1
23
+ @y_factor = 1
24
+ end
25
+
26
+ def draw
27
+ position = [@bot.position.x, @window.height - @bot.position.y]
28
+ draw_bot(position)
29
+ draw_name(position)
30
+ draw_health(position)
31
+ end
32
+
33
+ def grow(factor = 2, step = 0.002)
34
+ @x_factor += step unless @x_factor > factor
35
+ @y_factor += step unless @y_factor > factor
36
+ end
37
+
38
+ def draw_bot(position)
39
+ @body_image.draw_rot(position[0], position[1], ZOrder::BOT_BODY, Gosu.radians_to_degrees(@bot.heading.to_f), 0.5, 0.5, @x_factor, @y_factor)
40
+ @turret_image.draw_rot(position[0], position[1], ZOrder::BOT_TURRET, Gosu.radians_to_degrees(@bot.turret.heading.to_f), 0.5, 0.5, @x_factor, @y_factor)
41
+ @radar_image.draw_rot(position[0], position[1], ZOrder::BOT_RADAR, Gosu.radians_to_degrees(@bot.radar.heading.to_f), 0.5, 0.5, @x_factor, @y_factor)
42
+ end
43
+
44
+ def draw_name(position)
45
+ x,y = *position
46
+ @name_font.draw_rel(self.bot.name, x, y + (RTanque::Bot::RADIUS * @y_factor) + Window::SMALL_FONT_SIZE.to_i, ZOrder::BOT_NAME, 0.5, 0.5, @x_factor, @y_factor)
47
+ end
48
+
49
+ def draw_health(position)
50
+ x,y = *position
51
+ x_health = health.round(0)
52
+ health_color = color_for_health
53
+ @score_bar_image.paint {
54
+ rect 0, 0, HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, :color => [0,0,0,0], :fill => true
55
+ rect 0, 0, x_health, HEALTH_BAR_HEIGHT, :color => health_color, :fill => true
56
+ }
57
+ @score_bar_image.draw(x - (HEALTH_BAR_WIDTH/2) * @x_factor, y + (5 + RTanque::Bot::RADIUS) * @y_factor, ZOrder::BOT_HEALTH, @x_factor, @y_factor)
58
+ end
59
+
60
+ private
61
+
62
+ def color_for_health
63
+ HealthColorCalculator.new(health).color_as_rgb
64
+ end
65
+
66
+ def health
67
+ self.bot.health
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,37 @@
1
+ module RTanque
2
+ module Gui
3
+ class Bot
4
+ class HealthColorCalculator
5
+
6
+ # different health-clors as RGB values
7
+ FULL_HEALTH_COLOR = [ 74, 190, 74].map { |v| v/255.0 }
8
+ MEDIUM_HEALTH_COLOR = [255, 190, 0].map { |v| v/255.0 }
9
+ LOW_HEALTH_COLOR = [220, 0, 0].map { |v| v/255.0 }
10
+
11
+ attr_reader :health
12
+
13
+ def initialize(health)
14
+ @health = health
15
+ end
16
+
17
+ def color_as_rgb
18
+ if health > 50
19
+ percentage = ((100 - health) / 50)
20
+ color_between FULL_HEALTH_COLOR, MEDIUM_HEALTH_COLOR, percentage
21
+ else
22
+ percentage = ((50 - health) / 50)
23
+ color_between MEDIUM_HEALTH_COLOR, LOW_HEALTH_COLOR, percentage
24
+ end
25
+ end
26
+
27
+ def color_between(color_a, color_b, percentage)
28
+ [
29
+ (color_b[0] - color_a[0]) * percentage + color_a[0],
30
+ (color_b[1] - color_a[1]) * percentage + color_a[1],
31
+ (color_b[2] - color_a[2]) * percentage + color_a[2]
32
+ ]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ module RTanque
2
+ module Gui
3
+ class DrawGroup
4
+ include Enumerable
5
+
6
+ def initialize(tick_group, &create_drawable)
7
+ @tick_group = tick_group
8
+ @mapped_drawables = Hash.new do |h, tickable|
9
+ h[tickable] = create_drawable.call(tickable)
10
+ end
11
+ end
12
+
13
+ def each(&block)
14
+ @tick_group.each do |tickable|
15
+ if tickable.dead?
16
+ @mapped_drawables.delete(tickable)
17
+ else
18
+ block.call(@mapped_drawables[tickable]) # This invokes @mapped_drawables's block if tickable not already in the hash
19
+ end
20
+ end
21
+ end
22
+
23
+ def draw
24
+ self.each do |drawable|
25
+ drawable.draw
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end