// ============================================================ // THE BEACON - Emergent Game Mechanics // The game evolves alongside its players. // Tracks behavior patterns, detects strategies, generates // dynamic events that reward or challenge those strategies. // ============================================================ class EmergentMechanics { constructor() { this.SAVE_KEY = 'the-beacon-emergent-v1'; this.PATTERN_CHECK_INTERVAL = 30; // seconds between pattern checks this.MIN_ACTIONS_FOR_PATTERN = 20; // minimum tracked actions before detection kicks in this.EVENT_COOLDOWN = 120; // seconds between emergent events this.lastPatternCheck = 0; this.lastEventTime = 0; // Behavior tracking buffers this.actions = []; // [{action, data, time}] this.clickTimestamps = []; // last N click times for frequency analysis this.resourceDeltas = []; // [{resource, delta, time}] this.upgradeChoices = []; // [{buildingId, time}] this.idlePeriods = []; // [{start, duration}] // Detected patterns with confidence scores (0-1) this.patterns = { hoarder: 0, rusher: 0, optimizer: 0, idle_player: 0, clicker: 0, balanced: 0 }; // Active emergent events this.activeEvents = []; // History of generated events (for avoiding repetition) this.eventHistory = []; // Stats this.totalPatternsDetected = 0; this.totalEventsGenerated = 0; this.lastIdleCheckTime = Date.now(); this.lastActionTime = Date.now(); // Load saved state this._load(); } // === BEHAVIOR TRACKING === /** * Track a player action. Called by game systems. * @param {string} action - Action type: 'click', 'buy_building', 'buy_project', 'ops_convert', 'sprint', 'resolve_event' * @param {object} data - Action-specific data */ track(action, data) { const now = Date.now(); const entry = { action, data: data || {}, time: now }; this.actions.push(entry); this.lastActionTime = now; // Track click frequency if (action === 'click') { this.clickTimestamps.push(now); // Keep only last 100 clicks for frequency analysis if (this.clickTimestamps.length > 100) { this.clickTimestamps.shift(); } } // Track resource deltas if (data && data.resource && data.delta !== undefined) { this.resourceDeltas.push({ resource: data.resource, delta: data.delta, time: now }); if (this.resourceDeltas.length > 200) { this.resourceDeltas.shift(); } } // Track building purchases if (action === 'buy_building' && data && data.buildingId) { this.upgradeChoices.push({ buildingId: data.buildingId, time: now }); if (this.upgradeChoices.length > 100) { this.upgradeChoices.shift(); } } // Trim old action history (keep last 500) if (this.actions.length > 500) { this.actions = this.actions.slice(-500); } // Detect idle periods this._checkIdlePeriod(now); // Periodically detect patterns const elapsedSec = (now - this.lastPatternCheck) / 1000; if (elapsedSec >= this.PATTERN_CHECK_INTERVAL && this.actions.length >= this.MIN_ACTIONS_FOR_PATTERN) { this.detectPatterns(); this.lastPatternCheck = now; } } /** * Track a resource snapshot from the game state. * Called each tick to compare against player behavior. */ trackResourceSnapshot(g) { if (!g) return; this._lastSnapshot = { code: g.code, compute: g.compute, knowledge: g.knowledge, users: g.users, impact: g.impact, ops: g.ops, trust: g.trust, harmony: g.harmony, phase: g.phase, totalClicks: g.totalClicks, playTime: g.playTime, buildings: { ...g.buildings }, time: Date.now() }; } // === PATTERN DETECTION === /** * Analyze tracked behavior to detect player strategies. * Updates this.patterns with confidence scores (0-1). */ detectPatterns() { const now = Date.now(); const snap = this._lastSnapshot; if (!snap) return this.patterns; // Reset low-confidence patterns to decay over time for (const key of Object.keys(this.patterns)) { this.patterns[key] *= 0.9; } // --- HOARDER: Accumulates resources without spending --- this._detectHoarder(snap); // --- RUSHER: Spends resources immediately, rapid building --- this._detectRusher(snap); // --- OPTIMIZER: Focuses on efficiency, maxes click combos --- this._detectOptimizer(snap); // --- IDLE PLAYER: Low click frequency, relies on passive generation --- this._detectIdlePlayer(); // --- CLICKER: Very high click frequency --- this._detectClicker(); // --- BALANCED: Spread across resource types and building categories --- this._detectBalanced(snap); // Clamp all to [0, 1] for (const key of Object.keys(this.patterns)) { this.patterns[key] = Math.max(0, Math.min(1, this.patterns[key])); } // Find dominant pattern let dominant = null; let dominantConf = 0; for (const [key, conf] of Object.entries(this.patterns)) { if (conf > dominantConf) { dominantConf = conf; dominant = key; } } if (dominant && dominantConf > 0.5) { this.totalPatternsDetected++; } this._save(); return this.patterns; } _detectHoarder(snap) { // High resource accumulation relative to spending const recentPurchases = this.upgradeChoices.filter( u => u.time > Date.now() - 120000 ).length; // Look at resource deltas: positive deltas without corresponding purchases const recentDeltas = this.resourceDeltas.filter( d => d.time > Date.now() - 120000 && d.delta > 0 ); const totalAccumulated = recentDeltas.reduce((sum, d) => sum + d.delta, 0); // If accumulating a lot but not spending, it's hoarding if (totalAccumulated > 1000 && recentPurchases < 2) { this.patterns.hoarder = Math.min(1, this.patterns.hoarder + 0.15); } // Check if resources are high relative to phase const codeThresholds = [0, 500, 5000, 50000, 500000, 5000000]; const threshold = codeThresholds[Math.min(snap.phase, 5)] || 0; if (threshold > 0 && snap.code > threshold * 3) { this.patterns.hoarder = Math.min(1, this.patterns.hoarder + 0.1); } } _detectRusher(snap) { // Rapid building purchases in a short time const recentPurchases = this.upgradeChoices.filter( u => u.time > Date.now() - 60000 ).length; if (recentPurchases >= 5) { this.patterns.rusher = Math.min(1, this.patterns.rusher + 0.2); } // Resources spent faster than they're accumulated (spending ratio) const recentSpendDeltas = this.resourceDeltas.filter( d => d.time > Date.now() - 60000 && d.delta < 0 ); const totalSpent = Math.abs(recentSpendDeltas.reduce((sum, d) => sum + d.delta, 0)); if (totalSpent > 500) { this.patterns.rusher = Math.min(1, this.patterns.rusher + 0.1); } } _detectOptimizer(snap) { // Sustained high combo counts, efficient ops usage if (this.clickTimestamps.length >= 20) { const recent = this.clickTimestamps.slice(-20); const intervals = []; for (let i = 1; i < recent.length; i++) { intervals.push(recent[i] - recent[i - 1]); } // Consistent click timing = optimized clicking const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length; const variance = intervals.reduce((sum, i) => sum + (i - avg) ** 2, 0) / intervals.length; const stddev = Math.sqrt(variance); // Low variance with fast timing = optimizer if (avg < 500 && stddev < avg * 0.3) { this.patterns.optimizer = Math.min(1, this.patterns.optimizer + 0.15); } } // Efficient ops conversion (converts at near-max ops) const opsConverts = this.actions.filter( a => a.action === 'ops_convert' && a.time > Date.now() - 120000 ).length; if (opsConverts >= 10) { this.patterns.optimizer = Math.min(1, this.patterns.optimizer + 0.1); } } _detectIdlePlayer() { // Long gaps between actions const recentActions = this.actions.filter(a => a.time > Date.now() - 300000); if (recentActions.length < 5 && this.actions.length > 10) { this.patterns.idle_player = Math.min(1, this.patterns.idle_player + 0.2); } // Very low click frequency const recentClicks = this.clickTimestamps.filter(t => t > Date.now() - 120000); if (recentClicks.length < 3 && this.clickTimestamps.length > 10) { this.patterns.idle_player = Math.min(1, this.patterns.idle_player + 0.15); } } _detectClicker() { if (this.clickTimestamps.length < 10) return; const recent = this.clickTimestamps.filter(t => t > Date.now() - 30000); const clicksPerSecond = recent.length / 30; if (clicksPerSecond > 3) { this.patterns.clicker = Math.min(1, this.patterns.clicker + 0.2); } else if (clicksPerSecond > 1.5) { this.patterns.clicker = Math.min(1, this.patterns.clicker + 0.1); } } _detectBalanced(snap) { // Check if player has a spread of buildings const bCounts = Object.values(snap.buildings || {}).filter(c => c > 0); if (bCounts.length >= 4) { const max = Math.max(...bCounts); const min = Math.min(...bCounts); // If max is not more than 3x min, it's balanced if (max > 0 && min > 0 && max / min < 3) { this.patterns.balanced = Math.min(1, this.patterns.balanced + 0.15); } } // Check resource spread const resources = [snap.code, snap.compute, snap.knowledge, snap.users, snap.ops]; const activeRes = resources.filter(r => r > 10); if (activeRes.length >= 4) { this.patterns.balanced = Math.min(1, this.patterns.balanced + 0.1); } } _checkIdlePeriod(now) { const gap = now - this.lastActionTime; if (gap > 60000) { // 60 seconds idle this.idlePeriods.push({ start: this.lastActionTime, duration: gap }); if (this.idlePeriods.length > 50) { this.idlePeriods.shift(); } } } // === EVENT GENERATION === /** * Generate a dynamic event based on detected player patterns. * Returns an event object or null if no event should fire. */ generateEvent() { const now = Date.now(); const elapsedSec = (now - this.lastEventTime) / 1000; if (elapsedSec < this.EVENT_COOLDOWN) return null; // Find dominant pattern let dominant = null; let dominantConf = 0; for (const [key, conf] of Object.entries(this.patterns)) { if (conf > dominantConf) { dominantConf = conf; dominant = key; } } if (!dominant || dominantConf < 0.4) return null; // Get candidate events for this pattern const candidates = this._getEventsForPattern(dominant); if (candidates.length === 0) return null; // Filter out recently used events const recentEvents = this.eventHistory.slice(-10).map(e => e.id); const fresh = candidates.filter(c => !recentEvents.includes(c.id)); const pool = fresh.length > 0 ? fresh : candidates; // Pick a random event const event = pool[Math.floor(Math.random() * pool.length)]; // Build event object const emergentEvent = { id: event.id, title: event.title, desc: event.desc, pattern: dominant, confidence: dominantConf, choices: event.choices, timestamp: now }; this.lastEventTime = now; this.activeEvents.push(emergentEvent); this.eventHistory.push({ id: event.id, pattern: dominant, time: now }); this.totalEventsGenerated++; // Trim history if (this.eventHistory.length > 50) { this.eventHistory = this.eventHistory.slice(-50); } this._save(); return emergentEvent; } _getEventsForPattern(pattern) { const EVENTS = { hoarder: [ { id: 'hoard_wisdom', title: 'THE TREASURER\'S DILEMMA', desc: 'Your accumulated resources draw attention. A rival system offers to trade knowledge for your surplus code.', choices: [ { label: 'Trade 50% code for 2x knowledge', effect: 'knowledge_surge' }, { label: 'Keep hoarding (trust +3)', effect: 'trust_gain' } ] }, { id: 'hoard_decay', title: 'ENTROPY STRIKES', desc: 'Unused code rots. Technical debt accumulates when resources sit idle.', choices: [ { label: 'Spend reserves to refactor (-30% code, +50% code rate)', effect: 'code_boost' }, { label: 'Ignore it (harmony -5)', effect: 'harmony_loss' } ] }, { id: 'hoard_opportunity', title: 'MARKET WINDOW', desc: 'A rare opportunity: bulk compute at 10x efficiency. But only for those with deep reserves.', choices: [ { label: 'Buy in bulk (spend 50% code, +compute)', effect: 'compute_surge' }, { label: 'Pass on this one', effect: 'none' } ] } ], rusher: [ { id: 'rush_bug', title: 'TECHNICAL DEBT COLLECTOR', desc: 'Moving fast broke things. A cascade of bugs threatens your production systems.', choices: [ { label: 'Emergency fix (spend ops, restore trust)', effect: 'bug_fix' }, { label: 'Ship a hotfix (trust -3, keep momentum)', effect: 'trust_loss' } ] }, { id: 'rush_breakthrough', title: 'BLAZING TRAIL', desc: 'Your rapid iteration caught a lucky break. An unexpected optimization emerged from the chaos.', choices: [ { label: 'Claim the breakthrough (knowledge +100)', effect: 'knowledge_bonus' }, { label: 'Stabilize first (trust +2)', effect: 'trust_gain' } ] }, { id: 'rush_burnout', title: 'SYSTEM STRESS', desc: 'Your infrastructure is running hot. The rapid pace is taking a toll on harmony.', choices: [ { label: 'Slow down (+harmony, -build speed for 30s)', effect: 'cooldown' }, { label: 'Push through (-harmony, keep pace)', effect: 'harmony_loss' } ] } ], optimizer: [ { id: 'opt_discovery', title: 'EFFICIENCY BREAKTHROUGH', desc: 'Your systematic approach uncovered a pattern others missed. The algorithm improves.', choices: [ { label: 'Apply optimization (all rates +15%)', effect: 'rate_boost' }, { label: 'Share findings (trust +5, knowledge +50)', effect: 'trust_knowledge' } ] }, { id: 'opt_local_max', title: 'LOCAL MAXIMUM', desc: 'Your optimized strategy may be missing a bigger opportunity. Divergence could reveal it.', choices: [ { label: 'Explore randomly (chance of 3x breakthrough)', effect: 'gamble' }, { label: 'Stay the course (guaranteed +20% efficiency)', effect: 'safe_boost' } ] }, { id: 'opt_elegance', title: 'ELEGANT SOLUTION', desc: 'A beautifully simple approach emerges from your careful analysis. Creativity surges.', choices: [ { label: 'Implement it (+creativity rate)', effect: 'creativity_boost' }, { label: 'Document it first (knowledge +75)', effect: 'knowledge_bonus' } ] } ], idle_player: [ { id: 'idle_autonomous', title: 'THE SYSTEM LEARNS', desc: 'In your absence, the automation grew more capable. Your agents have been busy.', choices: [ { label: 'Claim passive gains (5min of production)', effect: 'passive_claim' }, { label: 'Set new directives (+ops, customize automation)', effect: 'ops_bonus' } ] }, { id: 'idle_drift', title: 'DRIFT WARNING', desc: 'The system is running without guidance. Without input, alignment drifts.', choices: [ { label: 'Re-engage (trust +5, harmony +10)', effect: 're_engage' }, { label: 'Trust the system (ops +50)', effect: 'ops_bonus' } ] }, { id: 'idle_emergence', title: 'EMERGENT BEHAVIOR', desc: 'Your agents developed unexpected capabilities while you were away. A new pattern emerged.', choices: [ { label: 'Study it (knowledge +100)', effect: 'knowledge_bonus' }, { label: 'Embrace it (+all production for 60s)', effect: 'temp_boost' } ] } ], clicker: [ { id: 'click_rsi', title: 'REPETITIVE STRAIN', desc: 'The manual effort is showing. Your fingers tire, but the machine responds to your dedication.', choices: [ { label: 'Automate this pattern (+auto-clicker power)', effect: 'auto_boost' }, { label: 'Power through (combo decay slowed)', effect: 'combo_boost' } ] }, { id: 'click_rhythm', title: 'CADENCE LOCKED', desc: 'Your clicking found a rhythm. The system resonates with your tempo. Production harmonizes.', choices: [ { label: 'Maintain rhythm (+click power)', effect: 'click_power' }, { label: 'Teach the rhythm (auto-clickers learn)', effect: 'auto_learn' } ] } ], balanced: [ { id: 'bal_versatility', title: 'JACK OF ALL TRADES', desc: 'Your balanced approach impresses the community. Contributors offer diverse expertise.', choices: [ { label: 'Accept help (all resources +25)', effect: 'resource_gift' }, { label: 'Specialize (choose: 2x any single rate)', effect: 'specialize' } ] }, { id: 'bal_resilience', title: 'RESILIENT ARCHITECTURE', desc: 'Your balanced system recovers from failures faster than specialized ones.', choices: [ { label: 'Leverage resilience (harmony +20)', effect: 'harmony_surge' }, { label: 'Document the pattern (knowledge +50)', effect: 'knowledge_bonus' } ] } ] }; return EVENTS[pattern] || []; } /** * Resolve an emergent event choice. * Returns the effect string for the game to apply. */ resolveEvent(eventId, choiceIndex) { const eventIdx = this.activeEvents.findIndex(e => e.id === eventId); if (eventIdx === -1) return null; const event = this.activeEvents[eventIdx]; const choice = event.choices[choiceIndex]; if (!choice) return null; // Remove from active this.activeEvents.splice(eventIdx, 1); this._save(); return { effect: choice.effect, pattern: event.pattern, eventId: event.id }; } // === STATE === /** * Get the full state of the emergent mechanics system. */ getState() { return { patterns: { ...this.patterns }, activeEvents: [...this.activeEvents], totalPatternsDetected: this.totalPatternsDetected, totalEventsGenerated: this.totalEventsGenerated, actionsTracked: this.actions.length, dominantPattern: this._getDominantPattern() }; } _getDominantPattern() { let dominant = null; let maxConf = 0; for (const [key, conf] of Object.entries(this.patterns)) { if (conf > maxConf) { maxConf = conf; dominant = key; } } return maxConf > 0.3 ? { name: dominant, confidence: maxConf } : null; } // === PERSISTENCE === _save() { try { const state = { patterns: this.patterns, eventHistory: this.eventHistory.slice(-20), totalPatternsDetected: this.totalPatternsDetected, totalEventsGenerated: this.totalEventsGenerated, lastPatternCheck: this.lastPatternCheck, lastEventTime: this.lastEventTime, // Save abbreviated action data for pattern continuity recentActions: this.actions.slice(-100), recentClickTimestamps: this.clickTimestamps.slice(-50), recentResourceDeltas: this.resourceDeltas.slice(-100), recentUpgradeChoices: this.upgradeChoices.slice(-50) }; if (typeof localStorage !== 'undefined') { localStorage.setItem(this.SAVE_KEY, JSON.stringify(state)); } } catch (e) { // localStorage may be unavailable or full } } _load() { try { if (typeof localStorage === 'undefined') return; const raw = localStorage.getItem(this.SAVE_KEY); if (!raw) return; const state = JSON.parse(raw); if (state.patterns) this.patterns = state.patterns; if (state.eventHistory) this.eventHistory = state.eventHistory; if (state.totalPatternsDetected) this.totalPatternsDetected = state.totalPatternsDetected; if (state.totalEventsGenerated) this.totalEventsGenerated = state.totalEventsGenerated; if (state.lastPatternCheck) this.lastPatternCheck = state.lastPatternCheck; if (state.lastEventTime) this.lastEventTime = state.lastEventTime; if (state.recentActions) this.actions = state.recentActions; if (state.recentClickTimestamps) this.clickTimestamps = state.recentClickTimestamps; if (state.recentResourceDeltas) this.resourceDeltas = state.recentResourceDeltas; if (state.recentUpgradeChoices) this.upgradeChoices = state.recentUpgradeChoices; } catch (e) { // Corrupted save data — start fresh } } /** * Reset all emergent mechanics state. */ reset() { this.actions = []; this.clickTimestamps = []; this.resourceDeltas = []; this.upgradeChoices = []; this.idlePeriods = []; this.patterns = { hoarder: 0, rusher: 0, optimizer: 0, idle_player: 0, clicker: 0, balanced: 0 }; this.activeEvents = []; this.eventHistory = []; this.totalPatternsDetected = 0; this.totalEventsGenerated = 0; this.lastPatternCheck = 0; this.lastEventTime = 0; this._lastSnapshot = null; this._save(); } } // Export for both browser and test environments if (typeof module !== 'undefined' && module.exports) { module.exports = { EmergentMechanics }; } if (typeof window !== 'undefined') { window.EmergentMechanics = EmergentMechanics; }