robot_wars 0.1.0

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +7 -0
  3. data/.loki +10 -0
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +17 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +145 -0
  8. data/RULES.md +121 -0
  9. data/Rakefile +23 -0
  10. data/docs/game_board.svg +88 -0
  11. data/examples/01_random_match.rb +53 -0
  12. data/examples/warriors/opportunist.md +18 -0
  13. data/examples/warriors/oppressor.md +17 -0
  14. data/examples/warriors/sentinel.md +18 -0
  15. data/examples/warriors/wanderer.md +19 -0
  16. data/examples/warriors/warmonger.md +17 -0
  17. data/lib/robot_wars/action.rb +66 -0
  18. data/lib/robot_wars/action_parser.rb +66 -0
  19. data/lib/robot_wars/board.rb +47 -0
  20. data/lib/robot_wars/conflict_resolver.rb +32 -0
  21. data/lib/robot_wars/direction.rb +30 -0
  22. data/lib/robot_wars/fixed_roll_generator.rb +16 -0
  23. data/lib/robot_wars/game.rb +79 -0
  24. data/lib/robot_wars/game_rules.md +44 -0
  25. data/lib/robot_wars/game_rules.rb +12 -0
  26. data/lib/robot_wars/illegal_move_resolver.rb +18 -0
  27. data/lib/robot_wars/llm_pilot.rb +17 -0
  28. data/lib/robot_wars/model_spec.rb +39 -0
  29. data/lib/robot_wars/move_resolver.rb +30 -0
  30. data/lib/robot_wars/occupancy_map.rb +45 -0
  31. data/lib/robot_wars/position.rb +13 -0
  32. data/lib/robot_wars/ranged_combat_resolver.rb +59 -0
  33. data/lib/robot_wars/robot.rb +49 -0
  34. data/lib/robot_wars/roll_generator.rb +15 -0
  35. data/lib/robot_wars/sensing_report.rb +61 -0
  36. data/lib/robot_wars/territory.rb +75 -0
  37. data/lib/robot_wars/turn_resolver.rb +265 -0
  38. data/lib/robot_wars/version.rb +3 -0
  39. data/lib/robot_wars/warrior.rb +22 -0
  40. data/lib/robot_wars.rb +29 -0
  41. data/sig/robot_wars.rbs +6 -0
  42. metadata +100 -0
