UNPKG

claude-code-tamagotchi

Version:

A virtual pet that lives in your Claude Code statusline

346 lines (292 loc) 12.1 kB
import { PetState } from './StateManager'; import { NeedThoughts } from './thoughts/needThoughts'; import { ComboThoughts } from './thoughts/comboThoughts'; import { CodingThoughts } from './thoughts/codingThoughts'; import { RandomThoughts } from './thoughts/randomThoughts'; import { ActionThoughts } from './thoughts/actionThoughts'; // Thought category types export type ThoughtCategory = 'need' | 'combo' | 'coding' | 'random' | 'mood' | 'reactive'; // Thought priority levels export enum ThoughtPriority { CRITICAL = 1, // Urgent needs (stats < 20%) REACTIVE = 2, // Response to keywords HIGH = 3, // Important needs (stats < 40%) COMBO = 4, // Multiple low stats NORMAL = 5, // Regular thoughts LOW = 6 // Random musings } // Configuration from environment const THOUGHT_FREQUENCY = parseInt(process.env.PET_THOUGHT_FREQUENCY || '15'); const NEED_THRESHOLD = parseInt(process.env.PET_NEED_THRESHOLD || '40'); const CRITICAL_THRESHOLD = parseInt(process.env.PET_CRITICAL_THRESHOLD || '20'); const THOUGHT_COOLDOWN = parseInt(process.env.PET_THOUGHT_COOLDOWN || '10'); const CHATTINESS = process.env.PET_CHATTINESS || 'normal'; // Category weights (should sum to 100) const WEIGHT_NEEDS = parseInt(process.env.PET_THOUGHT_WEIGHT_NEEDS || '40'); const WEIGHT_CODING = parseInt(process.env.PET_THOUGHT_WEIGHT_CODING || '25'); const WEIGHT_RANDOM = parseInt(process.env.PET_THOUGHT_WEIGHT_RANDOM || '20'); const WEIGHT_MOOD = parseInt(process.env.PET_THOUGHT_WEIGHT_MOOD || '15'); export class ThoughtSystem { private thoughtHistory: string[] = []; private categoryFatigue: Map<ThoughtCategory, number> = new Map(); private lastThoughtTime: number = 0; private thoughtCooldown: number; private escalationTracking: Map<string, number> = new Map(); constructor() { // Adjust cooldown based on chattiness const cooldownMultiplier = CHATTINESS === 'quiet' ? 2 : CHATTINESS === 'chatty' ? 0.5 : 1; this.thoughtCooldown = Math.floor(THOUGHT_COOLDOWN * cooldownMultiplier); // Initialize category fatigue this.categoryFatigue.set('need', 0); this.categoryFatigue.set('combo', 0); this.categoryFatigue.set('coding', 0); this.categoryFatigue.set('random', 0); this.categoryFatigue.set('mood', 0); this.categoryFatigue.set('reactive', 0); } // Main thought generation method generateThought(state: PetState, context?: { recentInput?: string, sessionData?: any }): string | null { // Update fatigue decay this.updateFatigue(); // Check if enough time has passed since last thought const updatesSinceLastThought = state.totalUpdateCount - this.lastThoughtTime; if (updatesSinceLastThought < this.thoughtCooldown) { return null; } // Priority check system const priority = this.checkPriority(state, context); let thought: string | null = null; switch (priority) { case ThoughtPriority.CRITICAL: thought = this.getCriticalThought(state); break; case ThoughtPriority.REACTIVE: if (context?.recentInput) { thought = this.getReactiveThought(context.recentInput, state); } break; case ThoughtPriority.COMBO: thought = this.getComboThought(state); break; default: // Normal thought selection using category wheel thought = this.getNormalThought(state, context); break; } // Verify thought isn't too similar to recent ones if (thought && !this.isTooSimilar(thought)) { this.addToHistory(thought); this.lastThoughtTime = state.totalUpdateCount; return thought; } // Try fallback if thought was rejected if (thought && updatesSinceLastThought > this.thoughtCooldown * 2) { thought = this.getFallbackThought(state); this.addToHistory(thought); this.lastThoughtTime = state.totalUpdateCount; return thought; } return null; } // Check what priority level of thought is needed private checkPriority(state: PetState, context?: any): ThoughtPriority { // Critical needs override everything if (state.hunger < CRITICAL_THRESHOLD || state.energy < CRITICAL_THRESHOLD || state.cleanliness < CRITICAL_THRESHOLD || state.happiness < CRITICAL_THRESHOLD) { return ThoughtPriority.CRITICAL; } // Reactive thoughts for keywords if (context?.recentInput && this.hasReactiveTrigger(context.recentInput)) { return ThoughtPriority.REACTIVE; } // Multiple low stats const lowStatCount = [state.hunger, state.energy, state.cleanliness, state.happiness] .filter(stat => stat < NEED_THRESHOLD).length; if (lowStatCount >= 2) { return ThoughtPriority.COMBO; } // Single important need if (state.hunger < NEED_THRESHOLD || state.energy < NEED_THRESHOLD || state.cleanliness < NEED_THRESHOLD || state.happiness < NEED_THRESHOLD) { return ThoughtPriority.HIGH; } return ThoughtPriority.NORMAL; } // Select category using weighted random with fatigue private selectCategory(): ThoughtCategory { // Calculate adjusted weights based on fatigue const adjustedWeights = { need: Math.max(1, WEIGHT_NEEDS * (100 - (this.categoryFatigue.get('need') || 0)) / 100), coding: Math.max(1, WEIGHT_CODING * (100 - (this.categoryFatigue.get('coding') || 0)) / 100), random: Math.max(1, WEIGHT_RANDOM * (100 - (this.categoryFatigue.get('random') || 0)) / 100), mood: Math.max(1, WEIGHT_MOOD * (100 - (this.categoryFatigue.get('mood') || 0)) / 100) }; const total = Object.values(adjustedWeights).reduce((a, b) => a + b, 0); const roll = Math.random() * total; let cumulative = 0; if (roll < (cumulative += adjustedWeights.need)) return 'need'; if (roll < (cumulative += adjustedWeights.coding)) return 'coding'; if (roll < (cumulative += adjustedWeights.random)) return 'random'; return 'mood'; } // Update category fatigue (decay over time) private updateFatigue(): void { this.categoryFatigue.forEach((value, key) => { this.categoryFatigue.set(key, Math.max(0, value - 1)); }); } // Add fatigue when category is used private addFatigue(category: ThoughtCategory): void { const current = this.categoryFatigue.get(category) || 0; this.categoryFatigue.set(category, Math.min(100, current + 30)); } // Check if thought is too similar to recent ones private isTooSimilar(thought: string): boolean { // Check exact duplicates in last 10 if (this.thoughtHistory.includes(thought)) { return true; } // Check for similar topics in last 3 const recentThoughts = this.thoughtHistory.slice(-3); for (const recent of recentThoughts) { if (this.areSimilarThoughts(thought, recent)) { return true; } } return false; } // Check if two thoughts are about the same topic private areSimilarThoughts(thought1: string, thought2: string): boolean { // Simple similarity check - can be enhanced const keywords = ['hungry', 'food', 'tired', 'sleep', 'dirty', 'clean', 'sad', 'happy']; for (const keyword of keywords) { if (thought1.toLowerCase().includes(keyword) && thought2.toLowerCase().includes(keyword)) { return true; } } return false; } // Add thought to history private addToHistory(thought: string): void { this.thoughtHistory.push(thought); if (this.thoughtHistory.length > 10) { this.thoughtHistory.shift(); } } // Check if input has reactive triggers private hasReactiveTrigger(input: string): boolean { const triggers = ['error', 'bug', 'fixed', 'success', 'todo', 'delete', '//', 'git']; return triggers.some(trigger => input.toLowerCase().includes(trigger)); } // Get critical thought for urgent needs private getCriticalThought(state: PetState): string { // Track escalation for critical needs const lowestStat = this.getLowestStat(state); const escalation = this.trackEscalation(lowestStat.name); return NeedThoughts.getThought(state, escalation); } private getLowestStat(state: PetState): {name: string, value: number} { const stats = [ {name: 'hunger', value: state.hunger}, {name: 'energy', value: state.energy}, {name: 'cleanliness', value: state.cleanliness}, {name: 'happiness', value: state.happiness} ]; return stats.reduce((min, stat) => stat.value < min.value ? stat : min ); } private getReactiveThought(input: string, state: PetState): string { return CodingThoughts.getReactiveThought(input, state); } private getComboThought(state: PetState): string { return ComboThoughts.getThought(state); } private getNormalThought(state: PetState, context?: any): string { // Select category and generate appropriate thought const category = this.selectCategory(); this.addFatigue(category); switch (category) { case 'need': const escalation = this.trackEscalation('general_need'); return NeedThoughts.getThought(state, escalation); case 'coding': return CodingThoughts.getObservation(state, state.sessionUpdateCount); case 'random': return RandomThoughts.getThought(state, context); case 'mood': // Mood-based thoughts based on current mood return this.getMoodThought(state); default: return RandomThoughts.getSillyThought(state); } } private getMoodThought(state: PetState): string { switch (state.currentMood) { case 'debugging': return CodingThoughts.getDebuggingThought(state); case 'celebrating': return ActionThoughts.getSpecialActionThought('celebrate', state); case 'tired': return NeedThoughts.getEnergyThought(state.energy, 1); case 'focused': return CodingThoughts.getCodeQualityThought(state); case 'sleeping': return ActionThoughts.getSleepingThought(state); default: return RandomThoughts.getMotivationalThought(state); } } private getFallbackThought(state: PetState): string { const fallbacks = [ "Just thinking... 💭", "Hi there! 👋", "How's the coding going? 💻", "I'm here for you! 🤗", "*stares at code* 👀" ]; return fallbacks[Math.floor(Math.random() * fallbacks.length)]; } // Track escalation for repeated needs trackEscalation(topic: string): number { const current = this.escalationTracking.get(topic) || 0; this.escalationTracking.set(topic, current + 1); return current + 1; } // Reset escalation when need is met resetEscalation(topic: string): void { this.escalationTracking.delete(topic); } // Get action-specific thoughts getActionThought(action: string, item?: string, state?: PetState): string { if (!state) return ''; switch (action) { case 'eating': return ActionThoughts.getEatingThought(item || 'food', state); case 'playing': return ActionThoughts.getPlayingThought(item || 'toy', state); case 'bathing': const cleanProgress = state.cleanliness; return ActionThoughts.getBathingThought(cleanProgress); case 'sleeping': return ActionThoughts.getSleepingThought(state); case 'petting': return ActionThoughts.getPettingThought(state); case 'waking': return ActionThoughts.getWakeUpThought(state); case 'training': return ActionThoughts.getTrainingThought(item || 'skill'); case 'medicine': return ActionThoughts.getMedicineThought(state); default: return ActionThoughts.getSpecialActionThought(action, state); } } } // Export a singleton instance export const thoughtSystem = new ThoughtSystem();