Files
BreakEscape/planning_notes/new_room_layout/VALIDATION.md
Z. Cliffe Schreuders 019986ceef docs: Add comprehensive room layout system redesign plan
Created detailed implementation plan for redesigning the room layout system
to support variable room sizes and four-direction connections.

Core Concepts:
- Grid unit system (5×4 tiles base, excluding 2-tile visual top)
- Valid room heights: 6, 10, 14, 18, 22, 26... (formula: 2 + 4N)
- Breadth-first room positioning from starting room
- Deterministic door placement with alignment for asymmetric connections
- Comprehensive scenario validation

Documents Created:
- OVERVIEW.md: High-level goals and changes
- TERMINOLOGY.md: Definitions and concepts
- GRID_SYSTEM.md: Grid unit system specification
- POSITIONING_ALGORITHM.md: Room positioning logic
- DOOR_PLACEMENT.md: Door placement rules and algorithms
- WALL_SYSTEM.md: Wall collision system updates
- VALIDATION.md: Scenario validation system
- IMPLEMENTATION_STEPS.md: Step-by-step implementation guide
- TODO_LIST.md: Detailed task checklist
- README.md: Quick start and overview

Review & Critical Fixes:
- review1/CRITICAL_REVIEW.md: Identified 4 critical issues
- review1/RECOMMENDATIONS.md: Solutions for all issues
- UPDATED_FILES_SUMMARY.md: Integration of review feedback

Critical Issues Identified & Resolved:
1. Grid height calculation (now: 6, 10, 14, 18...)
2. Door alignment for asymmetric connections (solution documented)
3. Code duplication (shared module approach specified)
4. Disconnected rooms (validation added)

Implementation Strategy:
- Incremental approach with feature flag
- Phase 1: Constants and helpers
- Phase 2a: North/South positioning
- Phase 2b: East/West support
- Phase 3: Door placement with critical fixes
- Phase 4: Validation
- Phase 5-6: Testing and documentation

Estimated time: 18-26 hours
Confidence: 9/10 (all critical issues addressed)

Ready for implementation.
2025-11-15 23:58:19 +00:00

15 KiB
Raw Permalink Blame History

Scenario Validation System

Overview

Validation ensures scenarios are correctly configured before the game starts. This catches authoring errors early and provides clear feedback.

Validation Checks

1. Room Size Validation

Check: All rooms are sized in multiples of grid units

function validateRoomSize(roomId, dimensions) {
    const { widthTiles, heightTiles } = dimensions;

    // Check width is multiple of 5
    const validWidth = (widthTiles % GRID_UNIT_WIDTH_TILES) === 0;

    // Check height: total height should be (gridUnits × 4) + 2 visual tiles
    const stackingHeight = heightTiles - VISUAL_TOP_TILES;
    const validHeight = (stackingHeight % GRID_UNIT_HEIGHT_TILES) === 0;

    if (!validWidth || !validHeight) {
        console.error(`❌ Invalid room size: ${roomId}`);
        console.error(`   Size: ${widthTiles}×${heightTiles} tiles`);
        console.error(`   Width must be multiple of ${GRID_UNIT_WIDTH_TILES} tiles`);
        console.error(`   Height must be (N×${GRID_UNIT_HEIGHT_TILES})+${VISUAL_TOP_TILES} tiles`);
        return false;
    }

    console.log(`✅ Room size valid: ${roomId} (${widthTiles}×${heightTiles} tiles)`);
    return true;
}

2. Grid Alignment Validation

Check: All room positions align to grid boundaries

function validateGridAlignment(roomId, position) {
    const { x, y } = position;

    const alignedX = (x % GRID_UNIT_WIDTH_PX) === 0;
    const alignedY = (y % GRID_UNIT_HEIGHT_PX) === 0;

    if (!alignedX || !alignedY) {
        console.error(`❌ Room not grid-aligned: ${roomId}`);
        console.error(`   Position: (${x}, ${y})`);
        console.error(`   Expected multiples of (${GRID_UNIT_WIDTH_PX}, ${GRID_UNIT_HEIGHT_PX})`);

        // Calculate nearest aligned position
        const nearestX = Math.round(x / GRID_UNIT_WIDTH_PX) * GRID_UNIT_WIDTH_PX;
        const nearestY = Math.round(y / GRID_UNIT_HEIGHT_PX) * GRID_UNIT_HEIGHT_PX;
        console.error(`   Nearest valid: (${nearestX}, ${nearestY})`);

        return false;
    }

    return true;
}

