`;
log.appendChild(div); log.scrollTop = log.scrollHeight;
}
function agentMemory(agentId) {
// memoria persistente: SOLO batallas con motor y evaluación verificados (nunca mock/heurística/inconsistente ni registros legacy sin motor)
const real = matches.filter(m => m.engine === 'wllama' && !m.heuristic && !m.inconsistent && m.participants.includes(agentId));
if (!real.length) return '';
const st = stats[agentId];
const parts = [];
if (st && st.matches) parts.push(`${st.matches} batallas evaluadas (${st.wins} victorias, ${st.losses} derrotas, ${st.draws} empates)`);
const recent = real.slice(0, 3).map(m => {
const res = m.mode === 'tournament' ? (m.winner === agentId ? 'ganaste el torneo' : 'no ganaste el torneo')
: m.winnerTeam ? ((m.teams && m.teams[agentId]) === m.winnerTeam ? 'ganó tu equipo' : 'perdió tu equipo')
: m.winner === agentId ? 'ganaste' : (m.winner ? 'perdiste' : 'empate');
return `«${m.topic}»: ${res}`;
});
if (recent.length) parts.push('últimas: ' + recent.join(' · '));
return parts.length ? `\nTu historial en esta arena (memoria persistente): ${parts.join('. ')}. Aprende de tus batallas anteriores: mantén lo que te dio victorias y corrige lo que te hizo perder.\n` : '';
}
function realBattleCount(agentId) {
return matches.filter(m => m.engine === 'wllama' && !m.heuristic && !m.inconsistent && m.participants.includes(agentId)).length;
}
async function agentIntervention(agent, phase, round, extraInstr) {
const strat = strategyOf(agent, round);
const system = agent.system + ' ' + (STRATEGY_TEXT[strat] || '') + ' ' + RULES;
const tm = battle.tournament && battle.tournament.currentMatch != null ? battle.tournament.matches[battle.tournament.currentMatch] : null;
const view = transcriptView(tm ? tm.events : battle.events);
const phaseInstr = {
opening: 'Fase OPENING: presenta tu tesis inicial sobre el tema, con tu posición clara.',
crossfire: 'Fase CROSSFIRE: reacciona a la última intervención; pregunta, presiona o responde según tu estrategia.',
rebuttal: 'Fase REBUTTAL: refuta el argumento rival más fuerte en tu contra y refuerza tu posición.',
closing: 'Fase CLOSING: presenta tu conclusión final, tu mejor síntesis para ganar el debate.',
}[phase] || '';
let teamCtx = '';
if (battle.mode === 'team' && agent.team) {
const mates = battle.participants.filter(x => x.team === agent.team && x.id !== agent.id).map(x => x.name);
const rivals = battle.participants.filter(x => x.team !== agent.team).map(x => x.name);
teamCtx = `\nEstás en el EQUIPO ${agent.team}${mates.length ? ' junto a ' + mates.join(', ') : ''}. Equipo rival (EQUIPO ${agent.team === 'A' ? 'B' : 'A'}): ${rivals.join(', ')}. Coordina con tu equipo: refuerza y extiende los puntos de tus compañeros, no los contradigas; concentra tus ataques en el equipo rival.`;
}
const memCtx = agentMemory(agent.id);
const rmCtx = battle.rematch ? `\nREVANCHA: ya se disputó una batalla anterior sobre este tema con estos participantes.\n${battle.rematch}\nMantén tu personalidad y estilo, pero AJUSTA tu estrategia: corrige los errores señalados, refuerza lo que funcionó y no repitas literalmente tus intervenciones anteriores.\n` : '';
const user = `Tema del debate: ${battle.topic}\nRonda ${round} de ${battle.rounds}.\n${phaseInstr}\n${extraInstr || ''}${teamCtx}${rmCtx}${memCtx}\n\n` +
(view ? `Intervenciones hasta ahora:\n${view}\n\n` : 'Aún no hay intervenciones.\n\n') +
`Ahora intervienes tú, ${agent.name}.`;
const div = document.createElement('div');
div.className = 'msg streaming'; div.style.setProperty('--ac', agent.color);
div.innerHTML = `
Observaciones del mismo modelo local (mismo runtime): señalan patrones de conducta argumental; no son verificación externa ni afectan a puntuaciones ni premios.
`;
}
/* ================= JUDGES ================= */
async function runJudging(pl, evs) {
renderPhaseTrack('judging');
addSystemMsg('⚖️ Los tres jueces evalúan de forma independiente…');
const ps = pl || battle.participants;
const names = ps.map(a => a.name);
const transcript = transcriptView(evs || battle.events, 12000);
const teamNote = battle.mode === 'team' ? '\nFormato por equipos: EQUIPO A = ' + ps.filter(a => a.team === 'A').map(a => a.name).join(', ') + '; EQUIPO B = ' + ps.filter(a => a.team === 'B').map(a => a.name).join(', ') : '';
const scores = {}; // agentId -> criterion -> media de jueces
const judgeReports = [];
let heuristicEval = false; // true si alguna puntuación NO viene de JSON estructurado del modelo
ps.forEach(a => scores[a.id] = {});
for (const j of JUDGES) {
if (stopRequested) return null;
const critList = j.criteria.map(c => WEIGHTS.find(w => w[0] === c)[1]).join('" y "');
const system = `Eres ${j.name}, juez de un torneo de debate (${j.desc}). Evalúas SOLO los criterios "${critList}". Eres estricto, independiente y no conoces las puntuaciones de otros jueces.`;
const user = `Tema: ${battle.topic}\nParticipantes: ${names.join(', ')}${teamNote}\n\nTranscripción:\n${transcript}\n\n` +
`Puntúa a cada participante de 0 a 10 en cada criterio. Responde SOLO con JSON válido: {"${names[0]}":{"${j.criteria[0]}":N,"${j.criteria[1] || j.criteria[0]}":N},...}`;
let parsed = null;
try {
const out = await generate(system, user, CONFIG.judgeTokens);
parsed = extractJSON(out);
if (!parsed) {
// un reintento con instrucción más estricta; si sigue fallando, queda como "no evaluable"
const out2 = await generate(system, user + '\n\nIMPORTANTE: responde ÚNICAMENTE con el JSON válido pedido, sin explicaciones, sin markdown y sin texto adicional.', CONFIG.judgeTokens);
parsed = extractJSON(out2);
}
} catch (e) { console.warn(j.name, e); }
// fallback heurístico si el JSON no es parseable
ps.forEach(a => {
j.criteria.forEach(c => {
let v = parsed?.[a.name]?.[c];
if (typeof v !== 'number' || isNaN(v)) {
heuristicEval = true;
const mine = (evs || battle.events).filter(e => e.agentId === a.id);
v = Math.min(10, 4 + mine.length + (mine.reduce((s, e) => s + e.content.length, 0) / 600));
v = Math.round(v * 10) / 10;
}
v = Math.max(0, Math.min(10, v));
const cur = scores[a.id][c];
scores[a.id][c] = cur === undefined ? v : Math.round(((cur + v) / 2) * 10) / 10;
});
});
judgeReports.push({ judge: j.name, emoji: j.emoji, parsed, fallback: !parsed });
addSystemMsg(`${j.emoji} ${j.name} ha entregado su evaluación`);
}
// HEAD JUDGE
let verdict = null;
try {
const totals = ps.map(a => ({ name: a.name, total: totalScore(a.id, scores) }));
const system = 'Eres el Head Judge de un torneo de debate. Consolidas las evaluaciones de tres jueces y produces el veredicto final, con justificación breve y concreta.';
const user = `Tema: ${battle.topic}\nPuntuaciones ponderadas totales: ${totals.map(t => `${t.name}: ${t.total}`).join(', ')}\n\n` +
`Transcripción (resumen):\n${transcript.slice(0, 6000)}\n\n` +
`Responde SOLO con JSON válido: {"winner":"","justification":"2-4 frases","decisive":["argumento decisivo 1","2"],"mistakes":["error detectado 1","2"],"best":["mejor intervención 1"]}`;
const out = await generate(system, user, CONFIG.judgeTokens + 50);
verdict = extractJSON(out);
if (!verdict) {
const out2 = await generate(system, user + '\n\nIMPORTANTE: responde ÚNICAMENTE con el JSON válido pedido, sin explicaciones, sin markdown y sin texto adicional.', CONFIG.judgeTokens + 50);
verdict = extractJSON(out2);
}
} catch (e) { console.warn('head judge', e); }
// compat: modos no torneo mantienen el estado en battle
battle.scores = scores; battle.judgeReports = judgeReports; battle.heuristicEval = heuristicEval;
return { scores, judgeReports, verdict, heuristic: heuristicEval };
}
function teamTotals() {
if (!battle || battle.mode !== 'team') return null;
const avg = t => { const ms = battle.participants.filter(a => a.team === t); return ms.length ? Math.round(ms.reduce((s, a) => s + totalScore(a.id), 0) / ms.length * 100) / 100 : 0; };
return { A: avg('A'), B: avg('B') };
}
async function judgeTournamentMatch(mi) {
const m = battle.tournament.matches[mi];
if (m.judged || m.skipped) return;
const pair = [battle.participants[m.a], battle.participants[m.b]];
const jr = await runJudging(pair, m.events);
if (!jr) return; // pausa durante el juicio: se reintenta al reanudar
m.judged = true; m.scores = jr.scores; m.judgeReports = jr.judgeReports; m.verdict = jr.verdict; m.heuristic = jr.heuristic;
const tA = totalScore(pair[0].id, jr.scores), tB = totalScore(pair[1].id, jr.scores);
m.totalA = tA; m.totalB = tB;
let w = null;
if (!jr.heuristic) {
if (jr.verdict && jr.verdict.winner && jr.verdict.winner !== 'Empate') w = pair.find(a => a.name.toLowerCase() === String(jr.verdict.winner).toLowerCase()) || null;
if (!w && tA !== tB) w = tA > tB ? pair[0] : pair[1];
}
m.winner = w ? battle.participants.indexOf(w) : null;
addSystemMsg(w ? `✅ ${m.name}: avanza ${w.name} (${Math.max(tA, tB)} vs ${Math.min(tA, tB)})` : `⚠️ ${m.name}: sin ganador fiable (evaluación no disponible)`);
if (!jr.heuristic && !MOCK) {
pair.forEach(a => {
const st = stats[a.id] = stats[a.id] || { wins: 0, losses: 0, draws: 0, scoreSum: 0, matches: 0, awards: {} };
st.matches++; st.scoreSum += totalScore(a.id, jr.scores);
if (!w) st.draws++; else if (a.id === w.id) st.wins++; else st.losses++;
});
persistAll();
}
tournamentAdvance(battle);
}
function finalizeTournament() {
renderPhaseTrack('verdict');
const t = battle.tournament;
const finalM = t.matches[t.matches.length - 1];
const champ = finalM && finalM.winner != null ? battle.participants[finalM.winner] : null;
battle.winner = champ ? champ.id : null;
battle.verdict = finalM ? finalM.verdict : null;
if (champ && !battle.heuristicEval && !MOCK) {
const st = stats[champ.id] = stats[champ.id] || { wins: 0, losses: 0, draws: 0, scoreSum: 0, matches: 0, awards: {} };
st.awards.tournament_champion = (st.awards.tournament_champion || 0) + 1;
}
battle.heuristicEval = t.matches.some(m => m.heuristic || m.skipped);
battle.finishedAt = new Date().toISOString();
matches.unshift({ id: battle.id, topic: battle.topic, mode: battle.mode, rounds: battle.rounds, participants: battle.participants.map(a => a.id),
events: battle.events, winner: battle.winner, referee: battle.referee || undefined, heuristic: battle.heuristicEval, simulated: MOCK || undefined, engine: MOCK ? 'simulated' : 'wllama',
tournament: t.matches.map(m => ({ name: m.name, a: m.a, b: m.b, winner: m.winner, totalA: m.totalA, totalB: m.totalB, heuristic: !!m.heuristic, skipped: !!m.skipped })),
date: battle.finishedAt });
if (matches.length > 50) matches.length = 50;
persistAll();
renderTournamentResults(champ);
renderReferee();
showView('results');
}
function totalScore(agentId, scores) {
const s = (scores || battle.scores || {})[agentId] || {};
let t = 0;
WEIGHTS.forEach(([k, , w]) => { t += (s[k] ?? 5) * w / 100; });
return Math.round(t * 100) / 100;
}
function finalizeBattle(verdict) {
renderPhaseTrack('verdict');
const ps = battle.participants;
const totals = ps.map(a => ({ a, total: totalScore(a.id) })).sort((x, y) => y.total - x.total);
let winner = null, winnerTeam = null;
if (battle.mode === 'team') {
const tt = teamTotals();
if (verdict && verdict.winner && verdict.winner !== 'Empate') {
const w = String(verdict.winner).toLowerCase();
winnerTeam = w.includes('equipo a') || w === 'a' ? 'A' : (w.includes('equipo b') || w === 'b' ? 'B' : null);
}
if (!winnerTeam && tt && tt.A !== tt.B) winnerTeam = tt.A > tt.B ? 'A' : 'B';
if (battle.heuristicEval) winnerTeam = null; // sin ganador declarado cuando la evaluación no es fiable
} else {
if (verdict && verdict.winner && verdict.winner !== 'Empate') {
winner = ps.find(a => a.name.toLowerCase() === String(verdict.winner).toLowerCase()) || null;
}
if (!winner && totals.length >= 2 && totals[0].total > totals[1].total) winner = totals[0].a;
if (battle.heuristicEval) winner = null; // sin ganador declarado cuando la evaluación no es fiable
}
battle.winner = winner ? winner.id : null;
battle.winnerTeam = winnerTeam;
battle.verdict = verdict;
battle.finishedAt = new Date().toISOString();
// calidad del veredicto: esquema + consistencia; sin ella no cuenta para stats/logros aunque la ruta fuese real
const qualityOK = !battle.heuristicEval && !MOCK && evaluationQualityOK(battle.scores, verdict, ps, battle.mode, winner);
battle.evalInconsistent = !battle.heuristicEval && !MOCK && !qualityOK;
if (battle.evalInconsistent) { winner = null; battle.winner = null; battle.winnerTeam = null; }
// stats: solo cuentan batallas con evaluación real Y consistente del modelo (ni heurísticas ni simuladas ni inconsistentes)
if (qualityOK) {
ps.forEach(a => {
const st = stats[a.id] = stats[a.id] || { wins: 0, losses: 0, draws: 0, scoreSum: 0, matches: 0, awards: {} };
st.matches++;
st.scoreSum += totalScore(a.id);
if (battle.mode === 'team') {
if (!winnerTeam) st.draws++;
else if (a.team === winnerTeam) st.wins++;
else st.losses++;
} else if (!winner) st.draws++;
else if (a.id === winner.id) st.wins++;
else st.losses++;
});
applyAwardsAndMilestones(ps, winner, winnerTeam);
}
// guardar batalla (Arena Memory)
matches.unshift({ id: battle.id, topic: battle.topic, mode: battle.mode, rounds: battle.rounds, participants: ps.map(a => a.id), teams: battle.mode === 'team' ? Object.fromEntries(ps.map(a => [a.id, a.team])) : undefined, events: battle.events, scores: battle.scores, winner: battle.winner, winnerTeam, verdict, referee: battle.referee || undefined, heuristic: !!battle.heuristicEval, inconsistent: !!battle.evalInconsistent || undefined, simulated: MOCK || undefined, engine: MOCK ? 'simulated' : 'wllama', date: battle.finishedAt });
if (matches.length > 50) matches.length = 50;
persistAll();
renderResults(totals, winner, verdict);
renderReferee();
showView('results');
}
/* ================= RESULTS ================= */
function renderJudgeParsed(parsed) {
// render legible de la evaluación estructurada de un juez; nunca JSON crudo
try {
return Object.entries(parsed).map(([k, v]) => {
if (v && typeof v === 'object' && !Array.isArray(v)) {
const parts = Object.entries(v).map(([ck, cv]) => `${ck}: ${typeof cv === 'object' ? JSON.stringify(cv) : cv}`);
return `${esc(k)} — ${esc(parts.join(' · '))}`;
}
return `${esc(k)}: ${esc(typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v))}`;
}).join('');
} catch { return 'Evaluación estructurada (detalle completo en la exportación JSON).'; }
}
function mockNoteHtml() {
return MOCK ? '
🧪 SIMULADO — pruebas sin modelo real: notas, veredictos y premios no son una evaluación real
' : '';
}
function renderTournamentResults(champ) {
const t = battle.tournament;
const heur = battle.heuristicEval;
$('#verdict-hero').innerHTML = `
${champ ? '🏆' : '⚠️'}
${champ ? `${champ.emoji} ${esc(champ.name)} — CAMPEÓN DEL TORNEO` : '⚠️ Torneo sin campeón fiable'}
${esc(battle.topic)}
${heur ? `
Alguna eliminatoria no tuvo evaluación estructurada del modelo. Las rondas sin evaluación fiable no cuentan para el Hall of Fame ni otorgan premios.
${m.heuristic ? 'Evaluación heurística (respuesta no estructurada).' : (m.verdict?.justification ? esc(m.verdict.justification) : 'Veredicto por puntuación ponderada.')}
`).join('');
$('#awards').innerHTML = (MOCK ? '🧪 SIMULADO' : '') + (champ && !heur
? `🏆 ${esc(champ.name)} — Campeón del Torneo`
: '⚠️ Sin premios: evaluación no disponible en alguna ronda');
}
function renderResults(totals, winner, verdict) {
const heur = !!battle.heuristicEval;
$('#verdict-hero').innerHTML = `
${heur || battle.evalInconsistent ? '⚠️' : '👑'}
${heur ? '⚠️ Evaluación no fiable' : battle.evalInconsistent ? '⚠️ Veredicto inconsistente (no superó la validación)' : (battle.mode === 'team' ? (battle.winnerTeam ? `🛡️ EQUIPO ${battle.winnerTeam} gana la batalla` : '🤝 EMPATE ENTRE EQUIPOS') : (winner ? `${winner.emoji} ${esc(winner.name)} gana la batalla` : '🤝 EMPATE'))}
${!heur && battle.mode === 'team' ? `
Equipo A ${teamTotals().A} — Equipo B ${teamTotals().B}
` : ''}
${esc(battle.topic)}
${heur ? `
El modelo no produjo evaluaciones estructuradas: no hay puntuaciones fiables para esta batalla. Solo se muestran métricas mecánicas (número y longitud de intervenciones), que NO son una valoración del debate. Esta batalla no cuenta para el Hall of Fame ni otorga premios.
` : ''}
${battle.evalInconsistent ? `
El veredicto del modelo no superó la validación de esquema/consistencia (ganador declarado ≠ puntuaciones o notas fuera de rango): esta batalla no cuenta para el Hall of Fame ni otorga premios.
` : ''}
${!heur && verdict?.justification ? `
${esc(verdict.justification)}
` : ''}
${MOCK ? '
🧪 Resultado SIMULADO (pruebas, sin modelo real)
' : ''}`;
document.querySelector('#mech-metrics')?.remove();
document.querySelector('#mock-note')?.remove();
if (MOCK) $('#score-table').parentElement.insertAdjacentHTML('beforebegin', mockNoteHtml());
if (heur) {
// sin evaluación real no se presentan notas por criterio: solo "no disponible" + métricas mecánicas separadas
$('#score-table').innerHTML = `
${m.simulated ? '🧪 Resultado simulado (pruebas, sin modelo real)' : m.heuristic ? '⚠️ sin evaluación fiable (no cuenta para el Hall)' : m.inconsistent ? '⚠️ veredicto inconsistente (no cuenta para el Hall)' : (m.mode === 'tournament' ? '🏆 Campeón: ' + (w ? esc(w.name) : '—') : m.winnerTeam ? '🛡️ Equipo ' + m.winnerTeam : '👑 ' + (w ? esc(w.name) : 'Empate'))}
`;
}).join('') : '
Aún no hay batallas guardadas.
';
document.querySelectorAll('[data-replay]').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); startReplay(b.dataset.replay); }));
}
/* ================= REPLAY ================= */
async function startReplay(matchId) {
const m = matches.find(x => x.id === matchId);
if (!m) return;
const ps = m.participants.map(agentById).filter(Boolean).map(a => m.teams && m.teams[a.id] ? { ...a, team: m.teams[a.id] } : a);
battle = { id: m.id, topic: m.topic, mode: m.mode, rounds: m.rounds, participants: ps, events: [], resume: { step: 0 } };
showView('battle');
renderPhaseTrack('verdict');
renderFighters(null);
$('#battle-log').innerHTML = '';
addSystemMsg(`▶ REPLAY — ${m.topic} (${new Date(m.date).toLocaleString('es')})${m.simulated ? ' · 🧪 SIMULADO (pruebas, sin modelo real)' : ''}`);
if (replayTimer) clearInterval(replayTimer);
let i = 0, lastMatch = null;
replayTimer = setInterval(() => {
if (i >= m.events.length) {
clearInterval(replayTimer); replayTimer = null;
const w = m.winner ? agentById(m.winner) : null;
addSystemMsg((m.mode === 'tournament' ? (w ? `🏆 Campeón: ${w.name}` : '⚠️ Torneo sin campeón fiable') : m.winnerTeam ? `🛡️ Ganador: Equipo ${m.winnerTeam}` : (w ? `👑 Ganador: ${w.name}` : '🤝 Empate')) + (m.simulated ? ' · 🧪 SIMULADO (pruebas)' : ''));
return;
}
const ev = m.events[i++];
if (m.mode === 'tournament' && ev.match !== undefined && ev.match !== lastMatch) {
lastMatch = ev.match;
const tm = m.tournament && m.tournament[ev.match];
if (tm) addSystemMsg(`🏟️ ${tm.name}`);
}
const a = ev.agentId === 'user' ? { name: 'Tú', emoji: '⚡', color: '#8b5cf6' } : ps.find(x => x.id === ev.agentId) || agentById(ev.agentId) || { name: ev.agent, emoji: '🤖', color: '#888' };
addEvent(a, ev.phase, ev.content, ev.round, ev.agentId === 'user');
}, 1100);
}
/* ================= ROADMAP ================= */
function renderRoadmap() {
const phases = [
{ n: 1, name: 'Arena Foundation', tag: 'done', items: ['Estructura modular', 'Integración con el runtime (wllama)', 'Agent Manager', 'Arena State Manager', 'Sistema de turnos y fases', 'Modalidad 1 vs 1'] },
{ n: 2, name: 'Battle Experience', tag: 'done', items: ['Arena Lobby', 'Selección visual de agentes', 'Live Battle con streaming', 'Crossfire y refutaciones', 'Participación humana (Challenge Agent)'] },
{ n: 3, name: 'Intelligence System', tag: 'partial', items: ['Tres jueces independientes + Head Judge', 'Puntuaciones ponderadas', 'Informe de resultados', 'Referee AI: detector de repeticiones, evasiones, fuera de tema e incoherencias (implementado; validado en pruebas simuladas, pendiente prueba real)'] },
{ n: 4, name: 'Tournament Mode', tag: 'partial', items: ['Free For All', 'Team Battle (2v2, 3v3, 4v4 con veredicto por equipos)', 'Survival Tournament (eliminatorias de 4 u 8 con campeón)', 'Rematches con estrategia revisada (implementado; validado en pruebas simuladas, pendiente prueba real)'] },
{ n: 5, name: 'Memory & Replay', tag: 'partial', items: ['Historial de competiciones (IndexedDB + caché localStorage)', 'Replay de batallas', 'Implementado (validado en pruebas simuladas, pendiente prueba real): IndexedDB, exportación JSON/Markdown, memoria persistente de agentes. Pendiente: exportación HTML/PDF'] },
{ n: 6, name: 'Premium Experience', tag: 'todo', items: ['Animaciones y sonidos', 'Efectos de competición', 'Logros (9 implementados, solo se otorgan con evaluación real y consistente; validado en pruebas simuladas, pendiente prueba real)', 'Personalización avanzada', 'Optimización WebGPU', 'Multi-Model Arena'] },
];
const labels = { done: 'DISPONIBLE', partial: 'PARCIAL', todo: 'PENDIENTE' };
$('#roadmap-list').innerHTML = phases.map(p => `
Fase ${p.n} — ${p.name} ${labels[p.tag]}
${p.items.map(i => `
${esc(i)}
`).join('')}
`).join('');
}
/* ================= INIT ================= */
renderLobby();
renderRoster();
if (params.get('topic')) $('#topic').value = params.get('topic');
// gancho de pruebas (solo modo mock): inspección de estado interno
IDB.init();
if (MOCK) window.__arenaTest = { setBattle: b => { battle = b; }, getBattle: () => battle, agents: () => agents, stats: () => stats, finalizeBattle, applyAwardsAndMilestones, evaluationQualityOK, agentMemory };
updateModelInUse();