colstrom-rtanque 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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