THE MOTEL - Personalization System Technical Specification

Executive Summary

This document provides a complete technical specification for THE MOTEL’s cookie-based personalization and progression system. The system enables 40 unique doors (existential threat experiences) to be dynamically assigned, tracked, and evolved based on each visitor’s journey, while ensuring friends have distinct experiences.


1. COOKIE/LOCAL STORAGE ARCHITECTURE

1.1 Data Structure Overview

// Primary Visitor State Object
const MOTEL_STATE_SCHEMA = {
  // Core Identity
  visitorId: "uuid-v4-string",           // Anonymous unique identifier
  visitorHash: "sha256-derived",         // Seed for deterministic generation
  firstVisit: "ISO-8601-timestamp",
  lastVisit: "ISO-8601-timestamp",
  visitCount: 0,
  
  // Progress Tracking
  totalProgress: 0.0,                    // 0.0 to 1.0 (percentage)
  doorsUnlocked: [],                     // Array of door IDs
  doorsCompleted: [],                    // Array of completed door IDs
  doorsAbandoned: [],                    // Started but not completed
  
  // Door Configuration (Personalized)
  doorMapping: {                         // Maps slot positions to actual doors
    "slot_01": "door_07",
    "slot_02": "door_23",
    // ... 40 slots mapped to 40 doors
  },
  mysteryDoorSlot: "slot_17",            // Which slot holds the mystery door
  
  // Per-Door State
  doorStates: {
    "door_07": {
      discovered: true,
      visited: true,
      completed: true,
      completionTime: 245000,            // milliseconds
      choicesMade: ["choice_a", "choice_c"],
      attempts: 1,
      lastVisited: "ISO-8601-timestamp"
    }
  },
  
  // Session State
  currentPosition: "hallway",            // hallway, door_XX, ending_XX
  lastDoorVisited: null,
  sessionStartTime: "ISO-8601-timestamp",
  totalTimeInMotel: 0,                   // cumulative milliseconds
  
  // Milestones & Unlocks
  milestonesReached: ["first_door", "halfway"],
  easterEggsFound: ["secret_message_03"],
  newGamePlusUnlocked: false,
  
  // Preferences
  settings: {
    audioEnabled: true,
    reducedMotion: false,
    highContrast: false
  },
  
  // Versioning
  schemaVersion: "1.0.0",
  lastMigrated: "ISO-8601-timestamp"
};
const COOKIE_NAMES = {
  // Primary State (compressed, base64)
  PRIMARY_STATE: 'motel_visitor_state',
  
  // Session-only cookies (deleted on browser close)
  SESSION_ID: 'motel_session_id',
  CURRENT_POSITION: 'motel_current_pos',
  
  // Lightweight flags (for quick checks)
  RETURNING_VISITOR: 'motel_returning',
  PROGRESS_PERCENT: 'motel_progress',
  
  // Friend differentiation
  FRIEND_CODE: 'motel_friend_code',      // Optional: shared friend code
  
  // Analytics (anonymous)
  ANALYTICS_CONSENT: 'motel_analytics_ok'
};

1.3 Storage Strategy & Limits

const STORAGE_CONFIG = {
  // Cookie storage (primary, server-readable)
  cookies: {
    maxSize: 4000,                       // bytes per cookie (browser safe)
    strategy: 'compressed_primary',      // LZ-string compression
    expiration: 365,                     // days
    secure: true,
    sameSite: 'Lax'
  },
  
  // localStorage (backup & extended data)
  localStorage: {
    key: 'motel_extended_state',
    maxSize: 5000000,                    // 5MB typical limit
    useFor: ['doorStates', 'easterEggsFound', 'detailedAnalytics']
  },
  
  // sessionStorage (temporary session data)
  sessionStorage: {
    key: 'motel_session_cache',
    useFor: ['currentAnimationState', 'scrollPosition', 'temporaryChoices']
  },
  
  // IndexedDB (future-proofing for rich media)
  indexedDB: {
    dbName: 'MotelExperienceDB',
    version: 1,
    stores: ['mediaCache', 'offlineProgress']
  }
};

1.4 Compression Strategy

// LZ-String compression for cookie storage
class StateCompressor {
  static compress(state) {
    const json = JSON.stringify(state);
    return LZString.compressToBase64(json);
  }
  
  static decompress(compressed) {
    const json = LZString.decompressFromBase64(compressed);
    return JSON.parse(json);
  }
  
  // Estimated sizes
  static estimateSize(state) {
    const compressed = this.compress(state);
    return {
      raw: JSON.stringify(state).length,
      compressed: compressed.length,
      ratio: compressed.length / JSON.stringify(state).length
    };
  }
}
 
// Typical compression: 60-70% reduction
// Full state (~3KB raw) → ~1KB compressed

1.5 Privacy & GDPR Compliance

const PRIVACY_CONFIG = {
  // Data collected (all anonymous)
  collectedData: {
    visitorId: 'anonymous_uuid',         // No PII linkage
    progress: 'game_state_only',         // No personal info
    choices: 'in_game_decisions',        // No external correlation
    timing: 'session_duration'           // No fingerprinting
  },
  
  // GDPR considerations
  gdpr: {
    // No personal data stored
    noPii: true,
    
    // Visitor can request data deletion
    rightToErasure: true,
    
    // Data is functional, not tracking
    legitimateInterest: true,
    
    // Clear cookie banner required
    consentRequired: true,
    
    // Data retention
    retentionDays: 365,
    
    // Automatic cleanup
    autoDeleteInactive: 730              // 2 years
  },
  
  // Cookie banner text
  consentText: {
    title: "THE MOTEL remembers its guests",
    description: "We use cookies to remember your progress through the hallway. No personal data is stored.",
    accept: "Enter the Motel",
    decline: "Remain Anonymous (progress won't be saved)"
  }
};

1.6 Data Expiration & Reset Mechanics

class DataLifecycleManager {
  // Check if state needs migration
  static checkVersion(state) {
    const CURRENT_VERSION = '1.0.0';
    if (!state.schemaVersion || state.schemaVersion !== CURRENT_VERSION) {
      return this.migrateState(state, CURRENT_VERSION);
    }
    return state;
  }
  
  // Migrate old state to new schema
  static migrateState(oldState, targetVersion) {
    // Migration logic for future updates
    const migrations = {
      '0.9.0_to_1.0.0': (state) => {
        // Add new fields, transform old ones
        state.schemaVersion = '1.0.0';
        state.mysteryDoorSlot = state.mysteryDoorSlot || null;
        return state;
      }
    };
    
    return migrations[`${oldState.schemaVersion}_to_${targetVersion}`]?.(oldState) || oldState;
  }
  
  // Reset options
  static resetOptions = {
    SOFT_RESET: 'soft',                  // Keep visitor ID, reset progress
    HARD_RESET: 'hard',                  // New visitor entirely
    NEW_GAME_PLUS: 'ngp',                // Keep unlocks, reshuffle doors
    DOOR_ONLY: 'door'                    // Reset specific door
  };
  
