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,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