ruby_arena 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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +31 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +118 -0
  8. data/Rakefile +5 -0
  9. data/bin/ruby_arena +7 -0
  10. data/lib/ruby_arena/ai.rb +68 -0
  11. data/lib/ruby_arena/arena.rb +75 -0
  12. data/lib/ruby_arena/bullet.rb +53 -0
  13. data/lib/ruby_arena/bullet_renderer.rb +53 -0
  14. data/lib/ruby_arena/class_loader.rb +20 -0
  15. data/lib/ruby_arena/command_parser.rb +41 -0
  16. data/lib/ruby_arena/game.rb +74 -0
  17. data/lib/ruby_arena/gui.rb +62 -0
  18. data/lib/ruby_arena/level.rb +42 -0
  19. data/lib/ruby_arena/level0.rb +6 -0
  20. data/lib/ruby_arena/level1.rb +9 -0
  21. data/lib/ruby_arena/movable.rb +18 -0
  22. data/lib/ruby_arena/option_parser.rb +29 -0
  23. data/lib/ruby_arena/robot.rb +196 -0
  24. data/lib/ruby_arena/robot_renderer.rb +180 -0
  25. data/lib/ruby_arena/value_sanitizer.rb +11 -0
  26. data/lib/ruby_arena/version.rb +3 -0
  27. data/lib/ruby_arena.rb +23 -0
  28. data/ruby_arena.gemspec +25 -0
  29. data/screenshot.png +0 -0
  30. data/spec/ai_spec.rb +11 -0
  31. data/spec/ai_test.rb +11 -0
  32. data/spec/arena_spec.rb +79 -0
  33. data/spec/bullet_renderer_spec.rb +4 -0
  34. data/spec/bullet_spec.rb +36 -0
  35. data/spec/class_loader_spec.rb +14 -0
  36. data/spec/command_parser_spec.rb +72 -0
  37. data/spec/fake_gui.rb +28 -0
  38. data/spec/game_spec.rb +4 -0
  39. data/spec/gui_spec.rb +50 -0
  40. data/spec/level_spec.rb +29 -0
  41. data/spec/movable_tests.rb +48 -0
  42. data/spec/option_parser_spec.rb +49 -0
  43. data/spec/robot_renderer_spec.rb +4 -0
  44. data/spec/robot_spec.rb +294 -0
  45. data/spec/spec_helper.rb +8 -0
  46. data/spec/value_sanitizer_spec.rb +21 -0
  47. metadata +163 -0