  static reset(state, option, targetDoor = null) {
    switch(option) {
      case 'soft':
        return {
          ...state,
          totalProgress: 0,
          doorsUnlocked: [],
          doorsCompleted: [],
          doorsAbandoned: [],
          doorStates: {},
          milestonesReached: [],
          easterEggsFound: []
        };
        
      case 'hard':
        return null; // Delete all cookies
        
      case 'ngp':
        return {
          ...state,
          doorMapping: this.generateNewMapping(state.visitorHash + '_ngp'),
          totalProgress: 0,
          doorsCompleted: [],
          newGamePlusUnlocked: false
        };
        
      case 'door':
        if (targetDoor) {
          const newDoorStates = { ...state.doorStates };
          delete newDoorStates[targetDoor];
          return {
            ...state,
            doorStates: newDoorStates,
            doorsCompleted: state.doorsCompleted.filter(d => d !== targetDoor)
          };
        }
        return state;
    }
  }
  
  // Auto-cleanup inactive visitors
  static shouldCleanup(state) {
    const INACTIVE_THRESHOLD = 730 * 24 * 60 * 60 * 1000; // 2 years
    const lastVisit = new Date(state.lastVisit).getTime();
    return Date.now() - lastVisit > INACTIVE_THRESHOLD;
  }
}

2. PROGRESSION SYSTEM

2.1 Door Unlock Mechanics

const PROGRESSION_RULES = {
  // Initial state
  startingDoors: {
    unlocked: ['slot_01'],               // First door always unlocked
    visible: ['slot_01', 'slot_02']      // Next door teased
  },
  
  // Unlock conditions
  unlockRules: [
    {
      condition: 'first_completion',
      trigger: (state) => state.doorsCompleted.length === 1,
      unlockSlots: ['slot_02', 'slot_03'],
      message: "The hallway extends..."
    },
    {
      condition: 'milestone_5',
      trigger: (state) => state.doorsCompleted.length === 5,
      unlockSlots: ['slot_04', 'slot_05', 'slot_06'],
      revealMystery: false,
      message: "You sense something watching..."
    },
    {
      condition: 'milestone_10',
      trigger: (state) => state.doorsCompleted.length === 10,
      unlockSlots: ['slot_07', 'slot_08', 'slot_09', 'slot_10'],
      revealMystery: true,
      message: "A door you've never noticed before..."
    },
    {
      condition: 'milestone_20',
      trigger: (state) => state.doorsCompleted.length === 20,
      unlockSlots: ['slot_11', 'slot_12', 'slot_13', 'slot_14', 'slot_15'],
      message: "Halfway. The motel knows you now."
    },
    {
      condition: 'milestone_30',
      trigger: (state) => state.doorsCompleted.length === 30,
      unlockSlots: ['slot_16', 'slot_17', 'slot_18', 'slot_19', 'slot_20'],
      message: "The end approaches. But does it?"
    },
    {
      condition: 'completion',
      trigger: (state) => state.doorsCompleted.length === 40,
      unlockSlots: [],
      unlockEnding: true,
      message: "Check-out time."
    }
  ],
  
  // Progressive reveal
  revealStrategy: {
    type: 'staggered',                   // Not all doors visible at once
    initialVisible: 3,
    incrementOnUnlock: 2,
    maxVisibleBeforeCompletion: 25       // Mystery maintained
  }
};

2.2 Completion Tracking

class ProgressTracker {
  static calculateProgress(state) {
    const totalDoors = 40;
    const completed = state.doorsCompleted.length;
    
    // Weight factors
    const weights = {
      completion: 0.7,                   // 70% for completing doors
      discovery: 0.2,                    // 20% for discovering doors
      exploration: 0.1                   // 10% for time/engagement
    };
    
    const completionScore = (completed / totalDoors) * weights.completion;
    const discoveryScore = (state.doorsUnlocked.length / totalDoors) * weights.discovery;
    const explorationScore = Math.min(
      state.totalTimeInMotel / (60 * 60 * 1000), // Cap at 1 hour
      1
    ) * weights.exploration;
    
    return Math.min(completionScore + discoveryScore + explorationScore, 1.0);
  }
  
  static getProgressTier(progress) {
    if (progress === 0) return 'newcomer';
    if (progress < 0.25) return 'wanderer';
    if (progress < 0.5) return 'explorer';
    if (progress < 0.75) return 'regular';
    if (progress < 1.0) return 'resident';
    return 'permanent_guest';
  }
  
  static getTierBenefits(tier) {
    const benefits = {
      newcomer: ['basic_access'],
      wanderer: ['basic_access', 'hint_system'],
      explorer: ['basic_access', 'hint_system', 'skip_option'],
      regular: ['basic_access', 'hint_system', 'skip_option', 'behind_scenes'],
      resident: ['basic_access', 'hint_system', 'skip_option', 'behind_scenes', 'director_commentary'],
      permanent_guest: ['all_access', 'new_game_plus', 'door_editor']
    };
    return benefits[tier] || benefits.newcomer;
  }
}

2.3 Milestone System

const MILESTONES = {
  // Discovery milestones
  'first_door': {
    condition: (s) => s.doorsCompleted.length >= 1,
    reward: 'unlocked_hint_system',
    message: "You've taken your first step."
  },
  'fifth_door': {
    condition: (s) => s.doorsCompleted.length >= 5,
    reward: 'unlocked_skip_single_door',
    message: "The motel recognizes persistence."
  },
  'tenth_door': {
    condition: (s) => s.doorsCompleted.length >= 10,
    reward: 'revealed_mystery_door',
    message: "Not all doors are where they appear."
  },
  
  // Progress milestones
  'halfway': {
    condition: (s) => s.doorsCompleted.length >= 20,
    reward: 'unlocked_behind_scenes',
    message: "Twenty doors. The motel knows your fears."
  },
  'three_quarters': {
    condition: (s) => s.doorsCompleted.length >= 30,
    reward: 'unlocked_director_commentary',
    message: "Almost there. Or are you?"
  },
  
  // Completion milestones
  'completion': {
    condition: (s) => s.doorsCompleted.length >= 40,
    reward: 'unlocked_new_game_plus',
    message: "You've seen all the motel has to offer. For now."
  },
  
  // Special milestones
  'speed_run': {
    condition: (s) => s.doorsCompleted.length >= 5 && s.totalTimeInMotel < 30 * 60 * 1000,
    reward: 'speed_runner_badge',
    message: "Moving quickly through the darkness."
  },
  'completionist': {
    condition: (s) => Object.keys(s.doorStates).every(d => s.doorStates[d].completed),
    reward: 'perfectionist_badge',
    message: "Every door holds a story. You've heard them all."
  },
  'return_visitor': {
    condition: (s) => s.visitCount >= 5,
    reward: 'welcome_home_message',
    message: "The motel remembers those who return."
  }
};

2.4 New Game Plus

class NewGamePlusManager {
  static isUnlocked(state) {
    return state.doorsCompleted.length === 40;
  }
  
