rtanque 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. data/.gitignore +22 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +34 -0
  4. data/.travis.yml +11 -0
  5. data/.yardopts +4 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.ci +6 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +120 -0
  10. data/Rakefile +1 -0
  11. data/bin/rtanque +108 -0
  12. data/lib/rtanque.rb +40 -0
  13. data/lib/rtanque/arena.rb +8 -0
  14. data/lib/rtanque/bot.rb +117 -0
  15. data/lib/rtanque/bot/brain.rb +50 -0
  16. data/lib/rtanque/bot/brain_helper.rb +23 -0
  17. data/lib/rtanque/bot/command.rb +23 -0
  18. data/lib/rtanque/bot/radar.rb +54 -0
  19. data/lib/rtanque/bot/sensors.rb +33 -0
  20. data/lib/rtanque/bot/turret.rb +14 -0
  21. data/lib/rtanque/configuration.rb +46 -0
  22. data/lib/rtanque/explosion.rb +23 -0
  23. data/lib/rtanque/gui.rb +24 -0
  24. data/lib/rtanque/gui/bot.rb +42 -0
  25. data/lib/rtanque/gui/draw_group.rb +30 -0
  26. data/lib/rtanque/gui/explosion.rb +25 -0
  27. data/lib/rtanque/gui/shell.rb +20 -0
  28. data/lib/rtanque/gui/window.rb +51 -0
  29. data/lib/rtanque/heading.rb +172 -0
  30. data/lib/rtanque/match.rb +67 -0
  31. data/lib/rtanque/match/tick_group.rb +50 -0
  32. data/lib/rtanque/movable.rb +51 -0
  33. data/lib/rtanque/normalized_attr.rb +69 -0
  34. data/lib/rtanque/point.rb +140 -0
  35. data/lib/rtanque/runner.rb +88 -0
  36. data/lib/rtanque/shell.rb +40 -0
  37. data/lib/rtanque/version.rb +3 -0
  38. data/resources/images/body.png +0 -0
  39. data/resources/images/bullet.png +0 -0
  40. data/resources/images/explosions/explosion2-1.png +0 -0
  41. data/resources/images/explosions/explosion2-10.png +0 -0
  42. data/resources/images/explosions/explosion2-11.png +0 -0
  43. data/resources/images/explosions/explosion2-12.png +0 -0
  44. data/resources/images/explosions/explosion2-13.png +0 -0
  45. data/resources/images/explosions/explosion2-14.png +0 -0
  46. data/resources/images/explosions/explosion2-15.png +0 -0
  47. data/resources/images/explosions/explosion2-16.png +0 -0
  48. data/resources/images/explosions/explosion2-17.png +0 -0
  49. data/resources/images/explosions/explosion2-18.png +0 -0
  50. data/resources/images/explosions/explosion2-19.png +0 -0
  51. data/resources/images/explosions/explosion2-2.png +0 -0
  52. data/resources/images/explosions/explosion2-20.png +0 -0
  53. data/resources/images/explosions/explosion2-21.png +0 -0
  54. data/resources/images/explosions/explosion2-22.png +0 -0
  55. data/resources/images/explosions/explosion2-23.png +0 -0
  56. data/resources/images/explosions/explosion2-24.png +0 -0
  57. data/resources/images/explosions/explosion2-25.png +0 -0
  58. data/resources/images/explosions/explosion2-26.png +0 -0
  59. data/resources/images/explosions/explosion2-27.png +0 -0
  60. data/resources/images/explosions/explosion2-28.png +0 -0
  61. data/resources/images/explosions/explosion2-29.png +0 -0
  62. data/resources/images/explosions/explosion2-3.png +0 -0
  63. data/resources/images/explosions/explosion2-30.png +0 -0
  64. data/resources/images/explosions/explosion2-31.png +0 -0
  65. data/resources/images/explosions/explosion2-32.png +0 -0
  66. data/resources/images/explosions/explosion2-33.png +0 -0
  67. data/resources/images/explosions/explosion2-34.png +0 -0
  68. data/resources/images/explosions/explosion2-35.png +0 -0
  69. data/resources/images/explosions/explosion2-36.png +0 -0
  70. data/resources/images/explosions/explosion2-37.png +0 -0
  71. data/resources/images/explosions/explosion2-38.png +0 -0
  72. data/resources/images/explosions/explosion2-39.png +0 -0
  73. data/resources/images/explosions/explosion2-4.png +0 -0
  74. data/resources/images/explosions/explosion2-40.png +0 -0
  75. data/resources/images/explosions/explosion2-41.png +0 -0
  76. data/resources/images/explosions/explosion2-42.png +0 -0
  77. data/resources/images/explosions/explosion2-43.png +0 -0
  78. data/resources/images/explosions/explosion2-44.png +0 -0
  79. data/resources/images/explosions/explosion2-45.png +0 -0
  80. data/resources/images/explosions/explosion2-46.png +0 -0
  81. data/resources/images/explosions/explosion2-47.png +0 -0
  82. data/resources/images/explosions/explosion2-48.png +0 -0
  83. data/resources/images/explosions/explosion2-49.png +0 -0
  84. data/resources/images/explosions/explosion2-5.png +0 -0
  85. data/resources/images/explosions/explosion2-50.png +0 -0
  86. data/resources/images/explosions/explosion2-51.png +0 -0
  87. data/resources/images/explosions/explosion2-52.png +0 -0
  88. data/resources/images/explosions/explosion2-53.png +0 -0
  89. data/resources/images/explosions/explosion2-54.png +0 -0
  90. data/resources/images/explosions/explosion2-55.png +0 -0
  91. data/resources/images/explosions/explosion2-56.png +0 -0
  92. data/resources/images/explosions/explosion2-57.png +0 -0
  93. data/resources/images/explosions/explosion2-58.png +0 -0
  94. data/resources/images/explosions/explosion2-59.png +0 -0
  95. data/resources/images/explosions/explosion2-6.png +0 -0
  96. data/resources/images/explosions/explosion2-60.png +0 -0
  97. data/resources/images/explosions/explosion2-61.png +0 -0
  98. data/resources/images/explosions/explosion2-62.png +0 -0
  99. data/resources/images/explosions/explosion2-63.png +0 -0
  100. data/resources/images/explosions/explosion2-64.png +0 -0
  101. data/resources/images/explosions/explosion2-65.png +0 -0
  102. data/resources/images/explosions/explosion2-66.png +0 -0
  103. data/resources/images/explosions/explosion2-67.png +0 -0
  104. data/resources/images/explosions/explosion2-68.png +0 -0
  105. data/resources/images/explosions/explosion2-69.png +0 -0
  106. data/resources/images/explosions/explosion2-7.png +0 -0
  107. data/resources/images/explosions/explosion2-70.png +0 -0
  108. data/resources/images/explosions/explosion2-71.png +0 -0
  109. data/resources/images/explosions/explosion2-8.png +0 -0
  110. data/resources/images/explosions/explosion2-9.png +0 -0
  111. data/resources/images/grass.png +0 -0
  112. data/resources/images/radar.png +0 -0
  113. data/resources/images/turret.png +0 -0
  114. data/rtanque.gemspec +33 -0
  115. data/sample_bots/camper.rb +79 -0
  116. data/sample_bots/keyboard.rb +50 -0
  117. data/sample_bots/seek_and_destroy.rb +51 -0
  118. data/screenshots/battle_1.png +0 -0
  119. data/screenshots/battle_2.png +0 -0
  120. data/spec/rtanque/bot_spec.rb +239 -0
  121. data/spec/rtanque/heading_spec.rb +279 -0
  122. data/spec/rtanque/match_spec.rb +36 -0
  123. data/spec/rtanque/normalized_attr_spec.rb +90 -0
  124. data/spec/rtanque/point_spec.rb +196 -0
  125. data/spec/rtanque/radar_spec.rb +87 -0
  126. data/spec/rtanque/shell_spec.rb +35 -0
  127. data/spec/rtanque_spec.rb +6 -0
  128. data/spec/spec_helper.rb +51 -0
  129. data/templates/bot.erb +11 -0
  130. metadata +310 -0
