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,69 @@
1
+ module RTanque
2
+ module NormalizedAttr
3
+ MAX_DELTA = 1.0 / 0.0 # INFINITY
4
+ def attr_normalized(attr_name, range, max_delta = MAX_DELTA)
5
+ @_normalized_attrs ||= {}
6
+ @_normalized_attrs[attr_name] = AttrContainer.new(range, max_delta)
7
+ const_set("MAX_#{attr_name.to_s.upcase}", @_normalized_attrs[attr_name].max)
8
+ const_set("MIN_#{attr_name.to_s.upcase}", @_normalized_attrs[attr_name].min)
9
+ define_method("normalize_#{attr_name}") do |current_value, new_value|
10
+ return new_value unless new_value
11
+ self.class.normalized_attrs(attr_name).normalize(self, current_value, new_value)
12
+ end
13
+ end
14
+
15
+ def normalized_attrs(attr_name)
16
+ @_normalized_attrs.fetch(attr_name)
17
+ end
18
+
19
+ class AttrContainer
20
+ def initialize(range, max_delta = MAX_DELTA)
21
+ @range = range
22
+ @max_delta = max_delta
23
+ end
24
+
25
+ def min
26
+ @range.first
27
+ end
28
+
29
+ def max
30
+ @range.last
31
+ end
32
+
33
+ def normalize(attached_instance, current_value, new_value)
34
+ self.enforce_range(self.enforce_delta(attached_instance, current_value, new_value))
35
+ end
36
+
37
+ def max_delta(attached_instance)
38
+ @max_delta.respond_to?(:call) ? @max_delta.call(attached_instance) : @max_delta
39
+ end
40
+
41
+ def enforce_delta(attached_instance, current_value, new_value)
42
+ current_delta = self.delta(current_value, new_value)
43
+ current_max_delta = self.max_delta(attached_instance)
44
+ if current_delta.abs > current_max_delta
45
+ current_delta > 0 ? current_value + current_max_delta : current_value - current_max_delta
46
+ else
47
+ new_value
48
+ end
49
+ end
50
+
51
+ def delta(current_value, new_value)
52
+ if current_value
53
+ # Heading responds to delta
54
+ current_value.respond_to?(:delta) ? current_value.delta(new_value) : (new_value - current_value)
55
+ else
56
+ 0
57
+ end
58
+ end
59
+
60
+ def enforce_range(value)
61
+ if @range.include?(value)
62
+ value
63
+ else
64
+ value > self.max ? self.max : self.min
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,140 @@
1
+ module RTanque
2
+ # A `Point` represents an [x, y] coordinate pair in the {RTanque::Arena}
3
+ #
4
+ # ##Usage
5
+ # @arena = RTanque::Arena.new(100, 100)
6
+ # # => #<struct RTanque::Arena width=100, height=100>
7
+ #
8
+ # @point_one = RTanque::Point.new(0, 1, @arena)
9
+ # # => #<struct RTanque::Point x=0, y=1, arena=#<struct RTanque::Arena width=100, height=100>>
10
+ #
11
+ # @point_one.on_top_wall?
12
+ # # => false
13
+ #
14
+ # @point_one.on_bottom_wall?
15
+ # # => false
16
+ #
17
+ # @point_one.on_right_wall?
18
+ # # => false
19
+ #
20
+ # @point_one.on_left_wall?
21
+ # # => true
22
+ #
23
+ # @point_one.on_wall?
24
+ # # => true
25
+ #
26
+ # @point_two = RTanque::Point.new(100, 1, @arena)
27
+ # # => #<struct RTanque::Point x=100, y=1, arena=#<struct RTanque::Arena width=100, height=100>>
28
+ #
29
+ # @point_two.within_radius?(@point_one, 10)
30
+ # # => false
31
+ #
32
+ # @point_two.within_radius?(@point_one, 100)
33
+ # # => true
34
+ #
35
+ # @point_two.distance(@point_one)
36
+ # # => 100.0
37
+ #
38
+ # @attr_reader [Numeric] x horizontal position (left edge is 0)
39
+ # @attr_reader [Numeric] y vertical position (bottom edge is 0)
40
+ # @attr_reader [RTanque::Arena] arena
41
+ #
42
+ # @!method distance(other_point) distance to other point
43
+ # @param [RTanque::Point]
44
+ # @return [Float]
45
+ #
46
+ # @!method heading(other_point) heading to other point
47
+ # @param [RTanque::Point]
48
+ # @return [RTanque::Heading]
49
+ #
50
+ # @!method on_top_wall?
51
+ # @return [Boolean]
52
+ #
53
+ # @!method on_bottom_wall?
54
+ # @return [Boolean]
55
+ #
56
+ # @!method on_right_wall?
57
+ # @return [Boolean]
58
+ #
59
+ # @!method on_left_wall?
60
+ # @return [Boolean]
61
+ #
62
+ # @!method on_wall?
63
+ # True if on any wall
64
+ # @return [Boolean]
65
+ Point = Struct.new(:x, :y, :arena) do
66
+ def initialize(*args, &block)
67
+ super
68
+ block.call(self) if block
69
+ self.freeze
70
+ end
71
+
72
+ def self.rand(arena)
73
+ self.new(Kernel.rand(arena.width), Kernel.rand(arena.height), arena)
74
+ end
75
+
76
+ def self.distance(a, b)
77
+ Math.hypot(a.x - b.x, a.y - b.y)
78
+ end
79
+
80
+ def ==(other_point)
81
+ self.x == other_point.x && self.y == other_point.y
82
+ end
83
+
84
+ def within_radius?(other_point, radius)
85
+ self.distance(other_point) <= radius
86
+ end
87
+
88
+ def on_top_wall?
89
+ self.y >= self.arena.height
90
+ end
91
+
92
+ def on_bottom_wall?
93
+ self.y <= 0
94
+ end
95
+
96
+ def on_left_wall?
97
+ self.x <= 0
98
+ end
99
+
100
+ def on_right_wall?
101
+ self.x >= self.arena.width
102
+ end
103
+
104
+ def on_wall?
105
+ self.on_top_wall? || self.on_bottom_wall? || self.on_right_wall? || self.on_left_wall?
106
+ end
107
+
108
+ def outside_arena?
109
+ self.y > self.arena.height || self.y < 0 || self.x > self.arena.width || self.x < 0
110
+ end
111
+
112
+ def move(heading, speed, bound_to_arena = true)
113
+ # round to avoid floating point errors
114
+ x = (self.x + (Math.sin(heading) * speed)).round(10)
115
+ y = (self.y + (Math.cos(heading) * speed)).round(10)
116
+ self.class.new(x, y, self.arena) { |point| point.bind_to_arena if bound_to_arena }
117
+ end
118
+
119
+ def bind_to_arena
120
+ if self.x < 0
121
+ self.x = 0.0
122
+ elsif self.x > self.arena.width
123
+ self.x = self.arena.width.to_f
124
+ end
125
+ if self.y < 0
126
+ self.y = 0.0
127
+ elsif self.y > self.arena.height
128
+ self.y = self.arena.height.to_f
129
+ end
130
+ end
131
+
132
+ def heading(other_point)
133
+ Heading.new_between_points(self, other_point)
134
+ end
135
+
136
+ def distance(other_point)
137
+ self.class.distance(self, other_point)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,88 @@
1
+ # Add the working directory so that loading of bots works as expected
2
+ $LOAD_PATH << Dir.pwd
3
+ # Add the gem root dir so that sample_bots can be loaded
4
+ $LOAD_PATH << File.expand_path('../../../', __FILE__)
5
+
6
+ module RTanque
7
+ # Runner manages running an {RTanque::Match}
8
+ class Runner
9
+ LoadError = Class.new(::LoadError)
10
+ attr_reader :match
11
+
12
+ # @param [Integer] width
13
+ # @param [Integer] height
14
+ # @param [*match_args] args provided to {RTanque::Match#initialize}
15
+ def initialize(width, height, *match_args)
16
+ @match = RTanque::Match.new(RTanque::Arena.new(width, height), *match_args)
17
+ end
18
+
19
+ # Attempts to load given {RTanque::Bot::Brain} given its path
20
+ # @param [String] brain_path
21
+ # @raise [RTanque::Runner::LoadError] if brain could not be loaded
22
+ def add_brain_path(brain_path)
23
+ parsed_path = self.parse_brain_path(brain_path)
24
+ bots = parsed_path.multiplier.times.map { self.new_bots_from_brain_path(parsed_path.path) }.flatten
25
+ self.match.add_bots(bots)
26
+ end
27
+
28
+ # Starts the match
29
+ # @param [Boolean] gui if false, runs headless match
30
+ def start(gui = true)
31
+ if gui
32
+ require 'rtanque/gui'
33
+ window = RTanque::Gui::Window.new(self.match)
34
+ trap(:INT) { window.close }
35
+ window.show
36
+ else
37
+ trap(:INT) { self.match.stop }
38
+ self.match.start
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def new_bots_from_brain_path(brain_path)
45
+ self.fetch_brain_klasses(brain_path).map do |brain_klass|
46
+ RTanque::Bot.new_random_location(self.match.arena, brain_klass)
47
+ end
48
+ end
49
+
50
+ def fetch_brain_klasses(brain_path)
51
+ @load_brain_klass_cache ||= Hash.new do |cache, path|
52
+ cache[path] = self.get_diff_in_object_space(RTanque::Bot::Brain) {
53
+ begin
54
+ require(path)
55
+ rescue ::LoadError => e
56
+ raise LoadError, e.message
57
+ end
58
+ }
59
+ raise LoadError, "No class of type #{RTanque::Bot::Brain} found in #{path}" if cache[path].empty?
60
+ cache[path]
61
+ end
62
+ @load_brain_klass_cache[brain_path]
63
+ end
64
+
65
+ def get_diff_in_object_space(klass)
66
+ current_object_space = self.get_descendants_of_class(klass)
67
+ yield
68
+ self.get_descendants_of_class(klass) - current_object_space
69
+ end
70
+
71
+ def get_descendants_of_class(klass)
72
+ ::ObjectSpace.each_object(::Class).select {|k| k < klass }
73
+ end
74
+
75
+ BRAIN_PATH_PARSER = /\A(.+?)\:[x|X](\d+)\z/
76
+ # @!visibility private
77
+ ParsedBrainPath = Struct.new(:path, :multiplier)
78
+ def parse_brain_path(brain_path)
79
+ path = brain_path.gsub('\.rb$', '')
80
+ multiplier = 1
81
+ brain_path.match(BRAIN_PATH_PARSER) { |m|
82
+ path = m[1]
83
+ multiplier = m[2].to_i
84
+ }
85
+ ParsedBrainPath.new(path, multiplier)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,44 @@
1
+ module RTanque
2
+ class Shell
3
+ include Movable
4
+ RATIO = Configuration.shell.ratio
5
+ SHELL_SPEED_FACTOR = Configuration.shell.speed_factor
6
+ attr_reader :bot, :arena, :fire_power
7
+
8
+ def self.speed fire_power
9
+ fire_power * SHELL_SPEED_FACTOR
10
+ end
11
+
12
+ def initialize(bot, position, heading, fire_power)
13
+ @bot = bot
14
+ @arena = bot.arena
15
+ @fire_power = fire_power
16
+ self.position = position
17
+ self.heading = heading
18
+ self.speed = self.class.speed(fire_power) # TODO: add bot's relative speed in this heading
19
+ @dead = false
20
+ end
21
+
22
+ def bound_to_arena
23
+ false
24
+ end
25
+
26
+ def dead?
27
+ @dead ||= self.position.outside_arena?
28
+ end
29
+
30
+ def dead!
31
+ @dead = true
32
+ end
33
+
34
+ def hits(bots, &on_hit)
35
+ bots.each do |hit_bot|
36
+ if hit_bot.position.within_radius?(self.position, Bot::RADIUS)
37
+ self.dead!
38
+ on_hit.call(self.bot, hit_bot) if on_hit
39
+ break
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module RTanque
2
+ VERSION = '0.1.3'
3
+ end
Binary file