rtanque 0.0.2

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