@@ -0,0 +1,172 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module RTanque
3
+ # A Heading represents an angle. Basically a wrapper around `Float` bound to `(0..Math::PI * 2)`
4
+ #
5
+ # 0.0 == `RTanque::Heading::NORTH` is 'up'
6
+ #
7
+ # ##Basic Usage
8
+ # RTanque::Heading.new(Math::PI)
9
+ # # => <RTanque::Heading: 1.0rad 180.0deg>
10
+ #
11
+ # RTanque::Heading.new(Math::PI) + RTanque::Heading.new(Math::PI)
12
+ # # => <RTanque::Heading: 0.0rad 0.0deg>
13
+ #
14
+ # RTanque::Heading.new(Math::PI / 2.0) + Math::PI
15
+ # # => <RTanque::Heading: 1.5rad 270.0deg>
16
+ #
17
+ # RTanque::Heading.new(0.0) == 0
18
+ # # => true
19
+ #
20
+ # ##Utility Methods
21
+ # RTanque::Heading.new_from_degrees(180.0)
22
+ # # => <RTanque::Heading: 1.0rad 180.0deg>
23
+ #
24
+ # RTanque::Heading.new(Math::PI).to_degrees
25
+ # # => 180.0
26
+ #
27
+ # RTanque::Heading.new_between_points(RTanque::Point.new(0,0), RTanque::Point.new(2,3))
28
+ # # => <RTanque::Heading: 0.1871670418109988rad 33.690067525979785deg>
29
+ #
30
+ # RTanque::Heading.new_from_degrees(1).delta(RTanque::Heading.new_from_degrees(359))
31
+ # # => -0.034906585039886195
32
+ class Heading < Numeric
33
+ FULL_ANGLE = Math::PI * 2.0
34
+ HALF_ANGLE = Math::PI
35
+ EIGHTH_ANGLE = Math::PI / 4.0
36
+ ONE_DEGREE = FULL_ANGLE / 360.0
37
+ FULL_RANGE = (0..FULL_ANGLE)
38
+
39
+ NORTH = N = 0.0
40
+ NORTH_EAST = NE = 1.0 * EIGHTH_ANGLE
41
+ EAST = E = 2.0 * EIGHTH_ANGLE
42
+ SOUTH_EAST = SE = 3.0 * EIGHTH_ANGLE
43
+ SOUTH = S = 4.0 * EIGHTH_ANGLE
44
+ SOUTH_WEST = SW = 5.0 * EIGHTH_ANGLE
45
+ WEST = W = 6.0 * EIGHTH_ANGLE
46
+ NORTH_WEST = NW = 7.0 * EIGHTH_ANGLE
47
+
48
+ def self.new_from_degrees(degrees)
49
+ self.new((degrees / 180.0) * Math::PI)
50
+ end
51
+
52
+ def self.new_between_points(from_point, to_point)
53
+ self.new(from_point == to_point ? 0.0 : Math.atan2(to_point.x - from_point.x, to_point.y - from_point.y))
54
+ end
55
+
56
+ def self.delta_between_points(from_point, from_point_heading, to_point)
57
+ rel_heading = self.new_between_points(from_point, to_point)
58
+ self.new(from_point_heading).delta(rel_heading)
59
+ end
60
+
61
+ def self.rand
62
+ self.new(Float.send(:rand) * FULL_ANGLE)
63
+ end
64
+
65
+ attr_reader :radians
66
+
67
+ # Creates a new RTanque::Heading
68
+ # @param [#to_f] radians degree to wrap (in radians)
69
+ def initialize(radians = NORTH)
70
+ @radians = self.extract_radians_from_value(radians) % FULL_ANGLE
71
+ @memoized = {} # allow memoization since @some_var ||= x doesn't work when frozen
72
+ self.freeze
73
+ end
74
+
75
+ # difference between `self` and `to` respecting negative angles
76
+ # @param [#to_f] to
77
+ # @return [Float]
78
+ def delta(to)
79
+ diff = (to.to_f - self.to_f).abs % FULL_ANGLE
80
+ diff = -(FULL_ANGLE - diff) if diff > Math::PI
81
+ to < self ? -diff : diff
82
+ end
83
+
84
+ # @return [RTanque::Heading]
85
+ def clone
86
+ self.class.new(self.radians)
87
+ end
88
+
89
+ # @param [#to_f] other_heading
90
+ # @return [Boolean]
91
+ def ==(other_heading)
92
+ self.to_f == other_heading.to_f
93
+ end
94
+
95
+ # continue with Numeric's pattern
96
+ # @param [#to_f] other_heading
97
+ # @return [Boolean]
98
+ def eql?(other_heading)
99
+ other_heading.instance_of?(self.class) && self.==(other_heading)
100
+ end
101
+
102
+ # @param [#to_f] other_heading
103
+ # @return [Boolean]
104
+ def <=>(other_heading)
105
+ self.to_f <=> other_heading.to_f
106
+ end
107
+
108
+ # @param [#to_f] other_heading
109
+ # @return [RTanque::Heading]
110
+ def +(other_heading)
111
+ self.class.new(self.radians + self.extract_radians_from_value(other_heading))
112
+ end
113
+
114
+ # @param [#to_f] other_heading
115
+ # @return [RTanque::Heading]
116
+ def -(other_heading)
117
+ self.+(-other_heading)
118
+ end
119
+
120
+ # @param [#to_f] other_heading
121
+ # @return [RTanque::Heading]
122
+ def *(other_heading)
123
+ self.class.new(self.radians * self.extract_radians_from_value(other_heading))
124
+ end
125
+
126
+ # @param [#to_f] other_heading
127
+ # @return [RTanque::Heading]
128
+ def /(other_heading)
129
+ self.*(1.0 / other_heading)
130
+ end
131
+
132
+ # unary operator
133
+ # @return [RTanque::Heading]
134
+ def +@
135
+ self.class.new(+self.radians)
136
+ end
137
+
138
+ # unary operator
139
+ # @return [RTanque::Heading]
140
+ def -@
141
+ self.class.new(-self.radians)
142
+ end
143
+
144
+ def to_s
145
+ self.to_f
146
+ end
147
+
148
+ def inspect
149
+ "<#{self.class.name}: #{self.radians}rad #{self.to_degrees}deg>"
150
+ end
151
+
152
+ # @return [Float]
153
+ def to_f
154
+ self.radians
155
+ end
156
+
157
+ # @return [Float]
158
+ def to_degrees
159
+ @memoized[:to_degrees] ||= (self.radians * 180.0) / Math::PI
160
+ end
161
+
162
+ protected
163
+
164
+ def extract_radians_from_value(value)
165
+ if value.respond_to?(:radians)
166
+ value.radians
167
+ else
168
+ value.to_f
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,67 @@
1
+ module RTanque
2
+ class Match
3
+ attr_reader :arena, :bots, :shells, :explosions, :ticks, :max_ticks
4
+
5
+ def initialize(arena, max_ticks = nil)
6
+ @arena = arena
7
+ @max_ticks = max_ticks
8
+ @ticks = 0
9
+ @shells = TickGroup.new
10
+ @bots = TickGroup.new
11
+ @explosions = TickGroup.new
12
+ @bots.pre_tick(&method(:pre_bot_tick))
13
+ @bots.post_tick(&method(:post_bot_tick))
14
+ @shells.pre_tick(&method(:pre_shell_tick))
15
+ @stopped = false
16
+ end
17
+
18
+ def max_ticks_reached?
19
+ self.max_ticks && self.ticks >= self.max_ticks
20
+ end
21
+
22
+ def finished?
23
+ @stopped || self.max_ticks_reached? || self.bots.count <= 1
24
+ end
25
+
26
+ def add_bots(*bots)
27
+ self.bots.add(*bots)
28
+ end
29
+
30
+ def start
31
+ self.tick until self.finished?
32
+ end
33
+
34
+ def stop
35
+ @stopped = true
36
+ end
37
+
38
+ def pre_bot_tick(bot)
39
+ bot.radar.scan(self.bots.all_but(bot))
40
+ end
41
+
42
+ def post_bot_tick(bot)
43
+ if bot.firing?
44
+ # shell starts life at the end of the turret
45
+ shell_position = bot.position.move(bot.turret.heading, RTanque::Bot::Turret::LENGTH)
46
+ @shells.add(RTanque::Shell.new(bot, shell_position, bot.turret.heading.clone, bot.fire_power))
47
+ end
48
+ end
49
+
50
+ def pre_shell_tick(shell)
51
+ shell.hits(self.bots.all_but(shell.bot)) do |origin_bot, bot_hit|
52
+ damage = (shell.fire_power ** RTanque::Shell::RATIO)
53
+ bot_hit.reduce_health(damage)
54
+ if bot_hit.dead?
55
+ @explosions.add(Explosion.new(bot_hit.position))
56
+ end
57
+ end
58
+ end
59
+
60
+ def tick
61
+ self.shells.tick
62
+ self.bots.tick
63
+ self.explosions.tick
64
+ @ticks += 1
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,50 @@
1
+ module RTanque
2
+ class Match
3
+ class TickGroup
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @members = []
8
+ @pre_tick = nil
9
+ @post_tick = nil
10
+ end
11
+
12
+ def each(&block)
13
+ @members.each(&block)
14
+ end
15
+
16
+ def all_but(*to_exclude)
17
+ self.reject { |member| to_exclude.include?(member) }
18
+ end
19
+
20
+ def delete_if(&block)
21
+ @members.delete_if(&block)
22
+ end
23
+
24
+ def add(*members)
25
+ @members += members.flatten
26
+ end
27
+
28
+ def pre_tick(&block)
29
+ @pre_tick = block
30
+ end
31
+
32
+ def post_tick(&block)
33
+ @post_tick = block
34
+ end
35
+
36
+ def tick
37
+ self.delete_if do |member|
38
+ if member.dead?
39
+ true
40
+ else
41
+ @pre_tick.call(member) if @pre_tick
42
+ member.tick
43
+ @post_tick.call(member) if @post_tick
44
+ false
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ module RTanque
2
+ module Movable
3
+ def tick
4
+ update_position
5
+ end
6
+
7
+ def dead?
8
+ false # should overwrite
9
+ end
10
+
11
+ def arena
12
+ @arena
13
+ end
14
+
15
+ def arena=(val)
16
+ @arena = val
17
+ end
18
+
19
+ def position
20
+ @position
21
+ end
22
+
23
+ def position=(val)
24
+ @position = val
25
+ end
26
+
27
+ def bound_to_arena
28
+ true
29
+ end
30
+
31
+ def update_position
32
+ @position = @position.move(self.heading, self.speed, self.bound_to_arena)
33
+ end
34
+
35
+ def heading
36
+ @heading
37
+ end
38
+
39
+ def heading=(val)
40
+ @heading = Heading.new(val) if val
41
+ end
42
+
43
+ def speed
44
+ @speed
45
+ end
46
+
47
+ def speed=(val)
48
+ @speed = val if val
49
+ end
50
+ end
51
+ end
@@ -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 = RTanque.round((self.x + (Math.sin(heading) * speed)), 10)
115
+ y = RTanque.round((self.y + (Math.cos(heading) * speed)), 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