Files
the-beacon/tests/emergent-mechanics.test.cjs

392 lines
13 KiB
JavaScript
Raw Normal View History

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);
});