colstrom-rtanque 0.1.3

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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/.yardopts +4 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.ci +6 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +168 -0
  12. data/Rakefile +1 -0
  13. data/bin/rtanque +117 -0
  14. data/lib/rtanque.rb +31 -0
  15. data/lib/rtanque/arena.rb +8 -0
  16. data/lib/rtanque/bot.rb +117 -0
  17. data/lib/rtanque/bot/brain.rb +50 -0
  18. data/lib/rtanque/bot/brain_helper.rb +23 -0
  19. data/lib/rtanque/bot/command.rb +23 -0
  20. data/lib/rtanque/bot/radar.rb +54 -0
  21. data/lib/rtanque/bot/sensors.rb +37 -0
  22. data/lib/rtanque/bot/turret.rb +14 -0
  23. data/lib/rtanque/configuration.rb +47 -0
  24. data/lib/rtanque/explosion.rb +23 -0
  25. data/lib/rtanque/gui.rb +25 -0
  26. data/lib/rtanque/gui/bot.rb +71 -0
  27. data/lib/rtanque/gui/bot/health_color_calculator.rb +37 -0
  28. data/lib/rtanque/gui/draw_group.rb +30 -0
  29. data/lib/rtanque/gui/explosion.rb +25 -0
  30. data/lib/rtanque/gui/shell.rb +31 -0
  31. data/lib/rtanque/gui/window.rb +64 -0
  32. data/lib/rtanque/heading.rb +162 -0
  33. data/lib/rtanque/match.rb +73 -0
  34. data/lib/rtanque/match/tick_group.rb +50 -0
  35. data/lib/rtanque/movable.rb +51 -0
  36. data/lib/rtanque/normalized_attr.rb +69 -0
  37. data/lib/rtanque/point.rb +140 -0
  38. data/lib/rtanque/runner.rb +88 -0
  39. data/lib/rtanque/shell.rb +44 -0
  40. data/lib/rtanque/version.rb +3 -0
  41. data/resources/images/body.png +0 -0
  42. data/resources/images/bullet.png +0 -0
  43. data/resources/images/explosions/explosion2-1.png +0 -0
  44. data/resources/images/explosions/explosion2-10.png +0 -0
  45. data/resources/images/explosions/explosion2-11.png +0 -0
  46. data/resources/images/explosions/explosion2-12.png +0 -0
  47. data/resources/images/explosions/explosion2-13.png +0 -0
  48. data/resources/images/explosions/explosion2-14.png +0 -0
  49. data/resources/images/explosions/explosion2-15.png +0 -0
  50. data/resources/images/explosions/explosion2-16.png +0 -0
  51. data/resources/images/explosions/explosion2-17.png +0 -0
  52. data/resources/images/explosions/explosion2-18.png +0 -0
  53. data/resources/images/explosions/explosion2-19.png +0 -0
  54. data/resources/images/explosions/explosion2-2.png +0 -0
  55. data/resources/images/explosions/explosion2-20.png +0 -0
  56. data/resources/images/explosions/explosion2-21.png +0 -0
  57. data/resources/images/explosions/explosion2-22.png +0 -0
  58. data/resources/images/explosions/explosion2-23.png +0 -0
  59. data/resources/images/explosions/explosion2-24.png +0 -0
  60. data/resources/images/explosions/explosion2-25.png +0 -0
  61. data/resources/images/explosions/explosion2-26.png +0 -0
  62. data/resources/images/explosions/explosion2-27.png +0 -0
  63. data/resources/images/explosions/explosion2-28.png +0 -0
  64. data/resources/images/explosions/explosion2-29.png +0 -0
  65. data/resources/images/explosions/explosion2-3.png +0 -0
  66. data/resources/images/explosions/explosion2-30.png +0 -0
  67. data/resources/images/explosions/explosion2-31.png +0 -0
  68. data/resources/images/explosions/explosion2-32.png +0 -0
  69. data/resources/images/explosions/explosion2-33.png +0 -0
  70. data/resources/images/explosions/explosion2-34.png +0 -0
  71. data/resources/images/explosions/explosion2-35.png +0 -0
  72. data/resources/images/explosions/explosion2-36.png +0 -0
  73. data/resources/images/explosions/explosion2-37.png +0 -0
  74. data/resources/images/explosions/explosion2-38.png +0 -0
  75. data/resources/images/explosions/explosion2-39.png +0 -0
  76. data/resources/images/explosions/explosion2-4.png +0 -0
  77. data/resources/images/explosions/explosion2-40.png +0 -0
  78. data/resources/images/explosions/explosion2-41.png +0 -0
  79. data/resources/images/explosions/explosion2-42.png +0 -0
  80. data/resources/images/explosions/explosion2-43.png +0 -0
  81. data/resources/images/explosions/explosion2-44.png +0 -0
  82. data/resources/images/explosions/explosion2-45.png +0 -0
  83. data/resources/images/explosions/explosion2-46.png +0 -0
  84. data/resources/images/explosions/explosion2-47.png +0 -0
  85. data/resources/images/explosions/explosion2-48.png +0 -0
  86. data/resources/images/explosions/explosion2-49.png +0 -0
  87. data/resources/images/explosions/explosion2-5.png +0 -0
  88. data/resources/images/explosions/explosion2-50.png +0 -0
  89. data/resources/images/explosions/explosion2-51.png +0 -0
  90. data/resources/images/explosions/explosion2-52.png +0 -0
  91. data/resources/images/explosions/explosion2-53.png +0 -0
  92. data/resources/images/explosions/explosion2-54.png +0 -0
  93. data/resources/images/explosions/explosion2-55.png +0 -0
  94. data/resources/images/explosions/explosion2-56.png +0 -0
  95. data/resources/images/explosions/explosion2-57.png +0 -0
  96. data/resources/images/explosions/explosion2-58.png +0 -0
  97. data/resources/images/explosions/explosion2-59.png +0 -0
  98. data/resources/images/explosions/explosion2-6.png +0 -0
  99. data/resources/images/explosions/explosion2-60.png +0 -0
  100. data/resources/images/explosions/explosion2-61.png +0 -0
  101. data/resources/images/explosions/explosion2-62.png +0 -0
  102. data/resources/images/explosions/explosion2-63.png +0 -0
  103. data/resources/images/explosions/explosion2-64.png +0 -0
  104. data/resources/images/explosions/explosion2-65.png +0 -0
  105. data/resources/images/explosions/explosion2-66.png +0 -0
  106. data/resources/images/explosions/explosion2-67.png +0 -0
  107. data/resources/images/explosions/explosion2-68.png +0 -0
  108. data/resources/images/explosions/explosion2-69.png +0 -0
  109. data/resources/images/explosions/explosion2-7.png +0 -0
  110. data/resources/images/explosions/explosion2-70.png +0 -0
  111. data/resources/images/explosions/explosion2-71.png +0 -0
  112. data/resources/images/explosions/explosion2-8.png +0 -0
  113. data/resources/images/explosions/explosion2-9.png +0 -0
  114. data/resources/images/grass.png +0 -0
  115. data/resources/images/radar.png +0 -0
  116. data/resources/images/turret.png +0 -0
  117. data/rtanque.gemspec +34 -0
  118. data/sample_bots/camper.rb +79 -0
  119. data/sample_bots/keyboard.rb +50 -0
  120. data/sample_bots/seek_and_destroy.rb +53 -0
  121. data/screenshots/battle_1.png +0 -0
  122. data/screenshots/battle_2.png +0 -0
  123. data/spec/rtanque/bot_spec.rb +239 -0
  124. data/spec/rtanque/heading_spec.rb +279 -0
  125. data/spec/rtanque/match_spec.rb +46 -0
  126. data/spec/rtanque/normalized_attr_spec.rb +90 -0
  127. data/spec/rtanque/point_spec.rb +196 -0
  128. data/spec/rtanque/radar_spec.rb +87 -0
  129. data/spec/rtanque/shell_spec.rb +35 -0
  130. data/spec/rtanque_spec.rb +34 -0
  131. data/spec/spec_helper.rb +51 -0
  132. data/templates/bot.erb +17 -0
  133. metadata +315 -0