@@ -0,0 +1,61 @@
1
+ module RobotWars
2
+ # What one robot is told before it acts (RULES.md 33-35): its own
3
+ # position and life, and the full map of owned squares — never any
4
+ # other robot's position.
5
+ #
6
+ # Two details rule 35 leaves open are implemented here as defaults,
7
+ # not settled rules (see notes.md): the map names each square's owner
8
+ # rather than just distinguishing mine/theirs, and the report says how
9
+ # many warriors remain and who they are.
10
+ class SensingReport
11
+ # attack_outcome: :hit or :miss when the robot attacked last turn
12
+ # (see TurnResolver::Report#attack_outcome_for), nil otherwise —
13
+ # Battleship-style feedback, the one thing an attacker learns about
14
+ # where its rivals are.
15
+ def initialize(game:, robot:, attack_outcome: nil)
16
+ @game = game
17
+ @robot = robot
18
+ @attack_outcome = attack_outcome
19
+ end
20
+
21
+ def to_s
22
+ [
23
+ "Turn #{@game.turn_number + 1}.",
24
+ "Your position: #{position_text}. Your life: #{@robot.life}.",
25
+ attack_feedback_text,
26
+ ownership_text,
27
+ survivors_text
28
+ ].compact.join("\n") << "\n"
29
+ end
30
+
31
+ private
32
+
33
+ def attack_feedback_text
34
+ @attack_outcome && "Your attack last turn was a #{@attack_outcome.to_s.upcase}."
35
+ end
36
+
37
+ def position_text
38
+ position = @game.occupancy.position_of(@robot)
39
+ "(#{position.x},#{position.y})"
40
+ end
41
+
42
+ def ownership_text
43
+ owned = @game.territory.each_owned.to_a
44
+ return "No squares are owned yet." if owned.empty?
45
+
46
+ lines = owned.map { |square, owner| " (#{square.x},#{square.y}) owned by #{owner.id}#{mine(owner)}" }
47
+ (["Owned squares:"] + lines).join("\n")
48
+ end
49
+
50
+ # :reek:ControlParameter -- comparing the owner against @robot is the method's entire purpose.
51
+ def mine(owner)
52
+ owner == @robot ? " (yours)" : ""
53
+ end
54
+
55
+ # :reek:FeatureEnvy -- `survivors` is a local snapshot formatted in place; there is nowhere better for it.
56
+ def survivors_text
57
+ survivors = @game.alive_robots
58
+ "#{survivors.size} warriors remain: #{survivors.map(&:id).join(', ')}."
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,75 @@
1
+ module RobotWars
2
+ # Square ownership (RULES.md 22-25). Ownership comes from conquest
3
+ # (#claim!, called by whoever resolves square conflicts) or from
4
+ # occupation (#tick!, three consecutive turns per rule 23), and is
5
+ # released when the owner dies (#release!).
6
+ class Territory
7
+ TURNS_TO_OWN_BY_OCCUPATION = 3
8
+
9
+ def initialize
10
+ @owner_by_position = {}
11
+ @streak_by_position = {}
12
+ end
13
+
14
+ def owner_of(position)
15
+ @owner_by_position[position]
16
+ end
17
+
18
+ def owned?(position)
19
+ @owner_by_position.key?(position)
20
+ end
21
+
22
+ def owned_by?(position, robot)
23
+ owner_of(position) == robot
24
+ end
25
+
26
+ def owned_by_other?(position, robot)
27
+ owned?(position) && !owned_by?(position, robot)
28
+ end
29
+
30
+ def claim!(position, robot)
31
+ @owner_by_position[position] = robot
32
+ end
33
+
34
+ def release!(robot)
35
+ @owner_by_position.delete_if { |_position, owner| owner == robot }
36
+ end
37
+
38
+ def each_owned(&block)
39
+ return enum_for(:each_owned) unless block
40
+
41
+ @owner_by_position.each_pair(&block)
42
+ end
43
+
44
+ # Advances every square's occupation streak from who is standing
45
+ # there this turn, claiming ownership on the 3rd consecutive turn.
46
+ # Returns the squares that BECAME owned on this tick as
47
+ # {position => robot} — squares the robot already owned don't
48
+ # reappear on later ticks.
49
+ def tick!(occupancy_map)
50
+ current_by_position = occupancy_map.each_occupied.to_h
51
+
52
+ stale_positions(current_by_position).each { |position| @streak_by_position.delete(position) }
53
+ current_by_position.filter_map { |position, robot| record_occupation(position, robot) }.to_h
54
+ end
55
+
56
+ private
57
+
58
+ def stale_positions(current_by_position)
59
+ @streak_by_position.keys.reject do |position|
60
+ current_by_position[position] == @streak_by_position[position].fetch(:robot)
61
+ end
62
+ end
63
+
64
+ # Advances the square's streak; returns [position, robot] when the
65
+ # occupation just turned into NEW ownership, nil otherwise.
66
+ def record_occupation(position, robot)
67
+ streak = (@streak_by_position[position] ||= { robot: robot, count: 0 })
68
+ streak[:count] += 1
69
+ return nil if streak.fetch(:count) < TURNS_TO_OWN_BY_OCCUPATION || owned_by?(position, robot)
70
+
71
+ claim!(position, robot)
72
+ [position, robot]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,265 @@
1
+ module RobotWars
2
+ # Runs one full turn end to end (RULES.md rule 40): the movement
3
+ # economy, square conflicts — including cascading return conflicts and
4
+ # displacement-or-death — ranged combat, death processing, and
5
+ # occupation-streak ticking.
6
+ #
7
+ # A returning loser (rule 17) is sent to the square it started the
8
+ # turn on. Two cases the rules leave implicit are resolved here and
9
+ # flagged for confirmation (see notes.md): a robot that never left its
10
+ # square (its "origin" IS the square it just lost) has nowhere
11
+ # meaningful to return to, so it goes straight to displacement rather
12
+ # than re-fighting the robot that just beat it; and each robot's own
13
+ # defeat count (not a per-event count) is what caps a chain at two
14
+ # losses, so displacing a robot only starts THAT robot's own count.
15
+ class TurnResolver
16
+ Report = Data.define(:deaths, :ranged_effects, :conflicts, :solo_conflicts, :claims) do
17
+ # Battleship-style feedback for an attacker: :hit when this
18
+ # robot's attack this turn found a robot on the target square,
19
+ # :miss when it found the square empty, nil when it didn't attack.
20
+ # Counter-fire and unchallenged-defense effects don't count — they
21
+ # aren't the robot's own shot.
22
+ # :reek:ControlParameter -- `robot` is the query subject being looked up, not a behavior switch.
23
+ # :reek:FeatureEnvy -- `effect` is the block's own search variable; there is no better home for a Report query.
24
+ def attack_outcome_for(robot)
25
+ ranged_effects.find { |effect| effect.source == robot && %i[hit miss].include?(effect.kind) }&.kind
26
+ end
27
+
28
+ # The squares this robot NEWLY came to own this turn — by conquest
29
+ # or by completing a 3-turn occupation streak. Squares it already
30
+ # owned are never repeated.
31
+ def claimed_squares_for(robot)
32
+ claims.select { |claim| claim.robot == robot }.map(&:square)
33
+ end
34
+ end
35
+
36
+ # One square becoming newly owned during the turn (rules 22-23).
37
+ Claim = Data.define(:robot, :square)
38
+
39
+ # One resolved square conflict (rules 13-16), kept on the Report so a
40
+ # match transcript can say what happened, not just the resulting life
41
+ # totals. `winner` is nil on a tie.
42
+ Conflict = Data.define(:square, :robots, :roll, :winner, :losers) do
43
+ def to_s
44
+ outcome = winner ? "#{winner.id} takes the square" : "tie, everyone loses"
45
+ "conflict at (#{square.x},#{square.y}): #{robots.map(&:id).join(' vs ')} — roll #{roll}, #{outcome}"
46
+ end
47
+ end
48
+
49
+ # One rule 21 solo conflict — an invalid action or illegal move
50
+ # punished with a random hit and no movement.
51
+ SoloConflict = Data.define(:robot, :roll) do
52
+ def to_s = "solo conflict: #{robot.id} — roll #{roll}, no movement"
53
+ end
54
+
55
+ # :reek:ControlParameter -- `x || Default.new` is an injectable-collaborator fallback, not behavior selection.
56
+ def initialize(board:, occupancy:, territory:, roll_generator: RollGenerator.new, random: Random.new,
57
+ move_resolver: nil, conflict_resolver: nil, illegal_move_resolver: nil)
58
+ @board = board
59
+ @occupancy = occupancy
60
+ @territory = territory
61
+ @random = random
62
+ @move_resolver = move_resolver || MoveResolver.new(board: board, territory: territory)
63
+ @conflict_resolver = conflict_resolver || ConflictResolver.new(roll_generator: roll_generator)
64
+ @illegal_move_resolver = illegal_move_resolver || IllegalMoveResolver.new(roll_generator: roll_generator)
65
+ @conflicts = []
66
+ @solo_conflicts = []
67
+ @claims = []
68
+ end
69
+
70
+ # actions: Hash{Robot => Action}, exactly one entry per living robot
71
+ # on the board (rule 8).
72
+ # :reek:TooManyStatements -- the rule 40 turn sequence, one linear step per phase; splitting it would hide the order.
73
+ def resolve!(actions)
74
+ reset_turn_log
75
+ origins = actions.keys.to_h { |robot| [robot, @occupancy.position_of(robot)] }
76
+
77
+ destinations = apply_movement_economy(actions)
78
+ settled, eliminated = resolve_square_conflicts(destinations, origins)
79
+ commit_occupancy(settled)
80
+
81
+ ranged_effects = resolve_ranged_combat(actions)
82
+ deaths = process_deaths(eliminated)
83
+
84
+ @territory.tick!(@occupancy).each { |square, robot| @claims << Claim.new(robot: robot, square: square) }
85
+
86
+ Report.new(deaths: deaths, ranged_effects: ranged_effects,
87
+ conflicts: @conflicts, solo_conflicts: @solo_conflicts, claims: @claims)
88
+ end
89
+
90
+ private
91
+
92
+ # The per-turn event log the Report is built from, emptied at the
93
+ # top of every resolve!.
94
+ def reset_turn_log
95
+ @conflicts = []
96
+ @solo_conflicts = []
97
+ @claims = []
98
+ end
99
+
100
+ # --- Movement and the life-point economy (rules 9-11, 21) ---------
101
+
102
+ def apply_movement_economy(actions)
103
+ actions.to_h { |robot, action| [robot, intended_square(robot, action)] }
104
+ end
105
+
106
+ # :reek:FeatureEnvy -- dispatching on the action's type is this method's whole job; the Action is pure data.
107
+ def intended_square(robot, action)
108
+ origin = @occupancy.position_of(robot)
109
+ return resolve_move(robot, origin, action.direction) if action.move?
110
+
111
+ if action.invalid?
112
+ punish_solo_conflict(robot)
113
+ elsif action.stay?
114
+ robot.heal(1)
115
+ end
116
+
117
+ origin
118
+ end
119
+
120
+ def resolve_move(robot, origin, direction)
121
+ result = @move_resolver.resolve(robot, origin, direction)
122
+ unless result.legal
123
+ punish_solo_conflict(robot)
124
+ return origin
125
+ end
126
+
127
+ robot.apply_damage(1)
128
+ result.destination
129
+ end
130
+
131
+ def punish_solo_conflict(robot)
132
+ result = @illegal_move_resolver.resolve(robot)
133
+ @solo_conflicts << SoloConflict.new(robot: robot, roll: result.roll)
134
+ end
135
+
136
+ # --- Square conflicts, cascading returns, displacement (13-20) ----
137
+
138
+ # :reek:TooManyStatements -- initial-conflict pass plus the cascading-return queue drain belong together (rules 13-20).
139
+ def resolve_square_conflicts(destinations, origins)
140
+ settled = {}
141
+ eliminated = []
142
+ defeats = Hash.new(0)
143
+ queue = []
144
+
145
+ groups_by_square(destinations).each { |square, contenders| settle(square, contenders, settled, defeats, queue) }
146
+
147
+ until queue.empty?
148
+ robot, lost_at = queue.shift.values_at(:robot, :lost_at)
149
+ return_home(robot, lost_at, origins, settled, defeats, eliminated, queue)
150
+ end
151
+
152
+ [settled, eliminated]
153
+ end
154
+
155
+ def groups_by_square(destinations)
156
+ groups = Hash.new { |hash, square| hash[square] = [] }
157
+ destinations.each { |robot, square| groups[square] << robot }
158
+ groups
159
+ end
160
+
161
+ # Resolves one square's contenders: the sole robot, or the winner of
162
+ # a conflict, settles there and (on conquest) claims it; every loser
163
+ # is queued to attempt its own return.
164
+ # :reek:TooManyStatements -- fight, log, settle-or-vacate, queue losers: one linear pass per contested square.
165
+ def settle(square, contenders, settled, defeats, queue)
166
+ if contenders.one?
167
+ settled[square] = contenders.first
168
+ return
169
+ end
170
+
171
+ @conflict_resolver.resolve(contenders) => { winner:, losers:, roll: }
172
+ @conflicts << Conflict.new(square: square, robots: contenders, roll: roll,
173
+ winner: winner, losers: losers)
174
+
175
+ if winner
176
+ settled[square] = winner
177
+ record_conquest(square, winner)
178
+ else
179
+ settled.delete(square)
180
+ end
181
+
182
+ losers.each do |loser|
183
+ defeats[loser] += 1
184
+ queue << { robot: loser, lost_at: square }
185
+ end
186
+ end
187
+
188
+ # A conquest is always NEW ownership — no robot can legally enter a
189
+ # square someone else owns (and displacement avoids them too), so a
190
+ # conflict never happens on a square its winner already holds.
191
+ def record_conquest(square, winner)
192
+ @territory.claim!(square, winner)
193
+ @claims << Claim.new(robot: winner, square: square)
194
+ end
195
+
196
+ # :reek:LongParameterList -- the cascade's working state (origins/settled/defeats/eliminated/queue) is one
197
+ # turn's transient data; promoting it to ivars or a context object would outlive its single resolve! pass.
198
+ def return_home(robot, lost_at, origins, settled, defeats, eliminated, queue)
199
+ home = origins[robot]
200
+
201
+ if home == lost_at || defeats[robot] >= 2
202
+ displace_or_eliminate(robot, lost_at, settled, eliminated)
203
+ return
204
+ end
205
+
206
+ occupant = settled[home]
207
+ if occupant.nil?
208
+ settled[home] = robot
209
+ return
210
+ end
211
+
212
+ settle(home, [robot, occupant], settled, defeats, queue)
213
+ end
214
+
215
+ def displace_or_eliminate(robot, near, settled, eliminated)
216
+ candidates = @board.neighbors_of(near).reject do |square|
217
+ settled.key?(square) || @territory.owned_by_other?(square, robot)
218
+ end
219
+
220
+ if candidates.empty?
221
+ robot.eliminate!
222
+ eliminated << robot
223
+ else
224
+ settled[candidates.sample(random: @random)] = robot
225
+ end
226
+ end
227
+
228
+ def commit_occupancy(settled)
229
+ @occupancy.each_occupied.to_a.each { |square, _robot| @occupancy.vacate(square) }
230
+ settled.each { |square, robot| @occupancy.place(robot, square) }
231
+ end
232
+
233
+ # --- Ranged combat (26-32) -----------------------------------------
234
+
235
+ def resolve_ranged_combat(actions)
236
+ attacks = actions.filter_map { |robot, action| build_attack(robot, action) if action.attack? }
237
+ defenses = actions.filter_map { |robot, action| build_defense(robot, action) if action.defend? }
238
+
239
+ RangedCombatResolver.new(occupancy_map: @occupancy).resolve(attacks: attacks, defenses: defenses)
240
+ end
241
+
242
+ def build_attack(robot, action)
243
+ RangedCombatResolver::AttackOrder.new(attacker: robot, square: action.square, points: action.points)
244
+ end
245
+
246
+ def build_defense(robot, action)
247
+ RangedCombatResolver::DefendOrder.new(defender: robot, points: action.points)
248
+ end
249
+
250
+ # --- Death processing (25, 36-38) -----------------------------------
251
+
252
+ def process_deaths(already_eliminated)
253
+ newly_dead = @occupancy.each_occupied.to_a.filter_map { |_square, robot| robot if robot.dead? }
254
+ deaths = (already_eliminated + newly_dead).uniq
255
+
256
+ deaths.each do |robot|
257
+ position = @occupancy.position_of(robot)
258
+ @occupancy.vacate(position) if position
259
+ @territory.release!(robot)
260
+ end
261
+
262
+ deaths
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,3 @@
1
+ module RobotWars
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,22 @@
1
+ module RobotWars
2
+ # Pairs a game-state Robot with whatever decides its actions each turn.
3
+ # Game only ever needs the Action that comes out the other end, so the
4
+ # pilot can be an LLMPilot, a scripted proc, or eventually a human —
5
+ # anything answering #decide(report).
6
+ class Warrior
7
+ attr_reader :robot, :pilot
8
+
9
+ def initialize(robot:, pilot:)
10
+ @robot = robot
11
+ @pilot = pilot
12
+ end
13
+
14
+ def id
15
+ robot.id
16
+ end
17
+
18
+ def decide(report)
19
+ pilot.decide(report)
20
+ end
21
+ end
22
+ end
data/lib/robot_wars.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "robot_lab"
2
+
3
+ require_relative "robot_wars/version"
4
+ require_relative "robot_wars/position"
5
+ require_relative "robot_wars/direction"
6
+ require_relative "robot_wars/board"
7
+ require_relative "robot_wars/roll_generator"
8
+ require_relative "robot_wars/fixed_roll_generator"
9
+ require_relative "robot_wars/robot"
10
+ require_relative "robot_wars/action"
11
+ require_relative "robot_wars/occupancy_map"
12
+ require_relative "robot_wars/territory"
13
+ require_relative "robot_wars/move_resolver"
14
+ require_relative "robot_wars/conflict_resolver"
15
+ require_relative "robot_wars/illegal_move_resolver"
16
+ require_relative "robot_wars/ranged_combat_resolver"
17
+ require_relative "robot_wars/turn_resolver"
18
+ require_relative "robot_wars/game"
19
+ require_relative "robot_wars/action_parser"
20
+ require_relative "robot_wars/model_spec"
21
+ require_relative "robot_wars/game_rules"
22
+ require_relative "robot_wars/sensing_report"
23
+ require_relative "robot_wars/warrior"
24
+ require_relative "robot_wars/llm_pilot"
25
+
26
+ module RobotWars
27
+ # Raised for RobotWars-specific misuse.
28
+ class Error < StandardError; end
29
+ end
@@ -0,0 +1,6 @@
1
+ module RobotWars
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: robot_wars
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dewayne VanHoozer
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: robot_lab
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ description: 'A turn-based game built on the RobotLab framework: each robot occupies
27
+ a square on a 2D grid, all robots move simultaneously each turn, conflicts are resolved
28
+ between turns, and the last robot remaining wins.'
29
+ email:
30
+ - dvanhoozer@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".envrc"
36
+ - ".loki"
37
+ - ".rubocop.yml"
38
+ - CHANGELOG.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - RULES.md
42
+ - Rakefile
43
+ - docs/game_board.svg
44
+ - examples/01_random_match.rb
45
+ - examples/warriors/opportunist.md
46
+ - examples/warriors/oppressor.md
47
+ - examples/warriors/sentinel.md
48
+ - examples/warriors/wanderer.md
49
+ - examples/warriors/warmonger.md
50
+ - lib/robot_wars.rb
51
+ - lib/robot_wars/action.rb
52
+ - lib/robot_wars/action_parser.rb
53
+ - lib/robot_wars/board.rb
54
+ - lib/robot_wars/conflict_resolver.rb
55
+ - lib/robot_wars/direction.rb
56
+ - lib/robot_wars/fixed_roll_generator.rb
57
+ - lib/robot_wars/game.rb
58
+ - lib/robot_wars/game_rules.md
59
+ - lib/robot_wars/game_rules.rb
60
+ - lib/robot_wars/illegal_move_resolver.rb
61
+ - lib/robot_wars/llm_pilot.rb
62
+ - lib/robot_wars/model_spec.rb
63
+ - lib/robot_wars/move_resolver.rb
64
+ - lib/robot_wars/occupancy_map.rb
65
+ - lib/robot_wars/position.rb
66
+ - lib/robot_wars/ranged_combat_resolver.rb
67
+ - lib/robot_wars/robot.rb
68
+ - lib/robot_wars/roll_generator.rb
69
+ - lib/robot_wars/sensing_report.rb
70
+ - lib/robot_wars/territory.rb
71
+ - lib/robot_wars/turn_resolver.rb
72
+ - lib/robot_wars/version.rb
73
+ - lib/robot_wars/warrior.rb
74
+ - sig/robot_wars.rbs
75
+ homepage: https://github.com/MadBomber/robot_wars
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/MadBomber/robot_wars
80
+ source_code_uri: https://github.com/MadBomber/robot_wars
81
+ changelog_uri: https://github.com/MadBomber/robot_wars/blob/main/CHANGELOG.md
82
+ rubygems_mfa_required: 'true'
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.2.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 4.0.20
98
+ specification_version: 4
99
+ summary: Turn-based last-robot-standing game for RobotLab robots on a 2D grid board.
100
+ test_files: []