  static startNewGamePlus(state) {
    // Generate new door arrangement with same visitor
    const newSeed = state.visitorHash + '_ngp_' + Date.now();
    
    return {
      ...state,
      visitorHash: newSeed,
      doorMapping: this.generateMapping(newSeed),
      totalProgress: 0,
      doorsUnlocked: [],
      doorsCompleted: [],
      doorsAbandoned: [],
      doorStates: {},
      currentPosition: 'hallway',
      newGamePlusUnlocked: true,
      ngpCount: (state.ngpCount || 0) + 1,
      
      // Carry over
      easterEggsFound: state.easterEggsFound,  // Permanent unlocks
      milestonesReached: state.milestonesReached,
      totalTimeInMotel: state.totalTimeInMotel
    };
  }
  
  static getNGPModifiers(ngpCount) {
    return {
      // Each NG+ makes doors... different
      doorIntensity: Math.min(1 + (ngpCount * 0.1), 2.0),  // Max 2x intensity
      hiddenDoors: Math.min(ngpCount * 2, 10),              // Extra hidden doors
      mysteryDoorChanges: ngpCount >= 2,                     // Mystery moves
      alternateEndings: ngpCount >= 3                        // New endings
    };
  }
}

3. PERSONALIZATION ALGORITHMS

3.1 Visitor Hash Generation

class VisitorIdentity {
  static generateVisitorId() {
    // UUID v4
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }
  
  static generateVisitorHash(visitorId, fingerprint = '') {
    // Deterministic hash from visitor ID + optional fingerprint
    // Fingerprint could include: user agent (hashed), screen size, timezone
    const combined = visitorId + fingerprint;
    return this.simpleHash(combined);
  }
  
  static simpleHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString(16);
  }
  
  // Seeded random number generator (deterministic)
  static seededRandom(seed) {
    let s = parseInt(seed, 16) || 12345;
    return () => {
      s = Math.sin(s) * 10000;
      return s - Math.floor(s);
    };
  }
}

3.2 Door Mapping Generation

class DoorMappingGenerator {
  // 40 doors, each visitor gets a unique arrangement
  static generateMapping(visitorHash) {
    const rng = VisitorIdentity.seededRandom(visitorHash);
    
    // All 40 door IDs
    const allDoors = Array.from({length: 40}, (_, i) => `door_${String(i + 1).padStart(2, '0')}`);
    
    // Fisher-Yates shuffle with seeded RNG
    const shuffled = [...allDoors];
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(rng() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    
    // Create slot mapping
    const mapping = {};
    shuffled.forEach((door, index) => {
      mapping[`slot_${String(index + 1).padStart(2, '0')}`] = door;
    });
    
    return mapping;
  }
  
  // Get which door is at a specific slot
  static getDoorAtSlot(mapping, slotNumber) {
    const slotKey = `slot_${String(slotNumber).padStart(2, '0')}`;
    return mapping[slotKey];
  }
  
  // Get slot number for a specific door
  static getSlotForDoor(mapping, doorId) {
    return Object.entries(mapping).find(([slot, door]) => door === doorId)?.[0];
  }
}

3.3 Mystery Door Assignment

class MysteryDoorManager {
  static assignMysteryDoor(visitorHash, mapping) {
    const rng = VisitorIdentity.seededRandom(visitorHash + '_mystery');
    
    // Mystery door appears at slot 15-35 (not too early, not at end)
    const minSlot = 15;
    const maxSlot = 35;
    const mysterySlotNumber = Math.floor(rng() * (maxSlot - minSlot + 1)) + minSlot;
    
    return {
      slot: `slot_${String(mysterySlotNumber).padStart(2, '0')}`,
      doorId: mapping[`slot_${String(mysterySlotNumber).padStart(2, '0')}`],
      revealed: false,
      accessible: false
    };
  }
  
  // Mystery door reveals itself under certain conditions
  static checkRevealConditions(state, mysteryDoor) {
    const completed = state.doorsCompleted.length;
    
    // Reveal after 10 doors completed
    if (completed >= 10 && !mysteryDoor.revealed) {
      return { revealed: true, accessible: false };
    }
    
    // Become accessible after 20 doors
    if (completed >= 20 && mysteryDoor.revealed && !mysteryDoor.accessible) {
      return { revealed: true, accessible: true };
    }
    
    return mysteryDoor;
  }
  
  // What the mystery door contains
  static getMysteryContent(visitorHash) {
    const rng = VisitorIdentity.seededRandom(visitorHash + '_mystery_content');
    
    const mysteryTypes = [
      'reflection',      // Mirror of visitor's choices
      'revelation',      // Hidden truth about the motel
      'choice',          // A door that asks a question
      'memory',          // Recalls previous doors
      'meta',            // Breaks fourth wall
      'gift'             // Unlocks something special
    ];
    
    return mysteryTypes[Math.floor(rng() * mysteryTypes.length)];
  }
}

3.4 Visible/Hidden Door Logic

class DoorVisibilityManager {
  static getVisibleDoors(state, includeTeasers = true) {
    const visible = [];
    const unlockedCount = state.doorsUnlocked.length;
    
    // Always show unlocked doors
    state.doorsUnlocked.forEach(slot => {
      visible.push({
        slot,
        doorId: state.doorMapping[slot],
        status: 'unlocked',
        accessible: true
      });
    });
    
    // Show teaser for next door(s)
    if (includeTeasers) {
      const nextSlotNumber = unlockedCount + 1;
      const nextSlot = `slot_${String(nextSlotNumber).padStart(2, '0')}`;
      
      if (state.doorMapping[nextSlot] && !state.doorsUnlocked.includes(nextSlot)) {
        visible.push({
          slot: nextSlot,
          doorId: null,  // Hidden until unlocked
          status: 'teaser',
          accessible: false
        });
      }
    }
    
    // Mystery door (if revealed)
    if (state.mysteryDoorSlot) {
      const mysteryState = state.doorStates[state.doorMapping[state.mysteryDoorSlot]];
      if (mysteryState?.discovered) {
        visible.push({
          slot: state.mysteryDoorSlot,
          doorId: state.doorMapping[state.mysteryDoorSlot],
          status: 'mystery',
          accessible: mysteryState?.accessible || false
        });
      }
    }
    
    return visible;
  }
  
  // Get door appearance based on state
  static getDoorAppearance(doorId, doorState) {
    const appearances = {
      locked: { style: 'dark', glow: 'none', label: '???' },
      unlocked: { style: 'wood', glow: 'subtle', label: doorId },
      visited: { style: 'wood', glow: 'warm', label: doorId, mark: 'visited' },
      completed: { style: 'aged', glow: 'golden', label: doorId, mark: 'completed' },
      mystery: { style: 'shifting', glow: 'pulsing', label: '?' },
      abandoned: { style: 'cracked', glow: 'flickering', label: doorId }
    };
    
    if (!doorState) return appearances.locked;
    if (doorState.completed) return appearances.completed;
    if (doorState.visited) return appearances.visited;
    return appearances.unlocked;
  }
}
class RecommendationEngine {
  static getRecommendedDoor(state) {
    const unlockedDoors = state.doorsUnlocked
      .map(slot => ({
        slot,
        doorId: state.doorMapping[slot],
        state: state.doorStates[state.doorMapping[slot]]
      }))
      .filter(d => !d.state?.completed);  // Only incomplete doors
    
    if (unlockedDoors.length === 0) {
      return null;  // All unlocked doors completed
    }
    
    // Score each door
    const scoredDoors = unlockedDoors.map(door => ({
      ...door,
      score: this.calculateDoorScore(door, state)
    }));
    
    // Sort by score (highest first)
    scoredDoors.sort((a, b) => b.score - a.score);
    
    return scoredDoors[0];
  }
  
