Building a Browser-Based MMORPG with Ruby on Rails: Documentation-Driven Development with AI
“The hardest part of building a game isn’t the code—it’s keeping 50+ interconnected systems coherent while shipping features fast.”
Most game development tutorials show you how to build a simple demo. Real games need authentication, real-time combat, economies, social systems, moderation, and dozens of other features that must work together seamlessly.
In this post, I’ll share our experience building Elselands—a browser-based, turn-based MMORPG inspired by classic games like Neverlands.ru. We’ll cover:
- Documentation-driven development that scales with complexity
- Feature decomposition from Game Design Document to implementation
- Flow documentation as the source of truth for implementation
- “Inspired-by” patterns for borrowing proven mechanics
- Testing strategies for game systems
- AI-assisted development that actually works for senior engineers
- Practical lessons from building 100+ interconnected systems
1. The Challenge: Building an MMORPG as a Rails Monolith
1.1 Why Rails for a Game?
Browser-based MMORPGs have unique requirements:
| Requirement | Why Rails Works |
|---|---|
| Server-authoritative logic | All game state lives in PostgreSQL, no client-side cheating |
| Real-time updates | ActionCable + Redis pub/sub handles chat, combat, presence |
| Rapid iteration | Convention over configuration means less boilerplate |
| Rich UI without SPA complexity | Hotwire (Turbo + Stimulus) delivers reactive UIs server-side |
| Moderation & admin tools | ActiveAdmin, Flipper, audit logging come free |
Our stack:
- Ruby 3.4.4 + Rails 8.1.1 (full-stack Hotwire monolith)
- PostgreSQL 18 (primary datastore)
- Redis (cache, Action Cable, Sidekiq)
- Hotwire (Turbo Frames, Turbo Streams, Stimulus)
- Sidekiq 8 (background jobs for combat/chat/events)
1.2 The Complexity Problem
A typical MMORPG has:
- 10+ major systems (auth, combat, economy, crafting, quests, etc.)
- 50+ models with complex relationships
- Hundreds of services, jobs, and controllers
- Real-time features requiring WebSockets
- Moderation, analytics, and admin tooling
Without a structured approach, this becomes unmaintainable. We solved it with documentation-driven development.
2. Documentation-Driven Development: From GDD to Implementation
2.1 The Documentation Hierarchy
We structured our documentation in three layers:
doc/
├── design/
│ └── gdd.md # Game Design Document (WHAT we're building)
├── features/
│ ├── 0_technical.md # Infrastructure & architecture
│ ├── 1_auth.md # Authentication & accounts
│ ├── 3_player.md # Character systems
│ ├── 8_gameplay_mechanics.md
│ └── ... # One file per major system
└── flow/
├── 0_technical.md # HOW each feature is implemented
├── 1_auth_presence.md
├── 8_gameplay_mechanics.md
└── ... # Implementation details with file references
Key insight: The GDD tells you what to build. Feature docs break it into systems. Flow docs tell you exactly how it’s implemented.
2.2 The Game Design Document (GDD)
Our GDD is intentionally high-level:
## Gameplay Mechanics
- **Player Movement**
- Tile-based grid movement
- Turn-based actions per player input
- **Combat System**
- PvE: Encounters against monsters and NPCs
- PvP: Player duels, group battles, arena tournaments
- **Character Progression**
- Experience points and leveling
- Stat points allocation per level
- Reputation and faction alignment
This is the north star—it never mentions implementation details like controllers or database schemas. It’s what you show stakeholders and use for high-level planning.
2.3 Feature Documentation: Breaking Down the GDD
Each major system gets its own feature doc (doc/features/*.md):
# 8_gameplay_mechanics.md
## Player Movement
- Grid-based tile system with terrain modifiers
- Turn-per-action enforcement (server-side)
- Cooldown multipliers for different terrain types
- Spawn points and respawn timers
## Combat System
- Turn-based initiative order
- Body-part targeting (head, torso, stomach, legs)
- Action points and mana management
- Status effects and buff/debuff system
Feature docs are implementation-ready specifications—detailed enough that an engineer knows what to build, but not yet tied to specific files.
2.4 Flow Documentation: The Implementation Source of Truth
This is where the magic happens. Flow docs (doc/flow/*.md) describe exactly how each feature is implemented:
# 8_gameplay_mechanics.md — Flow Documentation
## Combat Skill System
### Use Case: Player uses a skill during combat
1. Player clicks skill icon → `turn_combat_controller.js#useSkill()`
2. Stimulus sends action to `CombatController#action`
3. `TurnBasedCombatService` validates action points and mana
4. `Game::Combat::SkillExecutor` applies skill effects
5. `CombatLogEntry` records the action
6. Turbo Stream broadcasts update to all participants
### Key Behaviors
- Skills consume both action points AND mana slots
- Body-part targeting affects damage multipliers
- Critical hits based on agility vs target defense
### Responsible for Implementation Files
| Purpose | File |
|---------|------|
| Combat service | `app/services/game/combat/turn_based_combat_service.rb` |
| Skill executor | `app/services/game/combat/skill_executor.rb` |
| Stimulus controller | `app/javascript/controllers/turn_combat_controller.js` |
| Combat view | `app/views/combat/_battle.html.erb` |
| Action config | `config/gameplay/combat_actions.yml` |
Why flow docs matter:
- Context preservation — When returning to a feature after weeks, you know exactly where everything is
- AI assistance — Feed the flow doc to your AI assistant, and it understands the entire system
- Onboarding — New engineers can trace any feature from UI to database
- Code review — Reviewers can verify changes match the documented architecture
3. The “Inspired-By” Pattern: Borrowing Proven Mechanics
3.1 Why Reinvent the Wheel?
Classic games like Neverlands.ru have battle-tested UI/UX patterns. Instead of designing from scratch, we:
- Analyze the original implementation (JavaScript, CSS, HTML)
- Document the original patterns in
doc/features/neverlands_inspired.md - Adapt to modern Rails/Hotwire patterns
- Track implementation status
3.2 Example: Turn-Based Combat System
We received this Neverlands JavaScript:
// Original Neverlands combat logic
function CountOD() {
var totalOD = 0;
for (var i = 0; i < 10; i++) {
if (pos_atk[i] > 0) totalOD += pos_ochd[pos_atk[i]];
if (pos_blk[i] > 0) totalOD += pos_ochd[pos_blk[i]];
}
// pos_ochd contains action point costs per body part
return totalOD;
}
// Body part configuration
var pos_vars = ['', 'Head', 'Torso', 'Stomach', 'Groin', 'Left Leg', 'Right Leg'];
var pos_ochd = [0, 3, 2, 2, 2, 2, 2]; // Action point costs
3.3 Our Adaptation
We translated this to a Rails service with YAML configuration:
# config/gameplay/combat_actions.yml
body_parts:
head:
action_cost: 3
damage_multiplier: 1.5
block_bonus: 0.8
torso:
action_cost: 2
damage_multiplier: 1.0
block_bonus: 1.0
stomach:
action_cost: 2
damage_multiplier: 1.1
block_bonus: 0.9
# app/services/game/combat/turn_based_combat_service.rb
class Game::Combat::TurnBasedCombatService
def initialize(battle)
@battle = battle
@config = YAML.load_file(Rails.root.join("config/gameplay/combat_actions.yml"))
end
def calculate_action_points_used(attacks, blocks)
total = 0
attacks.each { |part| total += @config.dig("body_parts", part, "action_cost") || 2 }
blocks.each { |part| total += @config.dig("body_parts", part, "action_cost") || 2 }
total
end
end
And a Stimulus controller replacing the inline JavaScript:
// app/javascript/controllers/turn_combat_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["attackSelect", "blockSelect", "actionPoints", "submitButton"]
static values = { maxActions: Number, config: Object }
connect() {
this.updateActionPoints()
}
updateActionPoints() {
const used = this.calculateUsedPoints()
const remaining = this.maxActionsValue - used
this.actionPointsTarget.textContent = `${remaining}/${this.maxActionsValue}`
this.submitButtonTarget.disabled = remaining < 0
}
calculateUsedPoints() {
let total = 0
this.attackSelectTargets.forEach(select => {
if (select.value) total += this.configValue.body_parts[select.value]?.action_cost || 2
})
this.blockSelectTargets.forEach(select => {
if (select.value) total += this.configValue.body_parts[select.value]?.action_cost || 2
})
return total
}
}
3.4 Documentation Structure for Inspired Features
We maintain a dedicated file tracking all borrowed patterns:
# doc/features/neverlands_inspired.md
## Implementation Status Summary
| # | Feature | Status | Key Files |
|---|---------|--------|-----------|
| 1 | Chat System | ✅ Implemented | `chat_controller.js`, `moderation_service.rb` |
| 2 | Arena/PvP | ✅ Implemented | `arena_controller.rb`, `matchmaker.rb` |
| 3 | Turn-Based Combat | ✅ Implemented | `turn_based_combat_service.rb` |
| 4 | Combat Log | ✅ Implemented | `log_builder.rb`, `statistics_calculator.rb` |
## Chat System
### Original Neverlands Examples
[Original JavaScript code here]
### Elselands Implementation
- **Files:** `chat_controller.js`, `realtime_chat_channel.rb`
- **Key Adaptations:**
- Replaced `onclick` with Stimulus `data-action`
- WebSocket via ActionCable instead of polling
- Server-side moderation before broadcast
Benefits:
- Reference material — Original examples are preserved for future iterations
- Implementation tracking — Clear status of what’s done vs. planned
- Knowledge transfer — Anyone can understand why we made certain choices
4. Testing Strategies for Game Systems
4.1 Test Categories
Game systems require different testing strategies than typical CRUD apps:
| Category | Purpose | Example |
|---|---|---|
| Unit Tests | Individual service logic | TurnBasedCombatService action point calculation |
| Request Tests | API endpoints | POST /combat/action with attack data |
| Channel Tests | WebSocket functionality | BattleChannel broadcasts updates |
| Helper Tests | View helpers | format_combat_log_message output |
| System Tests | End-to-end flows | Full combat from start to victory |
4.2 Testing Real-Time Features
ActionCable channels need special setup:
# spec/channels/battle_channel_spec.rb
require "rails_helper"
RSpec.describe BattleChannel, type: :channel do
let(:user) { create(:user) }
let(:battle) { create(:battle) }
before do
stub_connection current_user: user
end
it "subscribes to battle stream" do
subscribe(battle_id: battle.id)
expect(subscription).to be_confirmed
expect(subscription).to have_stream_for(battle)
end
it "receives combat updates" do
subscribe(battle_id: battle.id)
expect {
BattleChannel.broadcast_to(battle, { type: "turn_update", round: 1 })
}.to have_broadcasted_to(battle)
end
end
4.3 Testing Deterministic Combat
Combat must be reproducible for debugging and replays:
# spec/services/game/combat/turn_based_combat_service_spec.rb
RSpec.describe Game::Combat::TurnBasedCombatService do
let(:battle) { create(:battle, :with_participants) }
let(:service) { described_class.new(battle) }
describe "#submit_turn" do
it "consumes action points correctly" do
attacks = [{ body_part: "head" }, { body_part: "torso" }]
blocks = [{ body_part: "stomach" }]
# Head (3) + Torso (2) + Stomach (2) = 7 action points
expect(service.calculate_action_points_used(attacks, blocks)).to eq(7)
end
context "with seeded RNG for reproducibility" do
it "produces consistent damage rolls" do
Game::Utils::Rng.seed(12345)
result1 = service.resolve_attack(attacker, defender, "head")
Game::Utils::Rng.seed(12345)
result2 = service.resolve_attack(attacker, defender, "head")
expect(result1[:damage]).to eq(result2[:damage])
end
end
end
end
4.4 Coverage Goals
We maintain explicit coverage targets:
| Category | Target | Rationale |
|---|---|---|
| Models | 90% | Core game logic lives here |
| Services | 95% | Business logic must be bulletproof |
| Controllers | 85% | Request/response handling |
| Channels | 80% | Real-time features are critical |
| Helpers | 90% | UI consistency |
| Views | 60% | Basic rendering verification |
| System | 40% | Key user flows |
5. AI-Assisted Development: Making It Work for Senior Engineers
5.1 The Problem with Generic AI Assistance
Most AI coding assistants fail on complex projects because they lack:
- Project context — They don’t know your architecture
- Convention awareness — They generate code that doesn’t match your style
- Domain knowledge — They don’t understand game mechanics
5.2 Our Solution: Structured Prompting with Documentation
We created a documentation hierarchy that the AI can reference:
# README.md (excerpt)
## 📄 Documentation Map
| File | When to Reference / Purpose |
|------|----------------------------|
| **AGENT.md** | Always loaded, highest authority |
| **GUIDE.md** | Rails standards or general best practices |
| **MMO_ADDITIONAL_GUIDE.md** | Gameplay/MMORPG domain-specific conventions |
| **doc/gdd.md** | Game design vision, classes, mechanics |
| **doc/features/*.md** | Per-system breakdown (technical specs) |
| **doc/flow/*.md** | Implementation details with file references |
5.3 Effective Prompting Patterns
Pattern 1: Feature Implementation
I want to start implementing the Player feature from `doc/features/3_player.md`.
Please follow:
- `AGENT.md` (always)
- `GUIDE.md` for general Rails patterns
- `MMO_ADDITIONAL_GUIDE.md` for gameplay logic architecture
- Use `MMO_ENGINE_SKELETON.md` for engine placement
**Task:**
1. Read `doc/features/3_player.md`
2. Identify required models, services, and UI components
3. Provide a detailed plan (files + responsibilities)
4. Wait for my confirmation before writing any code
Pattern 2: Inspired-By Implementation
Here's the Neverlands JavaScript for their arena system:
[paste code]
Please:
1. Analyze the original implementation
2. Document it in `doc/features/neverlands_inspired.md`
3. Propose a Rails/Hotwire adaptation using our patterns
4. Update flow docs with implementation details
Pattern 3: Bug Fix with Context
I'm seeing this error in the arena system:
[paste error]
Context:
- Check `doc/flow/11_arena_pvp.md` for the arena architecture
- The ArenaMatch model uses an enum that might conflict
Find the root cause and propose a fix that matches our conventions.
5.4 Why This Works for Senior Engineers
- You drive the architecture — AI implements your decisions, doesn’t make them
- Documentation is the contract — Changes must match documented patterns
- Context is preserved — Flow docs keep AI informed across sessions
- Code review is efficient — You verify against documented expectations
5.5 Prompts That Accelerate Development
For rapid prototyping:
Using AGENT.md rules, implement the next step from doc/features/3_player.md.
Use GUIDE.md for Rails logic, and MMO_ADDITIONAL_GUIDE.md for gameplay structure.
Touch only the necessary files.
For code quality:
After implementation, run:
1. `bundle exec rspec` for tests
2. `bundle exec standardrb` for linting
3. `bundle exec brakeman` for security
Fix any issues before considering this complete.
For documentation updates:
Update the corresponding flow doc (`doc/flow/X.md`) with:
- Use cases covered
- Key behaviors implemented
- Responsible files table
6. Practical Lessons from 100+ Systems
6.1 What Worked
1. Flow docs as the source of truth
Every PR must update the relevant flow doc. This creates a living architecture document that’s always accurate.
2. “Responsible Files” tables in flow docs
### Responsible for Implementation Files
| Purpose | File |
|---------|------|
| Combat service | `app/services/game/combat/turn_based_combat_service.rb` |
| Controller | `app/controllers/combat_controller.rb` |
| Stimulus | `app/javascript/controllers/turn_combat_controller.js` |
This makes it trivial to find code when debugging or extending features.
3. YAML configuration for game data
Instead of hardcoding values, we use config/gameplay/*.yml:
combat_actions.yml— Body parts, action costs, multipliersterrain_modifiers.yml— Movement cooldowns by terrainbiomes.yml— Encounter tables by region
Changes don’t require code deployments, and game designers can read/modify the configs.
4. Turbo Streams for real-time without complexity
Instead of building a custom WebSocket protocol:
# Broadcasting combat updates
Turbo::StreamsChannel.broadcast_update_to(
@battle,
target: "combat-log",
partial: "combat_logs/entry",
locals: { entry: log_entry }
)
The frontend gets updates automatically without custom JavaScript.
6.2 What We’d Do Differently
1. Start with comprehensive factories earlier
We had to backfill factories for models like NpcTemplate, WebhookEndpoint, etc. Building factories alongside models saves debugging time.
2. Enum naming requires foresight
ActiveRecord reserves certain method names (group, order, etc.). We had to rename fight_type: :group to fight_type: :team_battle to avoid conflicts.
3. Migration idempotency from day one
Adding if column_exists? checks to migrations would have prevented issues when re-running migrations:
def change
unless column_exists?(:battles, :share_token)
add_column :battles, :share_token, :string
end
end
6.3 Performance Considerations
Real-time features need Redis pub/sub:
# config/cable.yml
production:
adapter: redis
url: <%= ENV.fetch("REDIS_CABLE_URL") %>
channel_prefix: elselands_production
Separate Redis instances for different concerns:
| Purpose | Environment Variable |
|---|---|
| Rails cache | REDIS_CACHE_URL |
| Sidekiq queues | REDIS_SIDEKIQ_URL |
| Action Cable | REDIS_CABLE_URL |
This prevents a Sidekiq backup from affecting real-time chat.
7. The Complete Development Workflow
7.1 Adding a New Feature
1. GDD → Does this feature align with the game vision?
↓
2. Feature Doc → Create doc/features/X.md with:
- System requirements
- User-facing functionality
- Integration points
↓
3. Flow Doc → Create doc/flow/X.md with:
- Use cases (step-by-step flows)
- Key behaviors
- "Responsible Files" table (initially planned)
↓
4. Implementation → Using AI + documentation:
- Generate models/migrations
- Create services with tests
- Build controllers and views
- Add Stimulus controllers for interactivity
↓
5. Testing → Run full test suite:
- bundle exec rspec
- bundle exec standardrb
- bundle exec brakeman
↓
6. Documentation Update → Update flow doc with:
- Actual file paths
- Implementation notes
- Any deviations from plan
7.2 Debugging with Documentation
When something breaks:
- Find the flow doc for that feature
- Trace the use case — Which step is failing?
- Check the “Responsible Files” — Go directly to the right code
- Verify against “Key Behaviors” — Is the code doing what it should?
7.3 Onboarding New Engineers
- Read
README.mdfor project overview - Read
AGENT.mdandGUIDE.mdfor conventions - Pick a feature, read its flow doc
- Trace from UI to database using “Responsible Files”
- Make a small change, run tests, verify
8. Conclusion: Documentation as a Force Multiplier
Building a complex game is hard. Building it fast and maintaining quality is harder. Our documentation-driven approach made it possible:
| Challenge | Solution |
|---|---|
| Keeping 100+ systems coherent | Feature docs define boundaries |
| Finding code in a large codebase | Flow docs with “Responsible Files” |
| Preserving context across sessions | Flow docs are always up-to-date |
| Effective AI assistance | Structured prompts with documentation |
| Onboarding new engineers | Self-documenting architecture |
| Borrowing proven patterns | “Inspired-by” documentation |
The upfront investment in documentation pays dividends throughout the project. Every hour spent documenting saves five hours of confusion later.
Key Takeaways
- GDD → Features → Flows creates a traceable path from vision to implementation
- Flow docs with “Responsible Files” make code discoverable
- “Inspired-by” patterns let you borrow proven mechanics responsibly
- AI works best with structured context — feed it your documentation
- Tests are non-negotiable for game systems with complex interactions
- YAML configuration separates game data from code logic
Whether you’re building an MMORPG, a complex SaaS, or any large Rails application, these patterns will help you ship faster while maintaining quality.
Resources
- Rails 8.1 + Hotwire: hotwired.dev
- ActionCable Guide: guides.rubyonrails.org/action_cable_overview.html
- Stimulus Handbook: stimulus.hotwired.dev/handbook
- Flipper Feature Flags: github.com/flippercloud/flipper
This post was written while building Elselands—a browser-based MMORPG recreating the classic Neverlands.ru experience with modern Rails tooling. The documentation-driven approach described here enabled rapid development of 9 major Neverlands-inspired systems, 100+ models, and comprehensive test coverage in a fraction of the time traditional development would require.