3. Room Overlap Detection

Check: No two rooms occupy the same grid space

function detectRoomOverlaps(positions, dimensions) {
    console.log('\n=== Room Overlap Detection ===');

    const roomIds = Object.keys(positions);
    const overlaps = [];

    for (let i = 0; i < roomIds.length; i++) {
        for (let j = i + 1; j < roomIds.length; j++) {
            const room1Id = roomIds[i];
            const room2Id = roomIds[j];

            const overlap = checkOverlap(
                positions[room1Id],
                dimensions[room1Id],
                positions[room2Id],
                dimensions[room2Id]
            );

            if (overlap) {
                overlaps.push({ room1: room1Id, room2: room2Id, ...overlap });
            }
        }
    }

    if (overlaps.length > 0) {
        console.error(`❌ Found ${overlaps.length} room overlaps:`);
        overlaps.forEach(overlap => {
            console.error(`   ${overlap.room1}${overlap.room2}`);
            console.error(`     Overlap area: ${overlap.width}×${overlap.height} pixels`);
            console.error(`     Overlap position: (${overlap.x}, ${overlap.y})`);
        });
    } else {
        console.log('✅ No room overlaps detected');
    }

    return overlaps;
}

function checkOverlap(pos1, dim1, pos2, dim2) {
    // Use stacking height for overlap calculation
    const r1 = {
        left: pos1.x,
        right: pos1.x + dim1.widthPx,
        top: pos1.y,
        bottom: pos1.y + dim1.stackingHeightPx
    };

    const r2 = {
        left: pos2.x,
        right: pos2.x + dim2.widthPx,
        top: pos2.y,
        bottom: pos2.y + dim2.stackingHeightPx
    };

    // Check for overlap
    const noOverlap = (
        r1.right <= r2.left ||
        r2.right <= r1.left ||
        r1.bottom <= r2.top ||
        r2.bottom <= r1.top
    );

    if (noOverlap) {
        return null; // No overlap
    }

    // Calculate overlap area
    const overlapX = Math.max(r1.left, r2.left);
    const overlapY = Math.max(r1.top, r2.top);
    const overlapWidth = Math.min(r1.right, r2.right) - overlapX;
    const overlapHeight = Math.min(r1.bottom, r2.bottom) - overlapY;

    return {
        x: overlapX,
        y: overlapY,
        width: overlapWidth,
        height: overlapHeight
    };
}

4. Connection Reciprocity Validation

Check: All connections are bidirectional