  static calculateDoorScore(door, state) {
    let score = 0;
    const doorState = door.state || {};
    
    // Never visited = high priority
    if (!doorState.visited) {
      score += 100;
    }
    
    // Started but abandoned = medium priority
    if (doorState.visited && !doorState.completed) {
      score += 50;
    }
    
    // Time since last visit (prefer doors not visited recently)
    if (doorState.lastVisited) {
      const hoursSince = (Date.now() - new Date(doorState.lastVisited).getTime()) / (1000 * 60 * 60);
      score += Math.min(hoursSince / 24, 30);  // Max 30 points for time
    }
    
    // Door number preference (slight preference for earlier doors)
    const doorNum = parseInt(door.doorId.split('_')[1]);
    score += (40 - doorNum) * 0.5;
    
    // Mystery door bonus (if accessible)
    if (door.slot === state.mysteryDoorSlot && doorState.accessible) {
      score += 200;
    }
    
    return score;
  }
  
  // Get recommendation message
  static getRecommendationMessage(recommendedDoor, state) {
    if (!recommendedDoor) {
      return "All doors await your return.";
    }
    
    const doorNum = parseInt(recommendedDoor.doorId.split('_')[1]);
    const messages = {
      neverVisited: [
        "A door you haven't opened calls to you.",
        "Something waits behind Door {n}.",
        "Door {n} remains unexplored."
      ],
      abandoned: [
        "You left something behind at Door {n}.",
        "Door {n} remembers your hesitation.",
        "Perhaps you're ready for Door {n} now."
      ],
      mystery: [
        "The mystery door beckons.",
        "Some doors appear only to those who've seen enough."
      ]
    };
    
    const doorState = recommendedDoor.state || {};
    let category = 'neverVisited';
    
    if (recommendedDoor.slot === state.mysteryDoorSlot) {
      category = 'mystery';
    } else if (doorState.visited && !doorState.completed) {
      category = 'abandoned';
    }
    
    const msgs = messages[category];
    const msg = msgs[Math.floor(Math.random() * msgs.length)];
    
    return msg.replace('{n}', doorNum);
  }
}

4. FRIEND DIFFERENTIATION

4.1 Friend Code System

class FriendSystem {
  // Generate a shareable friend code
  static generateFriendCode(visitorId) {
    // Create a short, shareable code
    const hash = VisitorIdentity.simpleHash(visitorId);
    return hash.substring(0, 8).toUpperCase();
  }
  
  // Parse friend code to get comparison data
  static parseFriendCode(code) {
    // In production, this would lookup the friend's state
    // For now, we'll use the code as a seed
    return {
      friendHash: code,
      canCompare: true
    };
  }
  
  // Compare two visitors' progress
  static compareProgress(myState, friendState) {
    const myCompleted = new Set(myState.doorsCompleted);
    const friendCompleted = new Set(friendState.doorsCompleted);
    
    // Doors I've done that friend hasn't
    const onlyMine = [...myCompleted].filter(d => !friendCompleted.has(d));
    
    // Doors friend has done that I haven't
    const onlyFriends = [...friendCompleted].filter(d => !myCompleted.has(d));
    
    // Doors we've both done
    const both = [...myCompleted].filter(d => friendCompleted.has(d));
    
    return {
      myProgress: myState.totalProgress,
      friendProgress: friendState.totalProgress,
      onlyMine,
      onlyFriends,
      both,
      aheadBy: myCompleted.size - friendCompleted.size
    };
  }
}

4.2 Different Door Assignments

class DifferentiationEngine {
  // Ensure friends see different door arrangements
  static generateDifferentiatedMapping(visitorHash, friendHashes = []) {
    // Start with base mapping
    let mapping = DoorMappingGenerator.generateMapping(visitorHash);
    
    // If we have friend hashes, ensure differentiation
    if (friendHashes.length > 0) {
      // Get friend mappings
      const friendMappings = friendHashes.map(h => DoorMappingGenerator.generateMapping(h));
      
      // Check for similarity
      let attempts = 0;
      const maxAttempts = 100;
      
      while (attempts < maxAttempts) {
        const similarity = this.calculateSimilarity(mapping, friendMappings);
        
        if (similarity < 0.3) {  // Less than 30% similar
          break;  // Good enough
        }
        
        // Regenerate with modified seed
        const newSeed = visitorHash + '_diff_' + attempts;
        mapping = DoorMappingGenerator.generateMapping(newSeed);
        attempts++;
      }
    }
    
    return mapping;
  }
  
  static calculateSimilarity(mapping, friendMappings) {
    let totalSimilarity = 0;
    
    friendMappings.forEach(friendMap => {
      let same = 0;
      let total = 0;
      
      Object.entries(mapping).forEach(([slot, door]) => {
        if (friendMap[slot] === door) {
          same++;
        }
        total++;
      });
      
      totalSimilarity += same / total;
    });
    
    return totalSimilarity / friendMappings.length;
  }
}

4.3 Shared vs Private Experiences

const EXPERIENCE_SHARING = {
  // Always private (never shared)
  private: {
    doorChoices: true,                   // What you chose
    timePerDoor: true,                   // How long you spent
    abandonedDoors: true,                // Which you ran from
    easterEggsFound: true,               // Your discoveries
    personalNotes: true                  // Future feature
  },
  
  // Can be shared (with consent)
  shareable: {
    doorsCompleted: true,                // Which doors done
    totalProgress: true,                 // Overall percentage
    milestonesReached: true,             // Achievements
    visitCount: true,                    // How many returns
    totalTime: true                      // Cumulative time
  },
  
  // Always public (for leaderboard)
  public: {
    completionRank: true,                // Relative ranking
    speedRunTimes: true,                 // Fastest completion
    ngpCount: true                       // New Game+ count
  }
};

4.4 Comparison Messages

class ComparisonMessages {
  static generateFriendComparison(myState, friendState) {
    const comparison = FriendSystem.compareProgress(myState, friendState);
    const messages = [];
    
    // Progress comparison
    if (comparison.aheadBy > 5) {
      messages.push(`You're ${comparison.aheadBy} doors ahead.`);
    } else if (comparison.aheadBy < -5) {
      messages.push(`Your friend is ${Math.abs(comparison.aheadBy)} doors ahead.`);
    }
    
    // Unique discoveries
    if (comparison.onlyFriends.length > 0) {
      const randomDoor = comparison.onlyFriends[
        Math.floor(Math.random() * comparison.onlyFriends.length)
      ];
      const doorNum = parseInt(randomDoor.split('_')[1]);
      messages.push(`Your friend found Door ${doorNum}. What did they see?`);
    }
    
    // Shared experiences
    if (comparison.both.length > 0 && Math.random() > 0.5) {
      messages.push(`You've both seen ${comparison.both.length} of the same doors.`);
    }
    
    // Mystery door
    if (friendState.mysteryDoorSlot && !myState.doorStates[friendState.doorMapping[friendState.mysteryDoorSlot]]?.discovered) {
      messages.push("Your friend has discovered something you haven't...");
    }
    
    return messages;
  }
}

5. VISITOR STATE TRACKING

5.1 Session Management

class SessionManager {
  static startSession(state) {
    const sessionId = VisitorIdentity.generateVisitorId();
    const sessionStart = new Date().toISOString();
    
    // Update state
    const updatedState = {
      ...state,
      lastVisit: sessionStart,
      visitCount: (state.visitCount || 0) + 1,
      sessionStartTime: sessionStart
    };
    
    // Set session cookies
    CookieManager.set(COOKIE_NAMES.SESSION_ID, sessionId, { session: true });
    CookieManager.set(COOKIE_NAMES.CURRENT_POSITION, state.currentPosition || 'hallway', { session: true });
    
    return { state: updatedState, sessionId };
  }
  
