Systematic debugging for Starwards - four-phase framework (root cause investigation, pattern analysis, hypothesis testing, implementation) with Colyseus state inspection, Tweakpane debugging,...
Random fixes waste time. Quick patches mask underlying issues.
Core principle: ALWAYS find root cause before attempting fixes.
Starwards-specific: Debug state sync issues, decorator problems, UI/server mismatches, and multiplayer race conditions.
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
If you haven't completed Phase 1, you cannot propose fixes.
Any technical issue:
1. Read Error Messages Carefully
TypeScript errors:
npm run test:types
# Read FULL error including file:line
# Check @gameField decorator order (must be last)
# Verify @range types match property type
Jest test failures:
npm test
# Read expected vs actual values
# Check if float precision issue (use toBeCloseTo)
# Verify async operations completed (await harness.waitForSync())
Playwright failures:
npm run test:e2e
# Check screenshot diffs in test-results/
# Verify data-id selectors match actual panel titles
# Check if timing issue (add proper waits, not arbitrary timeouts)
Webpack errors:
cd modules/browser && npm start
# Check browser console (F12) for actual error
# Webpack overlay shows [object Object] - this is a known issue
# Look for import/export mismatches
2. Reproduce Consistently
For state sync issues:
// Add logging to both client and server
console.log('[CLIENT] Shield strength:', state.shield.strength);
console.log('[SERVER] Shield strength:', manager.state.shield.strength);
// Verify both show same value
// If different → state sync problem
// If same → UI rendering problem
For multiplayer bugs:
3. Check Recent Changes
git diff HEAD~1 # Last commit
git log --oneline -5 # Recent commits
git diff --stat origin/master # All changes on branch
Common culprits:
4. Gather Evidence in Multi-Component Systems
Starwards has 4 layers: Browser → WebSocket → Server → State
Add diagnostic logging at each boundary:
// Layer 1: Browser widget
console.log('[WIDGET] Sending command:', value);
room.send({type: '/Spaceship/ship-0/shield/power', value});
// Layer 2: Network (Chrome DevTools → Network → WS)
// Verify WebSocket message sent
// Layer 3: Server room
this.onMessage((client, message) => {
console.log('[SERVER] Received:', message);
});
// Layer 4: State manager
console.log('[MANAGER] Shield power updated:', this.state.shield.power);
Run once to see WHERE it breaks:
5. Trace Data Flow
Example: Shield strength not updating
Trace backward:
UI shows 500 ← Where does UI get value?
↓
pane.addBinding(shield, 'strength') ← What is shield?
↓
shield = ship.state.shield ← Does ship.state exist?
↓
ship.state from ShipDriver ← Is driver connected?
↓
driver.state from Colyseus room.state ← Is state synced?
Find the layer where data stops flowing correctly.
1. Find Working Examples
For ship systems:
# Look at existing systems
modules/core/src/ship/reactor.ts # Has @gameField
modules/core/src/ship/armor.ts # Has @range
modules/core/src/ship/warp.ts # Has commands
For widgets:
# Look at working widgets
modules/browser/src/widgets/armor.ts # Tweakpane panel
modules/browser/src/widgets/targeting.ts # Command sending
For tests:
# Look at test patterns
modules/core/test/ship-test-harness.ts # Multiplayer testing
modules/e2e/test/pilot-screen.spec.ts # E2E testing
2. Compare Against References
Read docs COMPLETELY:
docs/PATTERNS.md - Common patterns and gotchasdocs/TECHNICAL_REFERENCE.md - @gameField, JSON Pointer, decoratorsdocs/testing/UTILITIES.md - Test harness usage3. Identify Differences
Use diff tools:
# Compare your new system to existing one
diff modules/core/src/ship/shield.ts modules/core/src/ship/armor.ts
Look for:
4. Understand Dependencies
For new ship systems:
For new widgets:
For multiplayer features:
1. Form Single Hypothesis
Good:
Bad:
2. Test Minimally
Example: Test @gameField hypothesis
// Before: (no decorator)
strength = 1000;
// After: (add decorator)
@gameField('float32') strength = 1000;
// Rebuild core, restart server, verify
ONE change at a time.
3. Verify Before Continuing
# Did it work?
npm test
npm run test:e2e
# If NO: Form NEW hypothesis
# If YES: Proceed to Phase 4
4. When You Don't Know
Say: "I don't understand why Colyseus state isn't syncing"
Don't say: "Let me try adding more decorators and see what happens"
Ask for help or research in:
docs/PATTERNS.md1. Create Failing Test
Use starwards-tdd skill to write proper test.
For state sync bug:
test('shield strength syncs to client', async () => {
const harness = new ShipTestHarness();
await harness.connect();
harness.shipManager.state.shield.strength = 750;
await harness.waitForSync();
expect(harness.shipDriver.state.shield.strength).toBe(750);
await harness.cleanup();
});
2. Implement Single Fix
Address root cause only.
// If root cause is: @gameField missing
@gameField('float32') strength = 1000;
NO "while I'm here" improvements.
3. Verify Fix
# Run the specific test
npm test -- shield-sync.spec.ts
# Run full suite
npm test
npm run test:e2e
npm run test:types
4. If 3+ Fixes Failed
Pattern:
STOP and question:
# Access at http://localhost:2567/colyseus-monitor
# Login: admin / admin
# View: Active rooms, connected clients, state tree
Use for:
Network Tab → WS:
Console:
Sources:
Terminal 3:
# Instead of: node -r ts-node/register/transpile-only modules/server/src/dev.ts
# Use VSCode: F5 → "Run Server" (launches with debugger)
Set breakpoints in:
modules/server/src/ship/room.ts - Command handlersmodules/core/src/ship/ship-manager.ts - Update loopsmodules/core/src/logic/space-manager.ts - PhysicsAdd temporary debug panel:
const debugPane = createPane({title: 'DEBUG', container});
// Monitor live values
debugPane.addBinding(shield, 'strength', {readonly: true});
debugPane.addBinding(shield, 'power');
debugPane.addBinding(shield, 'effectiveness', {readonly: true});
ShipTestHarness:
const harness = new ShipTestHarness();
harness.shipManager.state // Server state
harness.shipDriver.state // Client state
await harness.waitForSync(); // Wait for replication
MultiClientDriver:
const driver = new MultiClientDriver();
await driver.start();
const [c1, c2] = await Promise.all([
driver.joinShip('ship-1'),
driver.joinShip('ship-1')
]);
// Test multiplayer scenarios
See docs/testing/UTILITIES.md for full API.
// GOOD: Scoped, structured
console.log('[ShieldManager.update] strength:', this.state.shield.strength, 'rate:', rechargeRate);
// BAD: Generic, unclear
console.log('shield', shield);
Use prefixes: [CLIENT], [SERVER], [MANAGER], [WIDGET]
| Symptom | Root Cause | Solution |
|---|---|---|
| State doesn't sync | Missing @gameField | Add decorator, rebuild core |
| Widget doesn't update | No onChange listener | Add state.onChange(() => update()) |
| Test fails with close values | Float32 precision | Use toBeCloseTo(expected, 1) |
| Angle values wrong | Not wrapped to [0, 360] | Use toPositiveDegreesDelta() |
| Effectiveness always 0 | System broken or no power | Check broken flag, power level |
| Webpack overlay shows [object Object] | Error wrapped by webpack | Check browser console (F12) |
| E2E test can't find panel | Wrong data-id | Use exact panel title in data-id |
| Build fails after core change | Core not rebuilt | npm run build:core or build:watch |
| Server doesn't see changes | Not restarted | Restart Terminal 3 (server) |
# 1. Verify build is fresh
npm run build
# 2. Check types
npm run test:types
# 3. Run tests
npm test
# 4. Check browser console
# Open http://localhost:3000, press F12
# 5. Check server logs
# Look at Terminal 3 output
# 6. Check network
# Chrome DevTools → Network → WS
# 7. Check Colyseus monitor
# http://localhost:2567/colyseus-monitor
Example: "Shield widget shows wrong value"
Phase 1: Investigate
Phase 2: Pattern
Phase 3: Hypothesis "Widget doesn't update because Tweakpane binding isn't reactive to Colyseus state changes"
Test minimally:
shield.state.onChange(() => {
pane.refresh(); // Force Tweakpane update
});
Phase 4: Implement
Done!