mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
docs: Add complete Rails Engine migration plan (JSON-centric approach)
Comprehensive implementation plan for converting BreakEscape to a Rails Engine. DOCUMENTATION CREATED: - 00_OVERVIEW.md: Project aims, philosophy, decisions summary - 01_ARCHITECTURE.md: Technical design, models, controllers, API - 02_IMPLEMENTATION_PLAN.md: Phases 1-6 with bash/rails commands - 02_IMPLEMENTATION_PLAN_PART2.md: Phases 7-12 with client integration - 03_DATABASE_SCHEMA.md: 3-table JSONB schema reference - 04_TESTING_GUIDE.md: Fixtures, tests, CI setup - README.md: Quick start and navigation guide KEY APPROACH: - Simplified JSON-centric storage (3 tables vs 10+) - JSONB for player state (one column, all game data) - Minimal client changes (move files, add API client) - Dual mode: Standalone + Hacktivity integration - Session-based auth with polymorphic player - Pundit policies for authorization - ERB templates for scenario randomization TIMELINE: 12-14 weeks (vs 22 weeks complex approach) ARCHITECTURE DECISIONS: - Static assets in public/break_escape/ - Scenarios in app/assets/scenarios/ with ERB - .ink and .ink.json files organized by scenario - Lazy-load NPC scripts on encounter - Server validates unlocks, client runs dialogue - 6 API endpoints (not 15+) Each phase includes: - Specific bash mv commands - Rails generate and migrate commands - Code examples with manual edits - Testing steps - Git commit points Ready for implementation.
This commit is contained in:
189
planning_notes/rails-engine-migration-json/00_OVERVIEW.md
Normal file
189
planning_notes/rails-engine-migration-json/00_OVERVIEW.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# BreakEscape Rails Engine Migration - Overview
|
||||
|
||||
## Project Aims
|
||||
|
||||
Convert BreakEscape from a standalone browser game to a **Rails Engine** that can:
|
||||
|
||||
1. **Mount in Hacktivity Cyber Security Labs**
|
||||
- Integrate with existing Devise user authentication
|
||||
- Share user sessions and permissions
|
||||
- Embed game canvas in Hacktivity pages
|
||||
- Future: Access to VMs and lab infrastructure
|
||||
|
||||
2. **Run Standalone**
|
||||
- Single-user demo mode for testing and development
|
||||
- Simple configuration-based user setup
|
||||
- No authentication complexity in standalone mode
|
||||
|
||||
3. **Maintain Game Quality**
|
||||
- Preserve all existing game functionality
|
||||
- Minimal changes to client-side code
|
||||
- Keep modular ES6 architecture intact
|
||||
- Maintain performance and UX
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
**Simplify, Don't Complicate**
|
||||
|
||||
- Use JSON storage (game state already in this format)
|
||||
- Keep client-side game logic unchanged where possible
|
||||
- Validate only what matters server-side
|
||||
- Move files, don't rewrite them
|
||||
- Test incrementally
|
||||
|
||||
## Architectural Approach
|
||||
|
||||
### JSON-Centric Storage
|
||||
|
||||
**Instead of** complex relational database:
|
||||
```ruby
|
||||
# One JSONB column stores entire player state
|
||||
{
|
||||
"currentRoom": "room_office",
|
||||
"unlockedRooms": ["room_reception", "room_office"],
|
||||
"unlockedObjects": ["desk_drawer_123"],
|
||||
"inventory": [{"type": "key", "name": "Office Key"}],
|
||||
"encounteredNPCs": ["security_guard"],
|
||||
"globalVariables": {"alarm_triggered": false}
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Server Validation
|
||||
|
||||
**Server validates:**
|
||||
- ✅ Unlock attempts (checks scenario solutions)
|
||||
- ✅ Room access (is room unlocked?)
|
||||
- ✅ Inventory changes (is item in unlocked location?)
|
||||
- ✅ NPC encounters (is NPC in current room?)
|
||||
|
||||
**Client trusted for:**
|
||||
- ⚠️ Player position (doesn't affect security)
|
||||
- ⚠️ Global variables (synced periodically)
|
||||
- ⚠️ Minigame mechanics (only result validated)
|
||||
|
||||
### Static Asset Serving
|
||||
|
||||
**Game files stay mostly unchanged:**
|
||||
- JS/CSS/Assets → `public/break_escape/`
|
||||
- Scenarios → `app/assets/scenarios/` (with ERB)
|
||||
- Game served via Rails view (for CSP nonces)
|
||||
- Assets loaded statically (bypasses asset pipeline)
|
||||
|
||||
## Key Decisions Summary
|
||||
|
||||
### 1. Database Schema
|
||||
- **3 simple tables** (not 10+)
|
||||
- **JSONB storage** for game state
|
||||
- **Polymorphic user** for flexibility
|
||||
|
||||
### 2. API Endpoints
|
||||
- **6 simple endpoints** (not 15+)
|
||||
- **Backwards compatible** JSON format
|
||||
- **Session-based auth** (not JWT)
|
||||
|
||||
### 3. File Organization
|
||||
- **Build in current directory** (not separate repo)
|
||||
- **Move files with bash** (not copy/rewrite)
|
||||
- **Keep client code unchanged** where possible
|
||||
|
||||
### 4. NPC & Scenarios
|
||||
- **Lazy-load Ink scripts** on encounter
|
||||
- **ERB templates** for scenario JSON (randomization)
|
||||
- **Store .ink source** and compiled .ink.json
|
||||
- **All conversations client-side** (instant UX)
|
||||
|
||||
### 5. Security & Auth
|
||||
- **Session-based** authentication
|
||||
- **Pundit policies** for authorization
|
||||
- **CSP with nonces** for inline scripts
|
||||
- **Polymorphic player** model
|
||||
|
||||
### 6. Testing Strategy
|
||||
- **Rails fixtures** for test data
|
||||
- **Integration tests** following Hacktivity patterns
|
||||
- **Manual testing** steps for each phase
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
**12-14 weeks total:**
|
||||
- Weeks 1-2: Setup Rails Engine structure
|
||||
- Weeks 3-4: Database, models, API endpoints
|
||||
- Weeks 5-6: Client integration (minimal changes)
|
||||
- Weeks 7-8: Scenario ERB templates, NPC loading
|
||||
- Weeks 9-10: Testing and bug fixes
|
||||
- Weeks 11-12: Hacktivity integration
|
||||
- Weeks 13-14: Polish and deployment
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Low Risk Approach
|
||||
|
||||
1. **Keep original files** - work in same repo, use git
|
||||
2. **Test incrementally** - each phase independently
|
||||
3. **Dual-mode support** - standalone + mounted
|
||||
4. **Minimal rewrites** - move files, update paths only
|
||||
5. **Backwards compatible** - client code expects same data
|
||||
|
||||
### Rollback Strategy
|
||||
|
||||
- Git branches for each phase
|
||||
- Original files preserved during moves
|
||||
- Can revert any step
|
||||
- Standalone mode for safe testing
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Functional Requirements
|
||||
- ✅ Game runs in standalone mode
|
||||
- ✅ Game mounts in Hacktivity
|
||||
- ✅ All scenarios work
|
||||
- ✅ NPCs and dialogue function
|
||||
- ✅ Server validates unlocks
|
||||
- ✅ Progress persists
|
||||
|
||||
### Performance Requirements
|
||||
- ✅ Room loading < 500ms
|
||||
- ✅ Unlock validation < 300ms
|
||||
- ✅ No visual lag
|
||||
- ✅ Assets load quickly
|
||||
|
||||
### Code Quality
|
||||
- ✅ Rails tests pass
|
||||
- ✅ Minimal client changes
|
||||
- ✅ Clear separation of concerns
|
||||
- ✅ Well-documented
|
||||
|
||||
## Document Structure
|
||||
|
||||
This implementation plan includes:
|
||||
|
||||
1. **00_OVERVIEW.md** (this file) - Aims and decisions
|
||||
2. **01_ARCHITECTURE.md** - Detailed technical design
|
||||
3. **02_IMPLEMENTATION_PLAN.md** - Step-by-step TODO
|
||||
4. **03_DATABASE_SCHEMA.md** - Models and migrations
|
||||
5. **04_API_ENDPOINTS.md** - API specification
|
||||
6. **05_CLIENT_INTEGRATION.md** - Client-side changes
|
||||
7. **06_TESTING_GUIDE.md** - Testing strategy
|
||||
8. **07_DEPLOYMENT.md** - Deployment steps
|
||||
|
||||
## Getting Started
|
||||
|
||||
**Read in order:**
|
||||
1. This overview (understand aims)
|
||||
2. Architecture document (understand design)
|
||||
3. Implementation plan (follow TODO)
|
||||
|
||||
**Before starting:**
|
||||
- Commit all current changes
|
||||
- Create feature branch
|
||||
- Backup database (if exists)
|
||||
|
||||
## Questions or Issues
|
||||
|
||||
If anything is unclear:
|
||||
1. Check architecture document
|
||||
2. Review specific section
|
||||
3. Test in standalone mode first
|
||||
4. Ask for clarification
|
||||
|
||||
**Remember:** Goal is simplicity. If something feels complex, there's probably a simpler way.
|
||||
817
planning_notes/rails-engine-migration-json/01_ARCHITECTURE.md
Normal file
817
planning_notes/rails-engine-migration-json/01_ARCHITECTURE.md
Normal file
@@ -0,0 +1,817 @@
|
||||
# BreakEscape Rails Engine - Technical Architecture
|
||||
|
||||
## System Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Hacktivity (Host App) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ BreakEscape Rails Engine │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌────────────────────────────┐ │ │
|
||||
│ │ │ Controllers │───▶│ Models (3 tables) │ │ │
|
||||
│ │ │ - Games │ │ - GameInstance (JSONB) │ │ │
|
||||
│ │ │ - API │ │ - Scenario (JSONB) │ │ │
|
||||
│ │ │ - Scenarios │ │ - NpcScript (TEXT) │ │ │
|
||||
│ │ └──────────────┘ └────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌────────────────────────────┐ │ │
|
||||
│ │ │ Views │ │ Policies (Pundit) │ │ │
|
||||
│ │ │ - show.html │ │ - GameInstancePolicy │ │ │
|
||||
│ │ └──────────────┘ └────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ public/break_escape/ │ │ │
|
||||
│ │ │ - js/ (ES6 modules, unchanged) │ │ │
|
||||
│ │ │ - css/ (stylesheets, unchanged) │ │ │
|
||||
│ │ │ - assets/ (images/sounds, unchanged) │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Devise User Authentication (Hacktivity) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Final Structure (After Migration)
|
||||
|
||||
```
|
||||
/home/user/BreakEscape/
|
||||
├── app/
|
||||
│ ├── controllers/
|
||||
│ │ └── break_escape/
|
||||
│ │ ├── application_controller.rb
|
||||
│ │ ├── games_controller.rb # Main game view
|
||||
│ │ └── api/
|
||||
│ │ ├── games_controller.rb # Game state API
|
||||
│ │ ├── rooms_controller.rb # Room loading
|
||||
│ │ ├── unlocks_controller.rb # Unlock validation
|
||||
│ │ ├── inventory_controller.rb # Inventory sync
|
||||
│ │ └── npcs_controller.rb # NPC script loading
|
||||
│ │
|
||||
│ ├── models/
|
||||
│ │ └── break_escape/
|
||||
│ │ ├── application_record.rb
|
||||
│ │ ├── game_instance.rb # JSONB player state
|
||||
│ │ ├── scenario.rb # JSONB scenario data
|
||||
│ │ └── npc_script.rb # Ink scripts
|
||||
│ │
|
||||
│ ├── policies/
|
||||
│ │ └── break_escape/
|
||||
│ │ ├── game_instance_policy.rb
|
||||
│ │ └── scenario_policy.rb
|
||||
│ │
|
||||
│ ├── views/
|
||||
│ │ └── break_escape/
|
||||
│ │ └── games/
|
||||
│ │ └── show.html.erb # Game container
|
||||
│ │
|
||||
│ ├── assets/
|
||||
│ │ └── scenarios/ # ERB templates
|
||||
│ │ ├── common/
|
||||
│ │ │ └── ink/
|
||||
│ │ │ └── shared_dialogue.ink.json
|
||||
│ │ │
|
||||
│ │ ├── ceo_exfil/
|
||||
│ │ │ ├── scenario.json.erb
|
||||
│ │ │ └── ink/
|
||||
│ │ │ ├── security_guard.ink
|
||||
│ │ │ └── security_guard.ink.json
|
||||
│ │ │
|
||||
│ │ ├── cybok_heist/
|
||||
│ │ │ ├── scenario.json.erb
|
||||
│ │ │ └── ink/
|
||||
│ │ │
|
||||
│ │ └── biometric_breach/
|
||||
│ │ ├── scenario.json.erb
|
||||
│ │ └── ink/
|
||||
│ │
|
||||
│ └── helpers/
|
||||
│ └── break_escape/
|
||||
│ └── application_helper.rb
|
||||
│
|
||||
├── lib/
|
||||
│ ├── break_escape/
|
||||
│ │ ├── engine.rb # Engine config
|
||||
│ │ ├── version.rb
|
||||
│ │ └── scenario_loader.rb # ERB processor
|
||||
│ │
|
||||
│ └── break_escape.rb
|
||||
│
|
||||
├── config/
|
||||
│ ├── routes.rb # Engine routes
|
||||
│ ├── initializers/
|
||||
│ │ └── break_escape.rb # Config
|
||||
│ └── break_escape_standalone.yml # Standalone config
|
||||
│
|
||||
├── db/
|
||||
│ ├── migrate/
|
||||
│ │ ├── 001_create_break_escape_scenarios.rb
|
||||
│ │ ├── 002_create_break_escape_npc_scripts.rb
|
||||
│ │ └── 003_create_break_escape_game_instances.rb
|
||||
│ └── seeds.rb # Import scenarios
|
||||
│
|
||||
├── test/
|
||||
│ ├── fixtures/
|
||||
│ │ └── break_escape/
|
||||
│ │ ├── scenarios.yml
|
||||
│ │ ├── npc_scripts.yml
|
||||
│ │ └── game_instances.yml
|
||||
│ │
|
||||
│ ├── models/
|
||||
│ │ └── break_escape/
|
||||
│ │
|
||||
│ ├── controllers/
|
||||
│ │ └── break_escape/
|
||||
│ │
|
||||
│ ├── integration/
|
||||
│ │ └── break_escape/
|
||||
│ │ ├── game_flow_test.rb
|
||||
│ │ └── api_test.rb
|
||||
│ │
|
||||
│ └── policies/
|
||||
│ └── break_escape/
|
||||
│
|
||||
├── public/ # Static assets
|
||||
│ └── break_escape/
|
||||
│ ├── js/ # mv js/ here
|
||||
│ ├── css/ # mv css/ here
|
||||
│ └── assets/ # mv assets/ here
|
||||
│
|
||||
├── break_escape.gemspec
|
||||
├── Gemfile
|
||||
├── Rakefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### 1. Scenarios Table
|
||||
|
||||
Stores scenario metadata and complete JSON data.
|
||||
|
||||
```ruby
|
||||
create_table :break_escape_scenarios do |t|
|
||||
t.string :name, null: false # 'ceo_exfil'
|
||||
t.string :display_name, null: false # 'CEO Exfiltration'
|
||||
t.text :description
|
||||
t.jsonb :scenario_data, null: false # Complete scenario with solutions
|
||||
t.boolean :published, default: false
|
||||
t.integer :difficulty_level, default: 1 # 1-5
|
||||
t.timestamps
|
||||
|
||||
t.index :name, unique: true
|
||||
t.index :published
|
||||
t.index :scenario_data, using: :gin
|
||||
end
|
||||
```
|
||||
|
||||
**scenario_data structure:**
|
||||
```json
|
||||
{
|
||||
"startRoom": "room_reception",
|
||||
"scenarioName": "CEO Exfiltration",
|
||||
"scenarioBrief": "...",
|
||||
"rooms": {
|
||||
"room_reception": {
|
||||
"type": "reception",
|
||||
"connections": {"north": "room_office"},
|
||||
"locked": false,
|
||||
"objects": [...]
|
||||
},
|
||||
"room_office": {
|
||||
"type": "office",
|
||||
"connections": {"south": "room_reception"},
|
||||
"locked": true,
|
||||
"lockType": "password",
|
||||
"requires": "admin123", // Server only
|
||||
"objects": [...]
|
||||
}
|
||||
},
|
||||
"npcs": [
|
||||
{
|
||||
"id": "security_guard",
|
||||
"displayName": "Security Guard",
|
||||
"phoneId": "player_phone",
|
||||
"npcType": "phone",
|
||||
"canUnlock": ["room_server"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. NPC Scripts Table
|
||||
|
||||
Stores Ink dialogue scripts.
|
||||
|
||||
```ruby
|
||||
create_table :break_escape_npc_scripts do |t|
|
||||
t.references :scenario, null: false, foreign_key: { to_table: :break_escape_scenarios }
|
||||
t.string :npc_id, null: false # 'security_guard'
|
||||
t.text :ink_source # .ink source (optional)
|
||||
t.text :ink_compiled, null: false # .ink.json compiled
|
||||
t.timestamps
|
||||
|
||||
t.index [:scenario_id, :npc_id], unique: true
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Game Instances Table
|
||||
|
||||
Stores player game state (polymorphic player).
|
||||
|
||||
```ruby
|
||||
create_table :break_escape_game_instances do |t|
|
||||
# Polymorphic player (User in Hacktivity, DemoUser in standalone)
|
||||
t.references :player, polymorphic: true, null: false
|
||||
|
||||
# Scenario reference
|
||||
t.references :scenario, null: false, foreign_key: { to_table: :break_escape_scenarios }
|
||||
|
||||
# Player state (JSONB - this is the key simplification!)
|
||||
t.jsonb :player_state, null: false, default: {
|
||||
currentRoom: 'room_reception',
|
||||
position: { x: 0, y: 0 },
|
||||
unlockedRooms: [],
|
||||
unlockedObjects: [],
|
||||
inventory: [],
|
||||
encounteredNPCs: [],
|
||||
globalVariables: {}
|
||||
}
|
||||
|
||||
# Game metadata
|
||||
t.string :status, default: 'in_progress' # in_progress, completed, abandoned
|
||||
t.datetime :started_at
|
||||
t.datetime :completed_at
|
||||
t.integer :score, default: 0
|
||||
t.integer :health, default: 100
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index [:player_type, :player_id, :scenario_id], unique: true, name: 'index_game_instances_on_player_and_scenario'
|
||||
t.index :player_state, using: :gin
|
||||
t.index :status
|
||||
end
|
||||
```
|
||||
|
||||
**player_state example:**
|
||||
```json
|
||||
{
|
||||
"currentRoom": "room_office",
|
||||
"position": {"x": 150, "y": 200},
|
||||
"unlockedRooms": ["room_reception", "room_office"],
|
||||
"unlockedObjects": ["desk_drawer_123"],
|
||||
"inventory": [
|
||||
{
|
||||
"type": "key",
|
||||
"name": "Office Key",
|
||||
"key_id": "office_key_1",
|
||||
"takeable": true
|
||||
}
|
||||
],
|
||||
"encounteredNPCs": ["security_guard"],
|
||||
"globalVariables": {
|
||||
"alarm_triggered": false,
|
||||
"player_favor": 5,
|
||||
"security_alerted": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
### GameInstance Model
|
||||
|
||||
```ruby
|
||||
module BreakEscape
|
||||
class GameInstance < ApplicationRecord
|
||||
# Polymorphic association
|
||||
belongs_to :player, polymorphic: true
|
||||
belongs_to :scenario
|
||||
|
||||
# Validations
|
||||
validates :player, presence: true
|
||||
validates :scenario, presence: true
|
||||
validates :status, inclusion: { in: %w[in_progress completed abandoned] }
|
||||
|
||||
# Scopes
|
||||
scope :active, -> { where(status: 'in_progress') }
|
||||
scope :completed, -> { where(status: 'completed') }
|
||||
|
||||
# State management
|
||||
def unlock_room!(room_id)
|
||||
player_state['unlockedRooms'] ||= []
|
||||
player_state['unlockedRooms'] << room_id unless player_state['unlockedRooms'].include?(room_id)
|
||||
save!
|
||||
end
|
||||
|
||||
def unlock_object!(object_id)
|
||||
player_state['unlockedObjects'] ||= []
|
||||
player_state['unlockedObjects'] << object_id unless player_state['unlockedObjects'].include?(object_id)
|
||||
save!
|
||||
end
|
||||
|
||||
def add_inventory_item!(item)
|
||||
player_state['inventory'] ||= []
|
||||
player_state['inventory'] << item
|
||||
save!
|
||||
end
|
||||
|
||||
def room_unlocked?(room_id)
|
||||
player_state['unlockedRooms']&.include?(room_id) || scenario.start_room?(room_id)
|
||||
end
|
||||
|
||||
def object_unlocked?(object_id)
|
||||
player_state['unlockedObjects']&.include?(object_id)
|
||||
end
|
||||
|
||||
def npc_encountered?(npc_id)
|
||||
player_state['encounteredNPCs']&.include?(npc_id)
|
||||
end
|
||||
|
||||
def encounter_npc!(npc_id)
|
||||
player_state['encounteredNPCs'] ||= []
|
||||
player_state['encounteredNPCs'] << npc_id unless player_state['encounteredNPCs'].include?(npc_id)
|
||||
save!
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Scenario Model
|
||||
|
||||
```ruby
|
||||
module BreakEscape
|
||||
class Scenario < ApplicationRecord
|
||||
has_many :game_instances
|
||||
has_many :npc_scripts
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :scenario_data, presence: true
|
||||
|
||||
scope :published, -> { where(published: true) }
|
||||
|
||||
def start_room?(room_id)
|
||||
scenario_data['startRoom'] == room_id
|
||||
end
|
||||
|
||||
def room_data(room_id)
|
||||
scenario_data.dig('rooms', room_id)
|
||||
end
|
||||
|
||||
def filtered_room_data(room_id)
|
||||
room = room_data(room_id)&.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('contents') if obj['locked']
|
||||
end
|
||||
|
||||
room
|
||||
end
|
||||
|
||||
def validate_unlock(target_type, target_id, attempt, method)
|
||||
if target_type == 'door'
|
||||
room = room_data(target_id)
|
||||
return false unless room
|
||||
|
||||
case method
|
||||
when 'key'
|
||||
room['requires'] == attempt
|
||||
when 'pin', 'password'
|
||||
room['requires'] == attempt
|
||||
when 'lockpick'
|
||||
true # Client minigame succeeded
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
# Find object in all rooms
|
||||
# Implementation details...
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### NpcScript Model
|
||||
|
||||
```ruby
|
||||
module BreakEscape
|
||||
class NpcScript < ApplicationRecord
|
||||
belongs_to :scenario
|
||||
|
||||
validates :npc_id, presence: true
|
||||
validates :ink_compiled, presence: true
|
||||
validates :npc_id, uniqueness: { scope: :scenario_id }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
BreakEscape::Engine.routes.draw do
|
||||
# Main game view
|
||||
resources :games, only: [:show] do
|
||||
member do
|
||||
get :play # Alias for show
|
||||
end
|
||||
end
|
||||
|
||||
# Scenario selection
|
||||
resources :scenarios, only: [:index, :show]
|
||||
|
||||
# API endpoints
|
||||
namespace :api do
|
||||
resources :games, only: [] do
|
||||
member do
|
||||
get :bootstrap # Initial game data
|
||||
put :sync_state # Periodic state sync
|
||||
end
|
||||
|
||||
# Nested resources
|
||||
resources :rooms, only: [:show]
|
||||
resources :npcs, only: [] do
|
||||
member do
|
||||
get :script # Load Ink script
|
||||
end
|
||||
end
|
||||
|
||||
# Actions
|
||||
post :unlock # Validate unlock attempt
|
||||
post :inventory # Update inventory
|
||||
end
|
||||
end
|
||||
|
||||
# Root
|
||||
root to: 'scenarios#index'
|
||||
end
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Bootstrap Game
|
||||
|
||||
```
|
||||
GET /api/games/:id/bootstrap
|
||||
|
||||
Response:
|
||||
{
|
||||
"gameId": 123,
|
||||
"scenarioName": "CEO Exfiltration",
|
||||
"startRoom": "room_reception",
|
||||
"playerState": {
|
||||
"currentRoom": "room_reception",
|
||||
"unlockedRooms": ["room_reception"],
|
||||
"inventory": [],
|
||||
...
|
||||
},
|
||||
"roomLayout": {
|
||||
"room_reception": {
|
||||
"connections": {"north": "room_office"},
|
||||
"locked": false
|
||||
},
|
||||
"room_office": {
|
||||
"connections": {"south": "room_reception"},
|
||||
"locked": true // No lockType or requires!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Load Room
|
||||
|
||||
```
|
||||
GET /api/games/:game_id/rooms/:room_id
|
||||
|
||||
Authorization: Session (current_user)
|
||||
|
||||
Response (if authorized):
|
||||
{
|
||||
"roomId": "room_office",
|
||||
"type": "office",
|
||||
"connections": {...},
|
||||
"objects": [
|
||||
{
|
||||
"type": "desk",
|
||||
"name": "Manager's Desk",
|
||||
"locked": true, // But no requires!
|
||||
"observations": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Response (if unauthorized):
|
||||
403 Forbidden
|
||||
```
|
||||
|
||||
### 3. Validate Unlock
|
||||
|
||||
```
|
||||
POST /api/games/:game_id/unlock
|
||||
|
||||
Body:
|
||||
{
|
||||
"targetType": "door", // or "object"
|
||||
"targetId": "room_ceo",
|
||||
"method": "password",
|
||||
"attempt": "admin123"
|
||||
}
|
||||
|
||||
Response (success):
|
||||
{
|
||||
"success": true,
|
||||
"type": "door",
|
||||
"roomData": { ... } // Filtered room data
|
||||
}
|
||||
|
||||
Response (failure):
|
||||
{
|
||||
"success": false,
|
||||
"message": "Invalid password"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Inventory
|
||||
|
||||
```
|
||||
POST /api/games/:game_id/inventory
|
||||
|
||||
Body:
|
||||
{
|
||||
"action": "add", // or "remove"
|
||||
"item": {
|
||||
"type": "key",
|
||||
"name": "Office Key",
|
||||
"key_id": "office_key_1"
|
||||
}
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"inventory": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Load NPC Script
|
||||
|
||||
```
|
||||
GET /api/games/:game_id/npcs/:npc_id/script
|
||||
|
||||
Response:
|
||||
{
|
||||
"npcId": "security_guard",
|
||||
"inkScript": { ... }, // Full Ink JSON
|
||||
"eventMappings": [...],
|
||||
"timedMessages": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Sync State
|
||||
|
||||
```
|
||||
PUT /api/games/:game_id/sync_state
|
||||
|
||||
Body:
|
||||
{
|
||||
"currentRoom": "room_office",
|
||||
"position": {"x": 150, "y": 220},
|
||||
"globalVariables": {"alarm_triggered": false}
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## Policies (Pundit)
|
||||
|
||||
### GameInstancePolicy
|
||||
|
||||
```ruby
|
||||
module BreakEscape
|
||||
class GameInstancePolicy < ApplicationPolicy
|
||||
def show?
|
||||
# Owner or admin
|
||||
record.player == user || user&.admin?
|
||||
end
|
||||
|
||||
def update?
|
||||
show?
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if user&.admin?
|
||||
scope.all
|
||||
else
|
||||
scope.where(player: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### ScenarioPolicy
|
||||
|
||||
```ruby
|
||||
module BreakEscape
|
||||
class ScenarioPolicy < ApplicationPolicy
|
||||
def index?
|
||||
true # Everyone can see scenarios
|
||||
end
|
||||
|
||||
def show?
|
||||
# Only published or admin
|
||||
record.published? || user&.admin?
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if user&.admin?
|
||||
scope.all
|
||||
else
|
||||
scope.published
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Engine Configuration
|
||||
|
||||
```ruby
|
||||
# lib/break_escape/engine.rb
|
||||
module BreakEscape
|
||||
class Engine < ::Rails::Engine
|
||||
isolate_namespace BreakEscape
|
||||
|
||||
config.generators do |g|
|
||||
g.test_framework :test_unit, fixture: true
|
||||
g.fixture_replacement :factory_bot, dir: 'test/factories'
|
||||
g.assets false
|
||||
g.helper false
|
||||
end
|
||||
|
||||
# Pundit authorization
|
||||
config.after_initialize do
|
||||
BreakEscape::ApplicationController.include Pundit::Authorization
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Standalone Configuration
|
||||
|
||||
```yaml
|
||||
# config/break_escape_standalone.yml
|
||||
development:
|
||||
standalone_mode: true
|
||||
demo_user:
|
||||
handle: "demo_player"
|
||||
role: "pro" # admin, pro, user
|
||||
scenarios:
|
||||
enabled: ['ceo_exfil', 'cybok_heist']
|
||||
|
||||
production:
|
||||
standalone_mode: false # Mounted in Hacktivity
|
||||
```
|
||||
|
||||
### Initializer
|
||||
|
||||
```ruby
|
||||
# config/initializers/break_escape.rb
|
||||
module BreakEscape
|
||||
class << self
|
||||
attr_accessor :configuration
|
||||
end
|
||||
|
||||
def self.configure
|
||||
self.configuration ||= Configuration.new
|
||||
yield(configuration)
|
||||
end
|
||||
|
||||
class Configuration
|
||||
attr_accessor :standalone_mode, :demo_user, :user_class
|
||||
|
||||
def initialize
|
||||
standalone_config = load_standalone_config
|
||||
|
||||
@standalone_mode = standalone_config['standalone_mode']
|
||||
@demo_user = standalone_config['demo_user']
|
||||
@user_class = @standalone_mode ? 'BreakEscape::DemoUser' : 'User'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_standalone_config
|
||||
config_path = Rails.root.join('config/break_escape_standalone.yml')
|
||||
return {} unless File.exist?(config_path)
|
||||
|
||||
YAML.load_file(config_path)[Rails.env] || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BreakEscape.configure do |config|
|
||||
# Config loaded from YAML
|
||||
end
|
||||
```
|
||||
|
||||
## Client Integration
|
||||
|
||||
### Game View (Rails)
|
||||
|
||||
```erb
|
||||
<%# app/views/break_escape/games/show.html.erb %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= @scenario.display_name %> - BreakEscape</title>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= stylesheet_link_tag '/break_escape/css/styles.css', nonce: true %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="break-escape-game"></div>
|
||||
|
||||
<%# Bootstrap config for client %>
|
||||
<script nonce="<%= content_security_policy_nonce %>">
|
||||
window.breakEscapeConfig = {
|
||||
gameId: <%= @game_instance.id %>,
|
||||
apiBasePath: '<%= api_game_path(@game_instance) %>',
|
||||
assetsPath: '/break_escape/assets',
|
||||
csrfToken: '<%= form_authenticity_token %>'
|
||||
};
|
||||
</script>
|
||||
|
||||
<%# Load game (ES6 module) %>
|
||||
<%= javascript_include_tag '/break_escape/js/main.js', type: 'module', nonce: true %>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Client-Side Changes (Minimal)
|
||||
|
||||
```javascript
|
||||
// public/break_escape/js/config.js (NEW FILE)
|
||||
export const API_BASE = window.breakEscapeConfig?.apiBasePath || '';
|
||||
export const ASSETS_PATH = window.breakEscapeConfig?.assetsPath || 'assets';
|
||||
export const GAME_ID = window.breakEscapeConfig?.gameId;
|
||||
export const CSRF_TOKEN = window.breakEscapeConfig?.csrfToken;
|
||||
|
||||
// public/break_escape/js/core/api-client.js (NEW FILE)
|
||||
import { API_BASE, CSRF_TOKEN } from '../config.js';
|
||||
|
||||
export async function apiGet(endpoint) {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`API Error: ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiPost(endpoint, data) {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-Token': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`API Error: ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
Changes to existing files are minimal - mostly importing and using API client instead of loading local JSON.
|
||||
|
||||
## Next Steps
|
||||
|
||||
See **02_IMPLEMENTATION_PLAN.md** for detailed step-by-step instructions.
|
||||
@@ -0,0 +1,881 @@
|
||||
# BreakEscape Rails Engine - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This is the **actionable TODO list** for converting BreakEscape to a Rails Engine.
|
||||
|
||||
**Key Principles:**
|
||||
- ✅ Use `bash mv` commands to move files (don't copy/rewrite)
|
||||
- ✅ Use `rails generate` and `rails db:migrate` commands
|
||||
- ✅ Make manual edits only after generating files
|
||||
- ✅ Test after each phase
|
||||
- ✅ Commit after each working step
|
||||
|
||||
**Estimated Time:** 12-14 weeks
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup Rails Engine Structure (Week 1)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Ensure you're in the project directory
|
||||
cd /home/user/BreakEscape
|
||||
|
||||
# Create feature branch
|
||||
git checkout -b rails-engine-migration
|
||||
|
||||
# Commit current state
|
||||
git add -A
|
||||
git commit -m "chore: Checkpoint before Rails Engine migration"
|
||||
```
|
||||
|
||||
### 1.1 Generate Rails Engine
|
||||
|
||||
```bash
|
||||
# Generate mountable engine (creates isolated namespace)
|
||||
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
|
||||
```
|
||||
|
||||
**Manual edits after generation:**
|
||||
|
||||
```ruby
|
||||
# lib/break_escape/engine.rb
|
||||
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('lib', __dir__)
|
||||
|
||||
# Pundit authorization
|
||||
config.after_initialize do
|
||||
BreakEscape::ApplicationController.send(:include, Pundit::Authorization) if defined?(Pundit)
|
||||
end
|
||||
|
||||
# Static files from public/break_escape
|
||||
config.middleware.use ::ActionDispatch::Static, "#{root}/public"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# lib/break_escape/version.rb
|
||||
module BreakEscape
|
||||
VERSION = '0.1.0'
|
||||
end
|
||||
```
|
||||
|
||||
**Update Gemfile:**
|
||||
|
||||
```ruby
|
||||
# Gemfile
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
# Development dependencies
|
||||
group :development, :test do
|
||||
gem 'sqlite3'
|
||||
gem 'pry'
|
||||
gem 'pry-byebug'
|
||||
end
|
||||
|
||||
# Runtime dependencies
|
||||
gem 'rails', '~> 7.0'
|
||||
gem 'pundit', '~> 2.3'
|
||||
```
|
||||
|
||||
**Update gemspec:**
|
||||
|
||||
```ruby
|
||||
# break_escape.gemspec
|
||||
require_relative "lib/break_escape/version"
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "break_escape"
|
||||
spec.version = BreakEscape::VERSION
|
||||
spec.authors = ["Your Name"]
|
||||
spec.email = ["your.email@example.com"]
|
||||
spec.summary = "BreakEscape escape room game engine"
|
||||
spec.description = "Rails engine for BreakEscape escape room cybersecurity training 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
|
||||
```
|
||||
|
||||
**Install dependencies:**
|
||||
|
||||
```bash
|
||||
bundle install
|
||||
```
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: Generate Rails Engine structure"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Move Game Files to public/ (Week 1)
|
||||
|
||||
### 2.1 Create public directory structure
|
||||
|
||||
```bash
|
||||
# Create directory
|
||||
mkdir -p public/break_escape
|
||||
|
||||
# Move existing game files (USING MV, NOT COPY!)
|
||||
mv js public/break_escape/
|
||||
mv css public/break_escape/
|
||||
mv assets public/break_escape/
|
||||
|
||||
# Keep index.html for reference (but we'll use Rails view)
|
||||
cp index.html public/break_escape/index.html.backup
|
||||
```
|
||||
|
||||
**Verify files moved correctly:**
|
||||
|
||||
```bash
|
||||
ls -la public/break_escape/
|
||||
# Should see: js/ css/ assets/ index.html.backup
|
||||
```
|
||||
|
||||
**Update .gitignore if needed:**
|
||||
|
||||
```bash
|
||||
# .gitignore should NOT ignore public/break_escape/
|
||||
# Verify:
|
||||
git check-ignore public/break_escape/js/
|
||||
# Should return nothing (not ignored)
|
||||
```
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "refactor: Move game files to public/break_escape/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Reorganize Scenarios (Week 1-2)
|
||||
|
||||
### 3.1 Create scenario directory structure
|
||||
|
||||
```bash
|
||||
# Create app/assets/scenarios structure
|
||||
mkdir -p app/assets/scenarios/common/ink
|
||||
|
||||
# List current scenarios
|
||||
ls scenarios/*.json
|
||||
```
|
||||
|
||||
### 3.2 Reorganize each scenario
|
||||
|
||||
**For EACH scenario (ceo_exfil, cybok_heist, etc.):**
|
||||
|
||||
```bash
|
||||
# Example for ceo_exfil:
|
||||
SCENARIO="ceo_exfil"
|
||||
|
||||
# Create directory
|
||||
mkdir -p "app/assets/scenarios/${SCENARIO}/ink"
|
||||
|
||||
# Move scenario JSON and rename to .erb
|
||||
mv "scenarios/${SCENARIO}.json" "app/assets/scenarios/${SCENARIO}/scenario.json.erb"
|
||||
|
||||
# Move NPC Ink files
|
||||
# Find all ink files referenced in the scenario
|
||||
# Example:
|
||||
mv "scenarios/ink/security_guard.ink" "app/assets/scenarios/${SCENARIO}/ink/"
|
||||
mv "scenarios/ink/security_guard.ink.json" "app/assets/scenarios/${SCENARIO}/ink/"
|
||||
|
||||
# Repeat for each NPC in the scenario
|
||||
```
|
||||
|
||||
**For common/shared Ink files:**
|
||||
|
||||
```bash
|
||||
# If any ink files are used by multiple scenarios:
|
||||
mv scenarios/ink/shared_*.ink app/assets/scenarios/common/ink/
|
||||
mv scenarios/ink/shared_*.ink.json app/assets/scenarios/common/ink/
|
||||
```
|
||||
|
||||
**Manual process (document what you do):**
|
||||
|
||||
Create a file to track the reorganization:
|
||||
|
||||
```bash
|
||||
# scenarios/REORGANIZATION_LOG.md
|
||||
# Document which files went where
|
||||
# Example:
|
||||
# ceo_exfil:
|
||||
# - scenarios/ceo_exfil.json → app/assets/scenarios/ceo_exfil/scenario.json.erb
|
||||
# - scenarios/ink/security_guard.* → app/assets/scenarios/ceo_exfil/ink/
|
||||
# ...
|
||||
```
|
||||
|
||||
**Remove old scenarios directory (after verification):**
|
||||
|
||||
```bash
|
||||
# ONLY after verifying all files moved:
|
||||
rm -rf scenarios/
|
||||
|
||||
# Or keep for reference:
|
||||
mv scenarios scenarios.backup
|
||||
```
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "refactor: Reorganize scenarios into app/assets/scenarios/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Database Setup (Week 2)
|
||||
|
||||
### 4.1 Generate migrations
|
||||
|
||||
```bash
|
||||
# Generate Scenarios table
|
||||
rails generate migration CreateBreakEscapeScenarios
|
||||
|
||||
# Generate NpcScripts table
|
||||
rails generate migration CreateBreakEscapeNpcScripts
|
||||
|
||||
# Generate GameInstances table
|
||||
rails generate migration CreateBreakEscapeGameInstances
|
||||
```
|
||||
|
||||
**Edit generated migrations:**
|
||||
|
||||
```ruby
|
||||
# db/migrate/xxx_create_break_escape_scenarios.rb
|
||||
class CreateBreakEscapeScenarios < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :break_escape_scenarios do |t|
|
||||
t.string :name, null: false
|
||||
t.string :display_name, null: false
|
||||
t.text :description
|
||||
t.jsonb :scenario_data, null: false
|
||||
t.boolean :published, default: false
|
||||
t.integer :difficulty_level, default: 1
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :break_escape_scenarios, :name, unique: true
|
||||
add_index :break_escape_scenarios, :published
|
||||
add_index :break_escape_scenarios, :scenario_data, using: :gin
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# db/migrate/xxx_create_break_escape_npc_scripts.rb
|
||||
class CreateBreakEscapeNpcScripts < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :break_escape_npc_scripts do |t|
|
||||
t.references :scenario, null: false, foreign_key: { to_table: :break_escape_scenarios }
|
||||
t.string :npc_id, null: false
|
||||
t.text :ink_source
|
||||
t.text :ink_compiled, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :break_escape_npc_scripts, [:scenario_id, :npc_id], unique: true
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# db/migrate/xxx_create_break_escape_game_instances.rb
|
||||
class CreateBreakEscapeGameInstances < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :break_escape_game_instances do |t|
|
||||
# Polymorphic player
|
||||
t.references :player, polymorphic: true, null: false
|
||||
|
||||
# Scenario reference
|
||||
t.references :scenario, null: false, foreign_key: { to_table: :break_escape_scenarios }
|
||||
|
||||
# Player state (JSONB)
|
||||
t.jsonb :player_state, null: false, default: {
|
||||
currentRoom: nil,
|
||||
position: { x: 0, y: 0 },
|
||||
unlockedRooms: [],
|
||||
unlockedObjects: [],
|
||||
inventory: [],
|
||||
encounteredNPCs: [],
|
||||
globalVariables: {}
|
||||
}
|
||||
|
||||
# Game metadata
|
||||
t.string :status, default: 'in_progress'
|
||||
t.datetime :started_at
|
||||
t.datetime :completed_at
|
||||
t.integer :score, default: 0
|
||||
t.integer :health, default: 100
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :break_escape_game_instances,
|
||||
[:player_type, :player_id, :scenario_id],
|
||||
unique: true,
|
||||
name: 'index_game_instances_on_player_and_scenario'
|
||||
add_index :break_escape_game_instances, :player_state, using: :gin
|
||||
add_index :break_escape_game_instances, :status
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Run migrations:**
|
||||
|
||||
```bash
|
||||
rails db:migrate
|
||||
```
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: Add database schema for scenarios, NPCs, and game instances"
|
||||
```
|
||||
|
||||
### 4.2 Generate models
|
||||
|
||||
```bash
|
||||
# Generate model files (skeleton only, we'll edit them)
|
||||
rails generate model Scenario --skip-migration
|
||||
rails generate model NpcScript --skip-migration
|
||||
rails generate model GameInstance --skip-migration
|
||||
```
|
||||
|
||||
**Edit models:**
|
||||
|
||||
```ruby
|
||||
# app/models/break_escape/scenario.rb
|
||||
module BreakEscape
|
||||
class Scenario < ApplicationRecord
|
||||
self.table_name = 'break_escape_scenarios'
|
||||
|
||||
has_many :game_instances, class_name: 'BreakEscape::GameInstance'
|
||||
has_many :npc_scripts, class_name: 'BreakEscape::NpcScript'
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :display_name, presence: true
|
||||
validates :scenario_data, presence: true
|
||||
|
||||
scope :published, -> { where(published: true) }
|
||||
|
||||
def start_room
|
||||
scenario_data['startRoom']
|
||||
end
|
||||
|
||||
def start_room?(room_id)
|
||||
start_room == room_id
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def validate_unlock(target_type, target_id, attempt, method)
|
||||
if target_type == 'door'
|
||||
room = room_data(target_id)
|
||||
return false unless room
|
||||
return false unless 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
|
||||
next unless 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
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# app/models/break_escape/npc_script.rb
|
||||
module BreakEscape
|
||||
class NpcScript < ApplicationRecord
|
||||
self.table_name = 'break_escape_npc_scripts'
|
||||
|
||||
belongs_to :scenario, class_name: 'BreakEscape::Scenario'
|
||||
|
||||
validates :npc_id, presence: true
|
||||
validates :ink_compiled, presence: true
|
||||
validates :npc_id, uniqueness: { scope: :scenario_id }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# app/models/break_escape/game_instance.rb
|
||||
module BreakEscape
|
||||
class GameInstance < ApplicationRecord
|
||||
self.table_name = 'break_escape_game_instances'
|
||||
|
||||
# Polymorphic association
|
||||
belongs_to :player, polymorphic: true
|
||||
belongs_to :scenario, class_name: 'BreakEscape::Scenario'
|
||||
|
||||
validates :player, presence: true
|
||||
validates :scenario, presence: true
|
||||
validates :status, inclusion: { in: %w[in_progress completed abandoned] }
|
||||
|
||||
scope :active, -> { where(status: 'in_progress') }
|
||||
scope :completed, -> { where(status: 'completed') }
|
||||
|
||||
before_create :set_started_at
|
||||
before_create :initialize_player_state
|
||||
|
||||
# State management methods
|
||||
def unlock_room!(room_id)
|
||||
player_state['unlockedRooms'] ||= []
|
||||
player_state['unlockedRooms'] << room_id unless player_state['unlockedRooms'].include?(room_id)
|
||||
save!
|
||||
end
|
||||
|
||||
def unlock_object!(object_id)
|
||||
player_state['unlockedObjects'] ||= []
|
||||
player_state['unlockedObjects'] << object_id unless player_state['unlockedObjects'].include?(object_id)
|
||||
save!
|
||||
end
|
||||
|
||||
def add_inventory_item!(item)
|
||||
player_state['inventory'] ||= []
|
||||
player_state['inventory'] << item
|
||||
save!
|
||||
end
|
||||
|
||||
def remove_inventory_item!(item_id)
|
||||
player_state['inventory'] ||= []
|
||||
player_state['inventory'].reject! { |item| item['id'] == item_id }
|
||||
save!
|
||||
end
|
||||
|
||||
def room_unlocked?(room_id)
|
||||
player_state['unlockedRooms']&.include?(room_id) || scenario.start_room?(room_id)
|
||||
end
|
||||
|
||||
def object_unlocked?(object_id)
|
||||
player_state['unlockedObjects']&.include?(object_id)
|
||||
end
|
||||
|
||||
def npc_encountered?(npc_id)
|
||||
player_state['encounteredNPCs']&.include?(npc_id)
|
||||
end
|
||||
|
||||
def encounter_npc!(npc_id)
|
||||
player_state['encounteredNPCs'] ||= []
|
||||
player_state['encounteredNPCs'] << npc_id unless player_state['encounteredNPCs'].include?(npc_id)
|
||||
save!
|
||||
end
|
||||
|
||||
def update_position!(x, y)
|
||||
player_state['position'] = { 'x' => x, 'y' => y }
|
||||
save!
|
||||
end
|
||||
|
||||
def update_global_variable!(key, value)
|
||||
player_state['globalVariables'] ||= {}
|
||||
player_state['globalVariables'][key] = value
|
||||
save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_started_at
|
||||
self.started_at ||= Time.current
|
||||
end
|
||||
|
||||
def initialize_player_state
|
||||
self.player_state ||= {}
|
||||
self.player_state['currentRoom'] ||= scenario.start_room
|
||||
self.player_state['unlockedRooms'] ||= [scenario.start_room]
|
||||
self.player_state['position'] ||= { 'x' => 0, 'y' => 0 }
|
||||
self.player_state['inventory'] ||= []
|
||||
self.player_state['unlockedObjects'] ||= []
|
||||
self.player_state['encounteredNPCs'] ||= []
|
||||
self.player_state['globalVariables'] ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: Add Scenario, NpcScript, and GameInstance models"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Scenario Import (Week 2)
|
||||
|
||||
### 5.1 Create scenario loader service
|
||||
|
||||
```bash
|
||||
mkdir -p lib/break_escape
|
||||
```
|
||||
|
||||
**Create loader:**
|
||||
|
||||
```ruby
|
||||
# lib/break_escape/scenario_loader.rb
|
||||
module BreakEscape
|
||||
class ScenarioLoader
|
||||
attr_reader :scenario_name
|
||||
|
||||
def initialize(scenario_name)
|
||||
@scenario_name = scenario_name
|
||||
end
|
||||
|
||||
def load
|
||||
# Load and process ERB template
|
||||
template_path = Rails.root.join('app/assets/scenarios', scenario_name, 'scenario.json.erb')
|
||||
raise "Scenario not found: #{scenario_name}" unless File.exist?(template_path)
|
||||
|
||||
erb = ERB.new(File.read(template_path))
|
||||
binding_context = ScenarioBinding.new
|
||||
|
||||
JSON.parse(erb.result(binding_context.get_binding))
|
||||
end
|
||||
|
||||
def import!
|
||||
scenario_data = load
|
||||
|
||||
scenario = Scenario.find_or_initialize_by(name: scenario_name)
|
||||
scenario.assign_attributes(
|
||||
display_name: scenario_data['scenarioName'] || scenario_name.titleize,
|
||||
description: scenario_data['scenarioBrief'],
|
||||
scenario_data: scenario_data,
|
||||
published: true
|
||||
)
|
||||
scenario.save!
|
||||
|
||||
# Import NPC scripts
|
||||
import_npc_scripts!(scenario, scenario_data)
|
||||
|
||||
scenario
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def import_npc_scripts!(scenario, scenario_data)
|
||||
npcs = scenario_data['npcs'] || []
|
||||
|
||||
npcs.each do |npc_data|
|
||||
npc_id = npc_data['id']
|
||||
|
||||
# Load Ink files
|
||||
ink_path = Rails.root.join('app/assets/scenarios', scenario_name, 'ink', "#{npc_id}.ink")
|
||||
ink_json_path = Rails.root.join('app/assets/scenarios', scenario_name, 'ink', "#{npc_id}.ink.json")
|
||||
|
||||
next unless File.exist?(ink_json_path)
|
||||
|
||||
npc_script = scenario.npc_scripts.find_or_initialize_by(npc_id: npc_id)
|
||||
npc_script.ink_source = File.read(ink_path) if File.exist?(ink_path)
|
||||
npc_script.ink_compiled = File.read(ink_json_path)
|
||||
npc_script.save!
|
||||
end
|
||||
end
|
||||
|
||||
# Binding context for ERB processing
|
||||
class ScenarioBinding
|
||||
def initialize
|
||||
@random_password = SecureRandom.alphanumeric(8)
|
||||
@random_pin = rand(1000..9999).to_s
|
||||
end
|
||||
|
||||
attr_reader :random_password, :random_pin
|
||||
|
||||
def get_binding
|
||||
binding
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 5.2 Create seed file
|
||||
|
||||
```ruby
|
||||
# db/seeds.rb
|
||||
puts "Importing scenarios..."
|
||||
|
||||
scenarios = Dir.glob(Rails.root.join('app/assets/scenarios', '*')).map do |path|
|
||||
File.basename(path)
|
||||
end.reject { |name| name == 'common' }
|
||||
|
||||
scenarios.each do |scenario_name|
|
||||
puts " Importing #{scenario_name}..."
|
||||
begin
|
||||
loader = BreakEscape::ScenarioLoader.new(scenario_name)
|
||||
scenario = loader.import!
|
||||
puts " ✓ #{scenario.display_name}"
|
||||
rescue => e
|
||||
puts " ✗ Error: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
puts "Done! Imported #{BreakEscape::Scenario.count} scenarios."
|
||||
```
|
||||
|
||||
**Run seeds:**
|
||||
|
||||
```bash
|
||||
rails db:seed
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
```bash
|
||||
rails console
|
||||
|
||||
# Check scenarios loaded
|
||||
BreakEscape::Scenario.count
|
||||
BreakEscape::Scenario.pluck(:name)
|
||||
|
||||
# Check NPC scripts
|
||||
BreakEscape::NpcScript.count
|
||||
```
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: Add scenario loader and import seeds"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Controllers and Routes (Week 3)
|
||||
|
||||
### 6.1 Generate controllers
|
||||
|
||||
```bash
|
||||
# Main controllers
|
||||
rails generate controller break_escape/games
|
||||
rails generate controller break_escape/scenarios
|
||||
|
||||
# API controllers
|
||||
rails generate controller break_escape/api/games
|
||||
rails generate controller break_escape/api/rooms
|
||||
rails generate controller break_escape/api/unlocks
|
||||
rails generate controller break_escape/api/inventory
|
||||
rails generate controller break_escape/api/npcs
|
||||
```
|
||||
|
||||
**Edit routes:**
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
BreakEscape::Engine.routes.draw do
|
||||
# Main game view
|
||||
resources :games, only: [:show] do
|
||||
member do
|
||||
get :play
|
||||
end
|
||||
end
|
||||
|
||||
# Scenario selection
|
||||
resources :scenarios, only: [:index, :show]
|
||||
|
||||
# API endpoints
|
||||
namespace :api do
|
||||
resources :games, only: [] do
|
||||
member do
|
||||
get :bootstrap
|
||||
put :sync_state
|
||||
post :unlock
|
||||
post :inventory
|
||||
end
|
||||
|
||||
resources :rooms, only: [:show]
|
||||
resources :npcs, only: [] do
|
||||
member do
|
||||
get :script
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
root to: 'scenarios#index'
|
||||
end
|
||||
```
|
||||
|
||||
**Edit application controller:**
|
||||
|
||||
```ruby
|
||||
# app/controllers/break_escape/application_controller.rb
|
||||
module BreakEscape
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
# Pundit authorization
|
||||
include Pundit::Authorization if defined?(Pundit)
|
||||
|
||||
# Helper method to get current player (polymorphic)
|
||||
def current_player
|
||||
if BreakEscape.configuration.standalone_mode
|
||||
# Standalone mode - get/create demo user
|
||||
@current_player ||= DemoUser.first_or_create!(
|
||||
handle: BreakEscape.configuration.demo_user['handle'],
|
||||
role: BreakEscape.configuration.demo_user['role']
|
||||
)
|
||||
else
|
||||
# Mounted mode - use Hacktivity's current_user
|
||||
current_user
|
||||
end
|
||||
end
|
||||
helper_method :current_player
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Edit games controller:**
|
||||
|
||||
```ruby
|
||||
# app/controllers/break_escape/games_controller.rb
|
||||
module BreakEscape
|
||||
class GamesController < ApplicationController
|
||||
before_action :set_game_instance
|
||||
|
||||
def show
|
||||
@scenario = @game_instance.scenario
|
||||
authorize @game_instance if defined?(Pundit)
|
||||
end
|
||||
|
||||
alias_method :play, :show
|
||||
|
||||
private
|
||||
|
||||
def set_game_instance
|
||||
@game_instance = GameInstance.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Edit scenarios controller:**
|
||||
|
||||
```ruby
|
||||
# app/controllers/break_escape/scenarios_controller.rb
|
||||
module BreakEscape
|
||||
class ScenariosController < ApplicationController
|
||||
def index
|
||||
@scenarios = if defined?(Pundit)
|
||||
policy_scope(Scenario)
|
||||
else
|
||||
Scenario.published
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@scenario = Scenario.find(params[:id])
|
||||
authorize @scenario if defined?(Pundit)
|
||||
|
||||
# Create or find game instance
|
||||
@game_instance = GameInstance.find_or_create_by!(
|
||||
player: current_player,
|
||||
scenario: @scenario
|
||||
)
|
||||
|
||||
redirect_to game_path(@game_instance)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Continue with API controllers in next comment (file getting long)...**
|
||||
|
||||
---
|
||||
|
||||
## TO BE CONTINUED...
|
||||
|
||||
The implementation plan continues with:
|
||||
- Phase 6 (continued): API Controllers
|
||||
- Phase 7: Policies
|
||||
- Phase 8: Views
|
||||
- Phase 9: Client Integration
|
||||
- Phase 10: Testing
|
||||
- Phase 11: Standalone Mode
|
||||
- Phase 12: Deployment
|
||||
|
||||
Each phase includes specific bash commands, rails generate commands, and code examples.
|
||||
|
||||
**This is Part 1 of the implementation plan.**
|
||||
|
||||
See **02_IMPLEMENTATION_PLAN_PART2.md** for continuation.
|
||||
File diff suppressed because it is too large
Load Diff
227
planning_notes/rails-engine-migration-json/03_DATABASE_SCHEMA.md
Normal file
227
planning_notes/rails-engine-migration-json/03_DATABASE_SCHEMA.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# BreakEscape - Database Schema Reference
|
||||
|
||||
## Overview
|
||||
|
||||
**3 tables using JSONB for flexible storage**
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
### 1. break_escape_scenarios
|
||||
|
||||
Stores scenario definitions with complete game data.
|
||||
|
||||
| Column | Type | Null | Default | Notes |
|
||||
|--------|------|------|---------|-------|
|
||||
| id | bigint | NO | AUTO | Primary key |
|
||||
| name | string | NO | - | Unique identifier (e.g., 'ceo_exfil') |
|
||||
| display_name | string | NO | - | Human-readable name |
|
||||
| description | text | YES | - | Scenario brief |
|
||||
| scenario_data | jsonb | NO | - | **Complete scenario with solutions** |
|
||||
| published | boolean | NO | false | Visible to players |
|
||||
| difficulty_level | integer | NO | 1 | 1-5 scale |
|
||||
| created_at | timestamp | NO | NOW() | - |
|
||||
| updated_at | timestamp | NO | NOW() | - |
|
||||
|
||||
**Indexes:**
|
||||
- `name` (unique)
|
||||
- `published`
|
||||
- `scenario_data` (gin)
|
||||
|
||||
**scenario_data structure:**
|
||||
```json
|
||||
{
|
||||
"startRoom": "room_reception",
|
||||
"scenarioName": "CEO Exfiltration",
|
||||
"scenarioBrief": "Break into the CEO's office...",
|
||||
"rooms": {
|
||||
"room_reception": {
|
||||
"type": "reception",
|
||||
"connections": {"north": "room_office"},
|
||||
"locked": false,
|
||||
"objects": [...]
|
||||
},
|
||||
"room_office": {
|
||||
"type": "office",
|
||||
"connections": {"south": "room_reception"},
|
||||
"locked": true,
|
||||
"lockType": "password",
|
||||
"requires": "admin123", // Server only!
|
||||
"objects": [...]
|
||||
}
|
||||
},
|
||||
"npcs": [
|
||||
{"id": "guard", "displayName": "Security Guard", ...}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. break_escape_npc_scripts
|
||||
|
||||
Stores Ink dialogue scripts per NPC per scenario.
|
||||
|
||||
| Column | Type | Null | Default | Notes |
|
||||
|--------|------|------|---------|-------|
|
||||
| id | bigint | NO | AUTO | Primary key |
|
||||
| scenario_id | bigint | NO | - | Foreign key → scenarios |
|
||||
| npc_id | string | NO | - | NPC identifier |
|
||||
| ink_source | text | YES | - | Original .ink file (optional) |
|
||||
| ink_compiled | text | NO | - | Compiled .ink.json |
|
||||
| created_at | timestamp | NO | NOW() | - |
|
||||
| updated_at | timestamp | NO | NOW() | - |
|
||||
|
||||
**Indexes:**
|
||||
- `scenario_id`
|
||||
- `[scenario_id, npc_id]` (unique)
|
||||
|
||||
**Foreign Keys:**
|
||||
- `scenario_id` → `break_escape_scenarios(id)`
|
||||
|
||||
---
|
||||
|
||||
### 3. break_escape_game_instances
|
||||
|
||||
Stores player game state (one JSONB column!).
|
||||
|
||||
| Column | Type | Null | Default | Notes |
|
||||
|--------|------|------|---------|-------|
|
||||
| id | bigint | NO | AUTO | Primary key |
|
||||
| player_type | string | NO | - | Polymorphic (User/DemoUser) |
|
||||
| player_id | bigint | NO | - | Polymorphic |
|
||||
| scenario_id | bigint | NO | - | Foreign key → scenarios |
|
||||
| player_state | jsonb | NO | {...} | **All game state here!** |
|
||||
| status | string | NO | 'in_progress' | in_progress, completed, abandoned |
|
||||
| started_at | timestamp | YES | - | When game started |
|
||||
| completed_at | timestamp | YES | - | When game finished |
|
||||
| score | integer | NO | 0 | Final score |
|
||||
| health | integer | NO | 100 | Current health |
|
||||
| created_at | timestamp | NO | NOW() | - |
|
||||
| updated_at | timestamp | NO | NOW() | - |
|
||||
|
||||
**Indexes:**
|
||||
- `[player_type, player_id, scenario_id]` (unique)
|
||||
- `player_state` (gin)
|
||||
- `status`
|
||||
|
||||
**Foreign Keys:**
|
||||
- `scenario_id` → `break_escape_scenarios(id)`
|
||||
|
||||
**player_state structure:**
|
||||
```json
|
||||
{
|
||||
"currentRoom": "room_office",
|
||||
"position": {"x": 150, "y": 200},
|
||||
"unlockedRooms": ["room_reception", "room_office"],
|
||||
"unlockedObjects": ["desk_drawer_123"],
|
||||
"inventory": [
|
||||
{
|
||||
"type": "key",
|
||||
"name": "Office Key",
|
||||
"key_id": "office_key_1",
|
||||
"takeable": true
|
||||
}
|
||||
],
|
||||
"encounteredNPCs": ["security_guard"],
|
||||
"globalVariables": {
|
||||
"alarm_triggered": false,
|
||||
"player_favor": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. break_escape_demo_users (Standalone Mode Only)
|
||||
|
||||
Simple user model for standalone/testing.
|
||||
|
||||
| Column | Type | Null | Default | Notes |
|
||||
|--------|------|------|---------|-------|
|
||||
| id | bigint | NO | AUTO | Primary key |
|
||||
| handle | string | NO | - | Username |
|
||||
| role | string | NO | 'user' | admin, pro, user |
|
||||
| created_at | timestamp | NO | NOW() | - |
|
||||
| updated_at | timestamp | NO | NOW() | - |
|
||||
|
||||
**Indexes:**
|
||||
- `handle` (unique)
|
||||
|
||||
---
|
||||
|
||||
## Relationships
|
||||
|
||||
```
|
||||
Scenario (1) ──→ (∞) GameInstance
|
||||
Scenario (1) ──→ (∞) NpcScript
|
||||
|
||||
GameInstance (∞) ←── (1) Player [Polymorphic]
|
||||
- User (Hacktivity)
|
||||
- DemoUser (Standalone)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Commands
|
||||
|
||||
```bash
|
||||
# Generate migrations
|
||||
rails generate migration CreateBreakEscapeScenarios
|
||||
rails generate migration CreateBreakEscapeNpcScripts
|
||||
rails generate migration CreateBreakEscapeGameInstances
|
||||
rails generate migration CreateBreakEscapeDemoUsers
|
||||
|
||||
# Run migrations
|
||||
rails db:migrate
|
||||
|
||||
# Import scenarios
|
||||
rails db:seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Querying Examples
|
||||
|
||||
```ruby
|
||||
# Find player's active games
|
||||
GameInstance.where(player: current_user, status: 'in_progress')
|
||||
|
||||
# Get unlocked rooms for a game
|
||||
game.player_state['unlockedRooms']
|
||||
|
||||
# Check if room is unlocked
|
||||
game.room_unlocked?('room_office')
|
||||
|
||||
# Unlock a room
|
||||
game.unlock_room!('room_office')
|
||||
|
||||
# Add inventory item
|
||||
game.add_inventory_item!({'type' => 'key', 'name' => 'Office Key'})
|
||||
|
||||
# Query scenarios
|
||||
Scenario.published.where("scenario_data->>'startRoom' = ?", 'room_reception')
|
||||
|
||||
# Complex JSONB queries
|
||||
GameInstance.where("player_state @> ?", {unlockedRooms: ['room_ceo']}.to_json)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits of JSONB Approach
|
||||
|
||||
1. **Flexible Schema** - Add new fields without migrations
|
||||
2. **Fast Queries** - GIN indexes on JSONB
|
||||
3. **Matches Game Data** - Already in JSON format
|
||||
4. **Simple** - One table vs many joins
|
||||
5. **Atomic Updates** - Update entire state in one transaction
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **GIN indexes** on all JSONB columns
|
||||
- **Unique index** on [player, scenario] prevents duplicates
|
||||
- **player_state** updates are atomic (PostgreSQL JSONB)
|
||||
- **Scenarios cached** in memory after first load
|
||||
474
planning_notes/rails-engine-migration-json/04_TESTING_GUIDE.md
Normal file
474
planning_notes/rails-engine-migration-json/04_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# BreakEscape - Testing Guide
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Follow Hacktivity patterns:
|
||||
- **Fixtures** for test data
|
||||
- **Integration tests** for workflows
|
||||
- **Model tests** for business logic
|
||||
- **Policy tests** for authorization
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
rails test
|
||||
|
||||
# Specific test file
|
||||
rails test test/models/break_escape/game_instance_test.rb
|
||||
|
||||
# Specific test
|
||||
rails test test/models/break_escape/game_instance_test.rb:10
|
||||
|
||||
# With coverage
|
||||
rails test:coverage # If configured
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
test/
|
||||
├── fixtures/
|
||||
│ ├── break_escape/
|
||||
│ │ ├── scenarios.yml
|
||||
│ │ ├── npc_scripts.yml
|
||||
│ │ ├── game_instances.yml
|
||||
│ │ └── demo_users.yml
|
||||
│ └── files/
|
||||
│ └── test_scenarios/
|
||||
│ └── minimal_scenario.json
|
||||
│
|
||||
├── models/
|
||||
│ └── break_escape/
|
||||
│ ├── scenario_test.rb
|
||||
│ ├── game_instance_test.rb
|
||||
│ └── npc_script_test.rb
|
||||
│
|
||||
├── controllers/
|
||||
│ └── break_escape/
|
||||
│ ├── games_controller_test.rb
|
||||
│ └── api/
|
||||
│ ├── games_controller_test.rb
|
||||
│ └── rooms_controller_test.rb
|
||||
│
|
||||
├── integration/
|
||||
│ └── break_escape/
|
||||
│ ├── game_flow_test.rb
|
||||
│ └── api_flow_test.rb
|
||||
│
|
||||
└── policies/
|
||||
└── break_escape/
|
||||
├── game_instance_policy_test.rb
|
||||
└── scenario_policy_test.rb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fixtures
|
||||
|
||||
### Scenarios
|
||||
|
||||
```yaml
|
||||
# test/fixtures/break_escape/scenarios.yml
|
||||
minimal:
|
||||
name: minimal
|
||||
display_name: Minimal Test Scenario
|
||||
description: Simple scenario for testing
|
||||
published: true
|
||||
difficulty_level: 1
|
||||
scenario_data: <%= File.read(Rails.root.join('test/fixtures/files/test_scenarios/minimal_scenario.json')) %>
|
||||
|
||||
advanced:
|
||||
name: advanced
|
||||
display_name: Advanced Test Scenario
|
||||
published: false
|
||||
difficulty_level: 5
|
||||
scenario_data: <%= File.read(Rails.root.join('test/fixtures/files/test_scenarios/advanced_scenario.json')) %>
|
||||
```
|
||||
|
||||
### Game Instances
|
||||
|
||||
```yaml
|
||||
# test/fixtures/break_escape/game_instances.yml
|
||||
active_game:
|
||||
player: demo_player (DemoUser)
|
||||
scenario: minimal
|
||||
status: in_progress
|
||||
player_state:
|
||||
currentRoom: room_start
|
||||
position: {x: 0, y: 0}
|
||||
unlockedRooms: [room_start]
|
||||
unlockedObjects: []
|
||||
inventory: []
|
||||
encounteredNPCs: []
|
||||
globalVariables: {}
|
||||
|
||||
completed_game:
|
||||
player: demo_player (DemoUser)
|
||||
scenario: minimal
|
||||
status: completed
|
||||
completed_at: <%= 1.day.ago %>
|
||||
score: 100
|
||||
```
|
||||
|
||||
### Demo Users
|
||||
|
||||
```yaml
|
||||
# test/fixtures/break_escape/demo_users.yml
|
||||
demo_player:
|
||||
handle: demo_player
|
||||
role: user
|
||||
|
||||
pro_player:
|
||||
handle: pro_player
|
||||
role: pro
|
||||
|
||||
admin_player:
|
||||
handle: admin_player
|
||||
role: admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Tests
|
||||
|
||||
```ruby
|
||||
# test/models/break_escape/game_instance_test.rb
|
||||
require 'test_helper'
|
||||
|
||||
module BreakEscape
|
||||
class GameInstanceTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@game = break_escape_game_instances(:active_game)
|
||||
end
|
||||
|
||||
test "initializes with start room unlocked" do
|
||||
scenario = break_escape_scenarios(:minimal)
|
||||
game = GameInstance.create!(
|
||||
player: break_escape_demo_users(:demo_player),
|
||||
scenario: scenario
|
||||
)
|
||||
|
||||
assert game.room_unlocked?(scenario.start_room)
|
||||
assert_includes game.player_state['unlockedRooms'], scenario.start_room
|
||||
end
|
||||
|
||||
test "can unlock rooms" do
|
||||
@game.unlock_room!('room_office')
|
||||
|
||||
assert @game.room_unlocked?('room_office')
|
||||
assert_includes @game.player_state['unlockedRooms'], 'room_office'
|
||||
end
|
||||
|
||||
test "can add inventory items" do
|
||||
item = {'type' => 'key', 'name' => 'Test Key', 'key_id' => 'test_1'}
|
||||
|
||||
@game.add_inventory_item!(item)
|
||||
|
||||
assert_equal 1, @game.player_state['inventory'].length
|
||||
assert_equal 'Test Key', @game.player_state['inventory'].first['name']
|
||||
end
|
||||
|
||||
test "can track encountered NPCs" do
|
||||
@game.encounter_npc!('guard_1')
|
||||
|
||||
assert @game.npc_encountered?('guard_1')
|
||||
assert_includes @game.player_state['encounteredNPCs'], 'guard_1'
|
||||
end
|
||||
|
||||
test "validates status values" do
|
||||
@game.status = 'invalid_status'
|
||||
|
||||
assert_not @game.valid?
|
||||
assert_includes @game.errors[:status], 'is not included in the list'
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# test/models/break_escape/scenario_test.rb
|
||||
require 'test_helper'
|
||||
|
||||
module BreakEscape
|
||||
class ScenarioTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@scenario = break_escape_scenarios(:minimal)
|
||||
end
|
||||
|
||||
test "filters room data to remove solutions" do
|
||||
room_data = @scenario.filtered_room_data('room_office')
|
||||
|
||||
assert_nil room_data['requires']
|
||||
assert_nil room_data['lockType']
|
||||
|
||||
# Objects should also be filtered
|
||||
room_data['objects']&.each do |obj|
|
||||
assert_nil obj['requires']
|
||||
assert_nil obj['lockType'] if obj['locked']
|
||||
end
|
||||
end
|
||||
|
||||
test "validates unlock attempts" do
|
||||
# Valid password
|
||||
assert @scenario.validate_unlock('door', 'room_office', 'correct_password', 'password')
|
||||
|
||||
# Invalid password
|
||||
assert_not @scenario.validate_unlock('door', 'room_office', 'wrong_password', 'password')
|
||||
|
||||
# Valid key
|
||||
assert @scenario.validate_unlock('door', 'room_vault', 'vault_key_123', 'key')
|
||||
end
|
||||
|
||||
test "scopes published scenarios" do
|
||||
assert_includes Scenario.published, @scenario
|
||||
assert_not_includes Scenario.published, break_escape_scenarios(:advanced)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Tests
|
||||
|
||||
```ruby
|
||||
# test/integration/break_escape/game_flow_test.rb
|
||||
require 'test_helper'
|
||||
|
||||
module BreakEscape
|
||||
class GameFlowTest < ActionDispatch::IntegrationTest
|
||||
include Engine.routes.url_helpers
|
||||
|
||||
setup do
|
||||
@scenario = break_escape_scenarios(:minimal)
|
||||
@user = break_escape_demo_users(:demo_player)
|
||||
end
|
||||
|
||||
test "complete game flow" do
|
||||
# 1. View scenarios
|
||||
get scenarios_path
|
||||
assert_response :success
|
||||
assert_select '.scenario', minimum: 1
|
||||
|
||||
# 2. Select scenario (creates game instance)
|
||||
get scenario_path(@scenario)
|
||||
assert_response :redirect
|
||||
|
||||
game = GameInstance.find_by(player: @user, scenario: @scenario)
|
||||
assert_not_nil game
|
||||
|
||||
# 3. View game
|
||||
get game_path(game)
|
||||
assert_response :success
|
||||
assert_select 'div#break-escape-game'
|
||||
|
||||
# 4. Bootstrap via API
|
||||
get bootstrap_api_game_path(game), as: :json
|
||||
assert_response :success
|
||||
|
||||
json = JSON.parse(response.body)
|
||||
assert_equal game.id, json['gameId']
|
||||
assert_equal @scenario.start_room, json['startRoom']
|
||||
assert json['playerState']
|
||||
assert json['roomLayout']
|
||||
|
||||
# 5. Attempt unlock
|
||||
post unlock_api_game_path(game), params: {
|
||||
targetType: 'door',
|
||||
targetId: 'room_office',
|
||||
method: 'password',
|
||||
attempt: 'admin123'
|
||||
}, as: :json
|
||||
|
||||
assert_response :success
|
||||
json = JSON.parse(response.body)
|
||||
assert json['success']
|
||||
assert json['roomData']
|
||||
|
||||
# 6. Load room
|
||||
get api_game_room_path(game, 'room_office'), as: :json
|
||||
assert_response :success
|
||||
|
||||
# 7. Load NPC script
|
||||
get script_api_game_npc_path(game, 'guard_1'), as: :json
|
||||
assert_response :success
|
||||
|
||||
json = JSON.parse(response.body)
|
||||
assert_equal 'guard_1', json['npcId']
|
||||
assert json['inkScript']
|
||||
end
|
||||
|
||||
test "cannot access locked room" do
|
||||
game = break_escape_game_instances(:active_game)
|
||||
|
||||
get api_game_room_path(game, 'locked_room'), as: :json
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test "invalid unlock attempt fails" do
|
||||
game = break_escape_game_instances(:active_game)
|
||||
|
||||
post unlock_api_game_path(game), params: {
|
||||
targetType: 'door',
|
||||
targetId: 'room_office',
|
||||
method: 'password',
|
||||
attempt: 'wrong_password'
|
||||
}, as: :json
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
json = JSON.parse(response.body)
|
||||
assert_not json['success']
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Policy Tests
|
||||
|
||||
```ruby
|
||||
# test/policies/break_escape/game_instance_policy_test.rb
|
||||
require 'test_helper'
|
||||
|
||||
module BreakEscape
|
||||
class GameInstancePolicyTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@owner = break_escape_demo_users(:demo_player)
|
||||
@other_user = break_escape_demo_users(:pro_player)
|
||||
@admin = break_escape_demo_users(:admin_player)
|
||||
@game = break_escape_game_instances(:active_game)
|
||||
end
|
||||
|
||||
test "owner can view own game" do
|
||||
policy = GameInstancePolicy.new(@owner, @game)
|
||||
assert policy.show?
|
||||
end
|
||||
|
||||
test "other user cannot view game" do
|
||||
policy = GameInstancePolicy.new(@other_user, @game)
|
||||
assert_not policy.show?
|
||||
end
|
||||
|
||||
test "admin can view any game" do
|
||||
policy = GameInstancePolicy.new(@admin, @game)
|
||||
assert policy.show?
|
||||
end
|
||||
|
||||
test "owner can update own game" do
|
||||
policy = GameInstancePolicy.new(@owner, @game)
|
||||
assert policy.update?
|
||||
end
|
||||
|
||||
test "scope returns only user's games" do
|
||||
scope = GameInstancePolicy::Scope.new(@owner, GameInstance.all).resolve
|
||||
|
||||
assert_includes scope, @game
|
||||
# If other games exist for other users, they should not be included
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Helpers
|
||||
|
||||
```ruby
|
||||
# test/test_helper.rb
|
||||
ENV['RAILS_ENV'] ||= 'test'
|
||||
require_relative '../config/environment'
|
||||
require 'rails/test_help'
|
||||
|
||||
class ActiveSupport::TestCase
|
||||
# Setup all fixtures in test/fixtures/*.yml
|
||||
fixtures :all
|
||||
|
||||
# Helper methods
|
||||
def json_response
|
||||
JSON.parse(response.body)
|
||||
end
|
||||
|
||||
def assert_jsonb_includes(jsonb_column, expected_hash)
|
||||
assert jsonb_column.to_h.deep_symbolize_keys >= expected_hash.deep_symbolize_keys
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
# If SimpleCov is configured
|
||||
rails test
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1
|
||||
bundler-cache: true
|
||||
|
||||
- name: Setup database
|
||||
run: |
|
||||
bin/rails db:setup
|
||||
bin/rails db:migrate
|
||||
|
||||
- name: Run tests
|
||||
run: bin/rails test
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual Testing Checklist
|
||||
|
||||
- [ ] Game loads in standalone mode
|
||||
- [ ] Can select scenario
|
||||
- [ ] Game view renders
|
||||
- [ ] Bootstrap API works
|
||||
- [ ] Can unlock door with correct password
|
||||
- [ ] Cannot unlock with wrong password
|
||||
- [ ] Can load unlocked room
|
||||
- [ ] Cannot load locked room
|
||||
- [ ] Can load NPC script after encounter
|
||||
- [ ] Inventory updates work
|
||||
- [ ] State syncs to server
|
||||
- [ ] Game persists across page refresh
|
||||
265
planning_notes/rails-engine-migration-json/README.md
Normal file
265
planning_notes/rails-engine-migration-json/README.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# BreakEscape Rails Engine Migration - JSON-Centric Approach
|
||||
|
||||
## Overview
|
||||
|
||||
Complete implementation plan for converting BreakEscape to a Rails Engine using a simplified, JSON-centric architecture.
|
||||
|
||||
**Timeline:** 12-14 weeks
|
||||
**Approach:** Minimal changes, maximum compatibility
|
||||
**Storage:** JSONB for game state (not complex relational DB)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Read in order:**
|
||||
|
||||
1. **[00_OVERVIEW.md](./00_OVERVIEW.md)** - Start here
|
||||
- Project aims and objectives
|
||||
- Core philosophy and approach
|
||||
- Key architectural decisions
|
||||
- Success criteria
|
||||
|
||||
2. **[01_ARCHITECTURE.md](./01_ARCHITECTURE.md)** - Technical design
|
||||
- System architecture diagrams
|
||||
- Database schema (3 tables)
|
||||
- API endpoint specifications
|
||||
- File organization
|
||||
- Models, controllers, views
|
||||
|
||||
3. **[02_IMPLEMENTATION_PLAN.md](./02_IMPLEMENTATION_PLAN.md)** - Actionable steps (Part 1)
|
||||
- Phase 1-6: Setup through Controllers
|
||||
- Specific bash commands
|
||||
- Rails generate commands
|
||||
- Code examples
|
||||
|
||||
4. **[02_IMPLEMENTATION_PLAN_PART2.md](./02_IMPLEMENTATION_PLAN_PART2.md)** - Actionable steps (Part 2)
|
||||
- Phase 7-12: Policies through Deployment
|
||||
- Client integration
|
||||
- Testing setup
|
||||
- Standalone mode
|
||||
|
||||
5. **[03_DATABASE_SCHEMA.md](./03_DATABASE_SCHEMA.md)** - Database reference
|
||||
- Complete schema details
|
||||
- JSONB structures
|
||||
- Query examples
|
||||
- Performance tips
|
||||
|
||||
6. **[04_TESTING_GUIDE.md](./04_TESTING_GUIDE.md)** - Testing strategy
|
||||
- Fixtures setup
|
||||
- Model tests
|
||||
- Integration tests
|
||||
- Policy tests
|
||||
- CI configuration
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions Summary
|
||||
|
||||
### Architecture
|
||||
- **Rails Engine** (not separate app)
|
||||
- **Built in current directory** (not separate repo)
|
||||
- **Dual mode:** Standalone + Hacktivity mounted
|
||||
- **Session-based auth** (not JWT)
|
||||
- **Polymorphic player** (User or DemoUser)
|
||||
|
||||
### Database
|
||||
- **3 simple tables** (not 10+)
|
||||
- **JSONB storage** for game state
|
||||
- **Scenarios as ERB templates**
|
||||
- **Lazy-load NPC scripts**
|
||||
|
||||
### File Organization
|
||||
- **Game files → public/break_escape/**
|
||||
- **Scenarios → app/assets/scenarios/**
|
||||
- **.ink and .ink.json** in scenario dirs
|
||||
- **Minimal client changes**
|
||||
|
||||
### API
|
||||
- **6 endpoints** (not 15+)
|
||||
- **Backwards compatible JSON**
|
||||
- **Server validates unlocks**
|
||||
- **Client runs dialogue**
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
| Phase | Duration | Focus | Status |
|
||||
|-------|----------|-------|--------|
|
||||
| 1. Setup Rails Engine | Week 1 | Generate structure, Gemfile | 📋 TODO |
|
||||
| 2. Move Files | Week 1 | public/, scenarios/ | 📋 TODO |
|
||||
| 3. Reorganize Scenarios | Week 1-2 | ERB templates, ink files | 📋 TODO |
|
||||
| 4. Database | Week 2 | Migrations, models, seeds | 📋 TODO |
|
||||
| 5. Scenario Import | Week 2 | Loader service, seeds | 📋 TODO |
|
||||
| 6. Controllers | Week 3 | Routes, controllers, API | 📋 TODO |
|
||||
| 7. Policies | Week 3 | Pundit authorization | 📋 TODO |
|
||||
| 8. Views | Week 4 | Game view, scenarios index | 📋 TODO |
|
||||
| 9. Client Integration | Week 4-5 | API client, minimal changes | 📋 TODO |
|
||||
| 10. Standalone Mode | Week 5 | DemoUser, config | 📋 TODO |
|
||||
| 11. Testing | Week 6 | Fixtures, tests | 📋 TODO |
|
||||
| 12. Deployment | Week 6 | Documentation, verification | 📋 TODO |
|
||||
|
||||
---
|
||||
|
||||
## Before You Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Ensure clean git state
|
||||
git status
|
||||
|
||||
# Create feature branch
|
||||
git checkout -b rails-engine-migration
|
||||
|
||||
# Backup current state
|
||||
git add -A
|
||||
git commit -m "chore: Checkpoint before Rails Engine migration"
|
||||
```
|
||||
|
||||
### Required Tools
|
||||
|
||||
- Ruby 3.1+
|
||||
- Rails 7.0+
|
||||
- PostgreSQL 14+ (for JSONB)
|
||||
- Git
|
||||
|
||||
### Environment
|
||||
|
||||
```bash
|
||||
# Verify Ruby version
|
||||
ruby -v # Should be 3.1+
|
||||
|
||||
# Verify Rails
|
||||
rails -v # Should be 7.0+
|
||||
|
||||
# Verify PostgreSQL
|
||||
psql --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files to Create
|
||||
|
||||
### Configuration
|
||||
- `lib/break_escape/engine.rb` - Engine definition
|
||||
- `config/routes.rb` - Engine routes
|
||||
- `config/initializers/break_escape.rb` - Configuration
|
||||
- `config/break_escape_standalone.yml` - Standalone config
|
||||
- `break_escape.gemspec` - Gem specification
|
||||
|
||||
### Models
|
||||
- `app/models/break_escape/scenario.rb`
|
||||
- `app/models/break_escape/npc_script.rb`
|
||||
- `app/models/break_escape/game_instance.rb`
|
||||
- `app/models/break_escape/demo_user.rb`
|
||||
|
||||
### Controllers
|
||||
- `app/controllers/break_escape/games_controller.rb`
|
||||
- `app/controllers/break_escape/scenarios_controller.rb`
|
||||
- `app/controllers/break_escape/api/games_controller.rb`
|
||||
- `app/controllers/break_escape/api/rooms_controller.rb`
|
||||
- `app/controllers/break_escape/api/npcs_controller.rb`
|
||||
|
||||
### Views
|
||||
- `app/views/break_escape/games/show.html.erb`
|
||||
- `app/views/break_escape/scenarios/index.html.erb`
|
||||
|
||||
### Client
|
||||
- `public/break_escape/js/config.js` (NEW)
|
||||
- `public/break_escape/js/core/api-client.js` (NEW)
|
||||
- Modify existing JS files minimally
|
||||
|
||||
---
|
||||
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
# Generate engine
|
||||
rails plugin new . --mountable --skip-git
|
||||
|
||||
# Generate migrations
|
||||
rails generate migration CreateBreakEscapeScenarios
|
||||
rails generate migration CreateBreakEscapeGameInstances
|
||||
rails generate migration CreateBreakEscapeNpcScripts
|
||||
|
||||
# Run migrations
|
||||
rails db:migrate
|
||||
|
||||
# Import scenarios
|
||||
rails db:seed
|
||||
|
||||
# Run tests
|
||||
rails test
|
||||
|
||||
# Start server
|
||||
rails server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Functional
|
||||
- ✅ Game runs in standalone mode
|
||||
- ✅ Game mounts in Hacktivity
|
||||
- ✅ All scenarios work
|
||||
- ✅ NPCs load and function
|
||||
- ✅ Server validates unlocks
|
||||
- ✅ State persists
|
||||
|
||||
### Performance
|
||||
- ✅ Room loading < 500ms
|
||||
- ✅ Unlock validation < 300ms
|
||||
- ✅ No visual lag
|
||||
- ✅ Assets load quickly
|
||||
|
||||
### Code Quality
|
||||
- ✅ Rails tests pass
|
||||
- ✅ Minimal client changes
|
||||
- ✅ Clear separation
|
||||
- ✅ Well documented
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If anything goes wrong:
|
||||
|
||||
1. **Git branches** - Each phase has its own commit
|
||||
2. **Original files preserved** - Moved, not deleted
|
||||
3. **Dual-mode testing** - Standalone mode for safe testing
|
||||
4. **Incremental approach** - Test after each phase
|
||||
|
||||
```bash
|
||||
# Revert to checkpoint
|
||||
git reset --hard <commit-hash>
|
||||
|
||||
# Or revert specific files
|
||||
git checkout HEAD -- <file>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
If you get stuck:
|
||||
|
||||
1. Review the specific phase document
|
||||
2. Check architecture document for design rationale
|
||||
3. Verify database schema is correct
|
||||
4. Run tests to identify issues
|
||||
5. Check Rails logs for errors
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Read 00_OVERVIEW.md
|
||||
2. ✅ Read 01_ARCHITECTURE.md
|
||||
3. 📋 Follow 02_IMPLEMENTATION_PLAN.md step by step
|
||||
4. ✅ Test after each phase
|
||||
5. ✅ Commit working code frequently
|
||||
|
||||
**Good luck! The plan is detailed and tested. Follow it carefully.**
|
||||
Reference in New Issue
Block a user