rubowar 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fac80228c1207c6235ca6d4eaa20f45fd1d3315a0d84355c1c6fb596f3c1de3a
4
+ data.tar.gz: 62a05e81bf17290816e8be0c8890cf0e27e0c082e843daa45ad4694b4da9bf86
5
+ SHA512:
6
+ metadata.gz: dc5de3e1387a90f2a090fb81cb0dc700f084c8370504604a85657d020cc1a97814d61dacf7858082766cc326b886eb45f9f93973c8b07a130157451193711c71
7
+ data.tar.gz: b8c6c84db2010b8a1730c7efc9a46189872409f6fdba77afc779d734bb6dc4e5e94c14a90703d6a7f10fbf8fa80eafbf04f1006037e696728b7fa42642c14280
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Utah Ruby Users Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Tad Thorley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Rubowar
2
+
3
+ A competitive programming game where Ruby club members write Ruby classes to control robots ("Rubots") that battle in an arena. The engine is a standalone Ruby gem with a pluggable renderer interface.
4
+
5
+ ## Quick Start
6
+
7
+ ```ruby
8
+ class MyRobot
9
+ include Rubot
10
+ size :medium # :small, :medium, or :large
11
+
12
+ def tick
13
+ turret(10) # Rotate turret
14
+ fire(5) if look(1) # Fire when we see someone
15
+ thrust(2) if speed < 5 # Keep moving
16
+ end
17
+ end
18
+ ```
19
+
20
+ ## Robot API
21
+
22
+ ### State Accessors (read-only)
23
+
24
+ | Method | Description |
25
+ |--------|-------------|
26
+ | `x`, `y` | Position in arena |
27
+ | `velocity_x`, `velocity_y` | Current velocity |
28
+ | `speed` | Velocity magnitude |
29
+ | `body_angle` | Body direction (0-360) |
30
+ | `turret_angle` | Turret direction (0-360, absolute) |
31
+ | `health` | Current HP (starts 100) |
32
+ | `energy` | Current energy (max 100) |
33
+ | `shield_level` | Shield strength (0-50, degrades 2/tick) |
34
+ | `arena_width`, `arena_height` | Arena dimensions |
35
+ | `friction` | Arena friction (default 0.95) |
36
+ | `tick_number` | Current game tick |
37
+ | `damage_dealt`, `damage_taken` | Match stats |
38
+ | `energons` | All energon positions (free) |
39
+ | `size` | Robot size (:small, :medium, :large) |
40
+
41
+ ### Actions
42
+
43
+ | Method | Cost | Effect |
44
+ |--------|------|--------|
45
+ | `thrust(energy)` | energy | velocity = sqrt(energy) * 1.5 |
46
+ | `turn(degrees)` | \|degrees\|/10 | Rotate body |
47
+ | `turret(degrees)` | \|degrees\|/30 | Rotate turret (cheaper) |
48
+ | `fire(energy)` | energy | Damage = 1.5 * energy, 18 u/tick |
49
+ | `shield(energy)` | energy | Add to shield (max 50) |
50
+
51
+ ### Sensing
52
+
53
+ | Method | Cost | Returns |
54
+ |--------|------|---------|
55
+ | `look(1-5)` | 1-5 | Line scan, more energy = more detail |
56
+ | `scan(width)` | width | Cone scan, returns robots + bullets |
57
+ | `pulse(radius)` | radius^2/10 | Circle scan, position + size only |
58
+
59
+ **look(energy) detail levels:**
60
+ - 1: position + size
61
+ - 2: + velocity
62
+ - 3: + shield_level
63
+ - 4: + health
64
+ - 5: + energy
65
+
66
+ ### Callbacks
67
+
68
+ ```ruby
69
+ def on_hit(damage, direction) # Projectile hit
70
+ def on_spawn # Match start
71
+ def on_death # Health reached 0
72
+ def on_wall # Wall collision (10 damage)
73
+ def on_collision(robot) # Robot collision (5 damage)
74
+ def on_energon(amount) # Collected energon
75
+ ```
76
+
77
+ ### Robot Sizes
78
+
79
+ | Size | Radius | Energy Regen | Collision Bonus |
80
+ |------|--------|--------------|-----------------|
81
+ | `:small` | 15 | +8/tick | Takes +3 from larger |
82
+ | `:medium` | 20 | +10/tick | Standard |
83
+ | `:large` | 25 | +12/tick | Deals +3 to smaller |
84
+
85
+ ## Arena
86
+
87
+ - **Dimensions**: Variable (default 800x600)
88
+ - **Origin**: Bottom-left (0,0)
89
+ - **Angles**: 0 = East, 90 = North, 180 = West, 270 = South
90
+ - **Friction**: 0.95 default (configurable)
91
+ - **Max speed**: 20 u/tick
92
+ - **Wall collision**: 10 damage + bounce
93
+ - **Robot collision**: 5 damage (with size modifiers)
94
+
95
+ ## Physics
96
+
97
+ - `thrust(energy)` adds velocity: sqrt(energy) * 1.5
98
+ - Bullets travel 18 u/tick
99
+ - Self-damage is possible (your bullets can hit you)
100
+ - Friction slows robots each tick (velocity *= friction)
101
+
102
+ ## Renderer Interface
103
+
104
+ The engine emits events for any renderer:
105
+
106
+ ```ruby
107
+ match = Rubowar::Match.new(robots: [Spinner, Tracker])
108
+
109
+ # Block-based (real-time)
110
+ match.on(:tick) { |state| render_frame(state) }
111
+ match.on(:hit) { |event| play_sound(:hit) }
112
+ match.run
113
+
114
+ # Collect events (replays)
115
+ events = match.run
116
+ save_replay(events)
117
+
118
+ # Built-in terminal
119
+ match.run(renderer: Rubowar::Renderers::Terminal)
120
+ ```
121
+
122
+ **Event types**: `:tick`, `:fire`, `:hit`, `:death`, `:wall_collision`, `:robot_collision`, `:energon_spawn`, `:energon_collect`, `:match_end`
123
+
124
+ ## Victory
125
+
126
+ - Last robot standing wins
127
+ - Tick limit (5000) prevents stalemates
128
+ - Tiebreaker: highest HP, then most damage dealt
129
+
130
+ ## Error Handling
131
+
132
+ If robot code crashes or times out: **10 damage** + skip tick.
133
+
134
+ ## Project Structure
135
+
136
+ ```
137
+ rubowar/
138
+ ├── lib/
139
+ │ ├── rubowar/
140
+ │ │ ├── rubot.rb # Module participants include
141
+ │ │ ├── arena.rb # Physics, collisions
142
+ │ │ ├── match.rb # Game loop
143
+ │ │ ├── robot_runner.rb # Sandboxed execution
144
+ │ │ ├── bullet.rb # Projectile tracking
145
+ │ │ ├── energon.rb # Energy power-up
146
+ │ │ └── events.rb # Event types
147
+ │ └── rubowar.rb
148
+ ├── test/
149
+ ├── robots/ # Example robots
150
+ ├── bin/rubowar # CLI
151
+ └── rubowar.gemspec
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT License - see [LICENSE](LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
data/TASKS.md ADDED
@@ -0,0 +1,138 @@
1
+ # Rubowar Implementation Tasks
2
+
3
+ ## Phase 1: Core Engine (MVP)
4
+
5
+ ### Project Setup
6
+ - [ ] Create `rubowar.gemspec`
7
+ - [ ] Create `Gemfile`
8
+ - [ ] Create `lib/rubowar.rb` (main entry point)
9
+ - [ ] Create `Rakefile` with test task
10
+ - [ ] Set up `test/test_helper.rb`
11
+
12
+ ### Rubot Module (`lib/rubowar/rubot.rb`)
13
+ - [ ] Module inclusion hook (registers robot class)
14
+ - [ ] `size` class method (`:small`, `:medium`, `:large`)
15
+ - [ ] State accessors: `x`, `y`, `velocity_x`, `velocity_y`, `speed`
16
+ - [ ] State accessors: `body_angle`, `turret_angle`
17
+ - [ ] State accessors: `health`, `energy`, `shield_level`
18
+ - [ ] State accessors: `arena_width`, `arena_height`, `friction`
19
+ - [ ] State accessors: `tick_number`, `damage_dealt`, `damage_taken`
20
+ - [ ] State accessors: `energons`, `size`
21
+ - [ ] Action method: `thrust(energy)`
22
+ - [ ] Action method: `turn(degrees)`
23
+ - [ ] Action method: `turret(degrees)`
24
+ - [ ] Action method: `fire(energy)`
25
+ - [ ] Action method: `look(energy)` (basic version)
26
+ - [ ] Action queuing system
27
+
28
+ ### Arena (`lib/rubowar/arena.rb`)
29
+ - [ ] Initialize with configurable width/height (default 800x600)
30
+ - [ ] Robot spawning (random positions, min distances)
31
+ - [ ] Position/velocity tracking for all robots
32
+ - [ ] Friction application each tick
33
+ - [ ] Wall collision detection + bounce + 10 damage
34
+ - [ ] Robot collision detection + push apart + 5 damage
35
+ - [ ] Max speed enforcement (20 u/tick)
36
+
37
+ ### Bullet (`lib/rubowar/bullet.rb`)
38
+ - [ ] Position, velocity, owner, damage tracking
39
+ - [ ] Movement (18 u/tick)
40
+ - [ ] Collision detection with robots
41
+ - [ ] Out-of-bounds removal
42
+
43
+ ### Match (`lib/rubowar/match.rb`)
44
+ - [ ] Initialize with robot classes and arena config
45
+ - [ ] Game loop structure
46
+ - [ ] Call robot `tick` methods
47
+ - [ ] Process queued actions
48
+ - [ ] Apply physics (movement, collisions)
49
+ - [ ] Update bullets
50
+ - [ ] Event emission system (`on` method for callbacks)
51
+ - [ ] Victory detection (last standing, tick limit, tiebreakers)
52
+
53
+ ### Terminal Renderer (`lib/rubowar/renderers/terminal.rb`)
54
+ - [ ] ASCII arena display
55
+ - [ ] Robot positions with direction indicator
56
+ - [ ] Health/energy bars
57
+ - [ ] Bullet positions
58
+ - [ ] Match status output
59
+
60
+ ### Example Robots (`robots/`)
61
+ - [ ] `spinner.rb` - Simple turret spinner, fires on sight
62
+ - [ ] `tracker.rb` - Seeks and tracks enemies
63
+
64
+ ### Tests
65
+ - [ ] Rubot module tests
66
+ - [ ] Arena physics tests
67
+ - [ ] Bullet tests
68
+ - [ ] Match runner tests
69
+
70
+ ---
71
+
72
+ ## Phase 2: Full Sensing & Combat
73
+
74
+ ### Enhanced Sensing
75
+ - [ ] `look(energy)` with tiered detail (1-5 energy levels)
76
+ - [ ] `scan(width)` cone scan with bullet detection
77
+ - [ ] `pulse(radius)` circle scan
78
+
79
+ ### Shield System
80
+ - [ ] `shield(energy)` action
81
+ - [ ] Shield degradation (2/tick)
82
+ - [ ] Damage absorption logic
83
+ - [ ] Max shield (50)
84
+
85
+ ### Callbacks
86
+ - [ ] `on_hit(damage, direction)`
87
+ - [ ] `on_spawn`
88
+ - [ ] `on_death`
89
+ - [ ] `on_wall`
90
+ - [ ] `on_collision(robot)`
91
+
92
+ ### Robot Sizes
93
+ - [ ] Size-based collision damage modifiers
94
+ - [ ] Size-based energy regeneration
95
+ - [ ] Size-based radius for collision
96
+
97
+ ---
98
+
99
+ ## Phase 3: Energons & Polish
100
+
101
+ ### Energons (`lib/rubowar/energon.rb`)
102
+ - [ ] Spawn logic (every ~150 ticks, max 2)
103
+ - [ ] Random energy value (20-80)
104
+ - [ ] Collection detection (15 unit radius)
105
+ - [ ] `on_energon(amount)` callback
106
+ - [ ] `energons` accessor (always visible, free)
107
+
108
+ ### CLI (`bin/rubowar`)
109
+ - [ ] Load robot files
110
+ - [ ] Run match with options (arena size, tick limit)
111
+ - [ ] Output results
112
+
113
+ ### Replay System
114
+ - [ ] Event log recording
115
+ - [ ] JSON serialization
116
+
117
+ ---
118
+
119
+ ## Phase 4: Sandboxing & Safety
120
+
121
+ ### Process Isolation (`lib/rubowar/robot_runner.rb`)
122
+ - [ ] Subprocess spawning for robot code
123
+ - [ ] JSON state serialization
124
+ - [ ] JSON action deserialization
125
+ - [ ] Timeout enforcement (10ms/tick)
126
+ - [ ] Dangerous method removal
127
+
128
+ ### Error Handling
129
+ - [ ] Crash detection → 10 damage + skip tick
130
+ - [ ] Timeout detection → 10 damage + skip tick
131
+
132
+ ---
133
+
134
+ ## Design Reference
135
+
136
+ See `README.md` for complete API documentation.
137
+
138
+ Full design spec available at: `~/.claude/plans/mutable-yawning-pascal.md`
data/design-doc.md ADDED
@@ -0,0 +1,537 @@
1
+ # Rubowar: Ruby Robot Battle Arena
2
+
3
+ ## Overview
4
+ A competitive programming game where Ruby club members write Ruby classes to control robots ("Rubots") that battle in an arena. The engine is a standalone Ruby gem with a pluggable renderer interface - bring your own visualization (terminal, web, desktop app, etc.).
5
+
6
+ ---
7
+
8
+ ## Core Design Decisions
9
+
10
+ ### Robot API
11
+ ```ruby
12
+ class Destructo
13
+ include Rubot
14
+
15
+ def tick
16
+ # Access state via methods: energy, health, x, y, turret_angle, body_angle, etc.
17
+ # Call action methods directly: move, fire, turn_turret, look, radar
18
+ # Actions are queued internally and executed after tick returns
19
+ end
20
+
21
+ # Optional callbacks
22
+ def on_hit(damage, direction)
23
+ end
24
+
25
+ def on_spawn
26
+ end
27
+
28
+ def on_death
29
+ end
30
+ end
31
+ ```
32
+
33
+ **Error Handling**: If a robot's code crashes or times out, it takes **10 damage** and skips that tick. This encourages robust code without instant disqualification.
34
+
35
+ ### Rubot Module - Complete API
36
+
37
+ #### Robot Size (chosen at class definition)
38
+
39
+ ```ruby
40
+ class MyRobot
41
+ include Rubot
42
+ size :medium # :small, :medium, or :large
43
+ end
44
+ ```
45
+
46
+ | Size | Radius | Energy Regen | Collision Bonus |
47
+ |------|--------|--------------|-----------------|
48
+ | `:small` | 15 units | +8/tick | Takes +3 damage from larger robots |
49
+ | `:medium` | 20 units | +10/tick | Standard collision damage |
50
+ | `:large` | 25 units | +12/tick | Deals +3 damage to smaller robots |
51
+
52
+ **Collision damage formula**: Base 5 + size bonus. Small vs Large = Small takes 8, Large takes 2.
53
+
54
+ #### State Accessors (read-only)
55
+ | Method | Type | Description |
56
+ |--------|------|-------------|
57
+ | `x`, `y` | Float | Current position in arena |
58
+ | `velocity_x`, `velocity_y` | Float | Current velocity vector |
59
+ | `speed` | Float | Magnitude of velocity (convenience) |
60
+ | `body_angle` | Float | Direction robot body faces (0-360°) |
61
+ | `turret_angle` | Float | Direction turret points (0-360°, absolute) |
62
+ | `health` | Integer | Current HP (starts at 100) |
63
+ | `energy` | Integer | Current energy (max 100, regen varies by size) |
64
+ | `shield_level` | Integer | Current shield level (0 = no shield, degrades 2/tick, max 50) |
65
+ | `arena_width`, `arena_height` | Integer | Arena dimensions |
66
+ | `friction` | Float | Arena friction coefficient (default 0.95) |
67
+ | `tick_number` | Integer | Current game tick |
68
+ | `damage_dealt` | Integer | Total damage dealt this match |
69
+ | `damage_taken` | Integer | Total damage received this match |
70
+ | `energons` | Array | All energon positions `[{x:, y:}]` (always visible, free) |
71
+ | `size` | Symbol | Robot's size (`:small`, `:medium`, `:large`) |
72
+
73
+ #### Action Methods
74
+ | Method | Cost | Description |
75
+ |--------|------|-------------|
76
+ | `thrust(energy)` | energy spent | Add velocity in body direction. **velocity = √energy × 1.5** (physics-based). |
77
+ | `turn(degrees)` | \|degrees\|/10 energy | Rotate body only. Negative = left, positive = right. |
78
+ | `turret(degrees)` | \|degrees\|/30 energy | Rotate turret only. Cheaper than body turn. |
79
+ | `fire(energy)` | energy spent | Fire projectile. Damage = **1.5 × energy**. Any amount up to current energy. All travel 15 u/tick. |
80
+ | `shield(energy)` | energy spent | Pump energy into shield. Shield strength = energy invested. Degrades 2/tick. |
81
+ | `look(energy)` | 1-5 energy | Line in turret direction. More energy = more detail (see below). |
82
+ | `scan(width)` | width energy | Cone of `width` degrees. Returns `[{distance:, angle:}]` for robots in cone. |
83
+ | `pulse(radius)` | radius²/10 energy | Circle around self. Returns `[{distance:, angle:}]` for robots in radius. (least detail) |
84
+
85
+ **Turn cost examples**: `turn(90)` costs 9 energy, `turn(-45)` costs 4.5 energy, `turret(90)` costs 3 energy
86
+
87
+ #### Sensing System
88
+
89
+ **`look(energy)`** - Variable detail line scan (size always free)
90
+
91
+ | Cost | Returns |
92
+ |------|---------|
93
+ | 1 | position + size |
94
+ | 2 | + velocity |
95
+ | 3 | + shield_level |
96
+ | 4 | + health |
97
+ | 5 | + energy |
98
+
99
+ ```ruby
100
+ look(1) → {x: 400, y: 300, size: :large}
101
+ look(3) → {x: 400, y: 300, size: :large, velocity_x: 5, velocity_y: -2, shield_level: 20}
102
+ look(5) → {x: 400, y: 300, size: :large, velocity_x: 5, velocity_y: -2, shield_level: 20, health: 75, energy: 50}
103
+ look(n) → nil # nothing in line of sight
104
+ ```
105
+
106
+ **`scan(width)`** - Cone scan (width degrees, costs width energy)
107
+ - Returns: position + body_angle + shield_level + size
108
+ - Also detects bullets: position + velocity
109
+
110
+ **`pulse(radius)`** - Circle scan (costs radius²/10 energy)
111
+ - Returns: position + size only
112
+ - Also detects bullets: position only
113
+
114
+ ```ruby
115
+ scan(30) → {
116
+ robots: [{x: 400, y: 300, body_angle: 45, shield_level: 10, size: :small}],
117
+ bullets: [{x: 350, y: 280, velocity_x: 10, velocity_y: 5}]
118
+ }
119
+
120
+ pulse(10) → {
121
+ robots: [{x: 400, y: 300, size: :medium}],
122
+ bullets: [{x: 350, y: 280}]
123
+ }
124
+ ```
125
+
126
+ **Bullet awareness**: scan and pulse detect incoming bullets!
127
+
128
+ #### Shield System
129
+
130
+ Shields absorb damage before health. They require continuous energy investment to maintain.
131
+
132
+ **Mechanics**:
133
+ - `shield(energy)` - Add energy to shield strength
134
+ - Shield degrades **2 points per tick** naturally
135
+ - Damage hits shield first, then health when shield = 0
136
+ - Shield absorbs damage 1:1 (10 damage removes 10 shield)
137
+ - Max shield strength: 50
138
+
139
+ **Example**:
140
+ ```ruby
141
+ def tick
142
+ # Maintain shield at ~20 level
143
+ shield(5) if shield_level < 20
144
+
145
+ # Or burst shield when under attack
146
+ shield(30) if under_fire?
147
+ end
148
+ ```
149
+
150
+ **Strategy**: Constant small investments (2-3 energy/tick) maintain a buffer. Burst shielding when you detect incoming bullets via `scan`.
151
+
152
+ #### Physics
153
+ - **Movement**: `thrust(energy)` adds velocity (√energy × 1.5). Friction slows robots each tick (default 0.95×, configurable per tournament).
154
+ - **Projectiles**: Travel at **18 units/tick** (slightly slower than max robot speed). Can hit anyone including the shooter. Disappear on contact or after leaving arena.
155
+ - **Wall collision**: **10 damage** + bounce. Walls hurt! Discourages reckless speed.
156
+ - **Robot collision**: **5 damage** to both robots + push apart. Less than walls.
157
+ - **Max speed**: Capped at 20 units/tick to prevent wall-slamming exploits.
158
+
159
+ #### Callbacks
160
+ ```ruby
161
+ def on_hit(damage, direction) # Called when taking damage from projectile
162
+ def on_spawn # Called at match start
163
+ def on_death # Called when health reaches 0
164
+ def on_wall # Called on wall collision (10 damage)
165
+ def on_collision(robot) # Called on robot collision (5 damage), receives other robot info
166
+ def on_energon(amount) # Called when collecting energon (20-80 energy)
167
+ ```
168
+
169
+ ### Arena
170
+
171
+ **Dimensions**: Variable (default 800×600 units)
172
+ - Configurable per match/tournament
173
+ - Robots access via `arena_width` and `arena_height` accessors
174
+
175
+ **Coordinate System**:
176
+ - Origin (0,0) at **bottom-left** corner
177
+ - X increases rightward (0 to arena_width)
178
+ - Y increases upward (0 to arena_height)
179
+ - **0° = Right (East)**, 90° = Up, 180° = Left, 270° = Down
180
+
181
+ **Spawning**:
182
+ - **Random positions** at match start
183
+ - Minimum distance from walls (50 units)
184
+ - Minimum distance between robots (100 units)
185
+ - Random starting angle
186
+
187
+ **Players**: 2-4 robots per match (1v1, 1v1v1, 1v1v1v1, or 2v2)
188
+
189
+ **Walls**: Collision causes 10 damage + bounce
190
+
191
+ ### Energons
192
+
193
+ Simple energy power-up system. No healing - damage is permanent.
194
+
195
+ | Type | Effect | Spawn Rate |
196
+ |------|--------|------------|
197
+ | Energon | +20 to +80 energy (random) | Every ~150 ticks |
198
+
199
+ **Energon mechanics:**
200
+ - Max 2 energons on field at once
201
+ - Spawn at random positions (not too close to robots or walls)
202
+ - Collect by touching (radius ~15 units)
203
+ - Energy is capped at 100 even with energons
204
+ - `on_energon(amount)` callback fires when collected
205
+
206
+ **Detection:** Energons are **always visible** via the `energons` accessor (free, no energy cost). Returns `[{x:, y:}]` for all energons on the field. The actual energy amount (20-80) is hidden until collected - adds risk/reward decisions.
207
+
208
+ ### Victory Condition
209
+ - Last robot standing wins
210
+ - Matches have a tick limit (e.g., 5000 ticks) to prevent stalemates
211
+ - If time expires: highest HP wins, then most damage dealt
212
+
213
+ ---
214
+
215
+ ## Architecture
216
+
217
+ ### Project Structure
218
+ ```
219
+ rubowar/
220
+ ├── lib/
221
+ │ ├── rubowar/
222
+ │ │ ├── rubot.rb # The module participants include
223
+ │ │ ├── arena.rb # Game world, physics, collision
224
+ │ │ ├── match.rb # Runs a single match
225
+ │ │ ├── robot_runner.rb # Sandboxed execution of robot code
226
+ │ │ ├── bullet.rb # Projectile tracking
227
+ │ │ ├── energon.rb # Energy power-up
228
+ │ │ └── events.rb # Event types for renderers
229
+ │ └── rubowar.rb
230
+ ├── test/
231
+ ├── robots/ # Example robots for learning
232
+ │ ├── spinner.rb
233
+ │ ├── tracker.rb
234
+ │ └── wall_hugger.rb
235
+ ├── bin/
236
+ │ └── rubowar # CLI to run matches
237
+ ├── Gemfile
238
+ ├── rubowar.gemspec
239
+ └── README.md
240
+ ```
241
+
242
+ ### Renderer Interface
243
+
244
+ The engine is renderer-agnostic. It emits events that any renderer can consume:
245
+
246
+ ```ruby
247
+ match = Rubowar::Match.new(robots: [Spinner, Tracker], width: 800, height: 600)
248
+
249
+ # Option 1: Block-based (for real-time renderers)
250
+ match.on(:tick) { |state| render_frame(state) }
251
+ match.on(:hit) { |event| play_sound(:hit) }
252
+ match.on(:death) { |event| show_explosion(event) }
253
+ match.run
254
+
255
+ # Option 2: Collect all events (for replays)
256
+ events = match.run # Returns array of all events
257
+ save_replay(events)
258
+
259
+ # Option 3: Simple terminal output (built-in)
260
+ match.run(renderer: Rubowar::Renderers::Terminal)
261
+ ```
262
+
263
+ **Event Types**:
264
+ - `:tick` - Full game state each tick
265
+ - `:fire` - Robot fired a projectile
266
+ - `:hit` - Projectile hit a robot
267
+ - `:death` - Robot destroyed
268
+ - `:wall_collision` - Robot hit wall
269
+ - `:robot_collision` - Robots collided
270
+ - `:energon_spawn` - Energon appeared
271
+ - `:energon_collect` - Robot collected energon
272
+ - `:match_end` - Match concluded with winner
273
+
274
+ ### Key Components
275
+
276
+ #### 1. Rubot Module (`lib/rubowar/rubot.rb`)
277
+ - Included by participant classes
278
+ - Registers the class with the game engine
279
+ - Provides helper methods: `move`, `fire`, `look`, `radar`, `turn_body`, `turn_turret`
280
+ - Actions are queued and returned from `tick`
281
+
282
+ #### 2. Arena (`lib/rubowar/arena.rb`)
283
+ - Manages game state: robot positions, velocities, health, energy
284
+ - Handles physics: movement, projectile travel, collisions
285
+ - Spawns and manages energons
286
+ - Emits events for renderers
287
+
288
+ #### 3. Match (`lib/rubowar/match.rb`)
289
+ - Loads robot classes (sandboxed)
290
+ - Runs game loop: call each robot's `tick`, process actions, update state
291
+ - Emits events via callbacks (renderer-agnostic)
292
+ - Determines winner
293
+
294
+ #### 4. Robot Runner (`lib/rubowar/robot_runner.rb`)
295
+ - **Sandboxing**: Runs robot code with restrictions
296
+ - No file I/O, network, system calls
297
+ - Time limit per tick (e.g., 10ms) to prevent infinite loops
298
+ - Memory limits
299
+ - Process isolation with JSON communication
300
+
301
+ #### 5. Terminal Renderer (`lib/rubowar/renderers/terminal.rb`)
302
+ - Built-in simple renderer for testing
303
+ - ASCII visualization of arena state
304
+ - Useful for development and debugging
305
+
306
+ ---
307
+
308
+ ## Implementation Phases
309
+
310
+ ### Phase 1: Core Engine (MVP)
311
+ 1. Project structure with gemspec
312
+ 2. `Rubot` module with basic API (state accessors, action methods)
313
+ 3. `Arena` with physics (movement, friction, collision)
314
+ 4. `Match` runner with event emission
315
+ 5. Basic actions: thrust, turn, turret, fire, look
316
+ 6. Terminal renderer for testing
317
+ 7. 2-3 example robots
318
+
319
+ ### Phase 2: Full Sensing & Combat
320
+ 1. Complete sensing system (look, scan, pulse)
321
+ 2. Shield system
322
+ 3. All callbacks (on_hit, on_wall, on_collision, etc.)
323
+ 4. Robot sizes with tradeoffs
324
+
325
+ ### Phase 3: Energons & Polish
326
+ 1. Energon spawning and collection
327
+ 2. CLI tool (`bin/rubowar`) for running matches
328
+ 3. Match replay recording (event log)
329
+
330
+ ### Phase 4: Sandboxing & Safety
331
+ 1. Process isolation for robot execution
332
+ 2. Time limits per tick
333
+ 3. Dangerous method removal
334
+
335
+ ---
336
+
337
+ ## Technical Decisions
338
+
339
+ ### Sandboxing (Process Isolation)
340
+
341
+ Since all code runs server-side, we need proper sandboxing to prevent cheating and abuse.
342
+
343
+ **Architecture:**
344
+ ```
345
+ Main Process (Rails) Robot Process (Sandboxed)
346
+ ┌─────────────────────┐ ┌─────────────────────┐
347
+ │ Match Runner │ JSON │ Robot Code │
348
+ │ - Arena physics │ ◄──────► │ - tick() execution │
349
+ │ - State management │ pipe │ - Isolated Ruby │
350
+ │ - WebSocket stream │ │ - No game access │
351
+ └─────────────────────┘ └─────────────────────┘
352
+ ```
353
+
354
+ **How it works:**
355
+ 1. Match runner spawns a subprocess for each robot
356
+ 2. Sends state as JSON: `{x:, y:, energy:, health:, energons:, ...}`
357
+ 3. Robot subprocess executes `tick()`, returns actions as JSON: `[{action: "thrust", power: 3}, ...]`
358
+ 4. Match runner validates actions, applies physics, repeats
359
+ 5. Subprocess is killed if it times out (10ms limit per tick)
360
+
361
+ **Security layers:**
362
+ - **Process isolation**: Robot can't access Arena/Match (different process)
363
+ - **No dangerous methods**: Subprocess loads robot in clean environment, removes `File`, `IO`, `Net::HTTP`, `ObjectSpace`, `Kernel.system`, `eval`, `require`, etc.
364
+ - **Timeout enforcement**: Process-level kill if tick exceeds time limit
365
+ - **Resource limits**: ulimit on memory, CPU
366
+ - **Optional Docker**: For extra isolation, run subprocess in container
367
+
368
+ **Implementation**: Use Ruby's `Open3.popen3` or a gem like `childprocess` for subprocess management.
369
+
370
+ ---
371
+
372
+ ## Example Robots
373
+
374
+ ### Basic: Spinner
375
+ ```ruby
376
+ class Spinner
377
+ include Rubot
378
+
379
+ def tick
380
+ turret(10) # Constantly rotate turret (cheap)
381
+ fire(1) if look # Fire when we see someone
382
+ thrust(1) if speed < 3 # Keep moving slowly
383
+ end
384
+ end
385
+ ```
386
+
387
+ ### Intermediate: Tracker
388
+ ```ruby
389
+ class Tracker
390
+ include Rubot
391
+
392
+ def tick
393
+ if energy > 30
394
+ # Radar scan for enemies
395
+ enemies = radar(5) # Costs 25 energy
396
+
397
+ if enemies.any?
398
+ target = enemies.min_by { |e| e[:distance] }
399
+
400
+ # Aim and fire
401
+ angle_diff = normalize_angle(target[:angle] - turret_angle)
402
+ turret(angle_diff)
403
+ fire(3) if angle_diff.abs < 10 && energy > 40
404
+
405
+ # Strafe perpendicular to target
406
+ turn(target[:angle] + 90 - body_angle)
407
+ thrust(3)
408
+ else
409
+ search_pattern
410
+ end
411
+ else
412
+ # Low energy - conserve, just look
413
+ turret(15)
414
+ fire(1) if look
415
+ end
416
+ end
417
+
418
+ def on_hit(damage, direction)
419
+ shield(true) if energy > 20 # Activate shield when hit
420
+ turn(direction + 90) # Turn perpendicular
421
+ thrust(5) # Burst away
422
+ end
423
+
424
+ private
425
+
426
+ def search_pattern
427
+ turn(5)
428
+ thrust(2) if speed < 5
429
+ end
430
+
431
+ def normalize_angle(angle)
432
+ angle = angle % 360
433
+ angle -= 360 if angle > 180
434
+ angle
435
+ end
436
+ end
437
+ ```
438
+
439
+ ### Advanced: Wall Hugger
440
+ ```ruby
441
+ class WallHugger
442
+ include Rubot
443
+
444
+ def on_spawn
445
+ @patrol_direction = 1
446
+ end
447
+
448
+ def tick
449
+ # Stay near walls for protection
450
+ near_wall = x < 50 || x > arena_width - 50 ||
451
+ y < 50 || y > arena_height - 50
452
+
453
+ if near_wall
454
+ patrol_along_wall
455
+ else
456
+ move_to_nearest_wall
457
+ end
458
+
459
+ # Always be scanning and shooting
460
+ scan_and_fire
461
+ end
462
+
463
+ def on_collision(what)
464
+ @patrol_direction *= -1 if what == :wall
465
+ end
466
+
467
+ private
468
+
469
+ def patrol_along_wall
470
+ # Move parallel to nearest wall
471
+ turn(90 * @patrol_direction)
472
+ thrust(3)
473
+ end
474
+
475
+ def move_to_nearest_wall
476
+ # Find nearest wall and head toward it
477
+ distances = {
478
+ 0 => arena_width - x, # right wall
479
+ 90 => arena_height - y, # top wall
480
+ 180 => x, # left wall
481
+ 270 => y # bottom wall
482
+ }
483
+ nearest_wall_angle = distances.min_by { |_, d| d }.first
484
+ turn(nearest_wall_angle - body_angle)
485
+ thrust(4)
486
+ end
487
+
488
+ def scan_and_fire
489
+ turret(20) # Sweep turret
490
+ distance = look
491
+ if distance && distance < 200
492
+ fire(4) # Heavy shot at close range
493
+ elsif distance
494
+ fire(2) # Light shot at distance
495
+ end
496
+ end
497
+ end
498
+ ```
499
+
500
+ ---
501
+
502
+ ## Open Questions / Future Features
503
+ - Team battles (2v2, 3v3)?
504
+ - Customizable robot appearance/colors?
505
+ - Achievement system?
506
+ - Spectator chat during live matches?
507
+ - API for external bot submission (GitHub integration)?
508
+
509
+ ---
510
+
511
+ ## Dependencies
512
+
513
+ ```ruby
514
+ # Gemfile
515
+ source 'https://rubygems.org'
516
+
517
+ gemspec
518
+
519
+ group :development, :test do
520
+ gem 'minitest', '~> 5.20'
521
+ gem 'rake'
522
+ end
523
+ ```
524
+
525
+ Minimal dependencies - the engine should be self-contained with no external runtime dependencies.
526
+
527
+ ---
528
+
529
+ ## Next Steps (Phase 1)
530
+ 1. Initialize gem structure (gemspec, lib/, test/, robots/)
531
+ 2. Implement `Rubot` module with state accessors and action methods
532
+ 3. Implement `Arena` with physics (movement, friction, wall collision)
533
+ 4. Implement `Match` runner with event emission
534
+ 5. Implement `Bullet` for projectile tracking
535
+ 6. Create terminal renderer
536
+ 7. Create example robots (Spinner, Tracker)
537
+ 8. Add tests for core functionality
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubowar
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rubowar.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubowar/version"
4
+
5
+ module Rubowar
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
data/sig/rubowar.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rubowar
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubowar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tad Thorley
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A competitive programming game where Ruby developers write classes to
13
+ control robots that battle in a physics-based arena.
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - LICENSE
19
+ - LICENSE.txt
20
+ - README.md
21
+ - Rakefile
22
+ - TASKS.md
23
+ - design-doc.md
24
+ - lib/rubowar.rb
25
+ - lib/rubowar/version.rb
26
+ - sig/rubowar.rbs
27
+ homepage: https://github.com/urug/rubowar
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ allowed_push_host: https://rubygems.org
32
+ homepage_uri: https://github.com/urug/rubowar
33
+ source_code_uri: https://github.com/urug/rubowar
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.2.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 4.0.3
49
+ specification_version: 4
50
+ summary: Ruby Robot Battle Arena
51
+ test_files: []