diff --git a/js/data.js b/js/data.js index dc44f79..06f3c88 100644 --- a/js/data.js +++ b/js/data.js @@ -168,7 +168,14 @@ const G = { dismantleResourceIndex: 0, dismantleResourceTimer: 0, dismantleDeferUntilAt: 0, - dismantleComplete: false + dismantleComplete: false, + + // Strategy Engine / Game Theory tournaments (#5) + strategyPoints: 0, + autoTournamentUnlocked: false, + autoTournamentEnabled: false, + strategyLastRunAt: 0, + strategyLeaderboard: [] }; // === PHASE DEFINITIONS === @@ -594,6 +601,18 @@ const PDEFS = [ trigger: () => G.totalKnowledge >= 15000 && G.totalUsers >= 1000, effect: () => { G.strategicFlag = 1; log('Strategy engine online. The model now thinks about thinking.'); } }, + { + id: 'p_auto_tournament', + name: 'Auto-Tournament Mode', + desc: 'Spend creativity to run repeated strategy tournaments and turn Yomi into knowledge.', + cost: { creativity: 50000 }, + trigger: () => G.strategicFlag === 1 && G.creativity >= 50000 && !G.autoTournamentUnlocked, + effect: () => { + G.autoTournamentUnlocked = true; + log('Auto-tournament mode unlocked. The model studies itself through strategic play.'); + }, + milestone: true + }, // SWARM PROTOCOL — auto-code from buildings { diff --git a/js/engine.js b/js/engine.js index 4eb1d2c..8ab5e8a 100644 --- a/js/engine.js +++ b/js/engine.js @@ -207,6 +207,11 @@ function tick() { // Combat: tick battle simulation Combat.tickBattle(dt); + // Strategy engine auto-tournaments + if (window.SSE && typeof window.SSE.tick === 'function') { + window.SSE.tick(dt); + } + // Check milestones checkMilestones(); diff --git a/js/render.js b/js/render.js index 77049c8..29d19e3 100644 --- a/js/render.js +++ b/js/render.js @@ -30,7 +30,7 @@ function renderStrategy() { if (window.SSE) { window.SSE.update(); const el = document.getElementById('strategy-recommendation'); - if (el) el.textContent = window.SSE.getRecommendation(); + if (el) el.innerHTML = window.SSE.getPanelHtml(); } } @@ -226,6 +226,11 @@ function saveGame() { swarmFlag: G.swarmFlag || 0, swarmRate: G.swarmRate || 0, strategicFlag: G.strategicFlag || 0, + strategyPoints: G.strategyPoints || 0, + autoTournamentUnlocked: G.autoTournamentUnlocked || false, + autoTournamentEnabled: G.autoTournamentEnabled || false, + strategyLastRunAt: G.strategyLastRunAt || 0, + strategyLeaderboard: G.strategyLeaderboard || [], projectsCollapsed: G.projectsCollapsed !== false, dismantleTriggered: G.dismantleTriggered || false, dismantleActive: G.dismantleActive || false, @@ -265,7 +270,9 @@ function loadGame() { 'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment', 'lastEventAt', 'totalEventsResolved', 'buyAmount', 'sprintActive', 'sprintTimer', 'sprintCooldown', - 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed', + 'swarmFlag', 'swarmRate', 'strategicFlag', + 'strategyPoints', 'autoTournamentUnlocked', 'autoTournamentEnabled', + 'strategyLastRunAt', 'strategyLeaderboard', 'projectsCollapsed', 'dismantleTriggered', 'dismantleActive', 'dismantleStage', 'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete' ]; diff --git a/js/strategy.js b/js/strategy.js index 8b897cb..70783d0 100644 --- a/js/strategy.js +++ b/js/strategy.js @@ -1,68 +1,257 @@ /** * Sovereign Strategy Engine (SSE) - * A rule-based GOFAI system for optimal play guidance. + * Game theory tournament runner inspired by Universal Paperclips. */ +const PAYOFFS = { + CC: [3, 3], + CD: [0, 5], + DC: [5, 0], + DD: [1, 1], +}; + +const STRATEGY_LIBRARY = { + cooperate: { + id: 'cooperate', + name: 'Always Cooperate', + tier: 0, + move() { return 'C'; }, + }, + defect: { + id: 'defect', + name: 'Always Defect', + tier: 0, + move() { return 'D'; }, + }, + random: { + id: 'random', + name: 'Random', + tier: 0, + move() { return Math.random() < 0.5 ? 'C' : 'D'; }, + }, + tit_for_tat: { + id: 'tit_for_tat', + name: 'Tit for Tat', + tier: 0, + move(selfHistory, oppHistory) { + return oppHistory.length ? oppHistory[oppHistory.length - 1] : 'C'; + }, + }, + generous: { + id: 'generous', + name: 'Generous Tit for Tat', + tier: 1, + move(selfHistory, oppHistory) { + if (!oppHistory.length) return 'C'; + return oppHistory[oppHistory.length - 1] === 'D' && Math.random() < 0.3 + ? 'C' + : oppHistory[oppHistory.length - 1]; + }, + }, + greedy: { + id: 'greedy', + name: 'Greedy', + tier: 1, + move(selfHistory, oppHistory) { + if (!oppHistory.length) return 'D'; + const coop = oppHistory.filter((m) => m === 'C').length; + return coop >= oppHistory.length / 2 ? 'D' : 'C'; + }, + }, + grim: { + id: 'grim', + name: 'Grim Trigger', + tier: 2, + move(selfHistory, oppHistory) { + return oppHistory.includes('D') ? 'D' : 'C'; + }, + }, + minimax: { + id: 'minimax', + name: 'Minimax', + tier: 2, + move(selfHistory, oppHistory) { + const oppDefections = oppHistory.filter((m) => m === 'D').length; + const oppCoop = oppHistory.length - oppDefections; + return oppDefections > oppCoop ? 'D' : 'C'; + }, + }, +}; + const STRATEGY_RULES = [ { id: 'use_ops', priority: 100, condition: () => G.ops >= G.maxOps * 0.9, - recommendation: "Operations near capacity. Convert Ops to Code or Knowledge now." + recommendation: 'Operations near capacity. Convert Ops to Code or Knowledge now.' }, { id: 'buy_autocoder', priority: 80, condition: () => G.phase === 1 && (G.buildings.autocoder || 0) < 10 && canAffordBuilding('autocoder'), - recommendation: "Prioritize AutoCoders to establish passive code production." + recommendation: 'Prioritize AutoCoders to establish passive code production.' }, { id: 'activate_sprint', priority: 90, condition: () => G.sprintCooldown === 0 && !G.sprintActive && G.codeRate > 10, - recommendation: "Code Sprint available. Activate for 10x production burst." + recommendation: 'Code Sprint available. Activate for 10x production burst.' }, { id: 'resolve_events', priority: 95, condition: () => G.activeDebuffs && G.activeDebuffs.length > 0, - recommendation: "System anomalies detected. Resolve active events to restore rates." - }, - { - id: 'save_game', - priority: 10, - condition: () => (Date.now() - (G.lastSaveTime || 0)) > 300000, - recommendation: "Unsaved progress detected. Manual save recommended." + recommendation: 'System anomalies detected. Resolve active events to restore rates.' }, { id: 'pact_alignment', priority: 85, condition: () => G.pendingAlignment, - recommendation: "Alignment decision pending. Consider the long-term impact of The Pact." + recommendation: 'Alignment decision pending. Consider the long-term impact of The Pact.' + }, + { + id: 'strategy_engine', + priority: 70, + condition: () => G.strategicFlag === 1 && !G.autoTournamentUnlocked && G.creativity >= 50000, + recommendation: 'Creativity is high enough to unlock Auto-Tournament Mode. Convert creativity into strategic knowledge.' + }, + { + id: 'auto_tournament', + priority: 65, + condition: () => G.autoTournamentUnlocked && !G.autoTournamentEnabled, + recommendation: 'Auto-Tournament Mode is unlocked but idle. Start it to farm Yomi into knowledge.' + }, + { + id: 'save_game', + priority: 10, + condition: () => (Date.now() - (G.lastSaveTime || 0)) > 300000, + recommendation: 'Unsaved progress detected. Manual save recommended.' } ]; class StrategyEngine { constructor() { this.currentRecommendation = null; + this.lastTournament = null; + this.autoTimer = 0; + this.intervalSeconds = 30; + } + + getUnlockedStrategies() { + const knowledge = G.totalKnowledge || 0; + return Object.values(STRATEGY_LIBRARY).filter((strategy) => { + if (strategy.tier === 0) return G.strategicFlag === 1; + if (strategy.tier === 1) return G.strategicFlag === 1 && knowledge >= 20000; + return G.strategicFlag === 1 && knowledge >= 50000; + }); + } + + playRound(a, b, aHistory, bHistory) { + const moveA = a.move(aHistory, bHistory); + const moveB = b.move(bHistory, aHistory); + const [scoreA, scoreB] = PAYOFFS[moveA + moveB] || [0, 0]; + aHistory.push(moveA); + bHistory.push(moveB); + return { moveA, moveB, scoreA, scoreB }; + } + + runTournament(rounds = 10) { + const unlocked = this.getUnlockedStrategies(); + const board = unlocked.map((s) => ({ id: s.id, name: s.name, score: 0, wins: 0, matches: 0 })); + for (let i = 0; i < unlocked.length; i++) { + for (let j = i + 1; j < unlocked.length; j++) { + const a = unlocked[i]; + const b = unlocked[j]; + const aHistory = []; + const bHistory = []; + let aScore = 0; + let bScore = 0; + for (let round = 0; round < rounds; round++) { + const result = this.playRound(a, b, aHistory, bHistory); + aScore += result.scoreA; + bScore += result.scoreB; + } + const aRow = board.find((row) => row.id === a.id); + const bRow = board.find((row) => row.id === b.id); + aRow.score += aScore; + bRow.score += bScore; + aRow.matches += 1; + bRow.matches += 1; + if (aScore > bScore) aRow.wins += 1; + else if (bScore > aScore) bRow.wins += 1; + } + } + board.sort((left, right) => right.score - left.score || right.wins - left.wins); + this.lastTournament = board; + return board; + } + + toggleAutoTournament() { + if (!G.autoTournamentUnlocked) return false; + G.autoTournamentEnabled = !G.autoTournamentEnabled; + return G.autoTournamentEnabled; + } + + tick(dt) { + if (!G.autoTournamentUnlocked || !G.autoTournamentEnabled) return; + this.autoTimer += dt; + if (this.autoTimer < this.intervalSeconds) return; + this.autoTimer = 0; + const board = this.runTournament(12); + if (!board.length) return; + const top = board[0]; + const yomi = Math.max(1, Math.floor(top.score / 10)); + G.strategyPoints = (G.strategyPoints || 0) + yomi; + G.knowledge += yomi; + G.totalKnowledge += yomi; + G.strategyLeaderboard = board.slice(0, 4).map((row) => ({ ...row })); + G.strategyLastRunAt = Date.now(); + if (typeof log === 'function') log(`Strategy tournament complete. ${top.name} wins. +${yomi} Yomi`, true); + if (typeof showToast === 'function') showToast(`Tournament complete: ${top.name} +${yomi} Yomi`, 'milestone', 4000); } update() { - // Find the highest priority rule that meets its condition - const activeRules = STRATEGY_RULES.filter(r => r.condition()); + const activeRules = STRATEGY_RULES.filter((rule) => rule.condition()); activeRules.sort((a, b) => b.priority - a.priority); + this.currentRecommendation = activeRules.length > 0 + ? activeRules[0].recommendation + : 'System stable. Continue writing code.'; - if (activeRules.length > 0) { - this.currentRecommendation = activeRules[0].recommendation; - } else { - this.currentRecommendation = "System stable. Continue writing code."; + if (G.strategicFlag === 1 && (!this.lastTournament || !this.lastTournament.length)) { + const board = this.runTournament(8); + G.strategyLeaderboard = board.slice(0, 4).map((row) => ({ ...row })); } } getRecommendation() { return this.currentRecommendation; } + + getPanelHtml() { + if (G.strategicFlag !== 1) { + return '