@codejoy/terminal-pet
Version:
A virtual pet that lives in your terminal and grows with your coding activity
358 lines (301 loc) • 9.61 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
class Pet {
constructor(name = 'CodeBuddy', type = 'cat') {
this.name = name;
this.type = type;
this.level = 1;
this.experience = 0;
this.health = 100;
this.happiness = 80;
this.hunger = 30;
this.energy = 100;
this.age = 0; // in days
this.lastFed = Date.now();
this.lastPlayed = Date.now();
this.lastCommit = null;
this.totalCommits = 0;
this.streak = 0;
this.maxStreak = 0;
this.mood = 'happy';
this.isAlive = true;
this.birthDate = Date.now();
this.achievements = [];
this.stats = {
linesOfCode: 0,
bugsFixed: 0,
featuresAdded: 0,
refactorings: 0
};
}
// Pet state management
update() {
if (!this.isAlive) return;
const now = Date.now();
const timeSinceLastUpdate = now - (this.lastUpdate || now);
const hoursPassed = timeSinceLastUpdate / (1000 * 60 * 60);
// Natural decay over time
this.hunger = Math.min(100, this.hunger + (hoursPassed * 2));
this.energy = Math.max(0, this.energy - (hoursPassed * 1.5));
this.happiness = Math.max(0, this.happiness - (hoursPassed * 0.5));
// Health is affected by hunger and happiness
if (this.hunger > 80) {
this.health = Math.max(0, this.health - (hoursPassed * 2));
}
if (this.happiness < 20) {
this.health = Math.max(0, this.health - (hoursPassed * 1));
}
// Update age
const daysPassed = (now - this.birthDate) / (1000 * 60 * 60 * 24);
this.age = Math.floor(daysPassed);
// Update mood based on stats
this.updateMood();
// Check if pet dies
if (this.health <= 0) {
this.isAlive = false;
this.mood = 'dead';
}
this.lastUpdate = now;
}
updateMood() {
if (!this.isAlive) {
this.mood = 'dead';
return;
}
if (this.health < 20) {
this.mood = 'sick';
} else if (this.hunger > 80) {
this.mood = 'hungry';
} else if (this.energy < 20) {
this.mood = 'tired';
} else if (this.happiness > 80 && this.health > 80) {
this.mood = 'ecstatic';
} else if (this.happiness > 60) {
this.mood = 'happy';
} else if (this.happiness < 30) {
this.mood = 'sad';
} else {
this.mood = 'neutral';
}
}
// Actions
feed() {
if (!this.isAlive) return false;
this.hunger = Math.max(0, this.hunger - 30);
this.happiness = Math.min(100, this.happiness + 10);
this.health = Math.min(100, this.health + 5);
this.lastFed = Date.now();
this.checkAchievements('feed');
return true;
}
play() {
if (!this.isAlive) return false;
if (this.energy < 20) return false;
this.happiness = Math.min(100, this.happiness + 20);
this.energy = Math.max(0, this.energy - 15);
this.hunger = Math.min(100, this.hunger + 5);
this.lastPlayed = Date.now();
this.checkAchievements('play');
return true;
}
sleep() {
if (!this.isAlive) return false;
this.energy = 100;
this.health = Math.min(100, this.health + 10);
this.hunger = Math.min(100, this.hunger + 10);
return true;
}
// Coding activity rewards
onCommit(commitMessage = '') {
if (!this.isAlive) return;
const now = Date.now();
const daysSinceLastCommit = this.lastCommit ?
(now - this.lastCommit) / (1000 * 60 * 60 * 24) : 0;
// Update streak
if (daysSinceLastCommit <= 1) {
this.streak += 1;
} else {
this.streak = 1;
}
this.maxStreak = Math.max(this.maxStreak, this.streak);
// Rewards based on commit
const baseReward = 10;
const streakBonus = Math.min(this.streak * 2, 20);
const experienceGain = baseReward + streakBonus;
this.experience += experienceGain;
this.happiness = Math.min(100, this.happiness + 15);
this.health = Math.min(100, this.health + 5);
this.hunger = Math.min(100, this.hunger + 5); // Coding makes you hungry!
// Analyze commit message for bonus rewards
this.analyzeCommitMessage(commitMessage);
this.totalCommits += 1;
this.lastCommit = now;
// Level up check
this.checkLevelUp();
this.checkAchievements('commit');
}
analyzeCommitMessage(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('fix') || lowerMessage.includes('bug')) {
this.stats.bugsFixed += 1;
this.experience += 5;
}
if (lowerMessage.includes('feat') || lowerMessage.includes('add')) {
this.stats.featuresAdded += 1;
this.experience += 8;
}
if (lowerMessage.includes('refactor') || lowerMessage.includes('clean')) {
this.stats.refactorings += 1;
this.experience += 6;
}
// Bonus for good commit messages
if (message.length > 20 && !lowerMessage.includes('wip')) {
this.experience += 3;
this.happiness = Math.min(100, this.happiness + 5);
}
}
checkLevelUp() {
const requiredExp = this.level * 100;
if (this.experience >= requiredExp) {
this.level += 1;
this.experience -= requiredExp;
this.happiness = Math.min(100, this.happiness + 20);
this.health = 100; // Full heal on level up
this.checkAchievements('levelup');
return true;
}
return false;
}
checkAchievements(action) {
const newAchievements = [];
// Commit-based achievements
if (action === 'commit') {
if (this.totalCommits === 1 && !this.achievements.includes('first_commit')) {
newAchievements.push('first_commit');
}
if (this.totalCommits === 100 && !this.achievements.includes('century')) {
newAchievements.push('century');
}
if (this.streak === 7 && !this.achievements.includes('week_streak')) {
newAchievements.push('week_streak');
}
if (this.streak === 30 && !this.achievements.includes('month_streak')) {
newAchievements.push('month_streak');
}
}
// Level-based achievements
if (action === 'levelup') {
if (this.level === 10 && !this.achievements.includes('double_digits')) {
newAchievements.push('double_digits');
}
if (this.level === 50 && !this.achievements.includes('half_century')) {
newAchievements.push('half_century');
}
}
// Care-based achievements
if (action === 'feed' && this.hunger === 0 && !this.achievements.includes('well_fed')) {
newAchievements.push('well_fed');
}
if (action === 'play' && this.happiness === 100 && !this.achievements.includes('pure_joy')) {
newAchievements.push('pure_joy');
}
this.achievements.push(...newAchievements);
return newAchievements;
}
// Getters for display
getStatusBar() {
const healthBar = this.createBar(this.health, '❤️', '💔');
const happinessBar = this.createBar(this.happiness, '😊', '😢');
const hungerBar = this.createBar(100 - this.hunger, '🍖', '🍽️');
const energyBar = this.createBar(this.energy, '⚡', '😴');
return {
health: healthBar,
happiness: happinessBar,
hunger: hungerBar,
energy: energyBar
};
}
createBar(value, fullChar, emptyChar) {
const barLength = 10;
const filledLength = Math.floor((value / 100) * barLength);
const emptyLength = barLength - filledLength;
return fullChar.repeat(filledLength) + emptyChar.repeat(emptyLength);
}
getInfo() {
return {
name: this.name,
type: this.type,
level: this.level,
experience: this.experience,
age: this.age,
mood: this.mood,
isAlive: this.isAlive,
streak: this.streak,
maxStreak: this.maxStreak,
totalCommits: this.totalCommits,
achievements: this.achievements,
stats: this.stats
};
}
// Persistence
save() {
const saveDir = path.join(os.homedir(), '.terminal-pet');
if (!fs.existsSync(saveDir)) {
fs.mkdirSync(saveDir, { recursive: true });
}
const saveFile = path.join(saveDir, 'pet.json');
const data = {
name: this.name,
type: this.type,
level: this.level,
experience: this.experience,
health: this.health,
happiness: this.happiness,
hunger: this.hunger,
energy: this.energy,
age: this.age,
lastFed: this.lastFed,
lastPlayed: this.lastPlayed,
lastCommit: this.lastCommit,
lastUpdate: this.lastUpdate,
totalCommits: this.totalCommits,
streak: this.streak,
maxStreak: this.maxStreak,
mood: this.mood,
isAlive: this.isAlive,
birthDate: this.birthDate,
achievements: this.achievements,
stats: this.stats
};
fs.writeFileSync(saveFile, JSON.stringify(data, null, 2));
}
static load() {
const saveFile = path.join(os.homedir(), '.terminal-pet', 'pet.json');
if (!fs.existsSync(saveFile)) {
return null;
}
try {
const data = JSON.parse(fs.readFileSync(saveFile, 'utf8'));
const pet = new Pet();
// Restore all properties
Object.keys(data).forEach(key => {
pet[key] = data[key];
});
// Update pet state based on time passed
pet.update();
return pet;
} catch (error) {
console.error('Error loading pet:', error);
return null;
}
}
// Reset pet (for new game)
static reset() {
const saveFile = path.join(os.homedir(), '.terminal-pet', 'pet.json');
if (fs.existsSync(saveFile)) {
fs.unlinkSync(saveFile);
}
}
}
module.exports = Pet;