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.
- checksums.yaml +7 -0
- data/.envrc +7 -0
- data/.loki +10 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +145 -0
- data/RULES.md +121 -0
- data/Rakefile +23 -0
- data/docs/game_board.svg +88 -0
- data/examples/01_random_match.rb +53 -0
- data/examples/warriors/opportunist.md +18 -0
- data/examples/warriors/oppressor.md +17 -0
- data/examples/warriors/sentinel.md +18 -0
- data/examples/warriors/wanderer.md +19 -0
- data/examples/warriors/warmonger.md +17 -0
- data/lib/robot_wars/action.rb +66 -0
- data/lib/robot_wars/action_parser.rb +66 -0
- data/lib/robot_wars/board.rb +47 -0
- data/lib/robot_wars/conflict_resolver.rb +32 -0
- data/lib/robot_wars/direction.rb +30 -0
- data/lib/robot_wars/fixed_roll_generator.rb +16 -0
- data/lib/robot_wars/game.rb +79 -0
- data/lib/robot_wars/game_rules.md +44 -0
- data/lib/robot_wars/game_rules.rb +12 -0
- data/lib/robot_wars/illegal_move_resolver.rb +18 -0
- data/lib/robot_wars/llm_pilot.rb +17 -0
- data/lib/robot_wars/model_spec.rb +39 -0
- data/lib/robot_wars/move_resolver.rb +30 -0
- data/lib/robot_wars/occupancy_map.rb +45 -0
- data/lib/robot_wars/position.rb +13 -0
- data/lib/robot_wars/ranged_combat_resolver.rb +59 -0
- data/lib/robot_wars/robot.rb +49 -0
- data/lib/robot_wars/roll_generator.rb +15 -0
- data/lib/robot_wars/sensing_report.rb +61 -0
- data/lib/robot_wars/territory.rb +75 -0
- data/lib/robot_wars/turn_resolver.rb +265 -0
- data/lib/robot_wars/version.rb +3 -0
- data/lib/robot_wars/warrior.rb +22 -0
- data/lib/robot_wars.rb +29 -0
- data/sig/robot_wars.rbs +6 -0
- 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,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
|
data/sig/robot_wars.rbs
ADDED
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: []
|