function validateConnections(gameScenario) {
    console.log('\n=== Connection Validation ===');

    const errors = [];

    Object.entries(gameScenario.rooms).forEach(([roomId, roomData]) => {
        if (!roomData.connections) return;

        Object.entries(roomData.connections).forEach(([direction, connected]) => {
            const connectedRooms = Array.isArray(connected) ? connected : [connected];

            connectedRooms.forEach(connectedRoomId => {
                // Check if connected room exists
                if (!gameScenario.rooms[connectedRoomId]) {
                    errors.push({
                        type: 'missing_room',
                        from: roomId,
                        to: connectedRoomId,
                        direction
                    });
                    return;
                }

                // Check if connection is reciprocal
                const oppositeDir = getOppositeDirection(direction);
                const connectedRoomData = gameScenario.rooms[connectedRoomId];

                if (!connectedRoomData.connections) {
                    errors.push({
                        type: 'missing_reciprocal',
                        from: roomId,
                        to: connectedRoomId,
                        direction,
                        expected: oppositeDir
                    });
                    return;
                }

                const reciprocalConnection = connectedRoomData.connections[oppositeDir];
                if (!reciprocalConnection) {
                    errors.push({
                        type: 'missing_reciprocal',
                        from: roomId,
                        to: connectedRoomId,
                        direction,
                        expected: oppositeDir
                    });
                    return;
                }

                // Check if this room is in the reciprocal connection
                const reciprocalRooms = Array.isArray(reciprocalConnection)
                    ? reciprocalConnection
                    : [reciprocalConnection];

                if (!reciprocalRooms.includes(roomId)) {
                    errors.push({
                        type: 'mismatched_reciprocal',
                        from: roomId,
                        to: connectedRoomId,
                        direction,
                        expected: oppositeDir,
                        actual: reciprocalRooms
                    });
                }
            });
        });
    });

    if (errors.length > 0) {
        console.error(`❌ Found ${errors.length} connection errors:`);
        errors.forEach(error => {
            if (error.type === 'missing_room') {
                console.error(`   ${error.from}${error.to}: Room does not exist`);
            } else if (error.type === 'missing_reciprocal') {
                console.error(`   ${error.from}${error.to} (${error.direction}): Missing reciprocal connection (${error.expected})`);
            } else if (error.type === 'mismatched_reciprocal') {
                console.error(`   ${error.from}${error.to} (${error.direction}): Reciprocal doesn't point back (expected ${error.from}, found ${error.actual})`);
            }
        });
    } else {
        console.log('✅ All connections are valid and reciprocal');
    }

    return errors;
}

function getOppositeDirection(direction) {
    const opposites = {
        'north': 'south',
        'south': 'north',
        'east': 'west',
        'west': 'east'
    };
    return opposites[direction];
}

5. Door Alignment Validation

Check: Doors between connected rooms align perfectly

function validateDoorAlignment(allDoors) {
    console.log('\n=== Door Alignment Validation ===');

    const errors = [];
    const tolerance = 1; // 1px tolerance for floating point

    // Build a map of door pairs (connections)
    const doorPairs = new Map();

    allDoors.forEach(door => {
        const key = door.roomId < door.connectedRoom
            ? `${door.roomId}:${door.connectedRoom}`
            : `${door.connectedRoom}:${door.roomId}`;

        if (!doorPairs.has(key)) {
            doorPairs.set(key, []);
        }
        doorPairs.get(key).push(door);
    });

    // Check each pair
    doorPairs.forEach((doors, pairKey) => {
        if (doors.length !== 2) {
            console.error(`❌ Connection ${pairKey} has ${doors.length} doors (expected 2)`);
            errors.push({
                type: 'door_count_mismatch',
                connection: pairKey,
                count: doors.length
            });
            return;
        }

        const [door1, door2] = doors;

        const deltaX = Math.abs(door1.x - door2.x);
        const deltaY = Math.abs(door1.y - door2.y);

        if (deltaX > tolerance || deltaY > tolerance) {
            console.error(`❌ Door misalignment: ${pairKey}`);
            console.error(`   Door 1: (${door1.x}, ${door1.y}) in ${door1.roomId}`);
            console.error(`   Door 2: (${door2.x}, ${door2.y}) in ${door2.roomId}`);
            console.error(`   Delta: (${deltaX}, ${deltaY})`);

            errors.push({
                type: 'door_misalignment',
                connection: pairKey,
                door1: { roomId: door1.roomId, x: door1.x, y: door1.y },
                door2: { roomId: door2.roomId, x: door2.x, y: door2.y },
                delta: { x: deltaX, y: deltaY }
            });
        }
    });

    if (errors.length === 0) {
        console.log(`✅ All ${doorPairs.size} door connections are properly aligned`);
    }

    return errors;
}

6. Starting Room Validation

Check: Starting room exists and is valid

function validateStartingRoom(gameScenario) {
    console.log('\n=== Starting Room Validation ===');

    const startRoom = gameScenario.startRoom;

    if (!startRoom) {
        console.error('❌ No starting room defined in scenario');
        return false;
    }

    if (!gameScenario.rooms[startRoom]) {
        console.error(`❌ Starting room "${startRoom}" does not exist`);
        return false;
    }

    console.log(`✅ Starting room "${startRoom}" is valid`);
    return true;
}

Complete Validation Pipeline

