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,51 @@
1
+ # Seek&Destroy: Sample Bot
2
+ #
3
+ # Enjoys following and target and firing many shots
4
+ class SeekAndDestroy < RTanque::Bot::Brain
5
+ NAME = 'Seek&Destroy'
6
+ include RTanque::Bot::BrainHelper
7
+
8
+ TURRET_FIRE_RANGE = RTanque::Heading::ONE_DEGREE * 5.0
9
+
10
+ def tick!
11
+ if (lock = self.get_radar_lock)
12
+ self.destroy_lock(lock)
13
+ @desired_heading = nil
14
+ else
15
+ self.seek_lock
16
+ end
17
+ end
18
+
19
+ def destroy_lock(reflection)
20
+ command.heading = reflection.heading
21
+ command.radar_heading = reflection.heading
22
+ command.turret_heading = reflection.heading
23
+ command.speed = reflection.distance > 200 ? MAX_BOT_SPEED : MAX_BOT_SPEED / 2.0
24
+ if (reflection.heading.delta(sensors.turret_heading)).abs < TURRET_FIRE_RANGE
25
+ command.fire(reflection.distance > 200 ? MAX_FIRE_POWER : MIN_FIRE_POWER)
26
+ end
27
+ end
28
+
29
+ def seek_lock
30
+ if sensors.position.on_wall?
31
+ @desired_heading = sensors.heading + RTanque::Heading::HALF_ANGLE
32
+ end
33
+ command.radar_heading = sensors.radar_heading + MAX_RADAR_ROTATION
34
+ command.speed = 1
35
+ if @desired_heading
36
+ command.heading = @desired_heading
37
+ command.turret_heading = @desired_heading
38
+ end
39
+ end
40
+
41
+ def get_radar_lock
42
+ @locked_on ||= nil
43
+ lock = if @locked_on
44
+ sensors.radar.find { |reflection| reflection.name == @locked_on } || sensors.radar.first
45
+ else
46
+ sensors.radar.first
47
+ end
48
+ @locked_on = lock.name if lock
49
+ lock
50
+ end
51
+ end
Binary file
Binary file
@@ -0,0 +1,239 @@
1
+ require "spec_helper"
2
+
3
+ describe RTanque::Bot do
4
+ before(:each) { @brain_tick_lambda = lambda { } }
5
+ let(:bot){ brain_bot(&@brain_tick_lambda) }
6
+
7
+ context '#dead?' do
8
+ it 'should not initially be dead' do
9
+ expect(bot.dead?).to be_false
10
+ end
11
+
12
+ it 'should be true if health is below min' do
13
+ bot.health = RTanque::Configuration.bot.health.min - 1
14
+ expect(bot.dead?).to be_true
15
+ end
16
+ end
17
+
18
+ context '#sensors' do
19
+ it 'should return a Sensors instance' do
20
+ expect(bot.sensors).to be_instance_of RTanque::Bot::Sensors
21
+ end
22
+
23
+ it 'should correctly transfer values from bot' do
24
+ bot.health = 5
25
+ bot.speed = -2
26
+ expect(bot.sensors.health).to eq 5
27
+ expect(bot.sensors.speed).to eq -2
28
+ end
29
+ end
30
+
31
+ context '#tick' do
32
+ context 'no commands' do
33
+ it 'should not update bot position, heading on tick' do
34
+ bot.tick
35
+ expect(bot.position.x).to eq 0.0
36
+ expect(bot.position.y).to eq 0.0
37
+ expect(bot.heading).to eq 0
38
+ end
39
+ end
40
+
41
+ context 'command with speed' do
42
+ before(:each) do
43
+ bot.speed = 1
44
+ bot.heading = 0
45
+ end
46
+
47
+ it 'should update bot position on tick' do
48
+ bot.tick
49
+ expect(bot.position.x).to eq 0.0
50
+ expect(bot.position.y).to eq 1.0
51
+ end
52
+
53
+ it 'should keep updating bot position' do
54
+ 5.times { bot.tick }
55
+ expect(bot.position.x).to eq 0.0
56
+ expect(bot.position.y).to eq 5.0
57
+ end
58
+
59
+ it 'should stop at arena limit' do
60
+ (@arena.height + 2).times { bot.tick }
61
+ expect(bot.position.x).to eq 0.0
62
+ expect(bot.position.y).to eq @arena.height
63
+ end
64
+ end
65
+
66
+ context 'command with heading' do
67
+ before(:each) do
68
+ bot.heading = RTanque::Heading::EAST
69
+ end
70
+
71
+ it 'should have heading east' do
72
+ bot.tick
73
+ expect(bot.heading).to eq RTanque::Heading::EAST
74
+ end
75
+
76
+ it 'should keep heading east' do
77
+ 5.times { bot.tick }
78
+ expect(bot.heading).to eq RTanque::Heading::EAST
79
+ end
80
+
81
+ it 'should maintain heading given null heading' do
82
+ bot.tick
83
+ @brain_tick_lambda = lambda { command.heading = nil }
84
+ expect(bot.heading).to eq RTanque::Heading::EAST
85
+ end
86
+
87
+ it 'should not change radar and turret headings' do
88
+ bot.tick
89
+ expect(bot.radar.heading).to eq 0.0
90
+ expect(bot.turret.heading).to eq 0.0
91
+ end
92
+ end
93
+
94
+ context 'radar heading' do
95
+ before(:each) do
96
+ bot.radar.heading = RTanque::Heading::EAST
97
+ bot.tick
98
+ end
99
+
100
+ it 'should change radar heading' do
101
+ expect(bot.radar.heading).to eq RTanque::Heading::EAST
102
+ end
103
+
104
+ it 'should not change bot heading' do
105
+ expect(bot.heading).to eq 0
106
+ end
107
+ end
108
+
109
+ context 'turret heading' do
110
+ before(:each) do
111
+ bot.turret.heading = RTanque::Heading::EAST
112
+ bot.tick
113
+ end
114
+
115
+ it 'should change radar heading' do
116
+ expect(bot.turret.heading).to eq RTanque::Heading::EAST
117
+ end
118
+
119
+ it 'should not change bot heading' do
120
+ expect(bot.heading).to eq 0
121
+ end
122
+ end
123
+
124
+ context 'fire power' do
125
+ it 'bot should have 0 fire_power' do
126
+ bot.tick
127
+ expect(bot.fire_power).to eq 0
128
+ end
129
+
130
+ it 'bot should have fire_power reset' do
131
+ bot.fire_power = 1
132
+ bot.tick
133
+ expect(bot.fire_power).to eq 0
134
+ end
135
+ end
136
+
137
+ context 'command with error' do
138
+ before(:each) do
139
+ @brain_tick_lambda = lambda { raise 'oops' }
140
+ end
141
+
142
+ it 'should capture error' do
143
+ expect{ bot.tick }.not_to raise_exception
144
+ end
145
+
146
+ it 'should reduce bot health' do
147
+ original_health = bot.health
148
+ bot.tick
149
+ expect(bot.health).to be < original_health
150
+ end
151
+ end
152
+ end
153
+
154
+ context 'bot command speed' do
155
+ before(:each) do
156
+ @brain_tick_lambda = lambda { command.speed = RTanque::Bot::MAX_SPEED + 1 }
157
+ bot.tick
158
+ end
159
+
160
+ it 'should respect step size' do
161
+ expect(bot.speed).to eq RTanque::Configuration.bot.speed_step
162
+ end
163
+
164
+ it 'should respect max speed' do
165
+ times = RTanque::Bot::MAX_SPEED / RTanque::Configuration.bot.speed_step
166
+ (times + 1).to_i.times { bot.tick }
167
+ expect(bot.speed).to eq RTanque::Bot::MAX_SPEED
168
+ end
169
+
170
+ it 'should respect min speed' do
171
+ @brain_tick_lambda = lambda { command.speed = -(RTanque::Bot::MAX_SPEED + 1) }
172
+ times = RTanque::Bot::MAX_SPEED / RTanque::Configuration.bot.speed_step
173
+ (times + 1).to_i.times { bot.tick }
174
+ expect(bot.speed).to eq RTanque::Bot::MAX_SPEED
175
+ end
176
+ end
177
+
178
+ context 'bot command heading' do
179
+ it 'should respect step size' do
180
+ @brain_tick_lambda = lambda { command.heading = RTanque::Heading::S }
181
+ bot.tick
182
+ expect(bot.heading).to eq RTanque::Heading.new(RTanque::Configuration.bot.turn_step)
183
+ end
184
+
185
+ it 'should respect step size in negative' do
186
+ @brain_tick_lambda = lambda { command.heading = -RTanque::Heading::S }
187
+ bot.tick
188
+ expect(bot.heading).to eq RTanque::Heading.new(-RTanque::Configuration.bot.turn_step)
189
+ end
190
+ end
191
+
192
+ context 'bot command radar heading' do
193
+ it 'should respect step size' do
194
+ @brain_tick_lambda = lambda { command.radar_heading = RTanque::Heading::S }
195
+ bot.tick
196
+ expect(bot.radar.heading).to eq RTanque::Heading.new(RTanque::Configuration.radar.turn_step)
197
+ end
198
+
199
+ it 'should respect step size in negative' do
200
+ @brain_tick_lambda = lambda { command.radar_heading = -RTanque::Heading::S }
201
+ bot.tick
202
+ expect(bot.radar.heading).to eq RTanque::Heading.new(-RTanque::Configuration.radar.turn_step)
203
+ end
204
+ end
205
+
206
+ context 'bot command turret heading' do
207
+ it 'should respect step size' do
208
+ @brain_tick_lambda = lambda { self.command.turret_heading = RTanque::Heading::S }
209
+ bot.tick
210
+ expect(bot.turret.heading).to eq RTanque::Heading.new(RTanque::Configuration.turret.turn_step)
211
+ end
212
+
213
+ it 'should respect step size in negative' do
214
+ @brain_tick_lambda = lambda { self.command.turret_heading = -RTanque::Heading::S }
215
+ bot.tick
216
+ expect(bot.turret.heading).to eq RTanque::Heading.new(-RTanque::Configuration.turret.turn_step)
217
+ end
218
+ end
219
+
220
+ context 'bot command fire_power' do
221
+ it 'should respect max' do
222
+ @brain_tick_lambda = lambda { command.fire_power = RTanque::Bot::MAX_FIRE_POWER + 1 }
223
+ bot.tick
224
+ expect(bot.fire_power).to eq RTanque::Bot::MAX_FIRE_POWER
225
+ end
226
+
227
+ it 'should respect min' do
228
+ @brain_tick_lambda = lambda { command.fire_power = RTanque::Bot::MIN_FIRE_POWER - 1 }
229
+ bot.tick
230
+ expect(bot.fire_power).to eq RTanque::Bot::MIN_FIRE_POWER
231
+ end
232
+
233
+ it 'should not allow constant shooting' do
234
+ @brain_tick_lambda = lambda { command.fire_power = RTanque::Bot::MAX_FIRE_POWER }
235
+ 5.times { bot.tick }
236
+ expect(bot.fire_power).not_to eq RTanque::Bot::MAX_FIRE_POWER
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,279 @@
1
+ require "spec_helper"
2
+
3
+ describe RTanque::Heading do
4
+ NINETY = Math::PI / 2.0
5
+
6
+ describe '#delta' do
7
+ before do
8
+ @instance = described_class.new(0)
9
+ end
10
+
11
+ it 'receives floats' do
12
+ other = 0.0
13
+ expect(@instance.delta(other)).to eql 0.0
14
+ end
15
+
16
+ it 'receives headings' do
17
+ other = described_class.new(0.0)
18
+ expect(@instance.delta(other)).to eql 0.0
19
+ end
20
+
21
+ it 'gives 0 when provided same a == b' do
22
+ expect(@instance.delta(@instance)).to eq 0
23
+ end
24
+
25
+ it 'outputs positive when a < b' do
26
+ expect(@instance.delta(@instance + 1)).to be > 0
27
+ end
28
+
29
+ it 'outputs negative when b < a' do
30
+ expect(@instance.delta(@instance - 1)).to be < 0
31
+ end
32
+
33
+ it 'correct output when difference < 180 deg' do
34
+ expect(@instance.delta(NINETY)).to eq NINETY
35
+ end
36
+
37
+ it 'correct output when difference > 180 deg' do
38
+ expect(@instance.delta(NINETY * 3)).to eq -NINETY
39
+ end
40
+
41
+ it 'correctly handles degress > 360' do
42
+ expect(@instance.delta(NINETY * 5)).to eq NINETY
43
+ end
44
+
45
+ it 'correctly handles differences > 180 in which a < b' do
46
+ expect(@instance.delta(NINETY * 3)).to eq -NINETY
47
+ end
48
+
49
+ it 'correctly handles differences > 180 in which a > b' do
50
+ expect(@instance.delta(-(NINETY * 3))).to eq NINETY
51
+ end
52
+
53
+ it 'correctly handles when a and b are on both sides of 180' do
54
+ @instance = described_class.new(RTanque::Heading::S - RTanque::Heading::ONE_DEGREE)
55
+ delta = RTanque.round(@instance.delta(RTanque::Heading::S + RTanque::Heading::ONE_DEGREE), 5)
56
+ expected = RTanque.round(RTanque::Heading::ONE_DEGREE * 2, 5)
57
+ expect(delta).to eq(expected)
58
+ end
59
+ end
60
+
61
+ describe '.new_from_degrees' do
62
+ it 'receives positive degrees' do
63
+ expect(described_class.new_from_degrees(90).to_f).to eql NINETY
64
+ end
65
+
66
+ it 'receives degrees large than 360' do
67
+ expect(described_class.new_from_degrees(90 + 360).to_f).to eql NINETY
68
+ end
69
+
70
+ it 'receives negative degrees' do
71
+ expect(described_class.new_from_degrees(-90).to_f).to eql NINETY * 3
72
+ end
73
+
74
+ it 'receives negative degrees less than -360' do
75
+ expect(described_class.new_from_degrees(-90 - 360).to_f).to eql NINETY * 3
76
+ end
77
+ end
78
+
79
+ describe '.delta_between_points' do
80
+ let(:from_point) { RTanque::Point.new(10, 10, @arena) }
81
+ let(:from_heading) { described_class.new(0) }
82
+ it 'is correct when 0 delta' do
83
+ to_point = RTanque::Point.new(10, 20, @arena)
84
+ expect(described_class.delta_between_points(from_point, from_heading, to_point)).to eq 0
85
+ end
86
+
87
+ it 'is correct when delta is negative' do
88
+ to_point = RTanque::Point.new(9, 11)
89
+ expect(described_class.delta_between_points(from_point, from_heading, to_point)).to eq -(NINETY / 2)
90
+ end
91
+
92
+ it 'is correct when delta is positive' do
93
+ to_point = RTanque::Point.new(11, 11)
94
+ expect(described_class.delta_between_points(from_point, from_heading, to_point)).to eq(NINETY / 2)
95
+ end
96
+
97
+ it 'is correct when delta is max' do
98
+ to_point = RTanque::Point.new(10, 9)
99
+ expect(described_class.delta_between_points(from_point, from_heading, to_point)).to eq(NINETY * 2)
100
+ end
101
+ end
102
+
103
+ describe '#initialize' do
104
+ it 'receives inits and sets radians to float' do
105
+ expect(described_class.new(0).radians).to eql 0.0
106
+ end
107
+
108
+ it 'receives negative floats' do
109
+ expect(described_class.new(-NINETY).radians).to eql NINETY * 3
110
+ end
111
+
112
+ it 'creates frozen object' do
113
+ expect(described_class.new(0).frozen?).to be_true
114
+ end
115
+ end
116
+
117
+ describe '#clone' do
118
+ it 'returns a Heading' do
119
+ expect(described_class.new.clone).to be_instance_of described_class
120
+ end
121
+
122
+ it 'returns a new object' do
123
+ original = described_class.new
124
+ copy = original.clone
125
+ expect(original.object_id).not_to eq copy.object_id
126
+ end
127
+
128
+ it 'returns a new object which does not affect old' do
129
+ original = described_class.new(0.0)
130
+ copy = original.clone
131
+ expect(copy).not_to equal original
132
+ end
133
+ end
134
+
135
+ describe '#==' do
136
+ it 'works like Numeric, ignoring type' do
137
+ a = described_class.new(1.0)
138
+ expect(a == 1).to be_true
139
+ end
140
+
141
+ it 'correctly compares two equal headings' do
142
+ a = described_class.new(1.0)
143
+ b = described_class.new(1.0)
144
+ expect(a == b).to be_true
145
+ end
146
+
147
+ it 'correctly compares two different headings' do
148
+ a = described_class.new(0)
149
+ b = described_class.new(1.0)
150
+ expect(a == b).to be_false
151
+ end
152
+
153
+ it 'compares to a numeric on LHS' do
154
+ a = described_class.new(Math::PI)
155
+ expect(Math::PI == a).to be_true
156
+ end
157
+ end
158
+
159
+ describe '#eql?' do
160
+ it 'compares types like Numeric, comparing type' do
161
+ a = described_class.new(1.0)
162
+ expect(a.eql?(1)).to be_false
163
+ end
164
+
165
+ it 'correctly compares two equal headings' do
166
+ a = described_class.new(1.0)
167
+ b = described_class.new(1.0)
168
+ expect(a.eql?(b)).to be_true
169
+ end
170
+
171
+ it 'correctly compares two different headings' do
172
+ a = described_class.new(0)
173
+ b = described_class.new(1.0)
174
+ expect(a.eql?(b)).to be_false
175
+ end
176
+ end
177
+
178
+ describe '#<=>' do
179
+ it 'receives headings' do
180
+ a = described_class.new
181
+ b = described_class.new(NINETY)
182
+ expect(a <=> b).to eq -1
183
+ end
184
+
185
+ it 'receives floats' do
186
+ a = described_class.new
187
+ expect(a <=> NINETY).to eq -1
188
+ end
189
+ end
190
+
191
+ describe '#+' do
192
+ it 'returns new instance' do
193
+ a = described_class.new(0.0)
194
+ result = a + NINETY
195
+ expect(result).not_to eql a
196
+ expect(result).not_to equal a
197
+ expect(result).to be_kind_of described_class
198
+ end
199
+
200
+ it 'leaves receiver unchanged' do
201
+ a = described_class.new(1.0)
202
+ a + NINETY
203
+ expect(a).to eql described_class.new(1.0)
204
+ end
205
+
206
+ it 'correctly adds' do
207
+ a = described_class.new(0.0) + NINETY
208
+ expect(a).to eq NINETY
209
+ end
210
+
211
+ it 'correctly adds negative numbers' do
212
+ a = described_class.new(0.0) + -NINETY
213
+ expect(a).to eq(NINETY * 3)
214
+ end
215
+
216
+ it 'correctly adds other headings' do
217
+ a = described_class.new(NINETY * 2)
218
+ b = described_class.new(NINETY)
219
+ expect(a + b).to eq(NINETY * 3)
220
+ end
221
+
222
+ it 'respects radian module ring' do
223
+ a = described_class.new(NINETY * 3)
224
+ b = described_class.new(NINETY * 3)
225
+ expect(a + b).to eq NINETY * 2
226
+ end
227
+ end
228
+
229
+ describe '#*' do
230
+ it 'returns new instance' do
231
+ a = described_class.new(NINETY)
232
+ result = a * NINETY
233
+ expect(result).not_to eql a
234
+ expect(result).not_to equal a
235
+ expect(result).to be_kind_of described_class
236
+ end
237
+
238
+ it 'correctly multiplies' do
239
+ a = described_class.new(NINETY)
240
+ expect(a * 2.0).to eq NINETY * 2.0
241
+ end
242
+ end
243
+
244
+ describe '#/' do
245
+ it 'returns new instance' do
246
+ a = described_class.new(NINETY)
247
+ result = a / NINETY
248
+ expect(result).not_to eql a
249
+ expect(result).not_to equal a
250
+ expect(result).to be_kind_of described_class
251
+ end
252
+
253
+ it 'correctly divides' do
254
+ a = described_class.new(NINETY * 2.0)
255
+ expect(a / 2.0).to eq NINETY
256
+ end
257
+ end
258
+
259
+ describe '#-@' do
260
+ it 'creates a new instance' do
261
+ a = described_class.new(1.0)
262
+ b = -a
263
+ expect(b).not_to equal a
264
+ end
265
+
266
+ it 'correctly negates itself' do
267
+ expect(-described_class.new(NINETY)).to eq(NINETY * 3)
268
+ end
269
+ end
270
+
271
+ describe '#to_degrees' do
272
+ it 'returns float' do
273
+ a = described_class.new
274
+ expect(a.to_degrees).to eq 0.0
275
+ b = described_class.new(NINETY)
276
+ expect(b.to_degrees).to eq 90.0
277
+ end
278
+ end
279
+ end