@@ -0,0 +1,25 @@
1
+ require 'gosu'
2
+
3
+ module RTanque
4
+ module Gui
5
+ class Explosion
6
+ FRAMES = (1..71)
7
+
8
+ def initialize(window, explosion)
9
+ @window = window
10
+ @explosion = explosion
11
+ @position = [explosion.position.x, window.height - explosion.position.y]
12
+ @explosion_images = FRAMES.map { |i| Gosu::Image.new(@window, Gui.resource_path("images/explosions/explosion2-#{i}.png")) }
13
+ end
14
+
15
+ def draw
16
+ frame.draw_rot(@position[0], @position[1], 5, ZOrder::EXPLOSION)
17
+ end
18
+
19
+ def frame
20
+ @frames_length ||= @explosion_images.length - 1
21
+ @explosion_images[(@explosion.percent_dead * @frames_length).floor]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'gosu'
2
+
3
+ module RTanque
4
+ module Gui
5
+ class Shell
6
+ attr_reader :shell
7
+
8
+ DEBUG = ENV["DEBUG_SHELLS"]
9
+
10
+ def initialize(window, shell)
11
+ @window = window
12
+ @shell = shell
13
+ @x0 = shell.position.x
14
+ @y0 = @window.height - shell.position.y
15
+ @shell_image = Gosu::Image.new(@window, Gui.resource_path("images/bullet.png"))
16
+ end
17
+
18
+ def draw
19
+ position = [self.shell.position.x, @window.height - self.shell.position.y]
20
+ @shell_image.draw_rot(position[0], position[1], ZOrder::SHELL, 0, 0.5, 0.5)
21
+
22
+ if DEBUG then
23
+ white = Gosu::Color::WHITE
24
+ pos = shell.position
25
+ x1, y1 = pos.x, @window.height - pos.y
26
+ @window.draw_line @x0, @y0, white, x1, y1, white, ZOrder::SHELL
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+ require 'gosu'
2
+
3
+ module RTanque
4
+ module Gui
5
+ class Window < Gosu::Window
6
+ UPDATE_INTERVAL = Configuration.gui.update_interval
7
+ FONT_NAME = Gosu::default_font_name
8
+ SMALL_FONT_SIZE = Configuration.gui.fonts.small
9
+
10
+ attr_reader :gui_bots, :gui_shells, :gui_explosions
11
+
12
+ def initialize(match)
13
+ @match = match
14
+ @arena = match.arena
15
+ match.bots.each { |bot| bot.gui_window = self }
16
+ @gui_bots = DrawGroup.new(match.bots) do |bot|
17
+ RTanque::Gui::Bot.new(self, bot)
18
+ end
19
+ @gui_shells = DrawGroup.new(match.shells) do |shell|
20
+ RTanque::Gui::Shell.new(self, shell)
21
+ end
22
+ @gui_explosions = DrawGroup.new(match.explosions) do |explosion|
23
+ RTanque::Gui::Explosion.new(self, explosion)
24
+ end
25
+ # Fullscreen: https://github.com/jlnr/gosu/issues/159#issuecomment-12473172
26
+ super(@arena.width, @arena.height, false, UPDATE_INTERVAL)
27
+ self.caption = self.class.name.split('::').first
28
+ @background = Gosu::Image.new(self, Gui.resource_path("images/grass.png"))
29
+
30
+ @draw_procs = []
31
+ end
32
+
33
+ def update
34
+ @match.tick
35
+ end
36
+
37
+ def draw
38
+ self.close if button_down?(Gosu::Button::KbEscape)
39
+ @background.draw(0, 0, ZOrder::BACKGROUND)
40
+ if @match.finished?
41
+ self.close if Configuration.quit_when_finished
42
+ self.gui_bots.each { |bot| bot.grow(2) }
43
+ end
44
+ self.draw_drawables
45
+ end
46
+
47
+ def draw_drawables
48
+ self.gui_bots.draw
49
+ self.gui_shells.draw
50
+ self.gui_explosions.draw
51
+
52
+ @draw_procs.each do |proc|
53
+ proc.call self
54
+ end
55
+
56
+ @draw_procs.clear
57
+ end
58
+
59
+ def add_draw_proc &b
60
+ @draw_procs << b
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,162 @@
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(Kernel.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 = radians.to_f % 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.to_f < self.to_f ? -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 + other_heading.to_f)
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 * other_heading.to_f)
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
+ end
162
+ end
@@ -0,0 +1,73 @@
1
+ module RTanque
2
+ class Match
3
+ attr_reader :arena, :bots, :shells, :explosions, :ticks, :max_ticks, :teams
4
+
5
+ def initialize(arena, max_ticks = nil, teams = false)
6
+ @arena = arena
7
+ @max_ticks = max_ticks
8
+ @teams = teams
9
+ @ticks = 0
10
+ @shells = TickGroup.new
11
+ @bots = TickGroup.new
12
+ @explosions = TickGroup.new
13
+ @bots.pre_tick(&method(:pre_bot_tick))
14
+ @bots.post_tick(&method(:post_bot_tick))
15
+ @shells.pre_tick(&method(:pre_shell_tick))
16
+ @stopped = false
17
+ end
18
+
19
+ def teams=(bool)
20
+ @teams = bool
21
+ end
22
+
23
+ def max_ticks_reached?
24
+ self.max_ticks && self.ticks >= self.max_ticks
25
+ end
26
+
27
+ def finished?
28
+ @stopped || self.max_ticks_reached? || self.bots.count <= 1 ||
29
+ (self.teams && self.bots.map(&:name).uniq.size == 1)
30
+ end
31
+
32
+ def add_bots(*bots)
33
+ self.bots.add(*bots)
34
+ end
35
+
36
+ def start
37
+ self.tick until self.finished?
38
+ end
39
+
40
+ def stop
41
+ @stopped = true
42
+ end
43
+
44
+ def pre_bot_tick(bot)
45
+ bot.radar.scan(self.bots.all_but(bot))
46
+ end
47
+
48
+ def post_bot_tick(bot)
49
+ if bot.firing?
50
+ # shell starts life at the end of the turret
51
+ shell_position = bot.position.move(bot.turret.heading, RTanque::Bot::Turret::LENGTH)
52
+ @shells.add(RTanque::Shell.new(bot, shell_position, bot.turret.heading.clone, bot.fire_power))
53
+ end
54
+ end
55
+
56
+ def pre_shell_tick(shell)
57
+ shell.hits(self.bots.all_but(shell.bot)) do |origin_bot, bot_hit|
58
+ damage = (shell.fire_power**RTanque::Shell::RATIO)
59
+ bot_hit.reduce_health(damage)
60
+ if bot_hit.dead?
61
+ @explosions.add(Explosion.new(bot_hit.position))
62
+ end
63
+ end
64
+ end
65
+
66
+ def tick
67
+ self.shells.tick
68
+ self.bots.tick
69
+ self.explosions.tick
70
+ @ticks += 1
71
+ end
72
+ end
73
+ 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.to_a - to_exclude
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