claude-code-tamagotchi
Version:
A virtual pet that lives in your Claude Code statusline
346 lines (292 loc) • 12.1 kB
text/typescript
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();