function validateScenario(gameScenario, positions, dimensions, allDoors) {
    console.log('\n╔════════════════════════════════════════╗');
    console.log('║  SCENARIO VALIDATION                   ║');
    console.log('╚════════════════════════════════════════╝\n');

    const results = {
        valid: true,
        errors: [],
        warnings: []
    };

    // 1. Validate starting room
    if (!validateStartingRoom(gameScenario)) {
        results.valid = false;
        results.errors.push('Invalid starting room');
    }

    // 2. Validate room sizes
    Object.entries(dimensions).forEach(([roomId, dim]) => {
        if (!validateRoomSize(roomId, dim)) {
            results.valid = false;
            results.errors.push(`Invalid room size: ${roomId}`);
        }
    });

    // 3. Validate grid alignment
    Object.entries(positions).forEach(([roomId, pos]) => {
        if (!validateGridAlignment(roomId, pos)) {
            results.warnings.push(`Room not grid-aligned: ${roomId}`);
            // Not a fatal error, but should be fixed
        }
    });

    // 4. Detect room overlaps
    const overlaps = detectRoomOverlaps(positions, dimensions);
    if (overlaps.length > 0) {
        results.errors.push(`${overlaps.length} room overlaps detected`);
        // Continue despite overlaps (per requirements)
    }

    // 5. Validate connections
    const connectionErrors = validateConnections(gameScenario);
    if (connectionErrors.length > 0) {
        results.valid = false;
        results.errors.push(`${connectionErrors.length} connection errors`);
    }

    // 6. Validate door alignment
    const doorErrors = validateDoorAlignment(allDoors);
    if (doorErrors.length > 0) {
        results.valid = false;
        results.errors.push(`${doorErrors.length} door alignment errors`);
    }

    // Print summary
    console.log('\n╔════════════════════════════════════════╗');
    console.log('║  VALIDATION SUMMARY                    ║');
    console.log('╚════════════════════════════════════════╝\n');

    if (results.errors.length === 0 && results.warnings.length === 0) {
        console.log('✅ All validation checks passed!');
    } else {
        if (results.errors.length > 0) {
            console.error(`❌ ${results.errors.length} errors found:`);
            results.errors.forEach(err => console.error(`   - ${err}`));
        }

        if (results.warnings.length > 0) {
            console.warn(`⚠️  ${results.warnings.length} warnings:`);
            results.warnings.forEach(warn => console.warn(`   - ${warn}`));
        }
    }

    console.log('');

    return results;
}

When to Run Validation

// In initializeRooms() function in js/core/rooms.js

export function initializeRooms(gameInstance) {
    gameRef = gameInstance;
    console.log('Initializing rooms');

    // ... existing setup code ...

    // Calculate room positions
    window.roomPositions = calculateRoomPositions(gameInstance);

    // Extract dimensions for validation
    const dimensions = extractAllRoomDimensions(gameInstance);

    // Calculate all door positions (before creating sprites)
    const allDoors = calculateAllDoorPositions(
        window.gameScenario,
        window.roomPositions,
        dimensions
    );

    // VALIDATE SCENARIO
    const validationResults = validateScenario(
        window.gameScenario,
        window.roomPositions,
        dimensions,
        allDoors
    );

    // Store validation results for debugging
    window.scenarioValidation = validationResults;

    // Continue initialization even if validation fails
    // (per requirements: log error but attempt to continue)

    // ... rest of initialization ...
}

Error Reporting

Validation errors should be clear and actionable:

❌ OVERLAP DETECTED: room1 and room2
   room1: (0, 0) 320×192px
   room2: (160, 0) 320×192px
   Overlap area: 160×192 pixels at (160, 0)

   SUGGESTED FIX:
   - Move room2 to (320, 0) to eliminate overlap
   - Or reduce room sizes to fit

Development Tools

Add console commands for testing:

// Check scenario validation results
window.checkScenario = function() {
    if (window.scenarioValidation) {
        console.log(window.scenarioValidation);
    } else {
        console.log('No validation results available');
    }
};

// Visualize room bounds
window.showRoomBounds = function() {
    // Draw debug rectangles around each room's stacking area
    // Useful for identifying overlaps visually
};