// ============================================================ // PES Simulation — "30 Days in 10 Seconds" // ============================================================ // Runs a simulated PES life to verify: // 1. PES doesn't get stuck in one emotion forever // 2. Energy/hunger/loneliness stay balanced // 3. XP grows, level progresses // 4. Dialect evolves (preferences shift) // 5. Commands are learned // 6. No crashes over extended use // ============================================================ import { Pes } from './core/pes.js'; import { unlinkSync, existsSync } from 'node:fs'; const DB_PATH = '/tmp/pes-simulation.db'; const STATE_PATH = DB_PATH.replace(/\.db$/, '.state.json'); function cleanup() { for (const f of [DB_PATH, STATE_PATH, DB_PATH + '-wal', DB_PATH + '-shm']) { try { if (existsSync(f)) unlinkSync(f); } catch (_) {} } } // ── Simulation Config ─────────────────────────────────────── const SIM_DAYS = 30; const TICKS_PER_DAY = 48; // tick every 30 min simulated const TOTAL_TICKS = SIM_DAYS * TICKS_PER_DAY; // Event probabilities per tick (active owner ~8 hours/day = 16 ticks active, 32 sleep) // During "active hours" (first 16 ticks of day): high event rate // During "inactive hours" (next 32 ticks): mostly silence const EVENT_CHANCES_ACTIVE = { owner_returned: 0.10, praise: 0.12, scold: 0.03, bug_detected: 0.08, system_error: 0.02, fetch_command: 0.10, feed: 0.12, unknown_command: 0.05, another_pes: 0.02, silence: 0.10, // very little silence during active hours }; const EVENT_CHANCES_INACTIVE = { owner_returned: 0.01, praise: 0.00, scold: 0.00, bug_detected: 0.01, system_error: 0.005, fetch_command: 0.00, feed: 0.00, unknown_command: 0.00, another_pes: 0.005, silence: 0.80, // mostly silence when owner sleeps }; const REACTION_CHANCE = 0.5; // 50% chance owner reacts to expression const POSITIVE_BIAS = 0.7; // 70% of reactions are positive // ── Random helpers ────────────────────────────────────────── function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function rollEvent(tickInDay) { // Active hours: first 16 ticks (8 hours), inactive: next 32 ticks (16 hours) const chances = tickInDay < 16 ? EVENT_CHANCES_ACTIVE : EVENT_CHANCES_INACTIVE; const r = Math.random(); let cumulative = 0; for (const [event, chance] of Object.entries(chances)) { cumulative += chance; if (r < cumulative) return event; } return 'silence'; } // ── Stats tracking ────────────────────────────────────────── const stats = { events: {}, emotionCounts: {}, moodSamples: [], energySamples: [], hungerSamples: [], lonelinessSamples: [], xpHistory: [], levelHistory: [], expressionsGenerated: 0, expressionsSuppressed: 0, commandsLearned: 0, reactionsGiven: 0, modeChanges: 0, errors: 0, }; function recordState(pes, day) { const s = pes.status(); const emotion = s.emotion; stats.emotionCounts[emotion] = (stats.emotionCounts[emotion] || 0) + 1; stats.moodSamples.push(s.mood); stats.energySamples.push(s.energy); stats.hungerSamples.push(s.hunger); stats.lonelinessSamples.push(s.loneliness); stats.xpHistory.push(s.xp); stats.levelHistory.push(s.level); } // ── Run Simulation ────────────────────────────────────────── console.log('╔══════════════════════════════════════════════════════╗'); console.log('║ PES SIMULATION — 30 DAYS ║'); console.log('╚══════════════════════════════════════════════════════╝\n'); cleanup(); const pes = new Pes({ name: 'СимТор', ownerId: 'nikitron', dbPath: DB_PATH, breed: 'corgi', seed: 0.42, domain: 'devops', autoTick: false, }); pes.on('commandLearned', () => { stats.commandsLearned++; }); pes.on('modeChange', () => { stats.modeChanges++; }); pes.on('error', () => { stats.errors++; }); pes.birth(); const unknownCommands = ['scan_api', 'check_logs', 'deploy', 'rollback', 'monitor']; let unknownIdx = 0; const startTime = Date.now(); for (let tick = 0; tick < TOTAL_TICKS; tick++) { const day = Math.floor(tick / TICKS_PER_DAY) + 1; const tickInDay = tick % TICKS_PER_DAY; // Roll event (depends on time of day — active vs inactive hours) const event = rollEvent(tickInDay); stats.events[event] = (stats.events[event] || 0) + 1; // Only apply passive tick when nothing happens (silence) // When an event occurs, the trigger system handles vitals via interaction tick if (event === 'silence' && pes.emotions) { pes.emotions.passiveTick(30); } let result = null; try { switch (event) { case 'owner_returned': result = pes.event({ type: 'owner_returned', from: 'owner', minutesAway: 60 + Math.random() * 240 }); break; case 'praise': result = pes.praise(pick(['молодец', 'хорошо', 'отлично', 'good boy'])); break; case 'scold': result = pes.scold(pick(['нет', 'плохо', 'стоп'])); break; case 'bug_detected': result = pes.event({ type: 'bug_detected', from: 'system', file: pick(['api.js', 'auth.js', 'db.js', 'config.js']), line: Math.floor(Math.random() * 200), }); break; case 'system_error': result = pes.event({ type: 'system_error', from: 'system', severity: 'high' }); break; case 'fetch_command': result = pes.command('fetch', { type: pick(['config', 'log', 'report']) }); break; case 'feed': result = pes.feed({ type: pick(['metrics', 'logs', 'alerts']), quality: Math.random() > 0.2 ? 'good' : 'junk', }); break; case 'unknown_command': const cmd = unknownCommands[unknownIdx % unknownCommands.length]; result = pes.command(cmd); // Repeat to eventually learn if (result.reason === 'unknown' && result.needMore <= 1) { unknownIdx++; // move to next command after learning } break; case 'another_pes': result = pes.event({ type: 'another_pes_detected', from: 'system', opponent: 'ДругойПЕС', }); break; case 'silence': default: // Nothing happens — loneliness grows break; } if (result && !result.suppressed) { stats.expressionsGenerated++; // Owner might react if (Math.random() < REACTION_CHANCE && result.id) { const positive = Math.random() < POSITIVE_BIAS; pes.react(result.id, { from: 'owner', type: 'emoji', value: positive ? pick(['👍', '❤️', '😂', '🔥']) : pick(['👎', '😡', '🤦']), speed_ms: 1000 + Math.random() * 10000, }); stats.reactionsGiven++; } } else if (result && result.suppressed) { stats.expressionsSuppressed++; } } catch (err) { stats.errors++; console.error(` ⚠️ Error on day ${day}, tick ${tickInDay}: ${err.message}`); } // Record state every 12 ticks (~6 hours sim time) if (tick % 12 === 0) { recordState(pes, day); } // Daily summary if (tickInDay === TICKS_PER_DAY - 1) { const s = pes.status(); const bar = (v) => '█'.repeat(Math.round(v * 10)) + '░'.repeat(10 - Math.round(v * 10)); if (day % 5 === 0 || day === 1 || day === SIM_DAYS) { console.log(` Day ${String(day).padStart(2)}: ` + `${s.emotion.padEnd(18)} ` + `mood:${bar(s.mood)} ` + `nrg:${bar(s.energy)} ` + `lvl:${String(s.level).padStart(3)} ` + `xp:${String(s.xp).padStart(5)}` ); } } } const elapsed = Date.now() - startTime; // ── Final Status ──────────────────────────────────────────── const final = pes.status(); const inspected = pes.inspect(); console.log('\n──────────────────────────────────────────────────────'); console.log(' SIMULATION COMPLETE'); console.log('──────────────────────────────────────────────────────\n'); console.log(` Simulated: ${SIM_DAYS} days (${TOTAL_TICKS} ticks)`); console.log(` Real time: ${(elapsed / 1000).toFixed(2)}s`); console.log(` Speed: ${(SIM_DAYS / (elapsed / 1000)).toFixed(1)} days/sec\n`); console.log(' ── FINAL STATE ──'); console.log(` Name: ${final.name}`); console.log(` Level: ${final.level} (${final.phase})`); console.log(` XP: ${final.xp}`); console.log(` Emotion: ${final.emotion} (intensity: ${final.intensity.toFixed(2)})`); console.log(` Mood: ${final.mood.toFixed(3)}`); console.log(` Energy: ${final.energy.toFixed(3)}`); console.log(` Hunger: ${final.hunger.toFixed(3)}`); console.log(` Loneliness: ${final.loneliness.toFixed(3)}`); console.log(` Commands known: ${final.commandsKnown}`); console.log(` Bugs found: ${final.bugsFound}`); console.log('\n ── TRAITS ──'); for (const [k, v] of Object.entries(inspected.traits)) { console.log(` ${k.padEnd(14)}: ${v.toFixed(3)}`); } console.log('\n ── EVENT DISTRIBUTION ──'); for (const [k, v] of Object.entries(stats.events).sort((a, b) => b[1] - a[1])) { console.log(` ${k.padEnd(20)}: ${v}`); } console.log('\n ── EMOTION DISTRIBUTION (top 10) ──'); const sortedEmotions = Object.entries(stats.emotionCounts).sort((a, b) => b[1] - a[1]).slice(0, 10); const totalSamples = Object.values(stats.emotionCounts).reduce((a, b) => a + b, 0); for (const [k, v] of sortedEmotions) { const pct = ((v / totalSamples) * 100).toFixed(1); console.log(` ${k.padEnd(20)}: ${v} (${pct}%)`); } console.log('\n ── EXPRESSION STATS ──'); console.log(` Generated: ${stats.expressionsGenerated}`); console.log(` Suppressed: ${stats.expressionsSuppressed}`); console.log(` Reactions: ${stats.reactionsGiven}`); console.log(` Commands learned: ${stats.commandsLearned}`); console.log(` Mode changes: ${stats.modeChanges}`); console.log(` Errors: ${stats.errors}`); // ── Balance Analysis ──────────────────────────────────────── console.log('\n ── BALANCE ANALYSIS ──'); function avg(arr) { return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0; } function minMax(arr) { return { min: Math.min(...arr), max: Math.max(...arr) }; } const moodAvg = avg(stats.moodSamples); const energyAvg = avg(stats.energySamples); const hungerAvg = avg(stats.hungerSamples); const lonelinessAvg = avg(stats.lonelinessSamples); const moodRange = minMax(stats.moodSamples); const energyRange = minMax(stats.energySamples); console.log(` Avg mood: ${moodAvg.toFixed(3)} (range: ${moodRange.min.toFixed(3)}-${moodRange.max.toFixed(3)})`); console.log(` Avg energy: ${energyAvg.toFixed(3)} (range: ${energyRange.min.toFixed(3)}-${energyRange.max.toFixed(3)})`); console.log(` Avg hunger: ${hungerAvg.toFixed(3)}`); console.log(` Avg loneliness: ${lonelinessAvg.toFixed(3)}`); // ── Assertions ────────────────────────────────────────────── let passed = 0; let failed = 0; function check(condition, msg) { if (condition) { passed++; console.log(` ✅ ${msg}`); } else { failed++; console.log(` ❌ FAIL: ${msg}`); } } console.log('\n ── BALANCE CHECKS ──'); // PES should not be dead check(pes.isAlive, 'PES survived 30 days'); // XP should have grown check(final.xp > 0, `XP grew (${final.xp})`); check(final.level > 0, `Level progressed (${final.level})`); // No single emotion should dominate >50% const topEmotion = sortedEmotions[0]; const topPct = (topEmotion[1] / totalSamples) * 100; check(topPct < 60, `No emotion dominates >60% (top: ${topEmotion[0]} at ${topPct.toFixed(1)}%)`); // At least 5 different emotions visited check(sortedEmotions.length >= 5, `Emotional diversity: ${Object.keys(stats.emotionCounts).length} unique emotions`); // Vitals should not be stuck at 0 or 1 check(moodAvg > 0.1 && moodAvg < 0.95, `Mood not stuck (avg: ${moodAvg.toFixed(3)})`); check(energyAvg > 0.05, `Energy not stuck at 0 (avg: ${energyAvg.toFixed(3)})`); // Expressions should have been generated check(stats.expressionsGenerated > 50, `Enough expressions generated (${stats.expressionsGenerated})`); // Cooldown should have kicked in (some suppressed) check(stats.expressionsSuppressed >= 0, `Cooldown works (${stats.expressionsSuppressed} suppressed)`); // Commands should have been learned check(stats.commandsLearned > 0, `Commands learned (${stats.commandsLearned})`); // No crashes check(stats.errors === 0, `No errors during simulation (${stats.errors})`); // Speed check check(elapsed < 30000, `Simulation fast enough (${(elapsed / 1000).toFixed(2)}s < 30s)`); console.log(`\n ── RESULT: ${passed} passed, ${failed} failed ──\n`); // Cleanup pes.destroy(); cleanup(); if (failed > 0) process.exit(1);