colstrom-rtanque 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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