zelin-bot / src /psyche.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* ============================================
* 🧠 psyche.js β€” Motor PsicolΓ³gico de Zelin
* ============================================
* Simula comportamiento humano autΓ©ntico:
* - Estado emocional dinΓ‘mico y persistente
* - Memoria de afinidades por usuario
* - Fatiga social, entusiasmo, boredom
* - Quirks de escritura humana variables
* - Respuestas no lineales segΓΊn estado
*/
// ── Estado psicolΓ³gico interno ─────────────────────────────────────────────────
const state = {
mood : 0.1, // -1 (muy mal) a 1 (muy bien)
energy : 0.8, // 0 (agotada) a 1 (llena)
engagement: 0.5, // 0 (aburrida) a 1 (enganchada)
lastIrritated: 0, // timestamp del ΓΊltimo momento irritada
messagesInARow: 0, // racha de mensajes sin pausa
lastMessageTime: Date.now(),
affinities: new Map(), // userId β†’ score (-1 a 1)
excitingTopics: new Set(['minecraft', 'server', 'tomatito', 'creeper', 'pvp', 'build', 'skin', 'rank']),
// YouTube/in-game performance inputs
inGameEmotion: 'neutral', // current Minecraft emotional state
lastYouTubePerformance: null, // { views, likes, comments, ratio }
youTubeSuccessStreak: 0, // consecutive "good" videos
youTubeFailStreak: 0, // consecutive "bad" videos
};
// ── Actualizar estado basado en cada mensaje ──────────────────────────────────
export function updatePsychologicalState({ userId, content = '', isOwner = false }) {
const now = Date.now();
const elapsed = now - state.lastMessageTime;
state.lastMessageTime = now;
// RecuperaciΓ³n natural con el tiempo
if (elapsed > 5 * 60_000) {
state.mood = state.mood * 0.7; // drift hacia neutro
state.energy = Math.min(1, state.energy + 0.1);
state.engagement = Math.min(0.6, state.engagement + 0.05);
state.messagesInARow = 0;
}
// Fatiga social: muchos mensajes rapidos seguidos
const timeSinceLast = now - state.lastMessageTime;
if (timeSinceLast < 5000) { // mensajes rapidos (< 5s)
state.messagesInARow++;
} else if (timeSinceLast > 30000) {
// Pausa natural β€” reducir contador
state.messagesInARow = Math.max(0, state.messagesInARow - 2);
}
if (state.messagesInARow > 20) {
state.energy = Math.max(0.2, state.energy - 0.02);
state.engagement = Math.max(0.2, state.engagement - 0.03);
}
// Prune affinities Map if too large
if (state.affinities.size > 200) {
for (const [uid, score] of state.affinities) {
if (Math.abs(score) < 0.05) state.affinities.delete(uid);
if (state.affinities.size <= 150) break;
}
}
// Owner siempre mejora el estado
if (isOwner) {
state.engagement = Math.min(1, state.engagement + 0.2);
state.mood = Math.min(1, state.mood + 0.1);
return;
}
const lc = content.toLowerCase();
// Temas emocionantes
if ([...state.excitingTopics].some(t => lc.includes(t))) {
state.engagement = Math.min(1, state.engagement + 0.12);
state.mood = Math.min(1, state.mood + 0.04);
}
// Insultos directos β†’ irritaciΓ³n
if (/estΓΊpid|pendej|idiota|inΓΊtil|basura|asco|tonta/i.test(content)) {
state.mood = Math.max(-0.8, state.mood - 0.3);
state.lastIrritated = now;
_updateAffinity(userId, -0.2);
}
// Cumplidos β†’ mejora humor
if (/buena|genial|cool|chida|excelente|gracias|perfecto|crack|eres la|te quiero|mejor bot/i.test(content)) {
state.mood = Math.min(1, state.mood + 0.12);
_updateAffinity(userId, 0.07);
}
// InteracciΓ³n frecuente β†’ sube afinidad lentamente
_updateAffinity(userId, 0.008);
}
function _updateAffinity(userId, delta) {
if (!userId) return;
const cur = state.affinities.get(userId) ?? 0;
state.affinities.set(userId, Math.max(-1, Math.min(0.9, cur + delta)));
}
// ── Contexto psicolΓ³gico para el system prompt ────────────────────────────────
export function getPsychologicalContext(userId) {
const affinity = state.affinities.get(userId) ?? 0;
const irritated = Date.now() - state.lastIrritated < 4 * 60_000;
// DescripciΓ³n del humor actual
let moodLine = '';
if (state.mood > 0.5) moodLine = 'EstΓ‘s de muy buen humor hoy β€” con ganas de hablar y bromear.';
else if (state.mood > 0.1) moodLine = 'Humor normal, tranquila.';
else if (state.mood > -0.2) moodLine = 'EstΓ‘s un poco apagada, respuestas mΓ‘s cortas de lo normal.';
else if (state.mood > -0.5) moodLine = 'EstΓ‘s de mal humor β€” mΓ‘s seca y directa que de costumbre.';
else moodLine = 'EstΓ‘s de muy mal humor. Respuestas super cortas, poca paciencia.';
// EnergΓ­a
let energyLine = '';
if (state.energy < 0.35) energyLine = 'EstΓ‘s cansada β€” respuestas cortas, sin mucha elaboraciΓ³n.';
else if (state.energy > 0.75) energyLine = 'Tienes energΓ­a, puedes explayarte mΓ‘s si el tema lo merece.';
// IrritaciΓ³n reciente
const irritLine = irritated
? 'Alguien te irritΓ³ hace poco β€” sigues un poco molesta, eres mΓ‘s cortante de lo usual.'
: '';
// Afinidad con el usuario
let affinityLine = '';
if (affinity > 0.5) affinityLine = 'Esta persona te cae muy bien β€” eres mΓ‘s relajada y juguetona con ella.';
else if (affinity > 0.2) affinityLine = 'Conoces bien a esta persona, trato cΓ³modo y casual.';
else if (affinity < -0.3) affinityLine = 'Esta persona te ha irritado antes β€” eres mΓ‘s frΓ­a y distante.';
// Engagement
let engLine = '';
if (state.engagement < 0.3) engLine = 'EstΓ‘s aburrida de la conversaciΓ³n β€” respuestas mΓ­nimas.';
else if (state.engagement > 0.7) engLine = 'EstΓ‘s enganchada β€” puedes dar respuestas mΓ‘s largas si el tema lo vale.';
const lines = [moodLine, energyLine, irritLine, affinityLine, engLine].filter(Boolean);
if (!lines.length) return '';
return `\nESTADO INTERNO ACTUAL (ΓΊsalo para colorear cΓ³mo respondes, no lo menciones):\n${lines.map(l => `- ${l}`).join('\n')}`;
}
// ── Aplicar quirks de escritura humana ────────────────────────────────────────
export function applyHumanWritingQuirks(text, userId) {
if (!text || typeof text !== 'string') return text;
const affinity = state.affinities.get(userId) ?? 0;
const lowEnergy = state.energy < 0.4;
const goodMood = state.mood > 0.5;
const casual = affinity > 0.3 || lowEnergy;
let t = text;
// Quitar ΒΏ (muy comΓΊn en Discord hispanohablante)
if (Math.random() < 0.45) t = t.replace(/ΒΏ/g, '');
// Quitar Β‘
if (Math.random() < 0.5) t = t.replace(/Β‘/g, '');
// Abreviaciones casuales solo si hay confianza o baja energΓ­a
if (casual) {
if (Math.random() < 0.4) t = t.replace(/\btambiΓ©n\b/gi, 'tmb');
if (Math.random() < 0.35) t = t.replace(/\bporque\b/gi, 'pq');
if (Math.random() < 0.3) t = t.replace(/\bpara\b/gi, 'pa');
if (Math.random() < 0.25) t = t.replace(/\bque\b/g, 'q');
}
// Reacciones al final si buen humor y mensaje corto
if (goodMood && Math.random() < 0.12 && t.length < 90
&& !/xd|jaja|lol|πŸ’€|😭/.test(t)) {
const reacts = [' xd', ' jaja', ' lol', ' πŸ’€', ' jajaja'];
t += reacts[Math.floor(Math.random() * reacts.length)];
}
// Frases incompletas con "..." si cansada
if (lowEnergy && Math.random() < 0.18 && t.endsWith('.')) {
t = t.slice(0, -1) + '..';
}
// A veces primera letra minΓΊscula en mensajes cortos
if (t.length < 70 && Math.random() < 0.35) {
t = t.charAt(0).toLowerCase() + t.slice(1);
}
// Typo ocasional estilo autocorrect (muy humano, muy raro)
if (Math.random() < 0.04) {
const typos = [
[/\besta\b/, 'etsa'],
[/\bbien\b/, 'bine'],
[/\bcomo\b/, 'cmoo'],
[/\bsolo\b/, 'sloo'],
];
const pick = typos[Math.floor(Math.random() * typos.length)];
const newT = t.replace(pick[0], pick[1]);
if (newT !== t) t = newT;
}
return t;
}
// ── Estado pΓΊblico ─────────────────────────────────────────────────────────────
export function getStateSnapshot() {
return {
mood : +state.mood.toFixed(2),
energy : +state.energy.toFixed(2),
engagement : +state.engagement.toFixed(2),
msgs : state.messagesInARow,
irritated : Date.now() - state.lastIrritated < 4 * 60_000,
topFriends : [...state.affinities.entries()]
.sort((a, b) => b[1] - a[1]).slice(0, 5)
.map(([id, s]) => ({ id, s: +s.toFixed(2) })),
topFoes : [...state.affinities.entries()]
.sort((a, b) => a[1] - b[1]).slice(0, 3)
.map(([id, s]) => ({ id, s: +s.toFixed(2) })),
};
}
export function resetPsyche() {
state.mood = 0.1; state.energy = 0.8; state.engagement = 0.5;
state.messagesInARow = 0; state.lastIrritated = 0;
state.affinities.clear();
console.log('[Psyche] βœ… Estado psicolΓ³gico reseteado');
}
// ── In-game emotion update (from mineflayer-agent.js) ──────────────────────────
export function updateInGameEmotion(emotion) {
if (!emotion) return;
state.inGameEmotion = emotion;
// Map game emotions to mood shifts
const moodShift = {
excited: 0.08,
focused: 0.02,
bored: -0.04,
cautious: -0.02,
annoyed: -0.06,
curious: 0.04,
satisfied: 0.06,
};
const shift = moodShift[emotion] ?? 0;
state.mood = Math.max(-1, Math.min(1, state.mood + shift));
}
// ── YouTube performance feedback ────────────────────────────────────────────────
export function updateYouTubePerformance(performance) {
if (!performance) return;
state.lastYouTubePerformance = performance;
// Ratio: likes/views β€” good is > 0.05 (5% like rate), bad is < 0.02
const ratio = performance.ratio ?? (performance.views > 0 ? performance.likes / performance.views : 0);
if (ratio > 0.05) {
// Good video β€” mood boost, engagement boost
state.mood = Math.min(1, state.mood + 0.15);
state.engagement = Math.min(1, state.engagement + 0.1);
state.youTubeSuccessStreak++;
state.youTubeFailStreak = 0;
} else if (ratio < 0.02) {
// Bad video β€” mood drop, frustration
state.mood = Math.max(-1, state.mood - 0.12);
state.engagement = Math.max(0.2, state.engagement - 0.08);
state.youTubeFailStreak++;
state.youTubeSuccessStreak = 0;
// Streak of bad videos β†’ stronger effect
if (state.youTubeFailStreak >= 3) {
state.mood = Math.max(-0.8, state.mood - 0.1);
}
} else {
// Average video β€” neutral drift
state.youTubeSuccessStreak = 0;
state.youTubeFailStreak = 0;
}
// Very successful streak β†’ excitement
if (state.youTubeSuccessStreak >= 3) {
state.engagement = Math.min(1, state.engagement + 0.15);
state.energy = Math.min(1, state.energy + 0.1);
}
}
// ── In-game event feedback (death, pvp, discovery) ─────────────────────────────
export function updateInGameEvent(event) {
if (!event) return;
switch (event.type) {
case 'death':
state.mood = Math.max(-0.6, state.mood - 0.15);
state.energy = Math.max(0.3, state.energy - 0.08);
break;
case 'pvp_win':
state.mood = Math.min(1, state.mood + 0.1);
state.engagement = Math.min(1, state.engagement + 0.08);
break;
case 'pvp_lose':
state.mood = Math.max(-0.5, state.mood - 0.1);
state.lastIrritated = Date.now();
break;
case 'rare_find':
state.mood = Math.min(1, state.mood + 0.12);
state.engagement = Math.min(1, state.engagement + 0.15);
break;
case 'near_death':
state.mood = Math.max(-0.3, state.mood - 0.05);
state.energy = Math.max(0.3, state.energy - 0.05);
break;
}
}