The game evolves alongside its players. Tracks behavior patterns (click frequency, resource spending, upgrade choices), detects player strategies (hoarder, rusher, optimizer, idle), and generates dynamic events that reward or challenge those strategies. - EmergentMechanics class: track(), detectPatterns(), generateEvent(), getState() - 6 pattern detectors: hoarder, rusher, optimizer, idle_player, clicker, balanced - 16 emergent events across all patterns with meaningful choices - localStorage persistence for cross-session behavior tracking - 25 unit tests, all passing - Hooks into writeCode, buyBuilding, doOps, and tick() - Stats panel shows emergent events count, pattern detections, active strategy - Self-contained: additive system, does not break existing mechanics
676 lines
26 KiB
JavaScript
676 lines
26 KiB
JavaScript
// ============================================================
|
|
// 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;
|
|
}
|