Files
BreakEscape/planning_notes/rails-engine-migration-simplified/03_IMPLEMENTATION_PLAN.md
Claude 368b1b6e7a feat: Add critical implementation details based on review
Based on comprehensive codebase review, enhanced implementation plans with:

## Phase 3 Updates (Scenario Conversion):
- Complete bash script to convert all 26 scenarios to ERB structure
- Explicit list of 3 main scenarios (ceo_exfil, cybok_heist, biometric_breach)
- List of 23 test/demo scenarios for development
- Instructions to rename .json to .erb (actual ERB code added later in Phase 4)
- Preserves git history with mv commands
- Both automated script and manual alternatives provided

## Phase 9 Updates (CSRF Token Handling):
NEW Section 9.3: "Setup CSRF Token Injection"
- Critical security implementation for Rails CSRF protection
- Complete view template with <%= form_authenticity_token %>
- JavaScript config injection via window.breakEscapeConfig
- CSRF token validation and error handling
- Browser console testing procedures
- 5 common CSRF issues with solutions
- Fallback to meta tag if config missing
- Development vs production considerations

## Phase 9 Updates (Async Unlock with Loading UI):
ENHANCED Section 9.5: "Update Unlock Validation with Loading UI"
- New file: unlock-loading-ui.js with Phaser.js throbbing tint effect
- showUnlockLoading(): Blue pulsing animation during server validation
- clearUnlockLoading(): Green flash on success, red flash on failure
- Alternative spinner implementation provided
- Complete unlockTarget() rewrite with async/await server validation
- Loading UI shows during API call (~100-300ms)
- Graceful error handling with user feedback
- Updates for ALL lock types: pin, password, key, lockpick, biometric, bluetooth, RFID
- Minigame callback updates to pass attempt and method to server
- Testing mode fallback (DISABLE_SERVER_VALIDATION)
- Preserves all existing unlock logic after server validation

## Key Features:
- Addresses 2 critical risks from review (CSRF tokens, async validation)
- Solves scenario conversion gap (26 files → ERB structure)
- Maintains backward compatibility during migration
- Comprehensive troubleshooting guidance
- Production-ready security implementation

Total additions: ~600 lines of detailed implementation guidance
2025-11-20 15:25:25 +00:00

1617 lines
38 KiB
Markdown