  static endSession(state) {
    const sessionEnd = Date.now();
    const sessionStart = new Date(state.sessionStartTime).getTime();
    const sessionDuration = sessionEnd - sessionStart;
    
    return {
      ...state,
      totalTimeInMotel: (state.totalTimeInMotel || 0) + sessionDuration
    };
  }
  
  static isReturningVisitor(state) {
    return (state.visitCount || 0) > 1;
  }
  
  static getTimeSinceLastVisit(state) {
    if (!state.lastVisit) return null;
    return Date.now() - new Date(state.lastVisit).getTime();
  }
}

5.2 Position Tracking

class PositionTracker {
  static POSITIONS = {
    HALLWAY: 'hallway',
    DOOR_PREFIX: 'door_',
    ENDING_PREFIX: 'ending_'
  };
  
  static setPosition(state, position) {
    const updatedState = {
      ...state,
      currentPosition: position
    };
    
    // Update session cookie for recovery
    CookieManager.set(COOKIE_NAMES.CURRENT_POSITION, position, { session: true });
    
    return updatedState;
  }
  
  static recordDoorVisit(state, doorId) {
    const now = new Date().toISOString();
    
    return {
      ...state,
      currentPosition: `door_${doorId}`,
      lastDoorVisited: doorId,
      doorStates: {
        ...state.doorStates,
        [doorId]: {
          ...state.doorStates[doorId],
          discovered: true,
          visited: true,
          lastVisited: now
        }
      }
    };
  }
  
  static recordDoorCompletion(state, doorId, choices = [], completionTime = 0) {
    const doorState = state.doorStates[doorId] || {};
    
    return {
      ...state,
      doorsCompleted: [...new Set([...state.doorsCompleted, doorId])],
      doorStates: {
        ...state.doorStates,
        [doorId]: {
          ...doorState,
          completed: true,
          completionTime: (doorState.completionTime || 0) + completionTime,
          choicesMade: [...(doorState.choicesMade || []), ...choices],
          attempts: (doorState.attempts || 0) + 1
        }
      }
    };
  }
}

5.3 Welcome Back Personalization

class WelcomeBackSystem {
  static generateWelcomeMessage(state) {
    const timeSince = SessionManager.getTimeSinceLastVisit(state);
    const daysSince = timeSince ? Math.floor(timeSince / (1000 * 60 * 60 * 24)) : 0;
    
    // Determine message category
    let category = 'short';
    if (daysSince > 365) category = 'year';
    else if (daysSince > 30) category = 'month';
    else if (daysSince > 7) category = 'week';
    else if (daysSince > 1) category = 'days';
    
    const messages = {
      short: [
        "Back so soon?",
        "The motel didn't expect you yet.",
        "You couldn't stay away."
      ],
      days: [
        `It's been ${daysSince} days. The motel has been waiting.`,
        "The hallway remembers your footsteps.",
        "Welcome back, wanderer."
      ],
      week: [
        "A week apart. The motel has missed you.",
        "Seven days. The doors have shifted slightly.",
        "You've been gone long enough to forget. But not long enough to escape."
      ],
      month: [
        "A month. The motel wondered if you'd return.",
        "Thirty days of silence. And now, footsteps again.",
        "You almost escaped. Almost."
      ],
      year: [
        "A year. The motel had begun to forget your face.",
        "365 days. Some guests never return. You did.",
        "The permanent guest returns. The motel remembers everything."
      ]
    };
    
    const categoryMessages = messages[category];
    return categoryMessages[Math.floor(Math.random() * categoryMessages.length)];
  }
  
  static generateProgressSummary(state) {
    const completed = state.doorsCompleted.length;
    const total = 40;
    const percent = Math.round((completed / total) * 100);
    
    if (completed === 0) {
      return "You stand at the beginning. Again.";
    }
    
    if (completed === total) {
      return "You've seen everything. But have you truly checked out?";
    }
    
    const summaries = [
      `${completed} of ${total} doors. ${percent}% of the motel knows you.`,
      `You've opened ${completed} doors. ${total - completed} remain.`,
      `${percent}% complete. The motel is ${percent}% yours.`,
      `${completed} experiences. ${total - completed} more await.`
    ];
    
    return summaries[Math.floor(Math.random() * summaries.length)];
  }
  
  static getRecommendedAction(state) {
    const recommendation = RecommendationEngine.getRecommendedDoor(state);
    
    if (!recommendation) {
      return {
        action: 'review',
        message: "All doors await your return. Perhaps check your completed rooms?"
      };
    }
    
    const doorNum = parseInt(recommendation.doorId.split('_')[1]);
    
    return {
      action: 'visit_door',
      doorId: recommendation.doorId,
      slot: recommendation.slot,
      message: RecommendationEngine.getRecommendationMessage(recommendation, state)
    };
  }
}

6. DYNAMIC CONTENT DELIVERY

6.1 Door Content Variants

class DynamicContentEngine {
  // Each door can have multiple variants based on visitor state
  static getDoorVariant(doorId, state) {
    const doorNum = parseInt(doorId.split('_')[1]);
    const variants = this.getAvailableVariants(doorId);
    
    // Select variant based on visitor hash and progress
    const rng = VisitorIdentity.seededRandom(state.visitorHash + doorId);
    const variantIndex = Math.floor(rng() * variants.length);
    
    // But also consider progress for some doors
    if (doorNum % 5 === 0) {  // Every 5th door adapts
      return this.getAdaptiveVariant(doorId, state, variants);
    }
    
    return variants[variantIndex];
  }
  
  static getAvailableVariants(doorId) {
    // Each door has base + variants
    const baseVariants = ['default', 'intense', 'subtle', 'alternate'];
    
    // Some doors have special variants
    const specialVariants = {
      'door_13': ['default', 'intense', 'subtle', 'alternate', 'meta'],
      'door_23': ['default', 'intense', 'subtle', 'alternate', 'choice_heavy'],
      'door_40': ['default', 'intense', 'subtle', 'alternate', 'true_ending', 'secret_ending']
    };
    
    return specialVariants[doorId] || baseVariants;
  }
  
