colstrom-rtanque 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +4 -0
- data/Gemfile +4 -0
- data/Gemfile.ci +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +168 -0
- data/Rakefile +1 -0
- data/bin/rtanque +117 -0
- data/lib/rtanque.rb +31 -0
- data/lib/rtanque/arena.rb +8 -0
- data/lib/rtanque/bot.rb +117 -0
- data/lib/rtanque/bot/brain.rb +50 -0
- data/lib/rtanque/bot/brain_helper.rb +23 -0
- data/lib/rtanque/bot/command.rb +23 -0
- data/lib/rtanque/bot/radar.rb +54 -0
- data/lib/rtanque/bot/sensors.rb +37 -0
- data/lib/rtanque/bot/turret.rb +14 -0
- data/lib/rtanque/configuration.rb +47 -0
- data/lib/rtanque/explosion.rb +23 -0
- data/lib/rtanque/gui.rb +25 -0
- data/lib/rtanque/gui/bot.rb +71 -0
- data/lib/rtanque/gui/bot/health_color_calculator.rb +37 -0
- data/lib/rtanque/gui/draw_group.rb +30 -0
- data/lib/rtanque/gui/explosion.rb +25 -0
- data/lib/rtanque/gui/shell.rb +31 -0
- data/lib/rtanque/gui/window.rb +64 -0
- data/lib/rtanque/heading.rb +162 -0
- data/lib/rtanque/match.rb +73 -0
- data/lib/rtanque/match/tick_group.rb +50 -0
- data/lib/rtanque/movable.rb +51 -0
- data/lib/rtanque/normalized_attr.rb +69 -0
- data/lib/rtanque/point.rb +140 -0
- data/lib/rtanque/runner.rb +88 -0
- data/lib/rtanque/shell.rb +44 -0
- data/lib/rtanque/version.rb +3 -0
- data/resources/images/body.png +0 -0
- data/resources/images/bullet.png +0 -0
- data/resources/images/explosions/explosion2-1.png +0 -0
- data/resources/images/explosions/explosion2-10.png +0 -0
- data/resources/images/explosions/explosion2-11.png +0 -0
- data/resources/images/explosions/explosion2-12.png +0 -0
- data/resources/images/explosions/explosion2-13.png +0 -0
- data/resources/images/explosions/explosion2-14.png +0 -0
- data/resources/images/explosions/explosion2-15.png +0 -0
- data/resources/images/explosions/explosion2-16.png +0 -0
- data/resources/images/explosions/explosion2-17.png +0 -0
- data/resources/images/explosions/explosion2-18.png +0 -0
- data/resources/images/explosions/explosion2-19.png +0 -0
- data/resources/images/explosions/explosion2-2.png +0 -0
- data/resources/images/explosions/explosion2-20.png +0 -0
- data/resources/images/explosions/explosion2-21.png +0 -0
- data/resources/images/explosions/explosion2-22.png +0 -0
- data/resources/images/explosions/explosion2-23.png +0 -0
- data/resources/images/explosions/explosion2-24.png +0 -0
- data/resources/images/explosions/explosion2-25.png +0 -0
- data/resources/images/explosions/explosion2-26.png +0 -0
- data/resources/images/explosions/explosion2-27.png +0 -0
- data/resources/images/explosions/explosion2-28.png +0 -0
- data/resources/images/explosions/explosion2-29.png +0 -0
- data/resources/images/explosions/explosion2-3.png +0 -0
- data/resources/images/explosions/explosion2-30.png +0 -0
- data/resources/images/explosions/explosion2-31.png +0 -0
- data/resources/images/explosions/explosion2-32.png +0 -0
- data/resources/images/explosions/explosion2-33.png +0 -0
- data/resources/images/explosions/explosion2-34.png +0 -0
- data/resources/images/explosions/explosion2-35.png +0 -0
- data/resources/images/explosions/explosion2-36.png +0 -0
- data/resources/images/explosions/explosion2-37.png +0 -0
- data/resources/images/explosions/explosion2-38.png +0 -0
- data/resources/images/explosions/explosion2-39.png +0 -0
- data/resources/images/explosions/explosion2-4.png +0 -0
- data/resources/images/explosions/explosion2-40.png +0 -0
- data/resources/images/explosions/explosion2-41.png +0 -0
- data/resources/images/explosions/explosion2-42.png +0 -0
- data/resources/images/explosions/explosion2-43.png +0 -0
- data/resources/images/explosions/explosion2-44.png +0 -0
- data/resources/images/explosions/explosion2-45.png +0 -0
- data/resources/images/explosions/explosion2-46.png +0 -0
- data/resources/images/explosions/explosion2-47.png +0 -0
- data/resources/images/explosions/explosion2-48.png +0 -0
- data/resources/images/explosions/explosion2-49.png +0 -0
- data/resources/images/explosions/explosion2-5.png +0 -0
- data/resources/images/explosions/explosion2-50.png +0 -0
- data/resources/images/explosions/explosion2-51.png +0 -0
- data/resources/images/explosions/explosion2-52.png +0 -0
- data/resources/images/explosions/explosion2-53.png +0 -0
- data/resources/images/explosions/explosion2-54.png +0 -0
- data/resources/images/explosions/explosion2-55.png +0 -0
- data/resources/images/explosions/explosion2-56.png +0 -0
- data/resources/images/explosions/explosion2-57.png +0 -0
- data/resources/images/explosions/explosion2-58.png +0 -0
- data/resources/images/explosions/explosion2-59.png +0 -0
- data/resources/images/explosions/explosion2-6.png +0 -0
- data/resources/images/explosions/explosion2-60.png +0 -0
- data/resources/images/explosions/explosion2-61.png +0 -0
- data/resources/images/explosions/explosion2-62.png +0 -0
- data/resources/images/explosions/explosion2-63.png +0 -0
- data/resources/images/explosions/explosion2-64.png +0 -0
- data/resources/images/explosions/explosion2-65.png +0 -0
- data/resources/images/explosions/explosion2-66.png +0 -0
- data/resources/images/explosions/explosion2-67.png +0 -0
- data/resources/images/explosions/explosion2-68.png +0 -0
- data/resources/images/explosions/explosion2-69.png +0 -0
- data/resources/images/explosions/explosion2-7.png +0 -0
- data/resources/images/explosions/explosion2-70.png +0 -0
- data/resources/images/explosions/explosion2-71.png +0 -0
- data/resources/images/explosions/explosion2-8.png +0 -0
- data/resources/images/explosions/explosion2-9.png +0 -0
- data/resources/images/grass.png +0 -0
- data/resources/images/radar.png +0 -0
- data/resources/images/turret.png +0 -0
- data/rtanque.gemspec +34 -0
- data/sample_bots/camper.rb +79 -0
- data/sample_bots/keyboard.rb +50 -0
- data/sample_bots/seek_and_destroy.rb +53 -0
- data/screenshots/battle_1.png +0 -0
- data/screenshots/battle_2.png +0 -0
- data/spec/rtanque/bot_spec.rb +239 -0
- data/spec/rtanque/heading_spec.rb +279 -0
- data/spec/rtanque/match_spec.rb +46 -0
- data/spec/rtanque/normalized_attr_spec.rb +90 -0
- data/spec/rtanque/point_spec.rb +196 -0
- data/spec/rtanque/radar_spec.rb +87 -0
- data/spec/rtanque/shell_spec.rb +35 -0
- data/spec/rtanque_spec.rb +34 -0
- data/spec/spec_helper.rb +51 -0
- data/templates/bot.erb +17 -0
- 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
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|