const test = require('node:test'); const assert = require('node:assert/strict'); const { EmergentMechanics } = require('../js/emergent-mechanics.js'); // Minimal localStorage mock function createStorage() { const store = new Map(); return { getItem: (k) => store.has(k) ? store.get(k) : null, setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k), clear: () => store.clear() }; } // Fresh storage per test function freshSetup() { global.localStorage = createStorage(); } test('constructor initializes with zero patterns', () => { freshSetup(); const em = new EmergentMechanics(); assert.deepEqual(em.patterns, { hoarder: 0, rusher: 0, optimizer: 0, idle_player: 0, clicker: 0, balanced: 0 }); assert.equal(em.actions.length, 0); assert.equal(em.activeEvents.length, 0); }); test('track records actions into the buffer', () => { freshSetup(); const em = new EmergentMechanics(); em.track('click'); em.track('buy_building', { buildingId: 'autocoder' }); em.track('ops_convert', { resource: 'code' }); assert.equal(em.actions.length, 3); assert.equal(em.actions[0].action, 'click'); assert.equal(em.actions[1].data.buildingId, 'autocoder'); assert.equal(em.clickTimestamps.length, 1); assert.equal(em.upgradeChoices.length, 1); }); test('track records resource deltas', () => { freshSetup(); const em = new EmergentMechanics(); em.track('click', { resource: 'code', delta: 10 }); em.track('buy_building', { resource: 'code', delta: -100, buildingId: 'server' }); assert.equal(em.resourceDeltas.length, 2); assert.equal(em.resourceDeltas[0].delta, 10); assert.equal(em.resourceDeltas[1].delta, -100); }); test('trackResourceSnapshot stores game state', () => { freshSetup(); const em = new EmergentMechanics(); const g = { code: 1000, compute: 50, knowledge: 200, users: 10, impact: 5, ops: 8, trust: 12, harmony: 55, phase: 2, totalClicks: 100, playTime: 300, buildings: { autocoder: 5, server: 2 } }; em.trackResourceSnapshot(g); assert.ok(em._lastSnapshot); assert.equal(em._lastSnapshot.code, 1000); assert.equal(em._lastSnapshot.phase, 2); assert.equal(em._lastSnapshot.buildings.autocoder, 5); }); test('detectPatterns returns pattern scores', () => { freshSetup(); const em = new EmergentMechanics(); // Provide a snapshot em.trackResourceSnapshot({ code: 100, compute: 10, knowledge: 10, users: 0, impact: 0, ops: 5, trust: 5, harmony: 50, phase: 1, totalClicks: 10, playTime: 60, buildings: { autocoder: 1 } }); const patterns = em.detectPatterns(); assert.ok(typeof patterns === 'object'); assert.ok('hoarder' in patterns); assert.ok('rusher' in patterns); assert.ok('optimizer' in patterns); assert.ok('idle_player' in patterns); assert.ok('clicker' in patterns); assert.ok('balanced' in patterns); }); test('hoarder pattern detects resource accumulation without spending', () => { freshSetup(); const em = new EmergentMechanics(); // Simulate accumulating resources over time (no purchases) for (let i = 0; i < 30; i++) { em.resourceDeltas.push({ resource: 'code', delta: 100, time: Date.now() }); } em.trackResourceSnapshot({ code: 20000, compute: 100, knowledge: 50, users: 0, impact: 0, ops: 5, trust: 5, harmony: 50, phase: 1, totalClicks: 10, playTime: 120, buildings: { autocoder: 1 } }); const patterns = em.detectPatterns(); assert.ok(patterns.hoarder > 0, 'Hoarder pattern should be detected'); }); test('clicker pattern detects high click frequency', () => { freshSetup(); const em = new EmergentMechanics(); const now = Date.now(); // Simulate rapid clicking (50 clicks in last 30 seconds) for (let i = 0; i < 50; i++) { em.clickTimestamps.push(now - (30 - i) * 600); // spread over 30 seconds } em.trackResourceSnapshot({ code: 100, compute: 10, knowledge: 10, users: 0, impact: 0, ops: 5, trust: 5, harmony: 50, phase: 1, totalClicks: 100, playTime: 60, buildings: { autocoder: 1 } }); const patterns = em.detectPatterns(); assert.ok(patterns.clicker > 0, 'Clicker pattern should be detected'); }); test('balanced pattern detects spread of buildings', () => { freshSetup(); const em = new EmergentMechanics(); em.trackResourceSnapshot({ code: 500, compute: 200, knowledge: 300, users: 100, impact: 50, ops: 10, trust: 15, harmony: 50, phase: 3, totalClicks: 200, playTime: 600, buildings: { autocoder: 5, server: 4, dataset: 3, trainer: 4, linter: 5 } }); const patterns = em.detectPatterns(); assert.ok(patterns.balanced > 0, 'Balanced pattern should be detected'); }); test('generateEvent returns null before cooldown expires', () => { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = Date.now(); // just set const event = em.generateEvent(); assert.equal(event, null); }); test('generateEvent returns null when no pattern is strong enough', () => { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = 0; // cooldown expired em.patterns = { hoarder: 0.1, rusher: 0.05, optimizer: 0.02, idle_player: 0, clicker: 0, balanced: 0.1 }; const event = em.generateEvent(); assert.equal(event, null); }); test('generateEvent returns a valid event when pattern is strong', () => { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = 0; // cooldown expired em.patterns.hoarder = 0.8; em.actions = new Array(30).fill({ action: 'click', data: {}, time: Date.now() }); const event = em.generateEvent(); assert.ok(event, 'Should generate an event'); assert.ok(event.id, 'Event should have an id'); assert.ok(event.title, 'Event should have a title'); assert.ok(event.desc, 'Event should have a description'); assert.equal(event.pattern, 'hoarder'); assert.ok(Array.isArray(event.choices), 'Event should have choices'); assert.ok(event.choices.length >= 2, 'Event should have at least 2 choices'); }); test('generateEvent adds to activeEvents and eventHistory', () => { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = 0; em.patterns.rusher = 0.9; em.actions = new Array(30).fill({ action: 'buy_building', data: {}, time: Date.now() }); const event = em.generateEvent(); assert.ok(event); assert.equal(em.activeEvents.length, 1); assert.equal(em.eventHistory.length, 1); assert.equal(em.totalEventsGenerated, 1); }); test('resolveEvent returns effect and removes from active', () => { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = 0; em.patterns.hoarder = 0.9; em.actions = new Array(30).fill({ action: 'click', data: {}, time: Date.now() }); const event = em.generateEvent(); assert.ok(event); const result = em.resolveEvent(event.id, 0); assert.ok(result); assert.ok(result.effect); assert.equal(result.eventId, event.id); assert.equal(em.activeEvents.length, 0); }); test('resolveEvent returns null for unknown event', () => { freshSetup(); const em = new EmergentMechanics(); const result = em.resolveEvent('nonexistent', 0); assert.equal(result, null); }); test('getState returns comprehensive state', () => { freshSetup(); const em = new EmergentMechanics(); em.track('click'); em.trackResourceSnapshot({ code: 100, compute: 10, knowledge: 10, users: 0, impact: 0, ops: 5, trust: 5, harmony: 50, phase: 1, totalClicks: 10, playTime: 60, buildings: { autocoder: 1 } }); const state = em.getState(); assert.ok(state.patterns); assert.ok(Array.isArray(state.activeEvents)); assert.equal(typeof state.totalPatternsDetected, 'number'); assert.equal(typeof state.totalEventsGenerated, 'number'); assert.equal(state.actionsTracked, 1); }); test('reset clears all state', () => { freshSetup(); const em = new EmergentMechanics(); em.track('click'); em.patterns.hoarder = 0.5; em.totalPatternsDetected = 3; em.reset(); assert.equal(em.actions.length, 0); assert.equal(em.patterns.hoarder, 0); assert.equal(em.totalPatternsDetected, 0); assert.equal(em.activeEvents.length, 0); }); test('track trims action buffer to 500', () => { freshSetup(); const em = new EmergentMechanics(); for (let i = 0; i < 600; i++) { em.track('click'); } assert.ok(em.actions.length <= 500, `Actions trimmed to ${em.actions.length}`); }); test('track trims clickTimestamps to 100', () => { freshSetup(); const em = new EmergentMechanics(); for (let i = 0; i < 150; i++) { em.track('click'); } assert.ok(em.clickTimestamps.length <= 100); }); test('track trims upgradeChoices to 100', () => { freshSetup(); const em = new EmergentMechanics(); for (let i = 0; i < 150; i++) { em.track('buy_building', { buildingId: 'autocoder' }); } assert.ok(em.upgradeChoices.length <= 100); }); test('event history is trimmed to 50', () => { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = 0; em.patterns.hoarder = 0.9; em.actions = new Array(30).fill({ action: 'click', data: {}, time: Date.now() }); for (let i = 0; i < 60; i++) { em.lastEventTime = 0; em.generateEvent(); } assert.ok(em.eventHistory.length <= 50); }); test('events from all patterns can be generated', () => { const patterns = ['hoarder', 'rusher', 'optimizer', 'idle_player', 'clicker', 'balanced']; for (const pattern of patterns) { freshSetup(); const em = new EmergentMechanics(); em.lastEventTime = 0; // Set pattern directly and prevent auto-detection from modifying it em.patterns[pattern] = 0.9; em.lastPatternCheck = Date.now() + 99999; // prevent detectPatterns auto-trigger em.actions = new Array(30).fill({ action: 'click', data: {}, time: Date.now() }); const event = em.generateEvent(); assert.ok(event, `Should generate event for pattern: ${pattern}`); assert.equal(event.pattern, pattern, `Event pattern should match for ${pattern}`); } }); test('idle_player pattern detection', () => { freshSetup(); const em = new EmergentMechanics(); const oldTime = Date.now() - 600000; // 10 minutes ago // Simulate old actions with no recent activity for (let i = 0; i < 15; i++) { em.actions.push({ action: 'click', data: {}, time: oldTime + i * 1000 }); } em.clickTimestamps = []; // no recent clicks em.lastActionTime = oldTime; // last action was 10 min ago em.trackResourceSnapshot({ code: 100, compute: 10, knowledge: 10, users: 0, impact: 0, ops: 5, trust: 5, harmony: 50, phase: 1, totalClicks: 15, playTime: 300, buildings: { autocoder: 2 } }); const patterns = em.detectPatterns(); assert.ok(patterns.idle_player > 0, `Idle player pattern should be detected, got ${patterns.idle_player}`); }); test('rusher pattern detection from rapid purchases', () => { freshSetup(); const em = new EmergentMechanics(); const now = Date.now(); // Simulate rapid building purchases for (let i = 0; i < 8; i++) { em.upgradeChoices.push({ buildingId: 'autocoder', time: now - i * 5000 }); } em.resourceDeltas.push({ resource: 'code', delta: -2000, time: now - 1000 }); em.trackResourceSnapshot({ code: 50, compute: 10, knowledge: 10, users: 0, impact: 0, ops: 5, trust: 5, harmony: 50, phase: 1, totalClicks: 50, playTime: 120, buildings: { autocoder: 10 } }); const patterns = em.detectPatterns(); assert.ok(patterns.rusher > 0, 'Rusher pattern should be detected'); }); test('optimizer pattern from consistent click timing', () => { freshSetup(); const em = new EmergentMechanics(); const now = Date.now(); // Simulate very consistent click intervals (every 300ms) for (let i = 0; i < 30; i++) { em.clickTimestamps.push(now - (30 - i) * 300); } em.trackResourceSnapshot({ code: 500, compute: 50, knowledge: 100, users: 0, impact: 0, ops: 10, trust: 5, harmony: 50, phase: 1, totalClicks: 100, playTime: 120, buildings: { autocoder: 3, linter: 2 } }); const patterns = em.detectPatterns(); assert.ok(patterns.optimizer > 0, 'Optimizer pattern should be detected'); }); test('save and load preserves state', () => { freshSetup(); const em1 = new EmergentMechanics(); em1.patterns.hoarder = 0.7; em1.totalPatternsDetected = 5; em1.totalEventsGenerated = 3; em1.track('click'); em1._save(); const em2 = new EmergentMechanics(); assert.equal(em2.patterns.hoarder, 0.7); assert.equal(em2.totalPatternsDetected, 5); assert.equal(em2.totalEventsGenerated, 3); assert.ok(em2.actions.length >= 1); });