  static getAdaptiveVariant(doorId, state, variants) {
    const completed = state.doorsCompleted.length;
    const abandoned = state.doorsAbandoned.length || 0;
    
    // Adapt based on play style
    if (abandoned > completed * 0.5) {
      // Player abandons often - use subtle variant
      return variants.find(v => v === 'subtle') || variants[0];
    }
    
    if (completed > 20) {
      // Experienced player - can handle intense
      return variants.find(v => v === 'intense') || variants[0];
    }
    
    // Default selection
    const rng = VisitorIdentity.seededRandom(state.visitorHash + doorId);
    return variants[Math.floor(rng() * variants.length)];
  }
}

6.2 Adaptive Difficulty

class AdaptiveDifficulty {
  static calculateDifficultyModifier(state) {
    const modifiers = {
      base: 1.0,
      experience: 0,
      completionRate: 0,
      timeModifier: 0,
      preference: 0
    };
    
    // Experience modifier (more doors = higher base difficulty acceptable)
    const completed = state.doorsCompleted.length;
    modifiers.experience = Math.min(completed * 0.02, 0.4);  // Max +0.4
    
    // Completion rate (if they complete most doors, they can handle more)
    const totalAttempts = Object.values(state.doorStates)
      .reduce((sum, d) => sum + (d.attempts || 0), 0);
    const completionRate = completed / Math.max(totalAttempts, 1);
    modifiers.completionRate = (completionRate - 0.5) * 0.2;  // -0.1 to +0.1
    
    // Time modifier (fast completers get harder content)
    const avgTime = state.totalTimeInMotel / Math.max(completed, 1);
    if (avgTime < 5 * 60 * 1000) {  // Less than 5 min per door
      modifiers.timeModifier = 0.1;
    }
    
    // User preference (if set)
    if (state.settings?.difficulty) {
      modifiers.preference = {
        'easy': -0.2,
        'normal': 0,
        'hard': 0.2
      }[state.settings.difficulty] || 0;
    }
    
    const totalModifier = Object.values(modifiers).reduce((a, b) => a + b, 0);
    return Math.max(0.5, Math.min(totalModifier, 2.0));  // Clamp 0.5x to 2x
  }
  
  static applyDifficulty(content, modifier) {
    return {
      ...content,
      intensity: content.intensity * modifier,
      duration: content.duration / modifier,  // Harder = faster
      choiceComplexity: Math.min(content.choiceComplexity * modifier, 5)
    };
  }
}

6.3 Story Branching

class StoryBranching {
  // Track choices across doors for narrative continuity
  static recordChoice(state, doorId, choice) {
    const choiceKey = `${doorId}:${choice}`;
    
    return {
      ...state,
      globalChoices: [...(state.globalChoices || []), choiceKey]
    };
  }
  
  // Get narrative state based on all choices
  static getNarrativeState(state) {
    const choices = state.globalChoices || [];
    
    return {
      // Has the visitor generally chosen fight or flight?
      tendency: this.calculateTendency(choices),
      
      // Have they shown curiosity or avoidance?
      curiosity: this.calculateCuriosity(choices),
      
      // Have they completed doors quickly or slowly?
      pace: this.calculatePace(state),
      
      // Overall narrative path
      path: this.determinePath(choices)
    };
  }
  
  static calculateTendency(choices) {
    const fight = choices.filter(c => c.includes('confront')).length;
    const flight = choices.filter(c => c.includes('avoid')).length;
    
    if (fight > flight * 1.5) return 'confrontational';
    if (flight > fight * 1.5) return 'avoidant';
    return 'balanced';
  }
  
  static calculateCuriosity(choices) {
    const curious = choices.filter(c => 
      c.includes('investigate') || c.includes('explore')
    ).length;
    return curious > choices.length * 0.3 ? 'high' : 'low';
  }
  
  static calculatePace(state) {
    const avgTime = state.totalTimeInMotel / Math.max(state.doorsCompleted.length, 1);
    if (avgTime < 3 * 60 * 1000) return 'fast';
    if (avgTime > 10 * 60 * 1000) return 'slow';
    return 'normal';
  }
  
  static determinePath(choices) {
    // Complex path determination based on choice patterns
    // Returns: 'seeker', 'survivor', 'observer', 'participant'
    // Implementation depends on specific choice taxonomy
    return 'seeker';  // Default
  }
  
  // Modify door content based on narrative state
  static adaptContentForNarrative(content, narrativeState) {
    const adaptations = {
      confrontational: {
        dialogue: 'direct',
        options: 'action_oriented'
      },
      avoidant: {
        dialogue: 'subtle',
        options: 'escape_focused'
      },
      high_curiosity: {
        reveals: 'extra_lore',
        secrets: 'more_visible'
      },
      fast_pace: {
        pacing: 'quicker',
        fluff: 'reduced'
      }
    };
    
    // Apply relevant adaptations
    let adapted = { ...content };
    
    if (adaptations[narrativeState.tendency]) {
      adapted = { ...adapted, ...adaptations[narrativeState.tendency] };
    }
    
    return adapted;
  }
}

6.4 Easter Egg System

class EasterEggSystem {
  static EASTER_EGGS = {
    'secret_message_01': {
      condition: (state) => state.doorsCompleted.includes('door_07') && state.doorsCompleted.includes('door_13'),
      hint: "Some doors speak to each other.",
      reward: 'lore_fragment_01'
    },
    'hidden_door_01': {
      condition: (state) => state.totalTimeInMotel > 60 * 60 * 1000,  // 1 hour total
      hint: "Patience reveals what haste conceals.",
      reward: 'bonus_door_access'
    },
    'speed_demon': {
      condition: (state) => state.doorsCompleted.length >= 10 && 
        state.totalTimeInMotel < 30 * 60 * 1000,
      hint: "The motel respects those who don't linger.",
      reward: 'speed_badge'
    },
    'completionist_secret': {
      condition: (state) => state.doorsCompleted.length === 40 && 
        Object.values(state.doorStates).every(d => d.choicesMade?.length > 0),
      hint: "Every choice matters. Every door holds more than one truth.",
      reward: 'true_ending_access'
    },
    'return_visitor_bonus': {
      condition: (state) => state.visitCount >= 10,
      hint: "The motel remembers those who keep returning.",
      reward: 'insider_knowledge'
    }
  };
  
  static checkEasterEggs(state) {
    const newlyUnlocked = [];
    
    Object.entries(this.EASTER_EGGS).forEach(([eggId, egg]) => {
      if (!state.easterEggsFound?.includes(eggId) && egg.condition(state)) {
        newlyUnlocked.push({
          id: eggId,
          ...egg
        });
      }
    });
    
    return newlyUnlocked;
  }
  
