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,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,40 @@
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 initialize(bot, position, heading, fire_power)
9
+ @bot = bot
10
+ @arena = bot.arena
11
+ @fire_power = fire_power
12
+ self.position = position
13
+ self.heading = heading
14
+ self.speed = (fire_power * SHELL_SPEED_FACTOR) # TODO: add bot's relative speed in this heading
15
+ @dead = false
16
+ end
17
+
18
+ def bound_to_arena
19
+ false
20
+ end
21
+
22
+ def dead?
23
+ @dead ||= self.position.outside_arena?
24
+ end
25
+
26
+ def dead!
27
+ @dead = true
28
+ end
29
+
30
+ def hits(bots, &on_hit)
31
+ bots.each do |hit_bot|
32
+ if hit_bot.position.within_radius?(self.position, Bot::RADIUS)
33
+ self.dead!
34
+ on_hit.call(self.bot, hit_bot) if on_hit
35
+ break
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module RTanque
2
+ VERSION = '0.0.2'
3
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
data/rtanque.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rtanque/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rtanque"
8
+ gem.version = RTanque::VERSION
9
+ gem.authors = ["Adam Williams"]
10
+ gem.email = ["pwnfactory@gmail.com"]
11
+ gem.summary = %q{RTanque is a game for programmers. Players program the brain of a tank and then send their tank into battle against other tanks.}
12
+ gem.description = %q{RTanque is a game for programmers. Players program the brain of a tank and then send their tank+brain into battle with other tanks. All tanks are otherwise equal.
13
+
14
+ Rules of the game are simple: Last bot standing wins. Gameplay is also pretty simple. Each tank has a base, turret and radar, each of which rotate independently. The base moves the tank, the turret has a gun mounted to it which can fire at other tanks, and the radar detects other tanks in its field of vision.
15
+
16
+ Have fun competing against friends' tanks or the sample ones included. Maybe you'll start a small league at your local Ruby meetup.}
17
+ gem.homepage = "https://github.com/awilliams/RTanque"
18
+ gem.license = 'MIT'
19
+
20
+ gem.files = `git ls-files`.split($/)
21
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
22
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
23
+ gem.require_paths = ["lib"]
24
+
25
+ gem.add_dependency 'gosu', '~> 0.7.45'
26
+ gem.add_dependency 'configuration', '~> 1.3.2'
27
+ gem.add_dependency 'octokit', '~> 1.23.0'
28
+ gem.add_dependency 'thor', '~> 0.17.0'
29
+
30
+ gem.add_development_dependency 'pry'
31
+ gem.add_development_dependency 'rspec', '~> 2.13.0'
32
+ gem.add_development_dependency 'rspec-mocks', '~> 2.13.0'
33
+ end
@@ -0,0 +1,79 @@
1
+ # Camper: Sample Bot
2
+ #
3
+ # Enjoys sitting in the corners and firing powerful shots
4
+ class Camper < RTanque::Bot::Brain
5
+ NAME = 'Camper'
6
+ include RTanque::Bot::BrainHelper
7
+
8
+ CORNERS = [:NW, :NE, :SE, :SW]
9
+ TURRET_FIRE_RANGE = RTanque::Heading::ONE_DEGREE * 1.0
10
+ SWITCH_CORNER_TICK_RANGE = (600..1000)
11
+
12
+ def tick!
13
+ self.hide_in_corners
14
+ if (target = self.nearest_target)
15
+ self.fire_upon(target)
16
+ else
17
+ self.detect_targets
18
+ end
19
+ end
20
+
21
+ def fire_upon(target)
22
+ self.command.radar_heading = target.heading
23
+ self.command.turret_heading = target.heading
24
+ if self.sensors.turret_heading.delta(target.heading).abs < TURRET_FIRE_RANGE
25
+ self.command.fire(MAX_FIRE_POWER)
26
+ end
27
+ end
28
+
29
+ def nearest_target
30
+ self.sensors.radar.min { |a,b| a.distance <=> b.distance }
31
+ end
32
+
33
+ def detect_targets
34
+ self.command.radar_heading = self.sensors.radar_heading + MAX_RADAR_ROTATION
35
+ self.command.turret_heading = self.sensors.heading + RTanque::Heading::HALF_ANGLE
36
+ end
37
+
38
+ def hide_in_corners
39
+ @corner_cycle ||= CORNERS.shuffle.cycle
40
+ self.at_tick_interval(self.camp_interval) {
41
+ self.corner = @corner_cycle.next
42
+ self.reset_camp_interval
43
+ }
44
+ self.corner ||= @corner_cycle.next
45
+ self.move_to_corner
46
+ end
47
+
48
+ def move_to_corner
49
+ if self.corner
50
+ command.heading = self.sensors.position.heading(RTanque::Point.new(*self.corner, self.arena))
51
+ command.speed = MAX_BOT_SPEED
52
+ end
53
+ end
54
+
55
+ def corner=(corner_name)
56
+ @corner = case corner_name
57
+ when :NE
58
+ [self.arena.width, self.arena.height]
59
+ when :SE
60
+ [self.arena.width, 0]
61
+ when :SW
62
+ [0, 0]
63
+ else
64
+ [0, self.arena.height]
65
+ end
66
+ end
67
+
68
+ def corner
69
+ @corner
70
+ end
71
+
72
+ def camp_interval
73
+ @camp_interval ||= self.reset_camp_interval
74
+ end
75
+
76
+ def reset_camp_interval
77
+ @camp_interval = rand(SWITCH_CORNER_TICK_RANGE.max - SWITCH_CORNER_TICK_RANGE.min) + SWITCH_CORNER_TICK_RANGE.min
78
+ end
79
+ end
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # Keyboard: Sample Bot
3
+ #
4
+ # Special bot controlled with the keyboard
5
+ #
6
+ # Controls:
7
+ # w drive forward
8
+ # s drive backwards
9
+ # a rotate left
10
+ # d rotate right
11
+ #
12
+ # ↑ fire powerful shot
13
+ # ↓ fire week shot
14
+ # ← rotate turret left
15
+ # → rotate turret right
16
+ class Keyboard < RTanque::Bot::Brain
17
+ include RTanque::Bot::BrainHelper
18
+
19
+ NAME = 'Keyboard'
20
+
21
+ def tick!
22
+ command.radar_heading = sensors.radar_heading + (RTanque::Heading::ONE_DEGREE * 30)
23
+
24
+ if sensors.button_down?(Gosu::KbLeft)
25
+ command.turret_heading = sensors.turret_heading - (RTanque::Heading::ONE_DEGREE * 10)
26
+ elsif sensors.button_down?(Gosu::KbRight)
27
+ command.turret_heading = sensors.turret_heading + (RTanque::Heading::ONE_DEGREE * 10)
28
+ end
29
+
30
+ if sensors.button_down?('a')
31
+ command.heading = sensors.heading - (RTanque::Heading::ONE_DEGREE * 10)
32
+ elsif sensors.button_down?('d')
33
+ command.heading = sensors.heading + (RTanque::Heading::ONE_DEGREE * 10)
34
+ end
35
+
36
+ if sensors.button_down?('w')
37
+ command.speed = 10
38
+ elsif sensors.button_down?('s')
39
+ command.speed = -10
40
+ else
41
+ command.speed = 0
42
+ end
43
+
44
+ if sensors.button_down?(Gosu::KbUp)
45
+ command.fire_power = 5
46
+ elsif sensors.button_down?(Gosu::KbDown)
47
+ command.fire_power = 1
48
+ end
49
+ end
50
+ end