# BreakEscape Rails Engine - Implementation Plan
**Complete step-by-step guide with explicit commands**
---
## How to Use This Plan
1. **Follow phases in order** - Each phase builds on previous ones
2. **Read the entire phase** before starting - Understand what you're doing
3. **Execute commands exactly as written** - Copy/paste to avoid errors
4. **Test after each phase** - Don't proceed if tests fail
5. **Commit after each phase** - Preserve working state
6. **Use mv, not cp** - Move files to preserve git history
---
## Prerequisites
Before starting Phase 1:
```bash
# Verify you're in the correct directory
cd /home/user/BreakEscape
pwd # Should print: /home/user/BreakEscape
# Verify git status
git status # Should be clean or have only expected changes
# Verify Ruby and Rails versions
ruby -v # Should be 3.0+
rails -v # Should be 7.0+
# Verify PostgreSQL is running (if testing locally)
psql --version # Should show PostgreSQL 14+
# Create a checkpoint
git add -A
git commit -m "chore: Checkpoint before Rails Engine migration"
git push
# Create feature branch
git checkout -b rails-engine-migration
```
---
## Phase 1: Setup Rails Engine Structure (Week 1, ~8 hours)
### Objectives
- Generate Rails Engine boilerplate
- Configure engine settings
- Set up gemspec and dependencies
- Verify engine loads
### 1.1 Generate Rails Engine
```bash
# Generate mountable engine with isolated namespace
# --mountable: Creates engine that can be mounted in routes
# --skip-git: Don't create new git repo (we're already in one)
# --dummy-path: Location for test dummy app
rails plugin new . --mountable --skip-git --dummy-path=test/dummy
# This creates:
# - lib/break_escape/engine.rb
# - lib/break_escape/version.rb
# - app/ directory structure
# - config/routes.rb
# - test/ directory structure
# - break_escape.gemspec
```
**Expected output:** Files created successfully
### 1.2 Configure Engine
Edit the generated engine file:
```bash
# Open engine file
vim lib/break_escape/engine.rb
```
**Replace entire contents with:**
```ruby
require 'pundit'
module BreakEscape
class Engine < ::Rails::Engine
isolate_namespace BreakEscape
config.generators do |g|
g.test_framework :test_unit, fixture: true
g.assets false
g.helper false
end
# Load lib directory
config.autoload_paths << File.expand_path('../', __dir__)
# Pundit authorization
config.after_initialize do
if defined?(Pundit)
BreakEscape::ApplicationController.include Pundit::Authorization
end
end
# Static files from public/break_escape
config.middleware.use ::ActionDispatch::Static, "#{root}/public"
end
end
```
**Save and close** (`:wq` in vim)
### 1.3 Update Version
```bash
vim lib/break_escape/version.rb
```
**Replace with:**
```ruby
module BreakEscape
VERSION = '1.0.0'
end
```
**Save and close**
### 1.4 Update Gemfile
```bash
vim Gemfile
```
**Replace entire contents with:**
```ruby
source 'https://rubygems.org'
gemspec
# Development dependencies
group :development, :test do
gem 'sqlite3'
gem 'pry'
gem 'pry-byebug'
end
```
**Save and close**
### 1.5 Update Gemspec
```bash
vim break_escape.gemspec
```
**Replace entire contents with:**
```ruby
require_relative "lib/break_escape/version"
Gem::Specification.new do |spec|
spec.name = "break_escape"
spec.version = BreakEscape::VERSION
spec.authors = ["BreakEscape Team"]
spec.email = ["team@example.com"]
spec.summary = "BreakEscape escape room game engine"
spec.description = "Rails engine for BreakEscape cybersecurity training escape room game"
spec.license = "MIT"
spec.files = Dir.chdir(File.expand_path(__dir__)) do
Dir["{app,config,db,lib,public}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
end
spec.add_dependency "rails", ">= 7.0"
spec.add_dependency "pundit", "~> 2.3"
end
```
**Save and close**
### 1.6 Install Dependencies
```bash
bundle install
```
**Expected output:** Dependencies installed successfully
### 1.7 Test Engine Loads
```bash
# Start Rails console
rails console
# Verify engine loads
puts BreakEscape::Engine.root
# Should print engine root path
# Exit console
exit
```
**Expected output:** Path printed successfully, no errors
### 1.8 Commit
```bash
git add -A
git status # Review changes
git commit -m "feat: Generate Rails Engine structure
- Create mountable engine with isolated namespace
- Configure Pundit authorization
- Set up gemspec with dependencies
- Configure generators for test_unit with fixtures"
git push -u origin rails-engine-migration
```
---
## Phase 2: Move Game Files to public/ (Week 1, ~4 hours)
### Objectives
- Move static game files to public/break_escape/
- Preserve git history using mv
- Update any absolute paths if needed
- Verify files accessible
### 2.1 Create Directory Structure
```bash
# Create public directory for game assets
mkdir -p public/break_escape
```
### 2.2 Move Game Files
**IMPORTANT:** Use `mv`, not `cp`, to preserve git history
```bash
# Move JavaScript files
mv js public/break_escape/
# Move CSS files
mv css public/break_escape/
# Move assets (images, sounds, Tiled maps)
mv assets public/break_escape/
# Keep index.html as reference (don't move, copy for backup)
cp index.html public/break_escape/index.html.reference
```
### 2.3 Verify Files Moved
```bash
# Check that files exist in new location
ls -la public/break_escape/
# Should see: js/ css/ assets/ index.html.reference
# Check that old locations are gone
ls js 2>/dev/null && echo "ERROR: js still exists!" || echo "✓ js moved"
ls css 2>/dev/null && echo "ERROR: css still exists!" || echo "✓ css moved"
ls assets 2>/dev/null && echo "ERROR: assets still exists!" || echo "✓ assets moved"
```
**Expected output:** ✓ for all three checks
### 2.4 Update .gitignore
```bash
# Ensure public/break_escape is NOT ignored
vim .gitignore
```
**Check that these lines are NOT present:**
```
public/break_escape/
public/break_escape/**/*
```
**If they are, remove them**
**Verify git sees the files:**
```bash
git status | grep "public/break_escape"
# Should show moved files
```
### 2.5 Commit
```bash
git add -A
git status # Review - should show renames/moves
git commit -m "refactor: Move game files to public/break_escape/
- Move js/ to public/break_escape/js/
- Move css/ to public/break_escape/css/
- Move assets/ to public/break_escape/assets/
- Preserve git history with mv command
- Keep index.html.reference for reference"
git push
```
---
## Phase 3: Create Scenario ERB Templates (Week 1-2, ~6 hours)
### Objectives
- Create app/assets/scenarios directory structure
- Convert scenario JSON files to ERB templates
- Add randomization for passwords/pins
- Keep .ink files in scenarios/ink/ (will be served directly)
### 3.1 Create Directory Structure
```bash
# Create scenarios directory
mkdir -p app/assets/scenarios
# List current scenarios
ls scenarios/*.json
```
**Note the scenario names** (e.g., ceo_exfil, cybok_heist, biometric_breach)
### 3.2 Process Each Scenario
**For EACH scenario file, follow these steps:**
#### Example: ceo_exfil
```bash
# Set scenario name
SCENARIO="ceo_exfil"
# Create scenario directory
mkdir -p "app/assets/scenarios/${SCENARIO}"
# Move scenario JSON and rename to .erb
mv "scenarios/${SCENARIO}.json" "app/assets/scenarios/${SCENARIO}/scenario.json.erb"
# Verify
ls -la "app/assets/scenarios/${SCENARIO}/"
# Should see: scenario.json.erb
```
#### Edit ERB Template to Add Randomization
```bash
vim "app/assets/scenarios/${SCENARIO}/scenario.json.erb"
```
**Find any hardcoded passwords/pins and replace:**
**Before:**
```json
{
"locked": true,
"lockType": "password",
"requires": "admin123"
}
```
**After:**
```erb
{
"locked": true,
"lockType": "password",
"requires": "<%= random_password %>"
}
```
**For PINs:**
```erb
"requires": "<%= random_pin %>"
```
**For codes:**
```erb
"requires": "<%= random_code %>"
```
**Save and close**
#### Repeat for All Scenarios
**Complete conversion script for all main scenarios:**
```bash
#!/bin/bash
# Convert all scenario JSON files to ERB structure
echo "Converting scenario files to ERB templates..."
# Main game scenarios (these are the production scenarios)
MAIN_SCENARIOS=(
"ceo_exfil"
"cybok_heist"
"biometric_breach"
)
# Test/demo scenarios (keep for testing)
TEST_SCENARIOS=(
"scenario1"
"scenario2"
"scenario3"
"scenario4"
"npc-hub-demo-ghost-protocol"
"npc-patrol-lockpick"
"npc-sprite-test2"
"test-multiroom-npc"
"test-npc-face-player"
"test-npc-patrol"
"test-npc-personal-space"
"test-npc-waypoints"
"test-rfid-multiprotocol"
"test-rfid"
"test_complex_multidirection"
"test_horizontal_layout"
"test_mixed_room_sizes"
"test_multiple_connections"
"test_vertical_layout"
"timed_messages_example"
"title-screen-demo"
)
# Process main scenarios
echo ""
echo "=== Processing Main Scenarios ==="
for scenario in "${MAIN_SCENARIOS[@]}"; do
if [ -f "scenarios/${scenario}.json" ]; then
echo "Processing: $scenario"
# Create directory
mkdir -p "app/assets/scenarios/${scenario}"
# Move and rename (just rename to .erb, don't modify content yet)
mv "scenarios/${scenario}.json" "app/assets/scenarios/${scenario}/scenario.json.erb"
echo " ✓ Moved to app/assets/scenarios/${scenario}/scenario.json.erb"
echo " → Edit later to add <%= random_password %>, <%= random_pin %>, etc."
else
echo " ⚠ File not found: scenarios/${scenario}.json (skipping)"
fi
done
# Process test scenarios
echo ""
echo "=== Processing Test Scenarios ==="
for scenario in "${TEST_SCENARIOS[@]}"; do
if [ -f "scenarios/${scenario}.json" ]; then
echo "Processing: $scenario"
# Create directory
mkdir -p "app/assets/scenarios/${scenario}"
# Move and rename
mv "scenarios/${scenario}.json" "app/assets/scenarios/${scenario}/scenario.json.erb"
echo " ✓ Moved to app/assets/scenarios/${scenario}/scenario.json.erb"
else
echo " ⚠ File not found: scenarios/${scenario}.json (skipping)"
fi
done
echo ""
echo "=== Summary ==="
echo "Converted files:"
find app/assets/scenarios -name "scenario.json.erb" | wc -l
echo ""
echo "Directory structure:"
ls -d app/assets/scenarios/*/
echo ""
echo "✓ Conversion complete!"
echo ""
echo "IMPORTANT:"
echo "- Files have been renamed to .erb but content is still JSON"
echo "- ERB randomization (random_password, etc.) will be added in Phase 4"
echo "- For now, scenarios work as-is with static passwords"
```
**Save this script** as `scripts/convert-scenarios.sh` and run:
```bash
chmod +x scripts/convert-scenarios.sh
./scripts/convert-scenarios.sh
```
**Alternative: Manual conversion for main scenarios only:**
```bash
# If you only want to convert the 3 main scenarios manually:
# CEO Exfiltration
mkdir -p app/assets/scenarios/ceo_exfil
mv scenarios/ceo_exfil.json app/assets/scenarios/ceo_exfil/scenario.json.erb
# CybOK Heist
mkdir -p app/assets/scenarios/cybok_heist
mv scenarios/cybok_heist.json app/assets/scenarios/cybok_heist/scenario.json.erb
# Biometric Breach
mkdir -p app/assets/scenarios/biometric_breach
mv scenarios/biometric_breach.json app/assets/scenarios/biometric_breach/scenario.json.erb
# Verify
ls -la app/assets/scenarios/*/scenario.json.erb
```
**Note:**
- Files are renamed to `.erb` extension but content remains valid JSON
- ERB randomization code (`<%= random_password %>`) will be added later in Phase 4
- This preserves git history and allows immediate testing
- Test scenarios are useful for development but don't need randomization
### 3.3 Handle Ink Files
**Keep .ink files in scenarios/ink/ - they will be served directly**
```bash
# Verify ink files are still in place
ls scenarios/ink/*.ink | wc -l
# Should show ~30 files
echo "✓ Ink files staying in scenarios/ink/ (served via JIT compilation)"
```
### 3.4 Remove Old scenarios Directory (Optional)
**Only after verifying all scenario.json.erb files are created:**
```bash
# Check if any .json files remain
ls scenarios/*.json 2>/dev/null
# If empty, safe to remove (or keep as backup)
# mv scenarios/old_scenarios_backup
```
### 3.5 Test ERB Processing
```bash
# Start Rails console
rails console
# Test ERB processing
template_path = Rails.root.join('app/assets/scenarios/ceo_exfil/scenario.json.erb')
erb = ERB.new(File.read(template_path))
# Create binding with random values
class TestBinding
def initialize
@random_password = 'TEST123'
@random_pin = '1234'
@random_code = 'abcd'
end
attr_reader :random_password, :random_pin, :random_code
def get_binding; binding; end
end
output = erb.result(TestBinding.new.get_binding)
json = JSON.parse(output)
puts "✓ ERB processing works!"
exit
```
**Expected output:** "✓ ERB processing works!" with no JSON parse errors
### 3.6 Commit
```bash
git add -A
git status # Review changes
git commit -m "refactor: Convert scenarios to ERB templates
- Move scenario JSON files to app/assets/scenarios/
- Rename to .erb extension
- Add randomization for passwords and PINs
- Keep .ink files in scenarios/ink/ for JIT compilation
- Each scenario now in own directory"
git push
```
---
## Phase 4: Database Setup (Week 2-3, ~6 hours)
### Objectives
- Generate database migrations
- Create Mission and Game models
- Set up polymorphic associations
- Run migrations
### 4.1 Generate Migrations
```bash
# Generate missions migration
rails generate migration CreateBreakEscapeMissions
# Generate games migration
rails generate migration CreateBreakEscapeGames
# List generated migrations
ls db/migrate/
```
### 4.2 Edit Missions Migration
```bash
# Find the missions migration file
MIGRATION=$(ls db/migrate/*_create_break_escape_missions.rb)
vim "$MIGRATION"
```
**Replace entire contents with:**
```ruby
class CreateBreakEscapeMissions < ActiveRecord::Migration[7.0]
def change
create_table :break_escape_missions do |t|
t.string :name, null: false
t.string :display_name, null: false
t.text :description
t.boolean :published, default: false, null: false
t.integer :difficulty_level, default: 1, null: false
t.timestamps
end
add_index :break_escape_missions, :name, unique: true
add_index :break_escape_missions, :published
end
end
```
**Save and close**
### 4.3 Edit Games Migration
```bash
# Find the games migration file
MIGRATION=$(ls db/migrate/*_create_break_escape_games.rb)
vim "$MIGRATION"
```
**Replace entire contents with:**
```ruby
class CreateBreakEscapeGames < ActiveRecord::Migration[7.0]
def change
create_table :break_escape_games do |t|
# Polymorphic player
t.references :player, polymorphic: true, null: false, index: true
# Mission reference
t.references :mission, null: false, foreign_key: { to_table: :break_escape_missions }
# Scenario snapshot (ERB-generated)
t.jsonb :scenario_data, null: false
# Player state
t.jsonb :player_state, null: false, default: {
currentRoom: nil,
unlockedRooms: [],
unlockedObjects: [],
inventory: [],
encounteredNPCs: [],
globalVariables: {},
biometricSamples: [],
biometricUnlocks: [],
bluetoothDevices: [],
notes: [],
health: 100
}
# Metadata
t.string :status, default: 'in_progress', null: false
t.datetime :started_at
t.datetime :completed_at
t.integer :score, default: 0, null: false
t.timestamps
end
add_index :break_escape_games,
[:player_type, :player_id, :mission_id],
unique: true,
name: 'index_games_on_player_and_mission'
add_index :break_escape_games, :scenario_data, using: :gin
add_index :break_escape_games, :player_state, using: :gin
add_index :break_escape_games, :status
end
end
```
**Save and close**
### 4.4 Run Migrations
```bash
# Run migrations
rails db:migrate
# Verify tables created
rails runner "puts ActiveRecord::Base.connection.tables"
# Should include: break_escape_missions, break_escape_games
```
**Expected output:** Tables listed successfully
### 4.5 Generate Model Files
```bash
# Generate Mission model (skip migration since we already created it)
rails generate model Mission --skip-migration
# Generate Game model
rails generate model Game --skip-migration
```
### 4.6 Edit Mission Model
```bash
vim app/models/break_escape/mission.rb
```
**Replace entire contents with:**
```ruby
module BreakEscape
class Mission < ApplicationRecord
self.table_name = 'break_escape_missions'
has_many :games, class_name: 'BreakEscape::Game', dependent: :destroy
validates :name, presence: true, uniqueness: true
validates :display_name, presence: true
validates :difficulty_level, inclusion: { in: 1..5 }
scope :published, -> { where(published: true) }
# Path to scenario directory
def scenario_path
Rails.root.join('app', 'assets', 'scenarios', name)
end
# Generate scenario data via ERB
def generate_scenario_data
template_path = scenario_path.join('scenario.json.erb')
raise "Scenario template not found: #{name}" unless File.exist?(template_path)
erb = ERB.new(File.read(template_path))
binding_context = ScenarioBinding.new
output = erb.result(binding_context.get_binding)
JSON.parse(output)
rescue JSON::ParserError => e
raise "Invalid JSON in #{name} after ERB processing: #{e.message}"
end
# Binding context for ERB variables
class ScenarioBinding
def initialize
@random_password = SecureRandom.alphanumeric(8)
@random_pin = rand(1000..9999).to_s
@random_code = SecureRandom.hex(4)
end
attr_reader :random_password, :random_pin, :random_code
def get_binding
binding
end
end
end
end
```
**Save and close**
### 4.7 Edit Game Model
```bash
vim app/models/break_escape/game.rb
```
**Replace entire contents with:**
```ruby
module BreakEscape
class Game < ApplicationRecord
self.table_name = 'break_escape_games'
# Associations
belongs_to :player, polymorphic: true
belongs_to :mission, class_name: 'BreakEscape::Mission'
# Validations
validates :player, presence: true
validates :mission, presence: true
validates :status, inclusion: { in: %w[in_progress completed abandoned] }
# Scopes
scope :active, -> { where(status: 'in_progress') }
scope :completed, -> { where(status: 'completed') }
# Callbacks
before_create :generate_scenario_data
before_create :initialize_player_state
before_create :set_started_at
# Room management
def unlock_room!(room_id)
player_state['unlockedRooms'] ||= []
player_state['unlockedRooms'] << room_id unless player_state['unlockedRooms'].include?(room_id)
save!
end
def room_unlocked?(room_id)
player_state['unlockedRooms']&.include?(room_id) || start_room?(room_id)
end
def start_room?(room_id)
scenario_data['startRoom'] == room_id
end
# Object management
def unlock_object!(object_id)
player_state['unlockedObjects'] ||= []
player_state['unlockedObjects'] << object_id unless player_state['unlockedObjects'].include?(object_id)
save!
end
def object_unlocked?(object_id)
player_state['unlockedObjects']&.include?(object_id)
end
# Inventory management
def add_inventory_item!(item)
player_state['inventory'] ||= []
player_state['inventory'] << item
save!
end
def remove_inventory_item!(item_id)
player_state['inventory']&.reject! { |item| item['id'] == item_id }
save!
end
# NPC tracking
def encounter_npc!(npc_id)
player_state['encounteredNPCs'] ||= []
player_state['encounteredNPCs'] << npc_id unless player_state['encounteredNPCs'].include?(npc_id)
save!
end
# Global variables (synced with client)
def update_global_variables!(variables)
player_state['globalVariables'] ||= {}
player_state['globalVariables'].merge!(variables)
save!
end
# Minigame state
def add_biometric_sample!(sample)
player_state['biometricSamples'] ||= []
player_state['biometricSamples'] << sample
save!
end
def add_bluetooth_device!(device)
player_state['bluetoothDevices'] ||= []
unless player_state['bluetoothDevices'].any? { |d| d['mac'] == device['mac'] }
player_state['bluetoothDevices'] << device
end
save!
end
def add_note!(note)
player_state['notes'] ||= []
player_state['notes'] << note
save!
end
# Health management
def update_health!(value)
player_state['health'] = value.clamp(0, 100)
save!
end
# Scenario data access
def room_data(room_id)
scenario_data.dig('rooms', room_id)
end
def filtered_room_data(room_id)
room = room_data(room_id)&.deep_dup
return nil unless room
# Remove solutions
room.delete('requires')
room.delete('lockType') if room['locked']
# Remove solutions from objects
room['objects']&.each do |obj|
obj.delete('requires')
obj.delete('lockType') if obj['locked']
obj.delete('contents') if obj['locked']
end
room
end
# Unlock validation
def validate_unlock(target_type, target_id, attempt, method)
if target_type == 'door'
room = room_data(target_id)
return false unless room && room['locked']
case method
when 'key'
room['requires'] == attempt
when 'pin', 'password'
room['requires'].to_s == attempt.to_s
when 'lockpick'
true # Client minigame succeeded
else
false
end
else
# Find object in all rooms
scenario_data['rooms'].each do |_room_id, room_data|
object = room_data['objects']&.find { |obj| obj['id'] == target_id }
next unless object && object['locked']
case method
when 'key'
return object['requires'] == attempt
when 'pin', 'password'
return object['requires'].to_s == attempt.to_s
when 'lockpick'
return true
end
end
false
end
end
private
def generate_scenario_data
self.scenario_data = mission.generate_scenario_data
end
def initialize_player_state
self.player_state ||= {}
self.player_state['currentRoom'] ||= scenario_data['startRoom']
self.player_state['unlockedRooms'] ||= [scenario_data['startRoom']]
self.player_state['unlockedObjects'] ||= []
self.player_state['inventory'] ||= []
self.player_state['encounteredNPCs'] ||= []
self.player_state['globalVariables'] ||= {}
self.player_state['biometricSamples'] ||= []
self.player_state['biometricUnlocks'] ||= []
self.player_state['bluetoothDevices'] ||= []
self.player_state['notes'] ||= []
self.player_state['health'] ||= 100
end
def set_started_at
self.started_at ||= Time.current
end
end
end
```
**Save and close**
### 4.8 Test Models
```bash
# Start Rails console
rails console
# Test Mission model
mission = BreakEscape::Mission.new(name: 'test', display_name: 'Test')
puts mission.valid? # Should be true
# Test scenario path
mission.name = 'ceo_exfil'
puts mission.scenario_path
# Should print: /home/user/BreakEscape/app/assets/scenarios/ceo_exfil
exit
```
**Expected output:** Valid model, correct path
### 4.9 Commit
```bash
git add -A
git status # Review changes
git commit -m "feat: Add database schema and models
- Create break_escape_missions table (metadata only)
- Create break_escape_games table (state + scenario snapshot)
- Add Mission model with ERB scenario generation
- Add Game model with state management methods
- Use JSONB for flexible state storage
- Polymorphic player association (User/DemoUser)"
git push
```
---
## Phase 5: Seed Data (Week 3, ~2 hours)
### Objectives
- Create simple seed file for mission metadata
- No scenario data in database (generated on-demand)
- Test mission creation
### 5.1 Create Seed File
```bash
vim db/seeds.rb
```
**Replace entire contents with:**
```ruby
puts "Creating BreakEscape missions..."
# List all scenario directories
scenario_dirs = Dir.glob(Rails.root.join('app/assets/scenarios/*')).select { |f| File.directory?(f) }
scenario_dirs.each do |dir|
scenario_name = File.basename(dir)
next if scenario_name == 'common' # Skip common directory if it exists
# Create mission metadata
mission = BreakEscape::Mission.find_or_initialize_by(name: scenario_name)
if mission.new_record?
mission.display_name = scenario_name.titleize
mission.description = "Play the #{scenario_name.titleize} scenario"
mission.published = true
mission.difficulty_level = 3 # Default, can be updated later
mission.save!
puts " ✓ Created: #{mission.display_name}"
else
puts " - Exists: #{mission.display_name}"
end
end
puts "Done! Created #{BreakEscape::Mission.count} missions."
```
**Save and close**
### 5.2 Run Seeds
```bash
# Run seeds
rails db:seed
# Verify missions created
rails runner "puts BreakEscape::Mission.pluck(:name, :display_name)"
```
**Expected output:** List of missions created
### 5.3 Test ERB Generation
```bash
# Start Rails console
rails console
# Test full flow
mission = BreakEscape::Mission.first
puts "Testing: #{mission.display_name}"
scenario_data = mission.generate_scenario_data
puts "✓ Scenario data generated (#{scenario_data.keys.length} keys)"
puts " Start room: #{scenario_data['startRoom']}"
# Check for randomization
if scenario_data.to_s.include?('random_password')
puts "✗ ERROR: ERB variable not replaced!"
else
puts "✓ ERB variables replaced"
end
exit
```
**Expected output:** Scenario generated successfully, no ERB variables in output
### 5.4 Commit
```bash
git add -A
git commit -m "feat: Add seed file for mission metadata
- Create missions from scenario directories
- Auto-discover scenarios in app/assets/scenarios/
- Simple metadata only (no scenario data in DB)
- Scenario data generated on-demand via ERB"
git push
```
---
## Phase 6: Controllers and Routes (Week 4-5, ~12 hours)
**This phase is long - broken into sub-phases**
### 6.1 Generate Controllers
```bash
# Generate main controllers
rails generate controller break_escape/missions index show
rails generate controller break_escape/games show
# Generate API controller
mkdir -p app/controllers/break_escape/api
rails generate controller break_escape/api/games --skip-routes
```
### 6.2 Configure Routes
```bash
vim config/routes.rb
```
**Replace entire contents with:**
```ruby
BreakEscape::Engine.routes.draw do
# Mission selection
resources :missions, only: [:index, :show]
# Game management
resources :games, only: [:show, :create] do
member do
# Scenario and NPC data
get 'scenario' # Returns scenario_data JSON
get 'ink' # Returns NPC script (JIT compiled)
# API endpoints
scope module: :api do
get 'bootstrap' # Initial game data
put 'sync_state' # Periodic state sync
post 'unlock' # Validate unlock attempt
post 'inventory' # Update inventory
end
end
end
root to: 'missions#index'
end
```
**Save and close**
### 6.3 Test Routes
```bash
# List routes
rails routes | grep break_escape
# Should see:
# - missions_path
# - mission_path
# - games_path
# - game_path
# - scenario_game_path
# - ink_game_path
# - bootstrap_game_path
# - etc.
```
**Expected output:** Routes listed successfully
### 6.4 Edit ApplicationController
```bash
vim app/controllers/break_escape/application_controller.rb
```
**Replace entire contents with:**
```ruby
module BreakEscape
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Include Pundit if available
include Pundit::Authorization if defined?(Pundit)
# Helper method to get current player (polymorphic)
def current_player
if BreakEscape.standalone_mode?
# Standalone mode - get/create demo user
@current_player ||= DemoUser.first_or_create!(handle: 'demo_player')
else
# Mounted mode - use Hacktivity's current_user
current_user
end
end
helper_method :current_player
# Handle authorization errors
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
end
end
```
**Save and close**
### 6.5 Edit MissionsController
```bash
vim app/controllers/break_escape/missions_controller.rb
```
**Replace entire contents with:**
```ruby
module BreakEscape
class MissionsController < ApplicationController
def index
@missions = if defined?(Pundit)
policy_scope(Mission)
else
Mission.published
end
end
def show
@mission = Mission.find(params[:id])
authorize @mission if defined?(Pundit)
# Create or find game instance for current player
@game = Game.find_or_create_by!(
player: current_player,
mission: @mission
)
redirect_to game_path(@game)
end
end
end
```
**Save and close**
### 6.6 Edit GamesController
```bash
vim app/controllers/break_escape/games_controller.rb
```
**Replace entire contents with:**
```ruby
require 'open3'
module BreakEscape
class GamesController < ApplicationController
before_action :set_game, only: [:show, :scenario, :ink]
def show
authorize @game if defined?(Pundit)
@mission = @game.mission
end
# GET /games/:id/scenario
# Returns scenario JSON for this game instance
def scenario
authorize @game if defined?(Pundit)
render json: @game.scenario_data
end
# GET /games/:id/ink?npc=helper1
# Returns NPC script (JIT compiled if needed)
def ink
authorize @game if defined?(Pundit)
npc_id = params[:npc]
return render_error('Missing npc parameter', :bad_request) unless npc_id.present?
# Find NPC in scenario data
npc = find_npc_in_scenario(npc_id)
return render_error('NPC not found in scenario', :not_found) unless npc
# Resolve ink file path and compile if needed
ink_json_path = resolve_and_compile_ink(npc['storyPath'])
return render_error('Ink script not found', :not_found) unless ink_json_path && File.exist?(ink_json_path)
# Serve compiled JSON
render json: JSON.parse(File.read(ink_json_path))
rescue JSON::ParserError => e
render_error("Invalid JSON in compiled ink: #{e.message}", :internal_server_error)
end
private
def set_game
@game = Game.find(params[:id])
end
def find_npc_in_scenario(npc_id)
@game.scenario_data['rooms']&.each do |_room_id, room_data|
npc = room_data['npcs']&.find { |n| n['id'] == npc_id }
return npc if npc
end
nil
end
# Resolve ink path and compile if necessary
def resolve_and_compile_ink(story_path)
base_path = Rails.root.join(story_path)
json_path = find_compiled_json(base_path)
ink_path = find_ink_source(base_path)
if ink_path && needs_compilation?(ink_path, json_path)
Rails.logger.info "[BreakEscape] Compiling #{File.basename(ink_path)}..."
json_path = compile_ink(ink_path)
end
json_path
end
def find_compiled_json(base_path)
return base_path if File.exist?(base_path)
ink_json_path = base_path.to_s.gsub(/\.json$/, '.ink.json')
return Pathname.new(ink_json_path) if File.exist?(ink_json_path)
json_path = base_path.to_s.gsub(/\.ink\.json$/, '.json')
return Pathname.new(json_path) if File.exist?(json_path)
nil
end
def find_ink_source(base_path)
ink_path = base_path.to_s.gsub(/\.(ink\.)?json$/, '.ink')
File.exist?(ink_path) ? Pathname.new(ink_path) : nil
end
def needs_compilation?(ink_path, json_path)
return true unless json_path && File.exist?(json_path)
File.mtime(ink_path) > File.mtime(json_path)
end
def compile_ink(ink_path)
output_path = ink_path.to_s.gsub(/\.ink$/, '.json')
inklecate_path = Rails.root.join('bin', 'inklecate')
stdout, stderr, status = Open3.capture3(
inklecate_path.to_s,
'-o', output_path,
ink_path.to_s
)
unless status.success?
Rails.logger.error "[BreakEscape] Ink compilation failed: #{stderr}"
raise "Ink compilation failed for #{File.basename(ink_path)}: #{stderr}"
end
if stderr.present?
Rails.logger.warn "[BreakEscape] Ink compilation warnings: #{stderr}"
end
Rails.logger.info "[BreakEscape] Compiled #{File.basename(ink_path)} (#{(File.size(output_path) / 1024.0).round(2)} KB)"
Pathname.new(output_path)
end
def render_error(message, status)
render json: { error: message }, status: status
end
end
end
```
**Save and close**
### 6.7 Edit API GamesController
```bash
vim app/controllers/break_escape/api/games_controller.rb
```
**Replace entire contents with:**
```ruby
module BreakEscape
module Api
class GamesController < ApplicationController
before_action :set_game
# GET /games/:id/bootstrap
# Initial game data for client
def bootstrap
authorize @game if defined?(Pundit)
render json: {
gameId: @game.id,
missionName: @game.mission.display_name,
startRoom: @game.scenario_data['startRoom'],
playerState: @game.player_state,
roomLayout: build_room_layout
}
end
# PUT /games/:id/sync_state
# Periodic state sync from client
def sync_state
authorize @game if defined?(Pundit)
# Update allowed fields
if params[:currentRoom]
@game.player_state['currentRoom'] = params[:currentRoom]
end
if params[:globalVariables]
@game.update_global_variables!(params[:globalVariables].to_unsafe_h)
end
@game.save!
render json: { success: true }
end
# POST /games/:id/unlock
# Validate unlock attempt
def unlock
authorize @game if defined?(Pundit)
target_type = params[:targetType]
target_id = params[:targetId]
attempt = params[:attempt]
method = params[:method]
is_valid = @game.validate_unlock(target_type, target_id, attempt, method)
if is_valid
if target_type == 'door'
@game.unlock_room!(target_id)
room_data = @game.filtered_room_data(target_id)
render json: {
success: true,
type: 'door',
roomData: room_data
}
else
@game.unlock_object!(target_id)
render json: {
success: true,
type: 'object'
}
end
else
render json: {
success: false,
message: 'Invalid attempt'
}, status: :unprocessable_entity
end
end
# POST /games/:id/inventory
# Update inventory
def inventory
authorize @game if defined?(Pundit)
action = params[:action]
item = params[:item]
case action
when 'add'
@game.add_inventory_item!(item.to_unsafe_h)
render json: { success: true, inventory: @game.player_state['inventory'] }
when 'remove'
@game.remove_inventory_item!(item['id'])
render json: { success: true, inventory: @game.player_state['inventory'] }
else
render json: { success: false, message: 'Invalid action' }, status: :bad_request
end
end
private
def set_game
@game = Game.find(params[:id])
end
def build_room_layout
layout = {}
@game.scenario_data['rooms'].each do |room_id, room_data|
layout[room_id] = {
connections: room_data['connections'],
locked: room_data['locked'] || false
}
end
layout
end
end
end
end
```
**Save and close**
### 6.8 Test Controllers
```bash
# Start Rails server
rails server
# In another terminal, test endpoints
# (Assuming you have a game with id=1)
# Test scenario endpoint
curl http://localhost:3000/break_escape/games/1/scenario
# Test bootstrap endpoint
curl http://localhost:3000/break_escape/games/1/bootstrap
```
**Expected output:** JSON responses (may get auth errors if Pundit enabled, that's fine for now)
### 6.9 Commit
```bash
git add -A
git commit -m "feat: Add controllers and routes
- Add MissionsController for scenario selection
- Add GamesController with scenario/ink endpoints
- Add JIT Ink compilation logic
- Add API::GamesController for game state management
- Configure routes with REST + API endpoints
- Add authorization hooks (Pundit)"
git push
```
---
**Continue to Phase 7 in next section...**
---
## Progress Tracking
Use this checklist to track your progress:
- [ ] Phase 1: Setup Rails Engine (8 hours)
- [ ] Phase 2: Move Game Files (4 hours)
- [ ] Phase 3: Create Scenario Templates (6 hours)
- [ ] Phase 4: Database Setup (6 hours)
- [ ] Phase 5: Seed Data (2 hours)
- [ ] Phase 6: Controllers and Routes (12 hours)
- [ ] Phase 7: Policies (4 hours)
- [ ] Phase 8: Views (6 hours)
- [ ] Phase 9: Client Integration (12 hours)
- [ ] Phase 10: Testing (8 hours)
- [ ] Phase 11: Standalone Mode (4 hours)
- [ ] Phase 12: Deployment (6 hours)
**Total: ~78 hours over 10-12 weeks**
---
## Continued in Part 2
This document contains Phases 1-6. Continue with the next document for:
- Phase 7: Policies
- Phase 8: Views
- Phase 9: Client Integration
- Phase 10: Testing
- Phase 11: Standalone Mode
- Phase 12: Deployment
See `03_IMPLEMENTATION_PLAN_PART2.md` for continuation.