  static unlockEasterEgg(state, eggId) {
    return {
      ...state,
      easterEggsFound: [...(state.easterEggsFound || []), eggId]
    };
  }
}

7. TECHNICAL IMPLEMENTATION

class CookieManager {
  static set(name, value, options = {}) {
    const defaults = {
      days: 365,
      secure: true,
      sameSite: 'Lax',
      path: '/'
    };
    
    const opts = { ...defaults, ...options };
    
    let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
    
    if (opts.days && !opts.session) {
      const date = new Date();
      date.setTime(date.getTime() + (opts.days * 24 * 60 * 60 * 1000));
      cookieString += `; expires=${date.toUTCString()}`;
    }
    
    if (opts.secure) cookieString += '; secure';
    if (opts.sameSite) cookieString += `; samesite=${opts.sameSite}`;
    if (opts.path) cookieString += `; path=${opts.path}`;
    
    document.cookie = cookieString;
  }
  
  static get(name) {
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
      const [cookieName, cookieValue] = cookie.trim().split('=');
      if (decodeURIComponent(cookieName) === name) {
        return decodeURIComponent(cookieValue);
      }
    }
    return null;
  }
  
  static delete(name) {
    this.set(name, '', { days: -1 });
  }
  
  static exists(name) {
    return this.get(name) !== null;
  }
}

7.2 State Manager

class MotelStateManager {
  constructor() {
    this.state = null;
    this.listeners = [];
    this.saveDebounce = null;
  }
  
  // Initialize or load existing state
  async init() {
    // Try to load from cookies
    const savedState = CookieManager.get(COOKIE_NAMES.PRIMARY_STATE);
    
    if (savedState) {
      try {
        this.state = StateCompressor.decompress(savedState);
        this.state = DataLifecycleManager.checkVersion(this.state);
        
        // Check for cleanup
        if (DataLifecycleManager.shouldCleanup(this.state)) {
          console.log('Motel: State expired, starting fresh');
          this.state = this.createNewState();
        }
      } catch (e) {
        console.error('Motel: Failed to load state, creating new');
        this.state = this.createNewState();
      }
    } else {
      this.state = this.createNewState();
    }
    
    // Start session
    const { state: updatedState } = SessionManager.startSession(this.state);
    this.state = updatedState;
    
    // Save initial state
    this.saveState();
    
    return this.state;
  }
  
  createNewState() {
    const visitorId = VisitorIdentity.generateVisitorId();
    const visitorHash = VisitorIdentity.generateVisitorHash(visitorId);
    
    return {
      visitorId,
      visitorHash,
      firstVisit: new Date().toISOString(),
      lastVisit: new Date().toISOString(),
      visitCount: 0,
      totalProgress: 0,
      doorsUnlocked: ['slot_01'],
      doorsCompleted: [],
      doorsAbandoned: [],
      doorMapping: DoorMappingGenerator.generateMapping(visitorHash),
      mysteryDoorSlot: null,  // Will be set after generation
      doorStates: {},
      currentPosition: 'hallway',
      lastDoorVisited: null,
      sessionStartTime: new Date().toISOString(),
      totalTimeInMotel: 0,
      milestonesReached: [],
      easterEggsFound: [],
      newGamePlusUnlocked: false,
      settings: {
        audioEnabled: true,
        reducedMotion: false,
        highContrast: false
      },
      schemaVersion: '1.0.0',
      lastMigrated: new Date().toISOString()
    };
  }
  
  // Update state with change
  updateState(updater) {
    const newState = updater(this.state);
    this.state = newState;
    
    // Notify listeners
    this.listeners.forEach(listener => listener(newState));
    
    // Debounced save
    clearTimeout(this.saveDebounce);
    this.saveDebounce = setTimeout(() => this.saveState(), 500);
    
    return newState;
  }
  
  // Save state to cookies
  saveState() {
    if (!this.state) return;
    
    const compressed = StateCompressor.compress(this.state);
    CookieManager.set(COOKIE_NAMES.PRIMARY_STATE, compressed);
    
    // Update lightweight progress cookie
    CookieManager.set(
      COOKIE_NAMES.PROGRESS_PERCENT, 
      Math.round(this.state.totalProgress * 100).toString(),
      { days: 365 }
    );
    
    // Mark as returning visitor
    if (this.state.visitCount > 1) {
      CookieManager.set(COOKIE_NAMES.RETURNING_VISITOR, 'true', { days: 365 });
    }
    
    // Sync to localStorage for backup
    try {
      localStorage.setItem(
        STORAGE_CONFIG.localStorage.key,
        JSON.stringify({
          doorStates: this.state.doorStates,
          easterEggsFound: this.state.easterEggsFound,
          lastSaved: new Date().toISOString()
        })
      );
    } catch (e) {
      // localStorage might be full or unavailable
    }
  }
  
  // Get current state
  getState() {
    return this.state;
  }
  
  // Subscribe to state changes
  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
  
  // End session and cleanup
  endSession() {
    this.state = SessionManager.endSession(this.state);
    this.saveState();
  }
}
 
// Singleton instance
const motelState = new MotelStateManager();

7.3 Tab Synchronization

class TabSyncManager {
  constructor(stateManager) {
    this.stateManager = stateManager;
    this.channel = new BroadcastChannel('motel_sync');
    
    this.channel.onmessage = (event) => {
      this.handleSyncMessage(event.data);
    };
    
    // Listen for storage events (for browsers without BroadcastChannel)
    window.addEventListener('storage', (e) => {
      if (e.key === STORAGE_CONFIG.localStorage.key) {
        this.handleStorageChange(e.newValue);
      }
    });
  }
  
  handleSyncMessage(message) {
    switch(message.type) {
      case 'STATE_UPDATED':
        // Another tab updated state
        this.stateManager.state = message.payload;
        break;
        
      case 'DOOR_OPENED':
        // Someone opened a door in another tab
        console.log('Motel: Door opened in another tab');
        break;
        
      case 'PROGRESS_MADE':
        // Progress in another tab
        this.stateManager.state = {
          ...this.stateManager.state,
          ...message.payload
        };
        break;
    }
  }
  
  handleStorageChange(newValue) {
    try {
      const data = JSON.parse(newValue);
      // Merge localStorage data with current state
      this.stateManager.state = {
        ...this.stateManager.state,
        doorStates: { ...this.stateManager.state.doorStates, ...data.doorStates },
        easterEggsFound: [...new Set([
          ...this.stateManager.state.easterEggsFound,
          ...(data.easterEggsFound || [])
        ])]
      };
    } catch (e) {
      console.error('Motel: Failed to sync from storage');
    }
  }
  
  // Broadcast state change to other tabs
  broadcast(type, payload) {
    this.channel.postMessage({ type, payload, timestamp: Date.now() });
  }
  
  // Notify other tabs of door interaction
  notifyDoorInteraction(doorId, action) {
    this.broadcast('DOOR_INTERACTION', { doorId, action });
  }
}

7.4 Data Versioning

const SCHEMA_VERSIONS = {
  '1.0.0': {
    fields: [
      'visitorId', 'visitorHash', 'firstVisit', 'lastVisit', 'visitCount',
      'totalProgress', 'doorsUnlocked', 'doorsCompleted', 'doorsAbandoned',
      'doorMapping', 'mysteryDoorSlot', 'doorStates', 'currentPosition',
      'lastDoorVisited', 'sessionStartTime', 'totalTimeInMotel',
      'milestonesReached', 'easterEggsFound', 'newGamePlusUnlocked',
      'settings', 'schemaVersion', 'lastMigrated'
    ],
    migrations: {}
  }
};
 