@@ -0,0 +1,6 @@
1
+ module RubyArena
2
+ class Level0 < Ai
3
+ def tick(events)
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module RubyArena
2
+ class Level1 < Ai
3
+ def tick(events)
4
+ set_radar_view_angle(5)
5
+ turn_gun(4)
6
+ fire if events[:scanned_robots].any?
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module RubyArena
2
+ module Movable
3
+ def move
4
+ @x += offset_x(heading, speed)
5
+ @y += offset_y(heading, speed)
6
+ end
7
+
8
+ private
9
+
10
+ def offset_x(heading, speed)
11
+ Gosu.offset_x(heading, speed)
12
+ end
13
+
14
+ def offset_y(heading, speed)
15
+ Gosu.offset_y(heading, speed)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ class OptionParser
2
+ def self.parse(args)
3
+ options = {}
4
+
5
+ opt_parser = OptionParser.new do |opts|
6
+ opts.banner = %{Usage: ruby_arena -l <level> [ai_file]
7
+ Usage: ruby_arena <ai_file1> [ai_file2] ... [ai_fileN]
8
+ }
9
+
10
+ opts.on("-l", "--level level_number", Integer, "Specify level") do |l|
11
+ options[:level] = l
12
+ end
13
+
14
+ if args.empty?
15
+ puts opts
16
+ exit
17
+ end
18
+
19
+ opts.on_tail("-h", "--help", "Show this message") do
20
+ puts opts
21
+ exit
22
+ end
23
+ end
24
+
25
+ opt_parser.parse!(args)
26
+
27
+ options
28
+ end
29
+ end
@@ -0,0 +1,196 @@
1
+ module RubyArena
2
+ class Robot
3
+ include Movable
4
+
5
+ SIZE = 40
6
+ MAX_SPEED = 8
7
+ MAX_TURN_ANGLE = 10
8
+ MAX_TURN_GUN_ANGLE = 30
9
+ MAX_TURN_RADAR_ANGLE = 60
10
+ RADAR_RANGE = 1000
11
+ DEFAULT_RADAR_VIEW_ANGLE = 10
12
+
13
+ attr_reader :ai, :tank, :command_parser, :arena
14
+ attr_reader :x, :y, :speed, :heading, :gun_heading, :radar_heading, :robot
15
+ attr_reader :energy, :gun_heat
16
+ attr_accessor :radar_view_angle
17
+
18
+ def initialize(args)
19
+ @command_parser = CommandParser.new
20
+ @ai = args.fetch(:ai).new(robot: self, command_parser: command_parser)
21
+ @arena = args.fetch(:arena)
22
+ @x = args[:x] || rand(arena.width - 2*size)
23
+ @y = args[:y] || rand(arena.height - 2*size)
24
+ @speed = args[:speed] || 0
25
+ @heading = args[:heading] || 0
26
+ @gun_heading = args[:gun_heading] || heading
27
+ @radar_heading = args[:radar_heading] || gun_heading
28
+ @energy = args[:energy] || 100
29
+ @scanned_robots = []
30
+ @gun_heat = args[:gun_heat] || 0
31
+ @radar_view_angle = args[:radar_view_angle] || DEFAULT_RADAR_VIEW_ANGLE
32
+ end
33
+
34
+ def tick
35
+ ai.tick(tick_events)
36
+ @gun_heat -= 0.1 if @gun_heat > 0
37
+ end
38
+
39
+ def update
40
+ execute_actions(actions)
41
+ reset_actions
42
+ move
43
+ fix_position
44
+ end
45
+
46
+ def execute_actions(actions)
47
+ fire if actions[:fire]
48
+ turn(actions[:turn]) if actions[:turn]
49
+ turn_gun(actions[:turn_gun]) if actions[:turn_gun]
50
+ turn_radar(actions[:turn_radar]) if actions[:turn_radar]
51
+ accelerate if actions[:accelerate]
52
+ decelerate if actions[:decelerate]
53
+ self.radar_view_angle = actions[:radar_view_angle] if actions[:radar_view_angle]
54
+ end
55
+
56
+ def actions
57
+ command_parser.actions
58
+ end
59
+
60
+ def reset_actions
61
+ command_parser.reset_actions
62
+ end
63
+
64
+ def accelerate
65
+ @speed += 1 if speed < MAX_SPEED
66
+ end
67
+
68
+ def decelerate
69
+ @speed -= 1 if speed > -MAX_SPEED
70
+ end
71
+
72
+ def turn(angle)
73
+ angle = sanitize_maximum_value(angle, MAX_TURN_ANGLE)
74
+ self.heading = heading + angle
75
+ self.gun_heading = gun_heading + angle
76
+ self.radar_heading = radar_heading + angle
77
+ end
78
+
79
+ def turn_gun(angle)
80
+ angle = sanitize_maximum_value(angle, MAX_TURN_GUN_ANGLE)
81
+ self.gun_heading = gun_heading + angle
82
+ self.radar_heading = radar_heading + angle
83
+ end
84
+
85
+ def turn_radar(angle)
86
+ angle = sanitize_maximum_value(angle, MAX_TURN_RADAR_ANGLE)
87
+ self.radar_heading = radar_heading + angle
88
+ end
89
+
90
+ def fire
91
+ unless gun_heat > 0
92
+ bullet = new_bullet
93
+ arena.add_bullet(bullet)
94
+ 3.times { bullet.update }
95
+ @gun_heat += 3
96
+ end
97
+ end
98
+
99
+ def hit(bullet)
100
+ @energy -= bullet.energy
101
+ end
102
+
103
+ def dead?
104
+ energy < 0
105
+ end
106
+
107
+ def scan
108
+ other_robots.map do |robot|
109
+ if robot_in_radar_view?(robot)
110
+ Gosu.distance(x, y, robot.x, robot.y)
111
+ end
112
+ end.compact
113
+ end
114
+
115
+ def size
116
+ SIZE
117
+ end
118
+
119
+ def radar_range
120
+ RADAR_RANGE
121
+ end
122
+
123
+ def time
124
+ arena.time
125
+ end
126
+
127
+ private
128
+
129
+ def heading=(angle)
130
+ @heading = normalize_angle(angle)
131
+ end
132
+
133
+ def gun_heading=(angle)
134
+ @gun_heading = normalize_angle(angle)
135
+ end
136
+
137
+ def radar_heading=(angle)
138
+ @radar_heading = normalize_angle(angle)
139
+ end
140
+
141
+ def normalize_angle(angle)
142
+ angle % 360
143
+ end
144
+
145
+ def tick_events
146
+ { scanned_robots: scan }
147
+ end
148
+
149
+ def new_bullet
150
+ Bullet.new(x: x, y: y, heading: gun_heading, arena: arena, origin: self)
151
+ end
152
+
153
+ def robot_in_radar_view?(robot)
154
+ angle_diff(radar_heading, enemy_angle(robot)).abs <= radar_view_angle/2
155
+ end
156
+
157
+ def enemy_angle(robot)
158
+ Gosu.angle(x, y, robot.x, robot.y)
159
+ end
160
+
161
+ def angle_diff(angle1, angle2)
162
+ Gosu.angle_diff(angle1, angle2)
163
+ end
164
+
165
+ def other_robots
166
+ arena.robots.find_all { |robot| robot != self }
167
+ end
168
+
169
+ def fix_position
170
+ @x = min_x if x < min_x
171
+ @x = max_x if x > max_x
172
+ @y = min_y if y < min_y
173
+ @y = max_y if y > max_y
174
+ end
175
+
176
+ def min_x
177
+ size/2
178
+ end
179
+
180
+ def max_x
181
+ arena.width - min_x
182
+ end
183
+
184
+ def min_y
185
+ size/2
186
+ end
187
+
188
+ def max_y
189
+ arena.height - min_y
190
+ end
191
+
192
+ def sanitize_maximum_value(number, maximum_change)
193
+ ValueSanitizer.sanitize(number, maximum_change)
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,180 @@
1
+ module RubyArena
2
+ class RobotRenderer
3
+ attr_reader :robot, :window
4
+
5
+ def initialize(args)
6
+ @robot = args.fetch(:robot)
7
+ @window = args.fetch(:window)
8
+ end
9
+
10
+ def draw
11
+ draw_tank_body
12
+ draw_gun
13
+ draw_radar
14
+ end
15
+
16
+ private
17
+
18
+ def draw_tank_body
19
+ window.rotate(heading, x, y) do
20
+ window.draw_quad(
21
+ x1, y1, color,
22
+ x2, y1, color,
23
+ x2, y2, color,
24
+ x1, y2, color
25
+ )
26
+ end
27
+ end
28
+
29
+ def x
30
+ robot.x
31
+ end
32
+
33
+ def y
34
+ robot.y
35
+ end
36
+
37
+ def x1
38
+ x - size/2
39
+ end
40
+
41
+ def x2
42
+ x + size/2
43
+ end
44
+
45
+ def y1
46
+ y - size/2
47
+ end
48
+
49
+ def y2
50
+ y + size/2
51
+ end
52
+
53
+ def size
54
+ robot.size
55
+ end
56
+
57
+ def heading
58
+ robot.heading
59
+ end
60
+
61
+ def color
62
+ Gosu::Color::WHITE
63
+ end
64
+
65
+ def draw_radar
66
+ color = Gosu::Color.new(40, 0, 255, 0)
67
+
68
+ window.draw_triangle(
69
+ x, y, color,
70
+ radar_x1, radar_y1, color,
71
+ radar_x2, radar_y2, color
72
+ )
73
+
74
+ window.rotate(radar_heading, x, y) do
75
+ window.draw_triangle(
76
+ radar_base_x1, y, color,
77
+ radar_base_x2, y, color,
78
+ x, radar_base_y3, color
79
+ )
80
+ end
81
+ end
82
+
83
+ def radar_x1
84
+ x + Gosu.offset_x(radar_heading - radar_view_angle / 2, radar_range)
85
+ end
86
+
87
+ def radar_x2
88
+ x + Gosu.offset_x(radar_heading + radar_view_angle / 2, radar_range)
89
+ end
90
+
91
+ def radar_y1
92
+ y + Gosu.offset_y(radar_heading - radar_view_angle / 2, radar_range)
93
+ end
94
+
95
+ def radar_y2
96
+ y + Gosu.offset_y(radar_heading + radar_view_angle / 2, radar_range)
97
+ end
98
+
99
+ def radar_base_x1
100
+ x + 4
101
+ end
102
+
103
+ def radar_base_x2
104
+ x - 4
105
+ end
106
+
107
+ def radar_base_y3
108
+ y + 6
109
+ end
110
+
111
+ def radar_heading
112
+ robot.radar_heading
113
+ end
114
+
115
+ def radar_view_angle
116
+ robot.radar_view_angle
117
+ end
118
+
119
+ def radar_range
120
+ robot.radar_range
121
+ end
122
+
123
+ def draw_gun
124
+ window.rotate(gun_heading, x, y) do
125
+ window.draw_quad(
126
+ gun_x1, gun_y1, gun_color,
127
+ gun_x2, gun_y1, gun_color,
128
+ gun_x2, gun_y2, gun_color,
129
+ gun_x1, gun_y2, gun_color
130
+ )
131
+ window.draw_quad(
132
+ gun_base_x1, gun_base_y1, gun_color,
133
+ gun_base_x2, gun_base_y1, gun_color,
134
+ gun_base_x2, gun_base_y2, gun_color,
135
+ gun_base_x1, gun_base_y2, gun_color
136
+ )
137
+ end
138
+ end
139
+
140
+ def gun_x1
141
+ x - 2
142
+ end
143
+
144
+ def gun_x2
145
+ x + 2
146
+ end
147
+
148
+ def gun_y1
149
+ y - size/1.5
150
+ end
151
+
152
+ def gun_y2
153
+ y
154
+ end
155
+
156
+ def gun_base_x1
157
+ x - size/4
158
+ end
159
+
160
+ def gun_base_x2
161
+ x + size/4
162
+ end
163
+
164
+ def gun_base_y1
165
+ y - size/4
166
+ end
167
+
168
+ def gun_base_y2
169
+ y + size/4
170
+ end
171
+
172
+ def gun_heading
173
+ robot.gun_heading
174
+ end
175
+
176
+ def gun_color
177
+ Gosu::Color::GRAY
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,11 @@
1
+ module RubyArena
2
+ class ValueSanitizer
3
+ def self.sanitize(value, maximum_allowed_change)
4
+ if value.abs <= maximum_allowed_change
5
+ value
6
+ else
7
+ value < 0 ? -maximum_allowed_change : maximum_allowed_change
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module RubyArena
2
+ VERSION = "0.0.2"
3
+ end
data/lib/ruby_arena.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'gosu'
2
+ require 'optparse'
3
+
4
+ require 'ruby_arena/ai'
5
+ require 'ruby_arena/arena'
6
+ require 'ruby_arena/bullet_renderer'
7
+ require 'ruby_arena/class_loader'
8
+ require 'ruby_arena/command_parser'
9
+ require 'ruby_arena/gui'
10
+ require 'ruby_arena/value_sanitizer'
11
+ require 'ruby_arena/movable'
12
+ require 'ruby_arena/robot'
13
+ require 'ruby_arena/level0'
14
+ require 'ruby_arena/level1'
15
+ require 'ruby_arena/level'
16
+ require 'ruby_arena/game'
17
+ require 'ruby_arena/bullet'
18
+ require 'ruby_arena/option_parser'
19
+ require 'ruby_arena/robot_renderer'
20
+ require "ruby_arena/version"
21
+
22
+ module RubyArena
23
+ end
@@ -0,0 +1,25 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'ruby_arena/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruby_arena"
7
+ spec.version = RubyArena::VERSION
8
+ spec.authors = ["Patrik Bóna"]
9
+ spec.email = ["hello@mrhead.sk"]
10
+ spec.summary = %q{Ruby Arena for fights between robots with own AI.}
11
+ spec.description = %q{Ruby Arena for fights between robots with own AI.}
12
+ spec.homepage = "https://github.com/mrhead/ruby_arena"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency "gosu"
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.5"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec", "~> 3.0.0.beta1"
25
+ end
data/screenshot.png ADDED
Binary file
data/spec/ai_spec.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyArena:: Ai do
4
+ describe '#tick' do
5
+ it 'raises NotImplementedError' do
6
+ ai = RubyArena::Ai.new(robot: nil, command_parser: nil)
7
+
8
+ expect { ai.tick }.to raise_error NotImplementedError
9
+ end
10
+ end
11
+ end
data/spec/ai_test.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ class AiTest
4
+ def initialize(args = {})
5
+
6
+ end
7
+
8
+ def tick
9
+ puts '#tick!'
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyArena:: Arena do
4
+ describe '#add_robot' do
5
+ it 'adds new robot' do
6
+ arena.add_robot(:robot)
7
+
8
+ expect(arena.robots).to eq [:robot]
9
+ end
10
+ end
11
+
12
+ describe '#add_bullet' do
13
+ it 'adds new robot' do
14
+ arena.add_bullet(:bullet)
15
+
16
+ expect(arena.bullets).to eq [:bullet]
17
+ end
18
+ end
19
+
20
+ describe '#update' do
21
+ it 'calls #tick on all robots and then #update on all robots' do
22
+ arena.add_robot(robot)
23
+
24
+ expect(robot).to receive(:tick)
25
+ expect(robot).to receive(:update)
26
+
27
+ arena.update
28
+ end
29
+
30
+ it 'removes robot if it is dead' do
31
+ robot = robot(dead?: true)
32
+
33
+ arena.add_robot(robot)
34
+ arena.update
35
+
36
+ expect(arena.robots).to eq []
37
+ end
38
+
39
+ it 'removes bullet if it is dead' do
40
+ bullet = robot(dead?: true)
41
+
42
+ arena.add_bullet(bullet)
43
+ arena.update
44
+
45
+ expect(arena.bullets).to eq []
46
+ end
47
+
48
+ it 'increases time by 1' do
49
+ arena.update
50
+
51
+ expect(arena.time).to be 1
52
+ end
53
+ end
54
+
55
+ describe '#game_over?' do
56
+ it 'returns false with empty arena' do
57
+ expect(arena.game_over?).to be false
58
+ end
59
+
60
+ it 'returns true if number of alive robots drops to 1 and less' do
61
+ robot1 = double('robot1', dead?: false, tick: nil, update: nil)
62
+ robot2 = double('robot2', dead?: true, tick: nil, update: nil)
63
+
64
+ arena.add_robot(robot1)
65
+ arena.add_robot(robot2)
66
+ arena.update
67
+
68
+ expect(arena.game_over?). to be true
69
+ end
70
+ end
71
+
72
+ def arena
73
+ @_arena ||= RubyArena::Arena.new
74
+ end
75
+
76
+ def robot(args = {})
77
+ @_robot ||= double('robot', tick: nil, dead?: args[:dead?], update: nil)
78
+ end
79
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyArena:: BulletRenderer do
4
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyArena:: Bullet do
4
+ it_behaves_like 'movable'
5
+
6
+ describe '#intersect?' do
7
+ it 'returns true if given object intersects bullet' do
8
+ robot = double('robot', x: 10, y: 10, size: 30)
9
+ bullet = bullet(x: 0, y: 0)
10
+
11
+ expect(bullet.intersect?(robot)).to be true
12
+ end
13
+
14
+ it 'returns false if given object doesnt intersect bullet' do
15
+ robot = double('robot', x: 100, y: 10, size: 30)
16
+ bullet = bullet(x: 0, y: 0)
17
+
18
+ expect(bullet.intersect?(robot)).to be false
19
+ end
20
+ end
21
+
22
+ def object(args = {})
23
+ bullet(args)
24
+ end
25
+
26
+ def bullet(args = {})
27
+ defaults = {
28
+ x: 0,
29
+ y: 0,
30
+ heading: 0,
31
+ arena: nil,
32
+ origin: nil
33
+ }
34
+ @_bullet ||= RubyArena::Bullet.new(defaults.merge(args))
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyArena:: ClassLoader do
4
+ describe '#class_from_file' do
5
+ it 'returns class loaded from a file' do
6
+ file = File.dirname(__FILE__) + '/ai_test.rb'
7
+ class_loader = RubyArena::ClassLoader.new(file)
8
+
9
+ klass = class_loader.get_class
10
+
11
+ expect(klass).to be AiTest
12
+ end
13
+ end
14
+ end