class SchemaValidator {
  static validate(state) {
    const version = state.schemaVersion || '0.0.0';
    const schema = SCHEMA_VERSIONS[version];
    
    if (!schema) {
      console.warn(`Motel: Unknown schema version ${version}`);
      return false;
    }
    
    // Check required fields
    const missing = schema.fields.filter(f => !(f in state));
    if (missing.length > 0) {
      console.warn(`Motel: Missing fields: ${missing.join(', ')}`);
      return false;
    }
    
    return true;
  }
  
  static migrate(state, targetVersion) {
    const currentVersion = state.schemaVersion || '0.0.0';
    
    if (currentVersion === targetVersion) {
      return state;
    }
    
    // Apply migrations in sequence
    // This would be expanded for future versions
    console.log(`Motel: Migrating from ${currentVersion} to ${targetVersion}`);
    
    return {
      ...state,
      schemaVersion: targetVersion,
      lastMigrated: new Date().toISOString()
    };
  }
}

8. ANALYTICS

8.1 Anonymous Tracking

class MotelAnalytics {
  constructor() {
    this.enabled = false;
    this.queue = [];
  }
  
  init() {
    // Check consent
    this.enabled = CookieManager.get(COOKIE_NAMES.ANALYTICS_CONSENT) === 'true';
  }
  
  // Track event (anonymous)
  track(eventName, properties = {}) {
    if (!this.enabled) return;
    
    const event = {
      name: eventName,
      properties: {
        ...properties,
        timestamp: Date.now(),
        sessionId: CookieManager.get(COOKIE_NAMES.SESSION_ID)
      },
      // No visitor ID - completely anonymous
    };
    
    this.queue.push(event);
    
    // Flush queue periodically
    if (this.queue.length >= 10) {
      this.flush();
    }
  }
  
  // Predefined events
  trackDoorEnter(doorId) {
    this.track('door_enter', { doorId });
  }
  
  trackDoorComplete(doorId, duration) {
    this.track('door_complete', { doorId, duration });
  }
  
  trackDoorAbandon(doorId, timeSpent) {
    this.track('door_abandon', { doorId, timeSpent });
  }
  
  trackMilestone(milestoneId) {
    this.track('milestone_reached', { milestoneId });
  }
  
  trackEasterEgg(eggId) {
    this.track('easter_egg_found', { eggId });
  }
  
  trackSessionEnd(duration, doorsCompleted) {
    this.track('session_end', { duration, doorsCompleted });
  }
  
  // Send to analytics endpoint
  flush() {
    if (this.queue.length === 0) return;
    
    const events = [...this.queue];
    this.queue = [];
    
    // Send to your analytics endpoint
    fetch('/api/motel/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ events }),
      keepalive: true
    }).catch(() => {
      // Re-queue on failure
      this.queue.unshift(...events);
    });
  }
}
 
const motelAnalytics = new MotelAnalytics();

8.2 Key Metrics to Track

const ANALYTICS_METRICS = {
  // Door popularity
  doorMetrics: {
    enterCount: 'How many times each door is entered',
    completionRate: 'Percentage who complete vs abandon',
    avgTime: 'Average time spent in each door',
    dropOffPoint: 'Where in the door people leave'
  },
  
  // Progress metrics
  progressMetrics: {
    completionRate: 'How many visitors complete all doors',
    avgDoorsPerSession: 'Average doors completed per visit',
    returnRate: 'How many visitors come back',
    timeToComplete: 'Total time to complete all doors'
  },
  
  // Engagement metrics
  engagementMetrics: {
    sessionDuration: 'How long people stay per session',
    doorsPerSession: 'How many doors per visit',
    returnFrequency: 'How often people return',
    ngpAdoption: 'How many start New Game+'
  },
  
  // Experience metrics
  experienceMetrics: {
    mysteryDoorDiscovery: 'How many find the mystery door',
    easterEggDiscovery: 'Which easter eggs are found most',
    variantSelection: 'Which door variants are experienced',
    endingReached: 'Which endings are discovered'
  }
};

8.3 Dashboard Data Structure

// Data structure for analytics dashboard
const DASHBOARD_DATA = {
  overview: {
    totalSessions: 0,
    uniqueVisitors: 0,  // Anonymous count
    completionRate: 0,
    avgSessionDuration: 0
  },
  
  doors: {
    // Per-door stats
    'door_01': {
      enters: 0,
      completes: 0,
      abandons: 0,
      avgTime: 0,
      completionRate: 0
    }
    // ... for all 40 doors
  },
  
  progression: {
    dropOffPoints: [],     // Where people stop
    milestoneReachRates: {}, // How many reach each milestone
    ngpStarts: 0
  },
  
  temporal: {
    sessionsByHour: [],
    sessionsByDay: [],
    completionTimeDistribution: []
  }
};

9. IMPLEMENTATION CHECKLIST

Phase 1: Core State Management

  • Implement CookieManager
  • Implement StateCompressor
  • Implement MotelStateManager
  • Create visitor ID generation
  • Set up cookie consent banner

Phase 2: Progression System

  • Implement door unlock logic
  • Create progress calculation
  • Build milestone system
  • Add New Game+ functionality

Phase 3: Personalization

  • Implement door mapping generator
  • Create mystery door system
  • Build recommendation engine
  • Add door visibility logic

Phase 4: Friend System

  • Generate friend codes
  • Implement comparison logic
  • Create differentiated mappings
  • Add comparison messages

Phase 5: Dynamic Content

  • Build content variant system
  • Implement adaptive difficulty
  • Create story branching
  • Add easter egg system

Phase 6: Polish

  • Add tab synchronization
  • Implement analytics
  • Create welcome back system
  • Add data migration

10. API REFERENCE

Quick Start

// Initialize the motel
const state = await motelState.init();
 
// Check if returning visitor
const isReturning = SessionManager.isReturningVisitor(state);
 
// Get welcome message
const welcome = WelcomeBackSystem.generateWelcomeMessage(state);
 
// Get recommended door
const recommendation = RecommendationEngine.getRecommendedDoor(state);
 
// Enter a door
motelState.updateState(s => PositionTracker.recordDoorVisit(s, 'door_07'));
 
// Complete a door
motelState.updateState(s => PositionTracker.recordDoorCompletion(s, 'door_07', ['choice_a'], 120000));
 
// Check for unlocks
const newUnlocks = PROGRESSION_RULES.unlockRules.filter(r => r.trigger(state));
 
// Check for easter eggs
const newEggs = EasterEggSystem.checkEasterEggs(state);

Appendix: Door ID Reference

door_01  - door_10  : Entry-level existential threats
door_11  - door_20  : Intermediate experiences  
door_21  - door_30  : Advanced encounters
door_31  - door_39  : Expert-level challenges
door_40            : Final door / True ending

Document Version: 1.0.0 Last Updated: 2024 THE MOTEL remembers its guests.