Tu primer juego con un LLM, y cómo saber si está bien#
Esta guía es para alguien que recién empieza a programar y programa con un modelo de lenguaje. Un modelo de lenguaje (o LLM, por sus siglas en inglés) es una inteligencia artificial generativa, como ChatGPT, Claude o Gemini, que puede escribir código a partir de tus pedidos en lenguaje cotidiano. Vas a armar, copiando y pegando código en archivos de texto simples en tu computadora, una batalla naval que corre en el navegador web (como Chrome, Firefox o Edge). «Corre en el navegador» significa que no necesitás instalar programas raros para jugar: funciona directamente como una página web en tu máquina al hacerle doble clic. Y vas a aprender a contestar la pregunta que el juego solo no puede contestar: ¿cumple las reglas de verdad?
Para vigilar las reglas creamos Oracle: un programa independiente que examina lo que pasó en el juego sin juzgarse a sí mismo. A las partidas de prueba (tanto las limpias como las que cometen trampas para ver si el sistema las nota) las llamamos casos. Y a las versiones de tus reglas alteradas a propósito con trampas para probar si tus defensas son sólidas las llamamos mutantes.
La guía es una travesía de nueve misiones. Cada una tiene la misma forma: una pregunta para que la pienses vos, la respuesta plegada (abrila cuando quieras), el bloque para copiar, y lo que tenés que ver. Si ves otra cosa, el paso te dice qué hacer.
En el sitio, además, se juega: antes de cada salida predecís qué va a decir Oracle, en la misión 5 rompés la regla en un tablero, y en la 6 cazás los mutantes de tu regla eligiendo casos. Cada cosa suma experiencia y algunas dan logros; la bitácora, abajo a la derecha, lleva la cuenta. Todo queda en tu navegador: no se manda a ningún lado.
El test de esta guía reconstruye los archivos en una carpeta temporal vacía y comprueba los comandos de Oracle y sus salidas. No instala las herramientas ni prueba los clics en tu navegador. Los tiempos de ejecución y algunos mensajes de instalación o de Git pueden variar en tu computadora.
Antes de empezar#
Antes de arrancar con la primera misión, necesitás tener lista tu computadora. No hace falta que sepas nada previo: acá tenés el paso a paso para preparar las herramientas.
Qué es una terminal y cómo se abre#
Una terminal (también llamada consola o línea de comandos) es una ventana donde le das órdenes directas a la computadora escribiendo texto con el teclado, en vez de hacer clics con el ratón. Cada línea de texto que escribís es un comando; cuando presionás la tecla Enter, la computadora ejecuta esa orden y te muestra el resultado en la pantalla.
Cómo abrirla según tu sistema operativo:
- En Windows: presioná la tecla Windows, escribí
Terminal(oPowerShell) y presioná Enter. Tip: en la terminal de Windows, si querés pegar texto yCtrl + Vno responde, hacé clic derecho adentro de la ventana negra para pegar. - En macOS: presioná las teclas
Cmd + Barra espaciadora, escribíTerminaly presioná Enter. - En Linux: presioná
Ctrl + Alt + To buscáTerminalen tu menú de aplicaciones.
Cómo moverte entre carpetas desde la terminal:
- Al abrir la terminal, estás parado en tu carpeta personal de usuario (por ejemplo
C:\Users\TuNombreen Windows o/home/tunombreen Linux). - Para saber en qué carpeta estás en cualquier momento, escribí
pwdy presioná Enter (en Windows PowerShell también funcionapwd). - Para entrar a una carpeta escribí
cd nombre-carpeta(cdsignifica change directory, cambiar directorio) y presioná Enter. - Para salir de la carpeta actual y retroceder a la carpeta anterior, escribí
cd ..y presioná Enter.
Qué instalar y cómo#
Necesitás tener instalados estos cuatro programas gratuitos:
- Python (versión 3.11 o posterior): es el lenguaje en el que está programado Oracle. Descargalo gratis desde python.org.
- Paso crítico en Windows: al iniciar el instalador, antes de tocar cualquier botón, marcá la casilla que dice «Add python.exe to PATH» abajo de todo. Si no la marcás, tu terminal no va a reconocer los comandos de Python.
uv: es una herramienta moderna y ultrarrápida para instalar y ejecutar aplicaciones de Python. Para instalarlo, abrí tu terminal y ejecutá el comando que corresponda a tu sistema:- En macOS y Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh - En Windows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" - Si preferís ver la guía oficial, consultá docs.astral.sh/uv.
- En macOS y Linux:
- Git: es un programa que guarda versiones de los archivos de tu proyecto para poder comparar cambios y volver a una versión anterior. Ese historial queda en tu computadora. Descargalo e instalalo desde git-scm.com manteniendo todas las opciones recomendadas por defecto.
- Un editor de texto para código: para crear y guardar los archivos del juego necesitás un editor de texto plano. Nunca uses Microsoft Word ni WordPad: agregan formatos invisibles y comillas tipográficas que rompen el código de programación. Te recomendamos descargar gratis Visual Studio Code. Si no querés instalar un editor nuevo, podés usar el Bloc de Notas de Windows (o TextEdit en Mac configurado en texto plano).
Cómo comprobar que quedó todo instalado#
Abrí una ventana nueva de la terminal (para que tome las instalaciones recientes) y escribí estos tres comandos, presionando Enter después de cada uno:
python --version(opython3 --versionen Mac y Linux): tiene que responderte conPython 3.11o un número mayor.uv --version: tiene que mostrar la versión instalada deuv.git --version: tiene que mostrar la versión instalada degit.
Si alguno te dice «comando no encontrado» (command not found), cerrá la terminal y volvé a abrirla. Si sigue sin reconocerlo, reinstalá ese programa asegurándote de marcar la opción de agregarlo al PATH del sistema.
Cómo crear carpetas, archivos y guardarlos con el nombre exacto#
- Cómo se leen las rutas con barras: cuando veas nombres como
css/style.cssocorpus/naval/005-barco.caso, la barra inclinada/significa que el archivo va adentro de una subcarpeta. Por ejemplo, paracss/style.css, primero creás la carpetacssy adentro guardás el archivostyle.css. - Cómo crear carpetas: podés crearlas haciendo clic derecho en tu explorador de archivos («Nueva carpeta») o desde la terminal escribiendo
mkdir nombre-carpetay presionando Enter. - Extensiones de archivo y nombres exactos: cada archivo de código termina en un punto y su tipo (
.html,.css,.js,.json,.caso,.oracle,.requisito). El nombre tiene que ser exactamente el indicado en la guía, con mayúsculas y minúsculas respetadas. - Cuidado en Windows con las extensiones ocultas: por defecto, Windows oculta las extensiones. En el Explorador de archivos andá a la pestaña Ver (o Ver -> Mostrar) y marcá la casilla «Extensiones de nombre de archivo». Si usás el Bloc de Notas para guardar un archivo, en la ventana de «Guardar como» seleccioná en el menú Tipo: «Todos los archivos (.)» antes de guardar. Si dejás «Documentos de texto (*.txt)», Windows le va a agregar
.txtal final (por ejemploindex.html.txt) y el juego no va a andar.
Cómo se leen los bloques de esta guía#
En cada misión vas a ver estos tipos de bloques:
- Bloque rotulado
bash: son comandos para la terminal. Copiás el texto del recuadro, lo pegás adentro de tu terminal y presionás Enter. Si el bloque tiene varias líneas, podés pegarlas juntas o escribirlas de a una. - Bloque rotulado con una ruta, como
css/style.css: indica código que tenés que guardar. Abrís tu editor de texto, copiás todo el contenido del bloque, lo pegás y lo guardás con el nombre y la ruta que figuran en el encabezado. - Bloque «lo que tenés que ver»: muestra la salida esperada de la terminal; no lo copies como comando.
- Bloque «así tiene que quedar tu carpeta»: muestra dónde va cada archivo. Comparalo con tus carpetas; las líneas y ramificaciones son un dibujo, no texto para pegar en un archivo.
- «Si ves otra cosa»: si en tu pantalla aparece un mensaje de error o algo distinto a la salida esperada, este apartado te explica qué pasó y cómo destrabarlo.
Paso 1 · Preparar la carpeta#
Oracle se instala una sola vez. Con Python 3.11+ y uv ya instalados en tu computadora (como comprobamos en «Antes de empezar»), abrí tu terminal y ejecutá este bloque de comandos. Podés copiarlos y pegarlos juntos, o escribir cada línea presionando Enter:
uv tool install oracle-metalenguajedescarga e instala la herramienta Oracle en tu sistema.oracle init batalla-navalcrea una carpeta nueva llamadabatalla-navalen el lugar donde esté parada tu terminal (en tu carpeta de usuario).cd batalla-navalte mete adentro de esa carpeta (cdsignifica change directory, cambiar directorio) para que todos los comandos siguientes se ejecuten allí.oracle testcorre una primera revisión del proyecto recién nacido.
uv tool install oracle-metalenguaje
oracle init batalla-naval
cd batalla-naval
oracle test
Proyecto Oracle inicializado en batalla-naval:
· catalogos/
· corpus/
· diferencial/
· relaciones/
· oracle.json
Próximos pasos:
1. Creá un caso: oracle caso <grupo/id>
2. Creá una medida: oracle nueva <dominio.nombre>
3. Verificá todo: oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS: sin casos guardados para verificar
SINTAXIS: salteado (sin medidas ni casos todavía)
ACEPTACIÓN: salteado (sin medidas ni casos todavía)
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
MUTACIÓN: salteada (sin medidas todavía)
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
PRODUCTO: sin nueva medición; no se ejecutó el producto.
VEREDICTO: SIN MEDICIÓN (advertencia: proyecto vacío: 0 medidas propias, 0 casos, 0 fixtures diferenciales)
batalla-naval/
├── catalogos/
├── corpus/
├── diferencial/
├── relaciones/
└── oracle.json
Lo que tenés que ver. En la terminal vas a ver varias líneas técnicas que terminan en VEREDICTO: SIN MEDICIÓN. No te asustes por las líneas que dicen «salteado» ni por la «advertencia: proyecto vacío»: como el proyecto recién empieza y todavía no tiene ninguna regla, es exactamente lo que tiene que pasar. Antes pueden aparecer mensajes de instalación de uv. En «Proyecto Oracle inicializado», tu terminal muestra la ruta completa de tu carpeta; acá la abreviamos como batalla-naval.
Si ves otra cosa. Si dice oracle: command not found (orden no encontrada), ejecutá uv tool update-shell para agregar la carpeta de herramientas al PATH (la lista de carpetas donde el sistema busca programas ejecutables). Cerrá la terminal y abrí una nueva. Si todavía no se creó batalla-naval, volvé a ejecutar oracle init batalla-naval; después ejecutá cd batalla-naval y oracle test. Si la carpeta ya existe, entrá con cd batalla-naval y ejecutá oracle test sin repetir la creación.
¿Por qué dice «sin medición» y no «verde»?
Porque todavía no hay ninguna regla. Un verde sin reglas diría «está todo bien» sin haber mirado nada. Oracle prefiere decir lo que pasó: no midió.
Una cosa más antes de seguir. Un catálogo es la carpeta donde se organizan todas las reglas de tu proyecto. Al crearlo, oracle init deja activado el catálogo base: una colección de reglas internas de Oracle sobre tus propias reglas. Son muy exigentes (piden unidades declaradas y evidencia observada de partidas reales) y las vamos a encender al final de la guía. Por ahora, las apagamos para avanzar paso a paso.
Para apagarlas vamos a modificar oracle.json. Un archivo JSON es un formato de texto estándar que guarda datos organizados entre llaves {} con nombres y valores legibles. (En proyectos avanzados, si una regla falla pero no la podés arreglar ya, también podés declararla en sombra en este archivo: una excepción temporal con fecha límite que avisa del problema sin voltear la corrida completa; por ahora sólo apagamos el catálogo base cambiando su valor a false).
Buscá el archivo oracle.json adentro de la carpeta batalla-naval, abrilo con tu editor de texto (o Bloc de Notas), reemplazá todo su contenido por este bloque tal cual y guardalo:
{
"esquema": "oracle.proyecto/v1",
"catalogo_base": false,
"perfiles": []
}
Paso 2 · El tablero#
Un juego en el navegador se compone de tres cosas: HTML (la estructura y el contenido que hay en la pantalla), CSS (los colores, tamaños y cómo se ve) y JavaScript (qué pasa cuando hacés clic o interactuás). Empezamos creando los dos primeros archivos en tu carpeta batalla-naval.
Abrí el desplegable que sigue, copiá todo el código, pegalo en un archivo nuevo en tu editor y guardalo en la carpeta principal batalla-naval con el nombre exacto index.html (si usás el Bloc de Notas, acordate de poner en Tipo: «Todos los archivos (.)» para que no termine guardado como index.html.txt):
Ver el archivo index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batalla Naval · HTML5 con Auditoría Formal</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- Encabezado de la Sala de Operaciones Navales -->
<header>
<div class="brand">
<div class="radar-logo"></div>
<div class="brand-text">
<h1>Batalla <span>Naval</span></h1>
<div class="tagline">Tácticas Navales · Auditoría Formal con Oracle</div>
</div>
</div>
<div class="header-controls">
<button id="btnOpenAudit" class="btn btn-accent" title="Verificar formalmente las reglas sobre la traza">
<span>⚖</span> Auditoría & Traza Oracle
</button>
<button id="btnToggleSound" class="btn" title="Activar/desactivar efectos sonoros">
🔊 Sonido
</button>
<button id="btnNewGame" class="btn btn-danger" title="Reiniciar y comenzar una nueva batalla">
↻ Nueva Partida
</button>
</div>
</header>
<!-- Contenedor Principal -->
<main>
<!-- Barra de Estado / Turnos / Métricas -->
<section class="status-banner">
<div class="status-info">
<div id="statusIndicator" class="status-indicator"></div>
<div class="status-text">
<h2 id="statusTitle">Fase de Despliegue</h2>
<p id="statusSubtitle">Ubicá tus 5 buques en el tablero o pulsá "Auto-Desplegar"</p>
</div>
</div>
<div class="game-metrics">
<div class="metric-item">
<span class="metric-label">Turno</span>
<span id="metricTurns" class="metric-value">0</span>
</div>
<div class="metric-item">
<span class="metric-label">Impactos</span>
<span id="metricHits" class="metric-value">0 / 17</span>
</div>
<div class="metric-item">
<span class="metric-label">Precisión</span>
<span id="metricAccuracy" class="metric-value">0%</span>
</div>
</div>
</section>
<!-- Herramientas para la fase de colocación de buques -->
<section id="placementTools" class="placement-tools">
<div class="placement-header">
<h3>Despliegue de la Flota Aliada</h3>
<div class="placement-actions">
<button id="btnRotate" class="btn" title="Rotar orientación del barco (tecla R)">
Rotar [R]: <strong id="orientationBadge">HORIZONTAL</strong>
</button>
<button id="btnAutoPlace" class="btn" title="Colocar todos los barcos al azar">
🎲 Auto-Desplegar
</button>
<button id="btnClearPlacement" class="btn" title="Limpiar todas las posiciones">
Limpiar
</button>
<button id="btnStartBattle" class="btn btn-primary" disabled title="Comenzar el combate">
⚔ Iniciar Batalla
</button>
</div>
</div>
<div id="shipsDock" class="ships-dock">
<!-- Generado dinámicamente por ui.js -->
</div>
</section>
<!-- Zona de Batalla: Tableros Navales y Paneles -->
<section class="battlefield">
<!-- Tablero Aliado (Tu Flota) -->
<div class="board-card">
<div class="board-header">
<div class="board-title">
<span>🛡 Tu Flota</span>
</div>
<span class="badge badge-player">Defensa</span>
</div>
<div class="grid-container">
<table id="playerGrid" class="grid-table player-grid">
<!-- Celdas generadas por JS -->
</table>
</div>
</div>
<!-- Tablero Hostil (Radar Enemigo) -->
<div class="board-card enemy-card">
<div class="board-header">
<div class="board-title">
<span>🎯 Radar Enemigo</span>
</div>
<span class="badge badge-enemy">Ataque</span>
</div>
<div class="grid-container">
<table id="enemyGrid" class="grid-table enemy-grid">
<!-- Celdas generadas por JS -->
</table>
</div>
</div>
<!-- Panel Lateral: Rastreador de Barcos y Bitácora -->
<aside class="side-panel">
<div class="side-card">
<h3>Estado de Flotas</h3>
<div class="fleet-roster">
<div class="roster-group-title">Enemigo (Objetivos)</div>
<div id="cpuRoster"></div>
<div class="roster-group-title" style="margin-top: 10px;">Aliados</div>
<div id="playerRoster"></div>
</div>
</div>
<div class="side-card">
<h3>Bitácora de Combate</h3>
<div id="combatLog" class="combat-log">
<!-- Entradas de registro -->
</div>
</div>
</aside>
</section>
</main>
<!-- Modal de Auditoría Formal de Reglas con Oracle -->
<div id="auditModal" class="modal-overlay">
<div class="modal-container">
<div class="modal-header">
<h3><span>⚖</span> Auditoría Formal de Reglas (Oracle Metalenguaje)</h3>
<button id="btnCloseAudit" class="modal-close">×</button>
</div>
<div class="modal-body">
<div class="modal-tabs">
<button class="tab-btn active" data-tab="tabRules">11 Medidas Formales</button>
<button class="tab-btn" data-tab="tabJson">Traza de Hechos (`hechos.json`)</button>
<button class="tab-btn" data-tab="tabCli">Verificar con CLI</button>
</div>
<!-- Pestaña 1: Medidas Formales Evaluadas -->
<div id="tabRules" class="tab-content active">
<p style="font-size: 0.82rem; color: var(--text-muted);">
Cada movimiento de la partida es registrado de forma inmutable. Estas 11 medidas formales
garantizan que ninguna regla del juego sea violada (espacialidad, alternancia, no-duplicación, veracidad de impacto y hundimiento).
</p>
<div id="rulesList" class="rules-list">
<!-- Generado dinámicamente por trace.js / ui.js -->
</div>
</div>
<!-- Pestaña 2: Traza JSON en Crudo -->
<div id="tabJson" class="tab-content">
<p style="font-size: 0.82rem; color: var(--text-muted);">
Esta es la evidencia relacional exacta producida por el juego, compuesta por las relaciones:
<code>celda_barco</code>, <code>tiro</code> y <code>partida</code>.
</p>
<pre id="jsonViewer" class="json-viewer"></pre>
</div>
<!-- Pestaña 3: Instrucciones de Verificación CLI con Oracle -->
<div id="tabCli" class="tab-content">
<div class="cli-instruction">
<strong>1. Descargá el archivo de evidencia:</strong>
<span>Pulsá el botón "Descargar hechos.json" abajo para guardar la traza de la partida.</span>
</div>
<div class="cli-instruction">
<strong>2. Ejecutá el evaluador independiente de Oracle en tu terminal:</strong>
<div class="cli-code">oracle juzgar --con hechos_partida.json</div>
<span>Oracle evaluará las 11 medidas del catálogo sobre tu archivo y emitirá el veredicto formal independiente con sus testigos y alcances.</span>
</div>
<div class="cli-instruction">
<strong>3. O corré el script de auditoría completa del proyecto:</strong>
<div class="cli-code">python3 verificar_oraculo.py</div>
<span>Ejecuta una partida legítima y además prueba que el oráculo detecta infracciones (falsabilidad).</span>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btnCopyJson" class="btn">Copiar JSON</button>
<button id="btnDownloadJson" class="btn btn-primary">⬇ Descargar hechos_partida.json</button>
</div>
</div>
</div>
<!-- Modal de Fin de Partida -->
<div id="gameOverModal" class="modal-overlay">
<div class="modal-container" style="max-width: 500px;">
<div class="gameover-modal-content">
<h2 id="gameOverTitle" class="gameover-title win">¡VICTORIA NAVAL!</h2>
<p style="color: var(--text-muted); font-size: 0.88rem;">
El combate ha concluido. Todas las operaciones han quedado registradas en la traza relacional.
</p>
<div class="gameover-stats">
<div class="gameover-stat-box">
<div id="goWinner" class="gameover-stat-val">JUGADOR</div>
<div class="gameover-stat-lbl">Ganador</div>
</div>
<div class="gameover-stat-box">
<div id="goTurns" class="gameover-stat-val">42</div>
<div class="gameover-stat-lbl">Turnos Totales</div>
</div>
<div class="gameover-stat-box">
<div id="goAccuracy" class="gameover-stat-val">48%</div>
<div class="gameover-stat-lbl">Precisión</div>
</div>
</div>
<div style="display: flex; gap: 10px; width: 100%; justify-content: center; margin-top: 10px;">
<button id="btnVerifyFromGameOver" class="btn btn-accent" style="flex: 1;">
⚖ Auditar con Oracle
</button>
<button id="btnRestart" class="btn btn-primary" style="flex: 1;">
↻ Jugar de Nuevo
</button>
</div>
</div>
</div>
</div>
<!-- Scripts del Juego -->
<script src="js/audio.js"></script>
<script src="js/trace.js"></script>
<script src="js/engine.js"></script>
<script src="js/ui.js"></script>
</body>
</html>
Ahora creá los estilos visuales. Adentro de la carpeta batalla-naval, creá una subcarpeta llamada css. Después abrí el siguiente desplegable, copiá el código y guardalo adentro de esa carpeta css con el nombre style.css (la ruta completa te tiene que quedar css/style.css):
Ver el archivo css/style.css
/* ==========================================================================
style.css - Batalla Naval: Tactical Naval Operations Theme
========================================================================== */
:root {
--bg-primary: #0a0f1d;
--bg-surface: #111a2e;
--bg-card: #16223b;
--bg-card-hover: #1c2b4a;
--border-color: #233554;
--border-active: #00f0ff;
--accent-cyan: #00f0ff;
--accent-blue: #0077b6;
--accent-green: #00f59b;
--accent-red: #ff3366;
--accent-yellow: #ffb703;
--accent-orange: #fb8500;
--text-main: #e2e8f0;
--text-muted: #8da2c0;
--text-bright: #ffffff;
--cell-size: 34px;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--radius: 8px;
--shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
--glow-cyan: 0 0 12px rgba(0, 240, 255, 0.45);
--glow-red: 0 0 12px rgba(255, 51, 102, 0.5);
--glow-green: 0 0 12px rgba(0, 245, 155, 0.45);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-primary);
color: var(--text-main);
font-family: var(--font-sans);
min-height: 100vh;
display: flex;
flex-direction: column;
background-image:
radial-gradient(circle at 50% 0%, rgba(0, 119, 182, 0.15) 0%, transparent 70%),
linear-gradient(rgba(10, 15, 29, 0.96), rgba(10, 15, 29, 0.98));
overflow-x: hidden;
}
/* Header */
header {
background: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
position: sticky;
top: 0;
z-index: 100;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.radar-logo {
width: 38px;
height: 38px;
border-radius: 50%;
border: 2px solid var(--accent-cyan);
position: relative;
box-shadow: var(--glow-cyan);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.radar-logo::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: conic-gradient(from 0deg, transparent 75%, rgba(0, 240, 255, 0.4) 100%);
animation: radar-sweep 2.5s linear infinite;
border-radius: 50%;
}
@keyframes radar-sweep {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.brand-text h1 {
font-size: 1.3rem;
letter-spacing: 2px;
font-weight: 800;
color: var(--text-bright);
text-transform: uppercase;
}
.brand-text h1 span {
color: var(--accent-cyan);
}
.brand-text .tagline {
font-size: 0.75rem;
color: var(--text-muted);
font-family: var(--font-mono);
}
.header-controls {
display: flex;
align-items: center;
gap: 10px;
}
/* Botones */
.btn {
background: var(--bg-card);
color: var(--text-main);
border: 1px solid var(--border-color);
padding: 8px 14px;
border-radius: var(--radius);
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover {
background: var(--bg-card-hover);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
box-shadow: var(--glow-cyan);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-blue), #0096c7);
border-color: var(--accent-cyan);
color: var(--text-bright);
}
.btn-primary:hover {
background: linear-gradient(135deg, #0096c7, var(--accent-cyan));
color: #000;
}
.btn-accent {
background: rgba(0, 245, 155, 0.15);
border-color: var(--accent-green);
color: var(--accent-green);
}
.btn-accent:hover {
background: var(--accent-green);
color: #000;
box-shadow: var(--glow-green);
}
.btn-danger {
background: rgba(255, 51, 102, 0.15);
border-color: var(--accent-red);
color: var(--accent-red);
}
.btn-danger:hover {
background: var(--accent-red);
color: #fff;
box-shadow: var(--glow-red);
}
.btn-icon {
padding: 8px 10px;
}
/* Contenedor Principal */
main {
flex: 1;
max-width: 1300px;
width: 100%;
margin: 0 auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Barra de Estado / Turno */
.status-banner {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
}
.status-banner::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--accent-cyan);
box-shadow: var(--glow-cyan);
}
.status-info {
display: flex;
align-items: center;
gap: 14px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: var(--glow-green);
animation: pulse-dot 1.8s infinite;
}
.status-indicator.waiting {
background: var(--accent-yellow);
box-shadow: 0 0 10px rgba(255, 183, 3, 0.5);
}
.status-indicator.cpu {
background: var(--accent-red);
box-shadow: var(--glow-red);
}
@keyframes pulse-dot {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.25); opacity: 0.7; }
}
.status-text h2 {
font-size: 1.05rem;
font-weight: 700;
color: var(--text-bright);
}
.status-text p {
font-size: 0.8rem;
color: var(--text-muted);
}
.game-metrics {
display: flex;
gap: 18px;
font-family: var(--font-mono);
font-size: 0.8rem;
}
.metric-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.metric-label {
color: var(--text-muted);
font-size: 0.7rem;
text-transform: uppercase;
}
.metric-value {
color: var(--accent-cyan);
font-weight: 700;
}
/* Área de Juego: Tableros y Paneles */
.battlefield {
display: grid;
grid-template-columns: auto auto 320px;
gap: 20px;
align-items: start;
justify-content: center;
}
@media (max-width: 1180px) {
.battlefield {
grid-template-columns: 1fr 1fr;
}
.side-panel {
grid-column: 1 / -1;
}
}
@media (max-width: 820px) {
.battlefield {
grid-template-columns: 1fr;
}
}
/* Tarjetas de Tablero */
.board-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 16px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
align-items: center;
}
.board-card.active-target {
border-color: var(--accent-cyan);
box-shadow: var(--glow-cyan);
}
.board-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.board-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.badge {
font-size: 0.65rem;
padding: 2px 8px;
border-radius: 12px;
font-weight: 700;
font-family: var(--font-mono);
text-transform: uppercase;
}
.badge-player {
background: rgba(0, 119, 182, 0.3);
color: var(--accent-cyan);
border: 1px solid var(--accent-cyan);
}
.badge-enemy {
background: rgba(255, 51, 102, 0.2);
color: var(--accent-red);
border: 1px solid var(--accent-red);
}
/* Grilla Naval 10x10 */
.grid-container {
display: inline-block;
background: #0d1527;
padding: 10px;
border-radius: 6px;
border: 1px solid #1a2948;
}
.grid-table {
border-collapse: collapse;
}
.grid-label {
width: var(--cell-size);
height: 22px;
text-align: center;
vertical-align: middle;
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
color: var(--text-muted);
}
.grid-label-row {
width: 22px;
height: var(--cell-size);
text-align: center;
vertical-align: middle;
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
color: var(--text-muted);
}
.grid-cell {
width: var(--cell-size);
height: var(--cell-size);
border: 1px solid #1b2845;
background-color: #0f192c;
position: relative;
cursor: default;
transition: all 0.15s ease;
user-select: none;
}
/* Celdas del Radar Enemigo (Interactivas) */
.enemy-grid .grid-cell {
cursor: crosshair;
}
.enemy-grid .grid-cell:hover:not(.shot) {
background-color: rgba(0, 240, 255, 0.2);
border-color: var(--accent-cyan);
box-shadow: inset 0 0 8px rgba(0, 240, 255, 0.4);
}
/* Estados de Casilla */
.grid-cell.ship-present {
background-color: #2a3d5e;
border-color: #3b537e;
}
.grid-cell.ship-present::before {
content: '';
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: #3e5884;
border-radius: 3px;
border: 1px solid #5a7ea8;
}
.grid-cell.shot.miss {
background-color: #0b1424;
}
.grid-cell.shot.miss::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
transform: translate(-50%, -50%);
background-color: #48cae4;
border-radius: 50%;
box-shadow: 0 0 6px rgba(72, 202, 228, 0.8);
animation: splash-pop 0.3s ease-out;
}
.grid-cell.shot.hit {
background-color: rgba(255, 51, 102, 0.25);
border-color: var(--accent-red);
}
.grid-cell.shot.hit::after {
content: '✕';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--accent-red);
font-weight: 900;
font-size: 1.1rem;
text-shadow: 0 0 8px var(--accent-red);
animation: hit-shake 0.3s ease-in-out;
}
.grid-cell.sunk {
background-color: rgba(255, 51, 102, 0.45) !important;
border-color: #ff0044 !important;
animation: sunk-flash 0.8s ease-in-out;
}
/* Hover de colocación de barcos */
.grid-cell.placement-valid {
background-color: rgba(0, 245, 155, 0.35) !important;
border-color: var(--accent-green) !important;
}
.grid-cell.placement-invalid {
background-color: rgba(255, 51, 102, 0.35) !important;
border-color: var(--accent-red) !important;
}
@keyframes splash-pop {
0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
60% { transform: translate(-50%, -50%) scale(1.4); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
@keyframes hit-shake {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
25% { transform: translate(-45%, -55%) scale(1.3); }
50% { transform: translate(-55%, -45%) scale(1.2); }
75% { transform: translate(-48%, -52%) scale(1.1); }
}
@keyframes sunk-flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Panel de Preparación / Colocación */
.placement-tools {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 14px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.placement-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.placement-header h3 {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-bright);
}
.ships-dock {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.ship-dock-item {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.2s;
min-width: 110px;
}
.ship-dock-item:hover:not(.placed) {
border-color: var(--accent-cyan);
background: var(--bg-card-hover);
}
.ship-dock-item.selected {
border-color: var(--accent-cyan);
background: rgba(0, 240, 255, 0.1);
box-shadow: var(--glow-cyan);
}
.ship-dock-item.placed {
opacity: 0.45;
border-color: #2a3a5a;
cursor: not-allowed;
}
.ship-dock-name {
font-size: 0.8rem;
font-weight: 600;
display: flex;
justify-content: space-between;
}
.ship-dock-name span {
color: var(--text-muted);
font-family: var(--font-mono);
}
.ship-preview-blocks {
display: flex;
gap: 3px;
}
.preview-block {
width: 14px;
height: 14px;
background: #3e5884;
border-radius: 2px;
}
.placement-actions {
display: flex;
gap: 10px;
align-items: center;
}
/* Panel Lateral: Rastreador de Flotas & Bitácora */
.side-panel {
display: flex;
flex-direction: column;
gap: 14px;
}
.side-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 14px;
box-shadow: var(--shadow);
}
.side-card h3 {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-bright);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Rastreador de Barcos */
.fleet-roster {
display: flex;
flex-direction: column;
gap: 8px;
}
.roster-group-title {
font-size: 0.7rem;
font-family: var(--font-mono);
color: var(--text-muted);
text-transform: uppercase;
margin-top: 4px;
}
.roster-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.78rem;
padding: 4px 8px;
background: var(--bg-card);
border-radius: 4px;
border: 1px solid #1a2742;
}
.roster-item.sunk {
opacity: 0.4;
text-decoration: line-through;
border-color: var(--accent-red);
}
.roster-blocks {
display: flex;
gap: 3px;
}
.roster-block {
width: 8px;
height: 8px;
border-radius: 1px;
background: var(--accent-green);
}
.roster-block.hit {
background: var(--accent-red);
}
/* Bitácora de Combate */
.combat-log {
height: 220px;
overflow-y: auto;
background: #0b1120;
border: 1px solid #18233c;
border-radius: 6px;
padding: 8px;
font-family: var(--font-mono);
font-size: 0.75rem;
display: flex;
flex-direction: column;
gap: 4px;
}
.combat-log::-webkit-scrollbar {
width: 5px;
}
.combat-log::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
.log-entry {
padding: 4px 6px;
border-radius: 3px;
line-height: 1.3;
}
.log-entry.player-shot {
border-left: 2px solid var(--accent-cyan);
background: rgba(0, 240, 255, 0.05);
}
.log-entry.cpu-shot {
border-left: 2px solid var(--accent-red);
background: rgba(255, 51, 102, 0.05);
}
.log-entry.sunk-event {
border-left: 2px solid var(--accent-yellow);
background: rgba(255, 183, 3, 0.1);
font-weight: 700;
color: var(--accent-yellow);
}
.log-turn {
color: var(--text-muted);
margin-right: 4px;
}
/* Modales */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(5, 8, 16, 0.85);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.modal-overlay.open {
opacity: 1;
pointer-events: auto;
}
.modal-container {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 10px;
width: 100%;
max-width: 820px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
animation: modal-enter 0.25s ease-out;
}
@keyframes modal-enter {
0% { transform: scale(0.95); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 1.15rem;
color: var(--text-bright);
display: flex;
align-items: center;
gap: 10px;
}
.modal-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.3rem;
cursor: pointer;
line-height: 1;
}
.modal-close:hover {
color: var(--text-bright);
}
.modal-body {
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-tabs {
display: flex;
gap: 8px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
.tab-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-family: var(--font-sans);
font-size: 0.88rem;
font-weight: 600;
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background: var(--bg-card);
color: var(--accent-cyan);
border: 1px solid var(--border-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: flex;
flex-direction: column;
gap: 14px;
}
/* Auditoría de Reglas Lista */
.rules-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.rule-item {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.rule-item.passed {
border-left: 3px solid var(--accent-green);
}
.rule-item.failed {
border-left: 3px solid var(--accent-red);
}
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.rule-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-bright);
}
.rule-id {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--text-muted);
}
.rule-status {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
}
.rule-status.green {
background: rgba(0, 245, 155, 0.15);
color: var(--accent-green);
}
.rule-status.red {
background: rgba(255, 51, 102, 0.15);
color: var(--accent-red);
}
.rule-desc {
font-size: 0.78rem;
color: var(--text-muted);
}
.json-viewer {
background: #080d19;
border: 1px solid #16223b;
border-radius: 6px;
padding: 14px;
font-family: var(--font-mono);
font-size: 0.75rem;
color: #a5d6a7;
max-height: 380px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.cli-instruction {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
font-size: 0.82rem;
display: flex;
flex-direction: column;
gap: 6px;
}
.cli-code {
background: #080d19;
padding: 6px 10px;
border-radius: 4px;
font-family: var(--font-mono);
color: var(--accent-cyan);
font-size: 0.82rem;
user-select: all;
}
.modal-footer {
padding: 14px 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* Modal de Fin de Juego */
.gameover-modal-content {
text-align: center;
padding: 30px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.gameover-title {
font-size: 2rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 2px;
}
.gameover-title.win {
color: var(--accent-green);
text-shadow: var(--glow-green);
}
.gameover-title.loss {
color: var(--accent-red);
text-shadow: var(--glow-red);
}
.gameover-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
width: 100%;
max-width: 440px;
margin: 10px 0;
}
.gameover-stat-box {
background: var(--bg-card);
padding: 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.gameover-stat-val {
font-size: 1.3rem;
font-weight: 800;
font-family: var(--font-mono);
color: var(--accent-cyan);
}
.gameover-stat-lbl {
font-size: 0.72rem;
color: var(--text-muted);
}
Abrí tu explorador de archivos, andá a tu carpeta batalla-naval y hacé doble clic sobre index.html para abrirlo en tu navegador web (también podés arrastrarlo a una pestaña de Chrome, Firefox o Edge, o escribir en tu terminal start index.html en Windows, open index.html en Mac o xdg-open index.html en Linux).
Lo que tenés que ver. Vas a ver la cabecera con el título del juego y los paneles oscuros vacíos, sin tableros todavía: los tableros y las cuadrículas los dibuja el código de JavaScript que agregamos en el próximo paso. Es normal que todavía no se pueda jugar.
Si ves otra cosa. Si el navegador te muestra el código fuente escrito en texto plano en vez de la página diseñada, revisá que el archivo se llame index.html y no index.html.txt. Si al hacerle doble clic no se abre tu navegador, hacé clic derecho en index.html, elegí «Abrir con» y seleccioná tu navegador web habitual.
Paso 3 · El juego#
Adentro de la carpeta batalla-naval, creá una carpeta nueva llamada js. Ahí adentro vas a guardar los cuatro archivos de JavaScript que le dan vida al juego: el sonido (audio.js), el registro de lo que pasa (trace.js), el motor con las reglas de la batalla (engine.js) y la interfaz visual que dibuja los tableros (ui.js).
Abrí el desplegable que sigue. Vas a encontrar cuatro bloques de código separados: copiá el contenido de cada bloque y guardalo en su propio archivo adentro de la subcarpeta js/ respetando el nombre exacto de cada cabecera (js/audio.js, js/trace.js, js/engine.js y js/ui.js):
Ver los cuatro archivos de JavaScript
/**
* audio.js - Sintetizador de efectos de sonido con Web Audio API
* Genera efectos militares/navales proceduralmente sin depender de archivos de audio externos.
*/
class SoundEffects {
constructor() {
this.ctx = null;
this.enabled = true;
}
_initContext() {
if (!this.ctx) {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (AudioCtx) {
this.ctx = new AudioCtx();
}
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
toggleSound() {
this.enabled = !this.enabled;
return this.enabled;
}
playSonarPing() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(840, t + 0.08);
osc.frequency.exponentialRampToValueAtTime(800, t + 0.6);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 1.2);
}
playShot() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Cañonazo: oscilador descendente + golpe
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(180, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.35);
gain.gain.setValueAtTime(0.35, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.35);
}
playMiss() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Chapuzón de agua (ruido blanco filtrado paso bajo)
const bufferSize = this.ctx.sampleRate * 0.4;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.25));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(450, t);
filter.frequency.linearRampToValueAtTime(150, t + 0.4);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(t);
}
playHit() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Explosión: ruido distorsionado + oscilador subgrave
const osc = this.ctx.createOscillator();
const oscGain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(120, t);
osc.frequency.exponentialRampToValueAtTime(25, t + 0.5);
oscGain.gain.setValueAtTime(0.4, t);
oscGain.gain.exponentialRampToValueAtTime(0.01, t + 0.5);
osc.connect(oscGain);
oscGain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.5);
// Ruido metálico/explosivo
const bufferSize = this.ctx.sampleRate * 0.45;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(500, t);
filter.Q.value = 1.2;
const noiseGain = this.ctx.createGain();
noiseGain.gain.setValueAtTime(0.5, t);
noiseGain.gain.exponentialRampToValueAtTime(0.01, t + 0.45);
noise.connect(filter);
filter.connect(noiseGain);
noiseGain.connect(this.ctx.destination);
noise.start(t);
}
playSunk() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Alarma / bocina naval grave
const osc1 = this.ctx.createOscillator();
const osc2 = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc1.type = 'sawtooth';
osc2.type = 'square';
osc1.frequency.setValueAtTime(95, t);
osc2.frequency.setValueAtTime(98, t);
gain.gain.setValueAtTime(0.3, t);
gain.gain.linearRampToValueAtTime(0.35, t + 0.4);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.9);
osc1.connect(gain);
osc2.connect(gain);
gain.connect(this.ctx.destination);
osc1.start(t);
osc2.start(t);
osc1.stop(t + 0.9);
osc2.stop(t + 0.9);
}
playVictory() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const notes = [261.63, 329.63, 392.00, 523.25]; // Do, Mi, Sol, Do agudo
notes.forEach((freq, idx) => {
const t = this.ctx.currentTime + idx * 0.14;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, t);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.4);
});
}
playDefeat() {
if (!this.enabled) return;
this._initContext();
if (!this.ctx) return;
const notes = [392.00, 369.99, 329.63, 277.18]; // Descenso menor
notes.forEach((freq, idx) => {
const t = this.ctx.currentTime + idx * 0.18;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(freq, t);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.45);
});
}
}
// Instancia global compartida
window.soundFX = new SoundEffects();
/**
* trace.js - Recolector de evidencia relacional formal y auditor de reglas
* Emite la traza de la partida en formato de hechos (JSON) compatible con oracle-metalenguaje.
*/
class TraceRecorder {
constructor() {
this.reset();
}
reset() {
this.celdaBarco = [];
this.tiros = [];
this.partida = [];
this._nextSegmentId = 0;
}
recordFleet(jugador, ships) {
for (const ship of ships) {
ship.coordinates.forEach((coord, idx) => {
this.celdaBarco.push({
id: this._nextSegmentId++,
jugador: jugador,
barco_id: ship.id,
segmento_idx: idx,
fila: coord.row,
columna: coord.col
});
});
}
}
recordShot(turno, tirador, receptor, fila, columna, esImpacto, hundioBarco, barcoHundido) {
this.tiros.push({
turno: turno,
tirador: tirador,
receptor: receptor,
fila: fila,
columna: columna,
es_impacto: Boolean(esImpacto),
hundio_barco: Boolean(hundioBarco),
barco_hundido: barcoHundido || 'ninguno'
});
}
recordGameOver(ganador, perdedor, impactosGanador, impactosPerdedor) {
const finalTurn = this.tiros.length > 0 ? this.tiros[this.tiros.length - 1].turno : 0;
this.partida = [{
estado: 'terminada',
ganador: ganador,
perdedor: perdedor,
turno_final: finalTurn,
total_tiros: this.tiros.length,
impactos_ganador: impactosGanador,
impactos_perdedor: impactosPerdedor
}];
}
toFactsObject() {
return {
celda_barco: this.celdaBarco,
tiro: this.tiros,
partida: this.partida.length > 0 ? this.partida : [{
estado: 'en_curso',
ganador: 'ninguno',
perdedor: 'ninguno',
turno_final: this.tiros.length > 0 ? this.tiros[this.tiros.length - 1].turno : 0,
total_tiros: this.tiros.length,
impactos_ganador: 0,
impactos_perdedor: 0
}]
};
}
toJSON(indent = 2) {
return JSON.stringify(this.toFactsObject(), null, indent);
}
downloadJSON(filename = 'hechos_partida.json') {
const jsonStr = this.toJSON(2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Auditoría espejo de las 11 medidas de Oracle evaluadas directamente en JavaScript.
* Permite al usuario ver el veredicto formal dentro de la interfaz web además del CLI.
*/
evaluateRules() {
const facts = this.toFactsObject();
const celdas = facts.celda_barco;
const tiros = facts.tiro;
const partida = facts.partida[0] || {};
const results = [];
// 1. naval.tiros_dentro_del_tablero
const invalidShots = tiros.filter(t => t.fila < 0 || t.fila > 9 || t.columna < 0 || t.columna > 9);
results.push({
id: 'naval.tiros_dentro_del_tablero',
name: 'Tiros dentro del tablero [0..9]',
passed: invalidShots.length === 0,
value: invalidShots.length,
threshold: '<= 0',
reason: 'El tablero reglamentario es de 10x10; cualquier disparo fuera de [0, 9] es inválido.',
witnesses: invalidShots
});
// 2. naval.tiros_sin_repeticion
const dupShots = [];
for (let i = 0; i < tiros.length; i++) {
for (let j = i + 1; j < tiros.length; j++) {
if (tiros[i].tirador === tiros[j].tirador &&
tiros[i].fila === tiros[j].fila &&
tiros[i].columna === tiros[j].columna) {
dupShots.push({ original: tiros[i], duplicado: tiros[j] });
}
}
}
results.push({
id: 'naval.tiros_sin_repeticion',
name: 'Sin disparos duplicados a la misma casilla',
passed: dupShots.length === 0,
value: dupShots.length,
threshold: '<= 0',
reason: 'Ningún jugador puede disparar dos veces a la misma casilla en una partida.',
witnesses: dupShots
});
// 3. naval.alternancia_turnos
const nonAlternating = [];
for (let i = 0; i < tiros.length - 1; i++) {
if (tiros[i + 1].turno === tiros[i].turno + 1 && tiros[i + 1].tirador === tiros[i].tirador) {
nonAlternating.push({ turno1: tiros[i], turno2: tiros[i + 1] });
}
}
results.push({
id: 'naval.alternancia_turnos',
name: 'Alternancia estricta de turnos',
passed: nonAlternating.length === 0,
value: nonAlternating.length,
threshold: '<= 0',
reason: 'Los turnos deben alternar estrictamente entre ambos jugadores en turnos consecutivos.',
witnesses: nonAlternating
});
// 4. naval.turnos_sin_huecos
let turnGaps = 0;
if (tiros.length > 0) {
const maxTurn = Math.max(...tiros.map(t => t.turno));
if (tiros.length !== maxTurn + 1) {
turnGaps = Math.abs(tiros.length - (maxTurn + 1));
}
}
results.push({
id: 'naval.turnos_sin_huecos',
name: 'Turnos continuos sin huecos ni saltos',
passed: turnGaps === 0,
value: turnGaps,
threshold: '<= 0',
reason: 'La secuencia de turnos debe ser continua, arrancando en 0 y avanzando de a 1.',
witnesses: turnGaps > 0 ? [{ tirosRegistrados: tiros.length }] : []
});
// 5. naval.barcos_dentro_del_tablero
const invalidCells = celdas.filter(c => c.fila < 0 || c.fila > 9 || c.columna < 0 || c.columna > 9);
results.push({
id: 'naval.barcos_dentro_del_tablero',
name: 'Barcos dentro de la cuadrícula [0..9]',
passed: invalidCells.length === 0,
value: invalidCells.length,
threshold: '<= 0',
reason: 'Todas las celdas ocupadas por barcos deben ubicarse dentro de 10x10.',
witnesses: invalidCells
});
// 6. naval.barcos_sin_solapamiento
const overlapping = [];
for (let i = 0; i < celdas.length; i++) {
for (let j = i + 1; j < celdas.length; j++) {
if (celdas[i].jugador === celdas[j].jugador &&
celdas[i].fila === celdas[j].fila &&
celdas[i].columna === celdas[j].columna) {
overlapping.push({ c1: celdas[i], c2: celdas[j] });
}
}
}
results.push({
id: 'naval.barcos_sin_solapamiento',
name: 'Barcos sin solapamiento entre sí',
passed: overlapping.length === 0,
value: overlapping.length,
threshold: '<= 0',
reason: 'Dos casillas de barco del mismo jugador no pueden coincidir en la misma celda.',
witnesses: overlapping
});
// 7. naval.flota_reglamentaria
const jugadorCeldas = celdas.filter(c => c.jugador === 'jugador').length;
const cpuCeldas = celdas.filter(c => c.jugador === 'cpu').length;
const flotaFlaws = [];
if (jugadorCeldas !== 17) flotaFlaws.push({ jugador: 'jugador', celdas: jugadorCeldas, esperado: 17 });
if (cpuCeldas !== 17) flotaFlaws.push({ jugador: 'cpu', celdas: cpuCeldas, esperado: 17 });
results.push({
id: 'naval.flota_reglamentaria',
name: 'Flota reglamentaria de 17 casillas por bando',
passed: flotaFlaws.length === 0,
value: flotaFlaws.length,
threshold: '<= 0',
reason: 'Cada bando debe tener exactamente 17 casillas (5+4+3+3+2).',
witnesses: flotaFlaws
});
// 8. naval.veracidad_impacto_positivo (Falso impacto / fantasma)
const ghostHits = [];
for (const t of tiros) {
if (t.es_impacto) {
const match = celdas.find(c => c.jugador === t.receptor && c.fila === t.fila && c.columna === t.columna);
if (!match) {
ghostHits.push(t);
}
}
}
results.push({
id: 'naval.veracidad_impacto_positivo',
name: 'Veracidad de impacto: ningún impacto fantasma',
passed: ghostHits.length === 0,
value: ghostHits.length,
threshold: '<= 0',
reason: 'Un disparo no puede declararse impacto si no había un segmento de barco rival.',
witnesses: ghostHits
});
// 9. naval.veracidad_impacto_negativo (Agua falsa / impacto omitido)
const falseMisses = [];
for (const t of tiros) {
if (!t.es_impacto) {
const match = celdas.find(c => c.jugador === t.receptor && c.fila === t.fila && c.columna === t.columna);
if (match) {
falseMisses.push({ tiro: t, celdaReal: match });
}
}
}
results.push({
id: 'naval.veracidad_impacto_negativo',
name: 'Veracidad de agua: ningún impacto omitido',
passed: falseMisses.length === 0,
value: falseMisses.length,
threshold: '<= 0',
reason: 'Un disparo en una casilla con barco no puede ser reportado como agua.',
witnesses: falseMisses
});
// 10. naval.fin_de_juego_sin_tiros_posteriores
const postGameShots = [];
if (partida.estado === 'terminada') {
const finalTurn = partida.turno_final;
for (const t of tiros) {
if (t.turno > finalTurn) {
postGameShots.push(t);
}
}
}
results.push({
id: 'naval.fin_de_juego_sin_tiros_posteriores',
name: 'Fin de partida: sin disparos posteriores',
passed: postGameShots.length === 0,
value: postGameShots.length,
threshold: '<= 0',
reason: 'Una vez finalizada la partida no se permiten más disparos.',
witnesses: postGameShots
});
// 11. naval.ganador_legitimo
let winnerValid = true;
const winnerWitnesses = [];
if (partida.estado === 'terminada') {
if (partida.impactos_ganador !== 17) {
winnerValid = false;
winnerWitnesses.push(partida);
}
}
results.push({
id: 'naval.ganador_legitimo',
name: 'Ganador legítimo: exactamente 17 impactos',
passed: winnerValid,
value: winnerValid ? 0 : 1,
threshold: '<= 0',
reason: 'El ganador debe haber destruido la totalidad de las 17 casillas enemigas.',
witnesses: winnerWitnesses
});
return results;
}
}
// Exportable para browser y Node
if (typeof module !== 'undefined' && module.exports) {
module.exports = { TraceRecorder };
} else {
window.TraceRecorder = TraceRecorder;
}
/**
* engine.js - Lógica pura del juego de Batalla Naval
* Diseñado para ejecutarse tanto en el navegador como en Node.js para testing y auditoría formal.
*/
const GRID_SIZE = 10;
const SHIPS_CONFIG = [
{ id: 'portaaviones', name: 'Portaaviones', length: 5 },
{ id: 'acorazado', name: 'Acorazado', length: 4 },
{ id: 'crucero', name: 'Crucero', length: 3 },
{ id: 'submarino', name: 'Submarino', length: 3 },
{ id: 'destructor', name: 'Destructor', length: 2 }
];
const TOTAL_FLEET_CELLS = SHIPS_CONFIG.reduce((acc, s) => acc + s.length, 0); // 17
class Ship {
constructor(config) {
this.id = config.id;
this.name = config.name;
this.length = config.length;
this.row = null;
this.col = null;
this.orientation = 'horizontal'; // 'horizontal' | 'vertical'
this.coordinates = [];
this.hits = 0;
}
place(row, col, orientation) {
this.row = row;
this.col = col;
this.orientation = orientation;
this.coordinates = [];
for (let i = 0; i < this.length; i++) {
if (orientation === 'horizontal') {
this.coordinates.push({ row: row, col: col + i });
} else {
this.coordinates.push({ row: row + i, col: col });
}
}
}
isSunk() {
return this.hits >= this.length;
}
}
class Board {
constructor(owner) {
this.owner = owner; // 'jugador' | 'cpu'
this.grid = Array.from({ length: GRID_SIZE }, () =>
Array.from({ length: GRID_SIZE }, () => ({
ship: null,
shot: false,
hit: false
}))
);
this.ships = [];
}
reset() {
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
this.grid[r][c] = { ship: null, shot: false, hit: false };
}
}
this.ships = [];
}
canPlaceShip(shipId, length, row, col, orientation) {
for (let i = 0; i < length; i++) {
const r = orientation === 'horizontal' ? row : row + i;
const c = orientation === 'horizontal' ? col + i : col;
// Fuera de límites
if (r < 0 || r >= GRID_SIZE || c < 0 || c >= GRID_SIZE) {
return false;
}
// Solapamiento
if (this.grid[r][c].ship !== null) {
return false;
}
}
return true;
}
placeShip(ship) {
if (!this.canPlaceShip(ship.id, ship.length, ship.row, ship.col, ship.orientation)) {
throw new Error(`Posición inválida para ${ship.name} en (${ship.row}, ${ship.col})`);
}
for (const coord of ship.coordinates) {
this.grid[coord.row][coord.col].ship = ship;
}
this.ships.push(ship);
return true;
}
receiveShot(row, col) {
if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE) {
throw new Error(`Coordenadas de disparo (${row}, ${col}) fuera de rango`);
}
const cell = this.grid[row][col];
if (cell.shot) {
return { alreadyShot: true, hit: false, sunk: false, ship: null };
}
cell.shot = true;
if (cell.ship !== null) {
cell.hit = true;
cell.ship.hits += 1;
const sunk = cell.ship.isSunk();
return {
alreadyShot: false,
hit: true,
sunk: sunk,
ship: cell.ship
};
} else {
return {
alreadyShot: false,
hit: false,
sunk: false,
ship: null
};
}
}
isAllSunk() {
return this.ships.length === SHIPS_CONFIG.length && this.ships.every(s => s.isSunk());
}
totalHitsReceived() {
return this.ships.reduce((acc, s) => acc + s.hits, 0);
}
autoPlaceFleet() {
this.reset();
for (const cfg of SHIPS_CONFIG) {
const ship = new Ship(cfg);
let placed = false;
let attempts = 0;
while (!placed && attempts < 500) {
attempts++;
const orientation = Math.random() < 0.5 ? 'horizontal' : 'vertical';
const maxR = orientation === 'horizontal' ? GRID_SIZE : GRID_SIZE - ship.length;
const maxC = orientation === 'horizontal' ? GRID_SIZE - ship.length : GRID_SIZE;
const r = Math.floor(Math.random() * maxR);
const c = Math.floor(Math.random() * maxC);
if (this.canPlaceShip(ship.id, ship.length, r, c, orientation)) {
ship.place(r, c, orientation);
this.placeShip(ship);
placed = true;
}
}
if (!placed) {
throw new Error(`No se pudo colocar automáticamente el barco ${ship.name}`);
}
}
}
}
/**
* Inteligencia Artificial para el CPU
* Estrategia de Caza y Blanco (Hunt and Target) con paridad matemática.
*/
class NavalAI {
constructor() {
this.reset();
}
reset() {
this.targetQueue = []; // Cola de celdas adyacentes a impactos no hundidos
this.currentShipHits = []; // Coordenadas de impactos del barco actual bajo ataque
}
getNextShot(opponentBoard) {
// 1. Modo Blanco: si tenemos celdas candidatas prioritarias en la cola
while (this.targetQueue.length > 0) {
const candidate = this.targetQueue.shift();
if (!opponentBoard.grid[candidate.row][candidate.col].shot) {
return candidate;
}
}
// 2. Modo Caza: búsqueda probabilística con paridad (celdas de tablero de ajedrez)
// Ya que el barco más chico mide 2 casillas, alcanza con disparar a (r + c) % 2 === 0
const parityCandidates = [];
const otherCandidates = [];
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
if (!opponentBoard.grid[r][c].shot) {
if ((r + c) % 2 === 0) {
parityCandidates.push({ row: r, col: c });
} else {
otherCandidates.push({ row: r, col: c });
}
}
}
}
const pool = parityCandidates.length > 0 ? parityCandidates : otherCandidates;
const chosenIndex = Math.floor(Math.random() * pool.length);
return pool[chosenIndex];
}
recordShotResult(row, col, hit, sunk) {
if (hit) {
this.currentShipHits.push({ row, col });
if (sunk) {
// Barco destruido: limpiar objetivos del barco y volver a cazar
this.currentShipHits = [];
this.targetQueue = [];
} else {
// Agregar vecinos ortogonales válidos
const neighbors = [
{ row: row - 1, col: col },
{ row: row + 1, col: col },
{ row: row, col: col - 1 },
{ row: row, col: col + 1 }
].filter(p => p.row >= 0 && p.row < GRID_SIZE && p.col >= 0 && p.col < GRID_SIZE);
if (this.currentShipHits.length >= 2) {
// Ya sabemos si el barco es horizontal o vertical
const isHorizontal = this.currentShipHits[0].row === this.currentShipHits[1].row;
const alignedNeighbors = neighbors.filter(n =>
isHorizontal ? n.row === this.currentShipHits[0].row : n.col === this.currentShipHits[0].col
);
// Poner los alineados al frente de la cola
this.targetQueue.unshift(...alignedNeighbors);
} else {
this.targetQueue.push(...neighbors);
}
}
}
}
}
/**
* Gestor de la partida (GameManager)
*/
class NavalGame {
constructor(traceRecorder) {
this.trace = traceRecorder;
this.playerBoard = new Board('jugador');
this.cpuBoard = new Board('cpu');
this.ai = new NavalAI();
this.phase = 'placement'; // 'placement' | 'battle' | 'game_over'
this.currentTurn = 'jugador';
this.turnNumber = 0;
this.winner = null;
this.stats = {
playerShots: 0,
playerHits: 0,
cpuShots: 0,
cpuHits: 0
};
}
newGame() {
this.trace.reset();
this.playerBoard.reset();
this.cpuBoard.reset();
this.ai.reset();
this.phase = 'placement';
this.currentTurn = 'jugador';
this.turnNumber = 0;
this.winner = null;
this.stats = {
playerShots: 0,
playerHits: 0,
cpuShots: 0,
cpuHits: 0
};
// La CPU siempre coloca su flota automáticamente
this.cpuBoard.autoPlaceFleet();
this.trace.recordFleet('cpu', this.cpuBoard.ships);
}
startBattle() {
if (this.playerBoard.ships.length !== SHIPS_CONFIG.length) {
throw new Error('Debés colocar los 5 barcos antes de iniciar la batalla.');
}
this.trace.recordFleet('jugador', this.playerBoard.ships);
this.phase = 'battle';
this.currentTurn = 'jugador';
this.turnNumber = 0;
}
firePlayerShot(row, col) {
if (this.phase !== 'battle') {
throw new Error('La partida no está en fase de batalla.');
}
if (this.currentTurn !== 'jugador') {
throw new Error('No es el turno del jugador.');
}
const shotResult = this.cpuBoard.receiveShot(row, col);
if (shotResult.alreadyShot) {
return { valid: false, message: 'Casilla ya atacada previamente.' };
}
this.stats.playerShots++;
if (shotResult.hit) this.stats.playerHits++;
this.trace.recordShot(
this.turnNumber,
'jugador',
'cpu',
row,
col,
shotResult.hit,
shotResult.sunk,
shotResult.sunk ? shotResult.ship.id : 'ninguno'
);
this.turnNumber++;
if (this.cpuBoard.isAllSunk()) {
this.endGame('jugador');
return {
valid: true,
hit: shotResult.hit,
sunk: shotResult.sunk,
ship: shotResult.ship,
gameOver: true,
winner: 'jugador'
};
}
// Pasa turno a la CPU
this.currentTurn = 'cpu';
return {
valid: true,
hit: shotResult.hit,
sunk: shotResult.sunk,
ship: shotResult.ship,
gameOver: false
};
}
executeCpuTurn() {
if (this.phase !== 'battle' || this.currentTurn !== 'cpu') {
return null;
}
const shotCoords = this.ai.getNextShot(this.playerBoard);
const shotResult = this.playerBoard.receiveShot(shotCoords.row, shotCoords.col);
this.stats.cpuShots++;
if (shotResult.hit) this.stats.cpuHits++;
this.ai.recordShotResult(shotCoords.row, shotCoords.col, shotResult.hit, shotResult.sunk);
this.trace.recordShot(
this.turnNumber,
'cpu',
'jugador',
shotCoords.row,
shotCoords.col,
shotResult.hit,
shotResult.sunk,
shotResult.sunk ? shotResult.ship.id : 'ninguno'
);
this.turnNumber++;
if (this.playerBoard.isAllSunk()) {
this.endGame('cpu');
return {
coords: shotCoords,
hit: shotResult.hit,
sunk: shotResult.sunk,
ship: shotResult.ship,
gameOver: true,
winner: 'cpu'
};
}
this.currentTurn = 'jugador';
return {
coords: shotCoords,
hit: shotResult.hit,
sunk: shotResult.sunk,
ship: shotResult.ship,
gameOver: false
};
}
endGame(winner) {
this.phase = 'game_over';
this.winner = winner;
const loser = winner === 'jugador' ? 'cpu' : 'jugador';
const hitsWinner = winner === 'jugador' ? this.stats.playerHits : this.stats.cpuHits;
const hitsLoser = winner === 'jugador' ? this.stats.cpuHits : this.stats.playerHits;
this.trace.recordGameOver(winner, loser, hitsWinner, hitsLoser);
}
}
// Exportable para Node y Browser
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
GRID_SIZE,
SHIPS_CONFIG,
TOTAL_FLEET_CELLS,
Ship,
Board,
NavalAI,
NavalGame
};
} else {
window.GRID_SIZE = GRID_SIZE;
window.SHIPS_CONFIG = SHIPS_CONFIG;
window.TOTAL_FLEET_CELLS = TOTAL_FLEET_CELLS;
window.Ship = Ship;
window.Board = Board;
window.NavalAI = NavalAI;
window.NavalGame = NavalGame;
}
/**
* ui.js - Interfaz de Usuario y Controlador de Eventos de Batalla Naval
*/
document.addEventListener('DOMContentLoaded', () => {
const trace = new TraceRecorder();
const game = new NavalGame(trace);
// Elementos DOM
const playerGridEl = document.getElementById('playerGrid');
const enemyGridEl = document.getElementById('enemyGrid');
const statusIndicator = document.getElementById('statusIndicator');
const statusTitle = document.getElementById('statusTitle');
const statusSubtitle = document.getElementById('statusSubtitle');
const metricTurns = document.getElementById('metricTurns');
const metricHits = document.getElementById('metricHits');
const metricAccuracy = document.getElementById('metricAccuracy');
const combatLog = document.getElementById('combatLog');
// Herramientas de colocación
const placementTools = document.getElementById('placementTools');
const shipsDock = document.getElementById('shipsDock');
const btnRotate = document.getElementById('btnRotate');
const btnAutoPlace = document.getElementById('btnAutoPlace');
const btnClearPlacement = document.getElementById('btnClearPlacement');
const btnStartBattle = document.getElementById('btnStartBattle');
const orientationBadge = document.getElementById('orientationBadge');
// Rastreador de barcos
const playerRoster = document.getElementById('playerRoster');
const cpuRoster = document.getElementById('cpuRoster');
// Modales
const auditModal = document.getElementById('auditModal');
const gameOverModal = document.getElementById('gameOverModal');
const btnOpenAudit = document.getElementById('btnOpenAudit');
const btnCloseAudit = document.getElementById('btnCloseAudit');
const btnCloseGameOver = document.getElementById('btnCloseGameOver');
const btnDownloadJson = document.getElementById('btnDownloadJson');
const btnCopyJson = document.getElementById('btnCopyJson');
const btnNewGame = document.getElementById('btnNewGame');
const btnRestart = document.getElementById('btnRestart');
const btnToggleSound = document.getElementById('btnToggleSound');
const btnVerifyFromGameOver = document.getElementById('btnVerifyFromGameOver');
// Tabs del modal de auditoría
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
const rulesListEl = document.getElementById('rulesList');
const jsonViewerEl = document.getElementById('jsonViewer');
// Estado local de UI para fase de colocación
let currentPlacementOrientation = 'horizontal'; // 'horizontal' | 'vertical'
let selectedShipConfig = null;
let playerPlacedShips = [];
const ROW_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
// Inicializar
initGame();
function initGame() {
game.newGame();
playerPlacedShips = [];
selectedShipConfig = SHIPS_CONFIG[0];
currentPlacementOrientation = 'horizontal';
renderEmptyGrid(playerGridEl, 'player');
renderEmptyGrid(enemyGridEl, 'enemy');
renderDock();
renderRosters();
updateHUD();
updateStatus('Fase de Despliegue', 'Ubicá tus 5 buques en el tablero o pulsá "Auto-Desplegar"', 'waiting');
clearCombatLog();
addLogEntry('Sistema listo. Desplegá tu flota de combate para iniciar las operaciones.', 'sunk-event');
placementTools.style.display = 'flex';
btnStartBattle.disabled = true;
}
// Renderizar Grilla con etiquetas A-J y 1-10
function renderEmptyGrid(tableEl, type) {
tableEl.innerHTML = '';
// Fila de encabezado numérico (1-10)
const headerRow = document.createElement('tr');
const cornerCell = document.createElement('th');
cornerCell.className = 'grid-label';
headerRow.appendChild(cornerCell);
for (let c = 1; c <= GRID_SIZE; c++) {
const th = document.createElement('th');
th.className = 'grid-label';
th.textContent = c;
headerRow.appendChild(th);
}
tableEl.appendChild(headerRow);
// Filas A-J con casillas
for (let r = 0; r < GRID_SIZE; r++) {
const tr = document.createElement('tr');
const rowLabel = document.createElement('th');
rowLabel.className = 'grid-label-row';
rowLabel.textContent = ROW_LABELS[r];
tr.appendChild(rowLabel);
for (let c = 0; c < GRID_SIZE; c++) {
const td = document.createElement('td');
td.className = 'grid-cell';
td.dataset.row = r;
td.dataset.col = c;
td.dataset.grid = type;
if (type === 'player') {
td.addEventListener('mouseenter', handlePlayerCellHover);
td.addEventListener('mouseleave', handlePlayerCellLeave);
td.addEventListener('click', handlePlayerCellClick);
} else {
td.addEventListener('click', handleEnemyCellClick);
}
tr.appendChild(td);
}
tableEl.appendChild(tr);
}
}
// Renderizar el Dock de selección de barcos para colocar
function renderDock() {
shipsDock.innerHTML = '';
const placedIds = playerPlacedShips.map(s => s.id);
SHIPS_CONFIG.forEach(cfg => {
const isPlaced = placedIds.includes(cfg.id);
const isSelected = selectedShipConfig && selectedShipConfig.id === cfg.id;
const item = document.createElement('div');
item.className = `ship-dock-item ${isPlaced ? 'placed' : ''} ${isSelected ? 'selected' : ''}`;
item.dataset.shipId = cfg.id;
const nameRow = document.createElement('div');
nameRow.className = 'ship-dock-name';
nameRow.innerHTML = `${cfg.name} <span>${cfg.length}</span>`;
item.appendChild(nameRow);
const blocks = document.createElement('div');
blocks.className = 'ship-preview-blocks';
for (let i = 0; i < cfg.length; i++) {
const b = document.createElement('div');
b.className = 'preview-block';
blocks.appendChild(b);
}
item.appendChild(blocks);
if (!isPlaced) {
item.addEventListener('click', () => {
selectedShipConfig = cfg;
renderDock();
});
}
shipsDock.appendChild(item);
});
orientationBadge.textContent = currentPlacementOrientation === 'horizontal' ? 'HORIZONTAL' : 'VERTICAL';
}
// Manejo de eventos de colocación en el tablero propio
function handlePlayerCellHover(e) {
if (game.phase !== 'placement' || !selectedShipConfig) return;
const row = parseInt(e.target.dataset.row, 10);
const col = parseInt(e.target.dataset.col, 10);
clearPlacementHover();
const canPlace = game.playerBoard.canPlaceShip(
selectedShipConfig.id,
selectedShipConfig.length,
row,
col,
currentPlacementOrientation
);
for (let i = 0; i < selectedShipConfig.length; i++) {
const r = currentPlacementOrientation === 'horizontal' ? row : row + i;
const c = currentPlacementOrientation === 'horizontal' ? col + i : col;
if (r < GRID_SIZE && c < GRID_SIZE) {
const cell = playerGridEl.querySelector(`[data-row="${r}"][data-col="${c}"]`);
if (cell) {
cell.classList.add(canPlace ? 'placement-valid' : 'placement-invalid');
}
}
}
}
function handlePlayerCellLeave() {
clearPlacementHover();
}
function clearPlacementHover() {
playerGridEl.querySelectorAll('.placement-valid, .placement-invalid').forEach(el => {
el.classList.remove('placement-valid', 'placement-invalid');
});
}
function handlePlayerCellClick(e) {
if (game.phase !== 'placement' || !selectedShipConfig) return;
const row = parseInt(e.target.dataset.row, 10);
const col = parseInt(e.target.dataset.col, 10);
if (game.playerBoard.canPlaceShip(selectedShipConfig.id, selectedShipConfig.length, row, col, currentPlacementOrientation)) {
const ship = new Ship(selectedShipConfig);
ship.place(row, col, currentPlacementOrientation);
game.playerBoard.placeShip(ship);
playerPlacedShips.push(ship);
window.soundFX.playSonarPing();
renderPlayerShips();
// Seleccionar siguiente barco no colocado
const placedIds = playerPlacedShips.map(s => s.id);
selectedShipConfig = SHIPS_CONFIG.find(c => !placedIds.includes(c.id)) || null;
renderDock();
if (playerPlacedShips.length === SHIPS_CONFIG.length) {
btnStartBattle.disabled = false;
updateStatus('Flota Completa', 'Pulsá "Iniciar Batalla" para trabar combate.', 'waiting');
}
} else {
window.soundFX.playMiss();
}
}
function renderPlayerShips() {
// Limpiar celdas previas de barco
playerGridEl.querySelectorAll('.grid-cell').forEach(c => c.classList.remove('ship-present'));
for (const ship of game.playerBoard.ships) {
for (const coord of ship.coordinates) {
const cell = playerGridEl.querySelector(`[data-row="${coord.row}"][data-col="${coord.col}"]`);
if (cell) cell.classList.add('ship-present');
}
}
}
// Rotar orientación
btnRotate.addEventListener('click', toggleOrientation);
window.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
toggleOrientation();
}
});
function toggleOrientation() {
currentPlacementOrientation = currentPlacementOrientation === 'horizontal' ? 'vertical' : 'horizontal';
orientationBadge.textContent = currentPlacementOrientation === 'horizontal' ? 'HORIZONTAL' : 'VERTICAL';
window.soundFX.playSonarPing();
}
// Auto-despliegue
btnAutoPlace.addEventListener('click', () => {
game.playerBoard.autoPlaceFleet();
playerPlacedShips = [...game.playerBoard.ships];
selectedShipConfig = null;
renderPlayerShips();
renderDock();
btnStartBattle.disabled = false;
window.soundFX.playSonarPing();
updateStatus('Flota Desplegada Automáticamente', 'Todos los buques en posición. Pulsá "Iniciar Batalla"', 'waiting');
});
// Limpiar colocación
btnClearPlacement.addEventListener('click', () => {
game.playerBoard.reset();
playerPlacedShips = [];
selectedShipConfig = SHIPS_CONFIG[0];
renderPlayerShips();
renderDock();
btnStartBattle.disabled = true;
updateStatus('Tablero Limpio', 'Seleccioná un barco y colocalo en la cuadrícula.', 'waiting');
});
// Iniciar Batalla
btnStartBattle.addEventListener('click', () => {
try {
game.startBattle();
placementTools.style.display = 'none';
document.querySelector('.enemy-card').classList.add('active-target');
window.soundFX.playSonarPing();
updateStatus('¡Combate Iniciado!', 'Tu turno: hacé clic en las coordenadas del radar enemigo.', 'player');
addLogEntry('¡Bafles abiertos! Batalla iniciada. Fuego a discreción.', 'sunk-event');
updateHUD();
renderRosters();
} catch (err) {
alert(err.message);
}
});
// Disparo del Jugador
function handleEnemyCellClick(e) {
if (game.phase !== 'battle') return;
if (game.currentTurn !== 'jugador') return;
const row = parseInt(e.target.dataset.row, 10);
const col = parseInt(e.target.dataset.col, 10);
const cellEl = e.target;
if (cellEl.classList.contains('shot')) return;
window.soundFX.playShot();
const result = game.firePlayerShot(row, col);
if (!result.valid) return;
// Pintar celda
cellEl.classList.add('shot');
const coordName = `${ROW_LABELS[row]}${col + 1}`;
if (result.hit) {
cellEl.classList.add('hit');
if (result.sunk) {
window.soundFX.playSunk();
markShipAsSunkOnGrid(enemyGridEl, result.ship);
addLogEntry(`[T-${game.turnNumber - 1}] ¡HUNDIDO! Has destruido el ${result.ship.name} enemigo en ${coordName}!`, 'sunk-event');
} else {
window.soundFX.playHit();
addLogEntry(`[T-${game.turnNumber - 1}] Jugador disparó a ${coordName}: ¡IMPACTO!`, 'player-shot');
}
} else {
cellEl.classList.add('miss');
window.soundFX.playMiss();
addLogEntry(`[T-${game.turnNumber - 1}] Jugador disparó a ${coordName}: Agua.`, 'player-shot');
}
updateHUD();
renderRosters();
if (result.gameOver) {
handleGameOver(result.winner);
return;
}
// Turno de la CPU
updateStatus('Turno Enemigo', 'El comandante enemigo está calculando vector de ataque...', 'cpu');
setTimeout(() => {
runCpuTurn();
}, 650);
}
// Turno de la CPU
function runCpuTurn() {
if (game.phase !== 'battle' || game.currentTurn !== 'cpu') return;
window.soundFX.playShot();
const cpuResult = game.executeCpuTurn();
if (!cpuResult) return;
const { row, col } = cpuResult.coords;
const playerCell = playerGridEl.querySelector(`[data-row="${row}"][data-col="${col}"]`);
const coordName = `${ROW_LABELS[row]}${col + 1}`;
playerCell.classList.add('shot');
if (cpuResult.hit) {
playerCell.classList.add('hit');
if (cpuResult.sunk) {
window.soundFX.playSunk();
markShipAsSunkOnGrid(playerGridEl, cpuResult.ship);
addLogEntry(`[T-${game.turnNumber - 1}] ¡ALERTA! El enemigo hundió nuestro ${cpuResult.ship.name}!`, 'sunk-event');
} else {
window.soundFX.playHit();
addLogEntry(`[T-${game.turnNumber - 1}] CPU disparó a ${coordName}: ¡Impacto en nuestro casco!`, 'cpu-shot');
}
} else {
playerCell.classList.add('miss');
window.soundFX.playMiss();
addLogEntry(`[T-${game.turnNumber - 1}] CPU disparó a ${coordName}: Agua.`, 'cpu-shot');
}
updateHUD();
renderRosters();
if (cpuResult.gameOver) {
handleGameOver(cpuResult.winner);
return;
}
updateStatus('Tu Turno', 'Elegí una coordenada en el radar enemigo para abrir fuego.', 'player');
}
function markShipAsSunkOnGrid(gridEl, ship) {
for (const coord of ship.coordinates) {
const cell = gridEl.querySelector(`[data-row="${coord.row}"][data-col="${coord.col}"]`);
if (cell) cell.classList.add('sunk');
}
}
// Fin de Juego
function handleGameOver(winner) {
const isPlayerWin = winner === 'jugador';
if (isPlayerWin) {
window.soundFX.playVictory();
updateStatus('¡VICTORIA NAVAL!', 'Toda la flota enemiga ha sido destruida.', 'player');
addLogEntry('¡VICTORIA TOTAL! Todos los barcos hostiles han sido enviados al fondo del mar.', 'sunk-event');
} else {
window.soundFX.playDefeat();
updateStatus('DERROTA NAVAL', 'Nuestra flota ha sido completamente aniquilada.', 'cpu');
addLogEntry('DERROTA: Hemos perdido todos nuestros navíos. Fin de las operaciones.', 'sunk-event');
}
document.getElementById('gameOverTitle').textContent = isPlayerWin ? '¡VICTORIA NAVAL!' : 'FLOTA HUNDIDA';
document.getElementById('gameOverTitle').className = `gameover-title ${isPlayerWin ? 'win' : 'loss'}`;
document.getElementById('goTurns').textContent = game.turnNumber;
document.getElementById('goAccuracy').textContent = `${calcAccuracy(game.stats.playerHits, game.stats.playerShots)}%`;
document.getElementById('goWinner').textContent = isPlayerWin ? 'JUGADOR' : 'CPU';
gameOverModal.classList.add('open');
}
// HUD y Métricas
function updateHUD() {
metricTurns.textContent = game.turnNumber;
metricHits.textContent = `${game.stats.playerHits} / 17`;
metricAccuracy.textContent = `${calcAccuracy(game.stats.playerHits, game.stats.playerShots)}%`;
}
function calcAccuracy(hits, shots) {
if (!shots || shots === 0) return 0;
return Math.round((hits / shots) * 100);
}
function updateStatus(title, sub, type) {
statusTitle.textContent = title;
statusSubtitle.textContent = sub;
statusIndicator.className = 'status-indicator';
if (type === 'waiting') statusIndicator.classList.add('waiting');
else if (type === 'cpu') statusIndicator.classList.add('cpu');
}
// Rosters de buques
function renderRosters() {
renderRosterList(playerRoster, game.playerBoard.ships, 'jugador');
renderRosterList(cpuRoster, game.cpuBoard.ships, 'cpu');
}
function renderRosterList(container, ships, owner) {
container.innerHTML = '';
const configToDisplay = SHIPS_CONFIG;
configToDisplay.forEach(cfg => {
const ship = ships.find(s => s.id === cfg.id);
const isSunk = ship ? ship.isSunk() : false;
const hits = ship ? ship.hits : 0;
const item = document.createElement('div');
item.className = `roster-item ${isSunk ? 'sunk' : ''}`;
const nameSpan = document.createElement('span');
nameSpan.textContent = cfg.name;
item.appendChild(nameSpan);
const blocks = document.createElement('div');
blocks.className = 'roster-blocks';
for (let i = 0; i < cfg.length; i++) {
const b = document.createElement('div');
b.className = `roster-block ${i < hits ? 'hit' : ''}`;
blocks.appendChild(b);
}
item.appendChild(blocks);
container.appendChild(item);
});
}
// Bitácora de combate
function clearCombatLog() {
combatLog.innerHTML = '';
}
function addLogEntry(msg, type = '') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = msg;
combatLog.appendChild(entry);
combatLog.scrollTop = combatLog.scrollHeight;
}
// Modal de Auditoría y Verificación
btnOpenAudit.addEventListener('click', openAuditModal);
btnCloseAudit.addEventListener('click', () => auditModal.classList.remove('open'));
btnVerifyFromGameOver.addEventListener('click', () => {
gameOverModal.classList.remove('open');
openAuditModal();
});
function openAuditModal() {
renderAuditData();
auditModal.classList.add('open');
}
function renderAuditData() {
// 1. Evaluar las 11 medidas formalmente
const rules = trace.evaluateRules();
rulesListEl.innerHTML = '';
rules.forEach(r => {
const item = document.createElement('div');
item.className = `rule-item ${r.passed ? 'passed' : 'failed'}`;
const header = document.createElement('div');
header.className = 'rule-header';
const left = document.createElement('div');
left.innerHTML = `<div class="rule-title">${r.name}</div><div class="rule-id">${r.id}</div>`;
const badge = document.createElement('div');
badge.className = `rule-status ${r.passed ? 'green' : 'red'}`;
badge.textContent = r.passed ? 'VERDE ✓' : `ROJO ✗ (${r.value})`;
header.appendChild(left);
header.appendChild(badge);
item.appendChild(header);
const desc = document.createElement('div');
desc.className = 'rule-desc';
desc.textContent = r.reason;
item.appendChild(desc);
if (!r.passed && r.witnesses.length > 0) {
const witnessEl = document.createElement('div');
witnessEl.style.fontSize = '0.72rem';
witnessEl.style.color = '#ff6b8b';
witnessEl.style.fontFamily = 'monospace';
witnessEl.textContent = `Testigo del defecto: ${JSON.stringify(r.witnesses[0])}`;
item.appendChild(witnessEl);
}
rulesListEl.appendChild(item);
});
// 2. Traza JSON
jsonViewerEl.textContent = trace.toJSON(2);
}
// Tabs
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
const target = document.getElementById(btn.dataset.tab);
if (target) target.classList.add('active');
});
});
// Descarga y Copia de JSON
btnDownloadJson.addEventListener('click', () => {
trace.downloadJSON('hechos_partida.json');
});
btnCopyJson.addEventListener('click', () => {
navigator.clipboard.writeText(trace.toJSON(2)).then(() => {
btnCopyJson.textContent = '¡Copiado!';
setTimeout(() => { btnCopyJson.textContent = 'Copiar JSON'; }, 2000);
});
});
// Reiniciar / Nueva Partida
btnNewGame.addEventListener('click', () => {
if (confirm('¿Reiniciar y comenzar una nueva partida?')) {
initGame();
}
});
btnRestart.addEventListener('click', () => {
gameOverModal.classList.remove('open');
initGame();
});
btnCloseGameOver.addEventListener('click', () => {
gameOverModal.classList.remove('open');
});
// Mute Sonido
btnToggleSound.addEventListener('click', () => {
const isEnabled = window.soundFX.toggleSound();
btnToggleSound.textContent = isEnabled ? '🔊 Sonido' : '🔇 Mute';
});
});
batalla-naval/
├── catalogos/
├── corpus/
├── css/
│ └── style.css
├── diferencial/
├── js/
│ ├── audio.js
│ ├── engine.js
│ ├── trace.js
│ └── ui.js
├── relaciones/
├── index.html
└── oracle.json
Volvé a la pestaña de tu navegador donde abriste index.html y recargá la página (presionando la tecla F5 o haciendo clic en el botón de la flechita circular). Ahora sí se juega: vas a ver aparecer dos tableros cuadriculados con letras y números listos para la batalla. Colocá tus barcos (o usá el botón de despliegue automático) y hacé clic en las casillas del tablero rival para dispararle a la computadora.
Si ves otra cosa. Si la página queda en blanco o los tableros no aparecen, hacé clic derecho en cualquier lugar vacío de la página, elegí «Inspeccionar» y hacé clic en la pestaña «Consola» (en la mayoría de los teclados también podés abrirla con la tecla F12). Si ves mensajes en letras rojas que avisan que falta un archivo (404 Not Found), revisá tu explorador de carpetas: adentro de batalla-naval/js tienen que quedar exactamente los cuatro archivos con sus nombres bien guardados: audio.js, trace.js, engine.js y ui.js.
Pensalo. El juego anda. ¿Eso quiere decir que cumple las reglas?
Ver la respuesta
No. Quiere decir que anda. Jugando una vez no sabés si un barco puede quedar fuera del tablero, si dos barcos se pueden pisar, si los turnos alternan siempre o si un «¡impacto!» tenía un barco abajo. En programación, los tests son programas automáticos diseñados para verificar si otra aplicación anda bien o tiene fallas. Y si le pedís al mismo modelo que escribió el juego que también escriba los tests, los tests van a confirmar lo que el modelo ya creía.
Paso 4 · Que el juego cuente lo que pasó#
Pensalo. Para juzgar un disparo después de la partida, ¿qué tendría que haber anotado el juego?
Ver la respuesta
El turno, quién tiró, a quién, en qué casilla, y si el juego lo dio por impacto. Para juzgar si ese impacto era verdad, también dónde estaba cada barco. Y para juzgar el final, quién ganó y con cuántos impactos.
Esa es la idea central de Oracle: el juego no se juzga a sí mismo, cuenta lo que pasó. El archivo js/trace.js, que ya guardaste, no opina sobre quién tiene razón: anota los acontecimientos en filas planas (listas simples de datos directos, como los renglones de una planilla de cálculo sin texto decorativo). En la base de hechos, cada tabla de eventos se llama relación, y cada columna o dato puntual dentro de la fila se llama campo:
| Relación | Una fila por… | Campos |
|---|---|---|
celda_barco | cada casilla ocupada por un barco | id, jugador, barco_id, segmento_idx, fila, columna |
tiro | cada disparo | turno, tirador, receptor, fila, columna, es_impacto, hundio_barco, barco_hundido |
partida | el final | estado, ganador, perdedor, turno_final, total_tiros, impactos_ganador, impactos_perdedor |
Durante la partida o al terminarla, mirá abajo a la derecha en la pantalla del juego: vas a ver un botón que dice «⚖ Auditoría & Traza Oracle». Al hacerle clic se abre un panel lateral con un botón para descargar esas filas como un archivo llamado hechos_partida.json.
El siguiente recuadro es únicamente una muestra para que veas qué aspecto tiene por dentro un renglón de ese archivo de registro en formato JSON (no hace falta que lo copies en ningún lado):
{"turno": 0, "tirador": "jugador", "receptor": "cpu", "fila": 2, "columna": 3, "es_impacto": false, "hundio_barco": false, "barco_hundido": "ninguno"}
Si trabajás con un modelo de inteligencia artificial (como ChatGPT o Claude), pedíselo con esta indicación textual: «agregá un registro que anote cada casilla de barco, cada tiro y el resultado final en filas planas, y que lo descargue como JSON». Y desconfiá si en vez de anotar los hechos puros, el registro anota conclusiones («partida válida: sí»): eso ya es opinión del juego sobre sí mismo, no un hecho objetivo.
Paso 5 · La primera regla, empezando por el caso rojo#
La regla más simple: ningún barco fuera del tablero de 10 por 10.
Pensalo. Antes de escribir la regla: ¿qué partida tendría que ponerla roja?
Ver la respuesta
Una con un barco en una casilla que no existe: por ejemplo, en la fila 10 (las filas van de 0 a 9). Escribir primero ese ejemplo te obliga a decir qué es un defecto antes de escribir cómo buscarlo.
En Oracle, una partida de prueba armada para comprobar si las reglas detectan defectos se llama caso. La colección de casos se guarda en una carpeta llamada corpus (del latín: cuerpo o conjunto de documentos de prueba). Guardalo en corpus/naval/. Como oracle init todavía no creó esa carpeta ni catalogos/naval/ (donde van a ir las reglas), crealas ahora: en tu explorador de archivos, abrí corpus y creá adentro una carpeta naval; después abrí catalogos y creá allí otra carpeta naval. También podés ejecutar mkdir corpus/naval y luego mkdir catalogos/naval en la terminal; las dos carpetas padre ya existen.
Adentro de corpus/naval/, creá un archivo nuevo, pegá el contenido de este bloque y guardalo con el nombre exacto 005-barco-fila-desbordada.caso (en el Bloc de Notas seleccioná Tipo: «Todos los archivos (.)» para que no le sume .txt al final):
caso 005-barco-fila-desbordada:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barco fila desbordada"
etiqueta: falso_verde
sintoma:
Una celda de barco cae en fila 10.
como_se_detecto: persona
medida: naval.barcos_dentro_del_tablero
evidencia:
celda_barco: id, jugador, fila, columna
"b1", "jugador", 10, 4
leccion:
Una celda de barco cae en fila 10.
Volvé a la ventana de la terminal (comprobando que sigas adentro de la carpeta batalla-naval), escribí oracle test y presioná Enter:
oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS OK · 1 casos · esquema, evidencia L0 y trazabilidad en regla
SINTAXIS OK · 0 medidas · 0 macros · 1 casos · 0 relaciones
catálogo: 0 medidas · corpus: 1 casos
defectos que se pusieron rojos: 0 · verdes correctos: 0 · sin evidencia esperada: 0 · huecos declarados: 0
nivel meta — el marco medido con sus propias medidas:
ACEPTACIÓN ✗ — 1 problema(s)
· 005-barco-fila-desbordada: reclama la medida «naval.barcos_dentro_del_tablero» y no está en el catálogo
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
MUTACIÓN: salteada (sin medidas todavía)
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
ALCANCE: verificación de medidas contra casos guardados del corpus.
PRODUCTO: sin nueva medición; la aceptación no reejecuta los comandos de origen ni el producto. El resultado no certifica su estado actual.
VEREDICTO: ROJO (falló: aceptación)
Lo que tenés que ver. Tenés que ver que la terminal muestra una cruz y dice VEREDICTO: ROJO (falló: aceptación). No te asustes: es exactamente lo que buscamos. El caso reclama una regla llamada naval.barcos_dentro_del_tablero que todavía no escribimos. En Oracle, cuando una prueba detecta un problema o falta una regla requerida, el veredicto es rojo.
Si ves otra cosa. Si la terminal te arroja un error de sintaxis o dice que no encuentra el archivo, revisá que esté guardado adentro de corpus/naval/ con el nombre exacto 005-barco-fila-desbordada.caso sin ninguna extensión .txt agregada.
Rojo, y está bien: el caso reclama una regla que todavía no existe. Ahora sí, la regla. En Oracle, a cada regla formal obligatoria la llamamos medida, porque mide con precisión matemática un aspecto de los hechos. Adentro de la carpeta catalogos/naval/, creá un archivo nuevo, pegá este bloque y guardalo con el nombre exacto naval.barcos_dentro_del_tablero.oracle:
ninguno naval.barcos_dentro_del_tablero:
de celda_barco c
donde c.fila < 0 o c.fila > 9 o c.columna < 0 o c.columna > 9
umbral <= 0 segun contrato porque "todas las celdas ocupadas por barcos deben ubicarse dentro de la cuadrícula de 10x10"
ambito universal
alcance "revisa los límites de cada celda de barco reportada. No verifica solapamientos ni continuidad."
Línea por línea: ninguno dice que no tiene que haber ninguna fila que ofenda; de celda_barco c elige qué tabla mirar; donde dice cuáles son las casillas que ofenden; umbral <= 0 fija el umbral (el límite máximo de faltas toleradas, en este caso cero infracciones permitidas); segun contrato y porque fundamentan el motivo de esa exigencia; y alcance declara abiertamente qué cosas no mira esta regla.
Oracle no acepta variantes de estilo ni discusiones estéticas: cada regla se escribe de una sola manera oficial, la que escribe su impresor automático. Si la escribe un modelo, oracle formatear la pone en esa forma única antes de medir nada. Para que el programa acomode los espacios y sangrías de tus archivos, escribí en la terminal:
oracle formatear . --escribir
catalogos/naval/naval.barcos_dentro_del_tablero.oracle: ya tiene forma única
corpus/naval/005-barco-fila-desbordada.caso: ya tiene forma única
batalla-naval/
├── catalogos/
│ └── naval/
│ └── naval.barcos_dentro_del_tablero.oracle
├── corpus/
│ └── naval/
│ └── 005-barco-fila-desbordada.caso
├── css/
│ └── style.css
├── diferencial/
├── js/
│ ├── audio.js
│ ├── engine.js
│ ├── trace.js
│ └── ui.js
├── relaciones/
├── index.html
└── oracle.json
El punto . le indica que formatee los archivos de la carpeta actual, y --escribir guarda los cambios directamente.
Pensalo. ¿Qué no está mirando esta regla?
Ver la respuesta
Lo dice su alcance: no mira si dos barcos se pisan ni si un barco es una línea continua. Un verde de esta regla no promete eso. Por eso el alcance es obligatorio: un verde que no dice qué dejó sin mirar se lee como «está todo bien».
Ahora que ya tenés el caso y la regla, volvé a ejecutar las pruebas en la terminal:
oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS OK · 1 casos · esquema, evidencia L0 y trazabilidad en regla
SINTAXIS OK · 1 medidas · 0 macros · 1 casos · 0 relaciones
catálogo: 1 medidas · corpus: 1 casos
ROJO 005-barco-fila-desbordada naval.barcos_dentro_del_tablero (valor 1)
defectos que se pusieron rojos: 1 · verdes correctos: 0 · sin evidencia esperada: 0 · huecos declarados: 0
nivel meta — el marco medido con sus propias medidas:
ACEPTACIÓN ✓ — 1 defectos en rojo, 0 sin evidencia esperada, 0 verdes correctos, 0 huecos declarados sin tapar
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
mutantes de medida (medida × mutador): 18 · murieron 10 · sobrevivieron 8
con 30 mutadores: 6 de quien escribió el lenguaje y 24 de otro autor (ver https://github.com/Segtem/oracle/blob/main/docs/decisiones/DECISION-011-LOS-MUTADORES-TIENEN-AUTOR.md)
de los muertos: 10 por conducta (invirtió el veredicto, cambió testigos o cambió el valor) · 0 rechazados por el álgebra sin evaluar
respaldo real: 0 de 10 muertos los mata al menos un caso observado; 10 sólo los sostiene evidencia construida, generada, sin procedencia o diferencial
detecciones evaluadas (mutante × caso): 18
sin políticas meta activas — se informa sólo el resultado operativo
lo que el corpus NO fija — ningún caso detecta estas mutaciones:
· mutar «quitar_filtro» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «alejar_limite_de_defecto» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «expresion:comparador@2.2.1.1:<→>=» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «expresion:comparador@2.2.1.3:<→>=» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «expresion:comparador@2.2.1.4:>→<=» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.1.1.2:fila→columna» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.3.1.2:columna→fila» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.4.1.2:columna→fila» en naval.barcos_dentro_del_tablero pasa inadvertido
Se tapa agregando un caso que SÍ lo note o declarando una equivalencia individual
demostrable; nunca debilitando el mutador. La polaridad y el borde también importan:
`quitar_filtro` suele pedir un verde; `aflojar_umbral`, un rojo junto al límite.
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
ALCANCE: verificación de medidas contra casos guardados del corpus.
PRODUCTO: sin nueva medición; la aceptación no reejecuta los comandos de origen ni el producto. El resultado no certifica su estado actual.
VEREDICTO: ROJO (falló: mutación)
Lo que tenés que ver. Tenés que ver que el resultado sigue siendo VEREDICTO: ROJO, pero ahora por falló: mutación. ¡Vas muy bien! Esto te avisa que la regla está cargada y el caso rojo fue detectado, pero Oracle te advierte que la regla todavía es tan fácil de engañar que no pasa los controles de robustez.
Paso 6 · Un caso rojo no alcanza: la mutación#
Todavía rojo, pero por otra razón: sobreviven mutantes. Para comprobar si tus casos de prueba son de verdad rigurosos, Oracle le hace trampitas a tu regla a propósito: le afloja el umbral (acepta faltas que no debería), invierte los signos de comparación (cambia un < por un >=) o le quita los filtros donde. A cada una de esas versiones adulteradas la llama mutante, y se fija si tus casos de prueba son capaces de detectar la trampa.
Pensalo. Si Oracle le quita el donde a la regla, pasa a contar todas las casillas. Tu único caso sigue saliendo rojo. ¿Qué caso haría falta para que se note?
Ver la respuesta
Uno verde: una partida con todos los barcos dentro, que la regla sin filtro pondría roja. Y conviene que esté en el borde (fila 9), para que también se note si alguien corre el límite.
Creá un archivo nuevo adentro de corpus/naval/, pegá este bloque y guardalo con el nombre exacto 006-barco-en-borde.caso:
caso 006-barco-en-borde:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barco en borde"
etiqueta: verde_correcto
sintoma:
La celda de barco está en el borde válido.
como_se_detecto: persona
medida: naval.barcos_dentro_del_tablero
evidencia:
celda_barco: id, jugador, fila, columna
"b1", "jugador", 0, 9
leccion:
La celda de barco está en el borde válido.
Volvé a correr las pruebas en la terminal:
oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS OK · 2 casos · esquema, evidencia L0 y trazabilidad en regla
SINTAXIS OK · 1 medidas · 0 macros · 2 casos · 0 relaciones
catálogo: 1 medidas · corpus: 2 casos
ROJO 005-barco-fila-desbordada naval.barcos_dentro_del_tablero (valor 1)
verde 006-barco-en-borde naval.barcos_dentro_del_tablero (valor 0)
defectos que se pusieron rojos: 1 · verdes correctos: 1 · sin evidencia esperada: 0 · huecos declarados: 0
nivel meta — el marco medido con sus propias medidas:
ACEPTACIÓN ✓ — 1 defectos en rojo, 0 sin evidencia esperada, 1 verdes correctos, 0 huecos declarados sin tapar
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
mutantes de medida (medida × mutador): 18 · murieron 14 · sobrevivieron 4
con 30 mutadores: 6 de quien escribió el lenguaje y 24 de otro autor (ver https://github.com/Segtem/oracle/blob/main/docs/decisiones/DECISION-011-LOS-MUTADORES-TIENEN-AUTOR.md)
de los muertos: 14 por conducta (invirtió el veredicto, cambió testigos o cambió el valor) · 0 rechazados por el álgebra sin evaluar
respaldo real: 0 de 14 muertos los mata al menos un caso observado; 14 sólo los sostiene evidencia construida, generada, sin procedencia o diferencial
detecciones evaluadas (mutante × caso): 36
sin políticas meta activas — se informa sólo el resultado operativo
lo que el corpus NO fija — ningún caso detecta estas mutaciones:
· mutar «alejar_limite_de_defecto» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.1.1.2:fila→columna» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.3.1.2:columna→fila» en naval.barcos_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.4.1.2:columna→fila» en naval.barcos_dentro_del_tablero pasa inadvertido
Se tapa agregando un caso que SÍ lo note o declarando una equivalencia individual
demostrable; nunca debilitando el mutador. La polaridad y el borde también importan:
`quitar_filtro` suele pedir un verde; `aflojar_umbral`, un rojo junto al límite.
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
ALCANCE: verificación de medidas contra casos guardados del corpus.
PRODUCTO: sin nueva medición; la aceptación no reejecuta los comandos de origen ni el producto. El resultado no certifica su estado actual.
VEREDICTO: ROJO (falló: mutación)
Lo que tenés que ver. Murieron 14 mutantes y ahora sobreviven sólo 4. El reporte te avisa qué trampas no fueron descubiertas todavía: para tapar esos huecos vas a necesitar agregar tanto partidas válidas (casos verdes) como partidas con errores justo en los bordes del tablero (casos rojos).
Quedan mutantes vivos porque la regla vigila cuatro condiciones (fila menor que 0, fila mayor que 9, columna menor que 0, columna mayor que 9) y tu caso rojo sólo probaba una de ellas. Creá tres archivos separados adentro de corpus/naval/: guardá el primer bloque como 026-barco-columna-desbordada.caso, el segundo como 027-barco-fila-negativa.caso y el tercero como 028-barco-columna-negativa.caso:
caso 026-barco-columna-desbordada:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barco columna desbordada"
etiqueta: falso_verde
sintoma:
Una celda de barco cae en columna 10.
como_se_detecto: persona
medida: naval.barcos_dentro_del_tablero
evidencia:
celda_barco: id, jugador, fila, columna
"b1", "jugador", 5, 10
leccion:
Una celda de barco cae en columna 10.
caso 027-barco-fila-negativa:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barco fila negativa"
etiqueta: falso_verde
sintoma:
Una celda de barco cae en fila negativa.
como_se_detecto: persona
medida: naval.barcos_dentro_del_tablero
evidencia:
celda_barco: id, jugador, fila, columna
"b1", "jugador", -1, 5
leccion:
Una celda de barco cae en fila negativa.
caso 028-barco-columna-negativa:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barco columna negativa"
etiqueta: falso_verde
sintoma:
Una celda de barco cae en columna negativa.
como_se_detecto: persona
medida: naval.barcos_dentro_del_tablero
evidencia:
celda_barco: id, jugador, fila, columna
"b1", "jugador", 5, -1
leccion:
Una celda de barco cae en columna negativa.
Ahora corré de nuevo oracle test en la terminal:
oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS OK · 5 casos · esquema, evidencia L0 y trazabilidad en regla
SINTAXIS OK · 1 medidas · 0 macros · 5 casos · 0 relaciones
catálogo: 1 medidas · corpus: 5 casos
ROJO 005-barco-fila-desbordada naval.barcos_dentro_del_tablero (valor 1)
verde 006-barco-en-borde naval.barcos_dentro_del_tablero (valor 0)
ROJO 026-barco-columna-desbordada naval.barcos_dentro_del_tablero (valor 1)
ROJO 027-barco-fila-negativa naval.barcos_dentro_del_tablero (valor 1)
ROJO 028-barco-columna-negativa naval.barcos_dentro_del_tablero (valor 1)
defectos que se pusieron rojos: 4 · verdes correctos: 1 · sin evidencia esperada: 0 · huecos declarados: 0
nivel meta — el marco medido con sus propias medidas:
ACEPTACIÓN ✓ — 4 defectos en rojo, 0 sin evidencia esperada, 1 verdes correctos, 0 huecos declarados sin tapar
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
mutantes de medida (medida × mutador): 18 · murieron 18 · sobrevivieron 0
con 30 mutadores: 6 de quien escribió el lenguaje y 24 de otro autor (ver https://github.com/Segtem/oracle/blob/main/docs/decisiones/DECISION-011-LOS-MUTADORES-TIENEN-AUTOR.md)
de los muertos: 18 por conducta (invirtió el veredicto, cambió testigos o cambió el valor) · 0 rechazados por el álgebra sin evaluar
respaldo real: 0 de 18 muertos los mata al menos un caso observado; 18 sólo los sostiene evidencia construida, generada, sin procedencia o diferencial
detecciones evaluadas (mutante × caso): 90
sin políticas meta activas — se informa sólo el resultado operativo
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
ALCANCE: verificación de medidas contra casos guardados del corpus.
PRODUCTO: sin nueva medición; la aceptación no reejecuta los comandos de origen ni el producto. El resultado no certifica su estado actual.
VEREDICTO: VERDE (todas las verificaciones aplicables en regla)
Lo que tenés que ver. Tenés que ver que la última línea de la terminal dice VEREDICTO: VERDE (todas las verificaciones aplicables en regla). Verde, y ahora el verde significa algo: tus cinco casos atraparon todas las trampas posibles, murieron los 18 mutantes y la regla del tablero quedó completamente asegurada.
Si ves otra cosa. Si todavía dice VEREDICTO: ROJO, leé las líneas de la terminal: te van a indicar qué mutante sigue vivo o qué caso no se ejecutó. Revisá que los nombres de los tres archivos nuevos en corpus/naval/ coincidan letra por letra.
Paso 7 · Diez reglas más, y la trampa de los pocos casos#
El juego tiene más reglas para ser una batalla naval completa. Adentro de la carpeta catalogos/naval/, vas a crear diez archivos de texto por separado. Abrí el desplegable que sigue, copiá el contenido de cada bloque y guardalo con el nombre exacto que figura en el encabezado de cada uno (catalogos/naval/...):
Ver las diez medidas
ninguno naval.tiros_dentro_del_tablero:
de tiro t
donde t.fila < 0 o t.fila > 9 o t.columna < 0 o t.columna > 9
umbral <= 0 segun contrato porque "el tablero reglamentario es de 10x10; cualquier coordenada con fila o columna fuera de [0, 9] es un tiro inválido"
ambito universal
alcance "comprueba que las coordenadas de cada disparo estén en el rango [0, 9]. No comprueba si el casillero ya fue disparado previamente ni la validez del turno."
medida naval.barcos_sin_solapamiento:
de celda_barco c1
unir celda_barco c2
donde c1.jugador == c2.jugador y c1.id < c2.id y c1.fila == c2.fila y c1.columna == c2.columna
resumen contar(1)
umbral <= 0 segun contrato porque "dos casillas de barco del mismo jugador no pueden coincidir en la misma fila y columna"
ambito universal
alcance "detecta pares de celdas distintas del mismo jugador con id1 < id2 que colisionan en la misma coordenada. No comprueba barcos de jugadores opuestos."
medida naval.flota_reglamentaria:
de celda_barco c
agrupar:
clave jugador = c.jugador
agregado celdas = contar(1)
donde celdas != 17
resumen contar(1)
umbral <= 0 segun contrato porque "la flota reglamentaria de cada jugador debe sumar exactamente 17 casillas (portaaviones 5 + acorazado 4 + crucero 3 + submarino 3 + destructor 2)"
requiere celda_barco
ambito universal
alcance "agrupa las casillas de barco por jugador y exige exactamente 17 casillas por flota. No comprueba la orientación lineal de los barcos."
medida naval.alternancia_turnos:
de tiro t1
unir tiro t2
donde t2.turno == t1.turno + 1 y t1.tirador == t2.tirador
resumen contar(1)
umbral <= 0 segun contrato porque "los turnos deben alternar estrictamente entre ambos jugadores en turnos consecutivos"
ambito universal
alcance "compara el tirador del turno k con el tirador del turno k+1. No valida la ausencia de saltos si falta un turno completo."
medida naval.turnos_sin_huecos:
de tiro t
agrupar:
agregado registrados = contar(1)
agregado ultimo = max(t.turno)
donde registrados != ultimo + 1
resumen contar(1)
umbral <= 0 segun contrato porque "la secuencia de turnos debe ser continua, arrancando en 0 y avanzando de a 1 sin huecos ni saltos"
requiere tiro
ambito universal
alcance "compara la cantidad total de tiros registrados contra el turno máximo reportado más uno. No valida la alternancia de tiradores."
medida naval.tiros_sin_repeticion:
de tiro t1
unir tiro t2
donde t1.tirador == t2.tirador y t1.turno < t2.turno y t1.fila == t2.fila y t1.columna == t2.columna
resumen contar(1)
umbral <= 0 segun contrato porque "un jugador no puede disparar dos veces a la misma casilla en una partida"
ambito universal
alcance "detecta disparos repetidos del mismo jugador a la misma celda con diferente número de turno. No evalúa si el tiro fue agua o impacto."
medida naval.veracidad_impacto_positivo:
de tiro t
sin celda_barco c donde c.jugador == t.receptor y c.fila == t.fila y c.columna == t.columna
donde t.es_impacto == true
resumen contar(1)
umbral <= 0 segun contrato porque "un disparo no puede declararse impacto si en esa casilla no había un segmento de barco rival"
ambito universal
alcance "detecta disparos registrados como impacto que no corresponden a ninguna celda ocupada por la flota del receptor. No verifica si el barco ya estaba completamente hundido."
medida naval.veracidad_impacto_negativo:
de tiro t
unir celda_barco c
donde c.jugador == t.receptor y c.fila == t.fila y c.columna == t.columna y t.es_impacto == false
resumen contar(1)
umbral <= 0 segun contrato porque "un disparo en una casilla que contiene un barco no puede ser reportado como agua"
ambito universal
alcance "cruza cada disparo contra las celdas de la flota rival y detecta impactos omitidos registrados falsamente como agua. No verifica la secuencia de turnos."
ninguno naval.ganador_legitimo:
de partida p
donde p.impactos_ganador != 17
umbral <= 0 segun contrato porque "el ganador de la partida debe haber destruido la totalidad de las 17 casillas de la flota enemiga"
ambito universal
alcance "comprueba que los impactos acumulados por el ganador sean exactamente 17. No valida si los tiros fueron asignados al tirador correcto."
medida naval.fin_de_juego_sin_tiros_posteriores:
de tiro t
unir partida p
donde t.turno > p.turno_final
resumen contar(1)
umbral <= 0 segun contrato porque "una vez finalizada la partida no se permiten más disparos"
ambito universal
alcance "compara el turno de cada tiro contra el turno final registrado en el estado de la partida. No valida si la partida debió terminar antes."
Tres reglas para mirar con calma, porque cada una enseña una palabra clave de consulta del lenguaje:
naval.alternancia_turnosusaunirpara cruzar cada tiro con el tiro siguiente y comprueba que los números de turno sean correlativos:t2.turno == t1.turno + 1(en programación, el doble igual==significa 'es igual a').naval.flota_reglamentariausaagruparpara contar cuántas casillas de barco tiene cada jugador y exigerequiere celda_barco: si el juego no envió ningún barco en el registro, la regla no sale verde, sale sin evidencia. Cero barcos no es una flota correcta, es un registro roto.naval.veracidad_impacto_positivousasinpara detectar contradicciones: busca los disparos reportados como acierto sin ningún barco rival en esa misma casilla.
Con el juego original venían sólo dos casos de prueba para la regla de disparos. Creá dos archivos nuevos en corpus/naval/: guardá el primer bloque como 001-tiro-fuera-de-tablero.caso y el segundo como 002-tiro-valido.caso:
caso 001-tiro-fuera-de-tablero:
fecha: "2026-09-22"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "Disparo con coordenada fuera del tablero de 10x10"
etiqueta: falso_verde
sintoma:
Un disparo reporta fila 10, fuera del tablero reglamentario.
como_se_detecto: persona
medida: naval.tiros_dentro_del_tablero
evidencia:
tiro: turno, tirador, fila, columna
0, "jugador", 10, 4
leccion:
Todo disparo debe acotarse estrictamente entre las filas 0 a 9 y columnas 0 a 9.
caso 002-tiro-valido:
fecha: "2026-09-22"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "Disparo válido dentro del tablero de 10x10"
etiqueta: verde_correcto
sintoma:
Un disparo dentro del rango válido no debe generar alertas.
como_se_detecto: persona
medida: naval.tiros_dentro_del_tablero
evidencia:
tiro: turno, tirador, fila, columna
0, "jugador", 5, 5
leccion:
Las coordenadas entre 0 y 9 son aceptadas sin faltas.
batalla-naval/
├── catalogos/
│ └── naval/
│ ├── naval.alternancia_turnos.oracle
│ ├── naval.barcos_dentro_del_tablero.oracle
│ ├── naval.barcos_sin_solapamiento.oracle
│ ├── naval.fin_de_juego_sin_tiros_posteriores.oracle
│ ├── naval.flota_reglamentaria.oracle
│ ├── naval.ganador_legitimo.oracle
│ ├── naval.tiros_dentro_del_tablero.oracle
│ ├── naval.tiros_sin_repeticion.oracle
│ ├── naval.turnos_sin_huecos.oracle
│ ├── naval.veracidad_impacto_negativo.oracle
│ └── naval.veracidad_impacto_positivo.oracle
├── corpus/
│ └── naval/
│ ├── 001-tiro-fuera-de-tablero.caso
│ ├── 002-tiro-valido.caso
│ ├── 005-barco-fila-desbordada.caso
│ ├── 006-barco-en-borde.caso
│ ├── 026-barco-columna-desbordada.caso
│ ├── 027-barco-fila-negativa.caso
│ └── 028-barco-columna-negativa.caso
├── css/
│ └── style.css
├── diferencial/
├── js/
│ ├── audio.js
│ ├── engine.js
│ ├── trace.js
│ └── ui.js
├── relaciones/
├── index.html
└── oracle.json
Pensalo. Once reglas y siete casos, casi todos de dos reglas. ¿Qué tendría que decir oracle test?
Ver la respuesta
Que la mayoría de las reglas no las prueba nadie. Una regla sin casos que puedan romperla es decoración: puede estar mal escrita y dar verde siempre, y nunca te vas a enterar.
Volvé a ejecutar las pruebas en la terminal:
oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS OK · 7 casos · esquema, evidencia L0 y trazabilidad en regla
MEDIDAS SIN CASOS ✗ — 9 medida(s) propias sin ejercitar:
· naval.alternancia_turnos: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.barcos_sin_solapamiento: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.fin_de_juego_sin_tiros_posteriores: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.flota_reglamentaria: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.ganador_legitimo: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.tiros_sin_repeticion: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.turnos_sin_huecos: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.veracidad_impacto_negativo: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.veracidad_impacto_positivo: escribí un caso rojo y uno verde que ejerzan esta medida
SINTAXIS OK · 11 medidas · 0 macros · 7 casos · 0 relaciones
catálogo: 11 medidas · corpus: 7 casos
ROJO 001-tiro-fuera-de-tablero naval.tiros_dentro_del_tablero (valor 1)
verde 002-tiro-valido naval.tiros_dentro_del_tablero (valor 0)
ROJO 005-barco-fila-desbordada naval.barcos_dentro_del_tablero (valor 1)
verde 006-barco-en-borde naval.barcos_dentro_del_tablero (valor 0)
ROJO 026-barco-columna-desbordada naval.barcos_dentro_del_tablero (valor 1)
ROJO 027-barco-fila-negativa naval.barcos_dentro_del_tablero (valor 1)
ROJO 028-barco-columna-negativa naval.barcos_dentro_del_tablero (valor 1)
defectos que se pusieron rojos: 5 · verdes correctos: 2 · sin evidencia esperada: 0 · huecos declarados: 0
nivel meta — el marco medido con sus propias medidas:
ACEPTACIÓN ✓ — 5 defectos en rojo, 0 sin evidencia esperada, 2 verdes correctos, 0 huecos declarados sin tapar
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
mutantes de medida (medida × mutador): 36 · murieron 32 · sobrevivieron 4
con 30 mutadores: 6 de quien escribió el lenguaje y 24 de otro autor (ver https://github.com/Segtem/oracle/blob/main/docs/decisiones/DECISION-011-LOS-MUTADORES-TIENEN-AUTOR.md)
de los muertos: 32 por conducta (invirtió el veredicto, cambió testigos o cambió el valor) · 0 rechazados por el álgebra sin evaluar
respaldo real: 0 de 32 muertos los mata al menos un caso observado; 32 sólo los sostiene evidencia construida, generada, sin procedencia o diferencial
detecciones evaluadas (mutante × caso): 126
medidas que no se pudieron mutar por falta de casos:
· naval.alternancia_turnos: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.barcos_sin_solapamiento: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.fin_de_juego_sin_tiros_posteriores: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.flota_reglamentaria: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.ganador_legitimo: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.tiros_sin_repeticion: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.turnos_sin_huecos: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.veracidad_impacto_negativo: escribí un caso rojo y uno verde que ejerzan esta medida
· naval.veracidad_impacto_positivo: escribí un caso rojo y uno verde que ejerzan esta medida
sin políticas meta activas — se informa sólo el resultado operativo
lo que el corpus NO fija — ningún caso detecta estas mutaciones:
· mutar «alejar_limite_de_defecto» en naval.tiros_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.1.1.2:fila→columna» en naval.tiros_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.3.1.2:columna→fila» en naval.tiros_dentro_del_tablero pasa inadvertido
· mutar «campo:2.2.1.4.1.2:columna→fila» en naval.tiros_dentro_del_tablero pasa inadvertido
Se tapa agregando un caso que SÍ lo note o declarando una equivalencia individual
demostrable; nunca debilitando el mutador. La polaridad y el borde también importan:
`quitar_filtro` suele pedir un verde; `aflojar_umbral`, un rojo junto al límite.
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
ALCANCE: verificación de medidas contra casos guardados del corpus.
PRODUCTO: sin nueva medición; la aceptación no reejecuta los comandos de origen ni el producto. El resultado no certifica su estado actual.
VEREDICTO: ROJO (falló: medidas sin casos, mutación)
Lo que tenés que ver. La terminal te muestra MEDIDAS SIN CASOS: te lista nueve medidas que no tienen pruebas y frena el avance. Es una protección esencial: una regla sin pruebas no te da ninguna garantía.
Para blindar las once reglas necesitamos 26 casos más en corpus/naval/: para cada regla, al menos uno que la ponga roja cerca del borde y uno verde. Abrí el desplegable que sigue y creá adentro de corpus/naval/ cada uno de estos 26 archivos con el nombre exacto de su encabezado:
Ver los casos que faltan
caso 023-columna-desbordada:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "columna desbordada"
etiqueta: falso_verde
sintoma:
La columna 10 está fuera del tablero.
como_se_detecto: persona
medida: naval.tiros_dentro_del_tablero
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 5, 10, false
leccion:
La columna 10 está fuera del tablero.
caso 024-fila-negativa:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "fila negativa"
etiqueta: falso_verde
sintoma:
La fila negativa está fuera del tablero.
como_se_detecto: persona
medida: naval.tiros_dentro_del_tablero
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", -1, 5, false
leccion:
La fila negativa está fuera del tablero.
caso 025-columna-negativa:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "columna negativa"
etiqueta: falso_verde
sintoma:
La columna negativa está fuera del tablero.
como_se_detecto: persona
medida: naval.tiros_dentro_del_tablero
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 5, -1, false
leccion:
La columna negativa está fuera del tablero.
caso 007-barcos-superpuestos:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barcos superpuestos"
etiqueta: falso_verde
sintoma:
Dos barcos del mismo jugador comparten una casilla.
como_se_detecto: persona
medida: naval.barcos_sin_solapamiento
evidencia:
celda_barco: id, jugador, fila, columna
"a", "jugador", 3, 4
"b", "jugador", 3, 4
leccion:
Dos barcos del mismo jugador comparten una casilla.
caso 008-barcos-separados:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barcos separados"
etiqueta: verde_correcto
sintoma:
Dos barcos del mismo jugador ocupan casillas distintas.
como_se_detecto: persona
medida: naval.barcos_sin_solapamiento
evidencia:
celda_barco: id, jugador, fila, columna
"a", "jugador", 3, 4
"b", "jugador", 3, 5
leccion:
Dos barcos del mismo jugador ocupan casillas distintas.
caso 011-flota-de-dieciseis:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "flota de dieciseis"
etiqueta: falso_verde
sintoma:
La flota registra 16 casillas y necesita 17.
como_se_detecto: persona
medida: naval.flota_reglamentaria
evidencia:
celda_barco: id, jugador, fila, columna
"b0", "jugador", 0, 0
"b1", "jugador", 0, 1
"b2", "jugador", 0, 2
"b3", "jugador", 0, 3
"b4", "jugador", 0, 4
"b5", "jugador", 0, 5
"b6", "jugador", 0, 6
"b7", "jugador", 0, 7
"b8", "jugador", 0, 8
"b9", "jugador", 0, 9
"b10", "jugador", 1, 0
"b11", "jugador", 1, 1
"b12", "jugador", 1, 2
"b13", "jugador", 1, 3
"b14", "jugador", 1, 4
"b15", "jugador", 1, 5
leccion:
La flota registra 16 casillas y necesita 17.
caso 012-flota-de-diecisiete:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "flota de diecisiete"
etiqueta: verde_correcto
sintoma:
La flota registra exactamente 17 casillas.
como_se_detecto: persona
medida: naval.flota_reglamentaria
evidencia:
celda_barco: id, jugador, fila, columna
"b0", "jugador", 0, 0
"b1", "jugador", 0, 1
"b2", "jugador", 0, 2
"b3", "jugador", 0, 3
"b4", "jugador", 0, 4
"b5", "jugador", 0, 5
"b6", "jugador", 0, 6
"b7", "jugador", 0, 7
"b8", "jugador", 0, 8
"b9", "jugador", 0, 9
"b10", "jugador", 1, 0
"b11", "jugador", 1, 1
"b12", "jugador", 1, 2
"b13", "jugador", 1, 3
"b14", "jugador", 1, 4
"b15", "jugador", 1, 5
"b16", "jugador", 1, 6
leccion:
La flota registra exactamente 17 casillas.
caso 029-flota-sin-celdas:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "flota sin celdas"
etiqueta: falso_verde
espera: sin_evidencia
sintoma:
Sin ninguna celda de barco no se puede declarar reglamentaria la flota.
como_se_detecto: persona
medida: naval.flota_reglamentaria
evidencia:
celda_barco:
leccion:
Sin ninguna celda de barco no se puede declarar reglamentaria la flota.
caso 003-mismo-tirador-seguido:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "mismo tirador seguido"
etiqueta: falso_verde
sintoma:
Dos turnos consecutivos pertenecen al mismo tirador.
como_se_detecto: persona
medida: naval.alternancia_turnos
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 1, 1, false
1, "jugador", "cpu", 2, 2, false
leccion:
Dos turnos consecutivos pertenecen al mismo tirador.
caso 004-tiradores-alternos:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "tiradores alternos"
etiqueta: verde_correcto
sintoma:
Los turnos consecutivos cambian de tirador.
como_se_detecto: persona
medida: naval.alternancia_turnos
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 1, 1, false
1, "cpu", "jugador", 2, 2, false
leccion:
Los turnos consecutivos cambian de tirador.
caso 017-turno-saltado:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "turno saltado"
etiqueta: falso_verde
sintoma:
La secuencia salta el turno 1.
como_se_detecto: persona
medida: naval.turnos_sin_huecos
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 1, 1, false
2, "cpu", "jugador", 2, 2, false
leccion:
La secuencia salta el turno 1.
caso 018-turnos-contiguos:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "turnos contiguos"
etiqueta: verde_correcto
sintoma:
La secuencia comienza en 0 y no tiene huecos.
como_se_detecto: persona
medida: naval.turnos_sin_huecos
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 1, 1, false
1, "cpu", "jugador", 2, 2, false
leccion:
La secuencia comienza en 0 y no tiene huecos.
caso 030-sin-tiros-registrados:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "sin tiros registrados"
etiqueta: falso_verde
espera: sin_evidencia
sintoma:
Sin tiros registrados no se puede afirmar continuidad de turnos.
como_se_detecto: persona
medida: naval.turnos_sin_huecos
evidencia:
tiro:
leccion:
Sin tiros registrados no se puede afirmar continuidad de turnos.
caso 015-tiro-repetido:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "tiro repetido"
etiqueta: falso_verde
sintoma:
El mismo tirador repite la misma casilla.
como_se_detecto: persona
medida: naval.tiros_sin_repeticion
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, false
2, "jugador", "cpu", 2, 3, false
leccion:
El mismo tirador repite la misma casilla.
caso 016-tiros-distintos:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "tiros distintos"
etiqueta: verde_correcto
sintoma:
El mismo tirador elige dos casillas distintas.
como_se_detecto: persona
medida: naval.tiros_sin_repeticion
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, false
2, "jugador", "cpu", 2, 4, false
leccion:
El mismo tirador elige dos casillas distintas.
caso 031-distinta-fila-igual-turno-ajeno:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "distinta fila igual turno ajeno"
etiqueta: verde_correcto
sintoma:
La fila cambia aunque coincida con el número de turno previo.
como_se_detecto: persona
medida: naval.tiros_sin_repeticion
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 1, 3, false
2, "jugador", "cpu", 0, 3, false
leccion:
La fila cambia aunque coincida con el número de turno previo.
caso 033-fila-confundida-con-turno:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "fila confundida con turno"
etiqueta: verde_correcto
sintoma:
La fila del primer tiro coincide con el turno del segundo, pero las casillas son distintas.
como_se_detecto: persona
medida: naval.tiros_sin_repeticion
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, false
2, "jugador", "cpu", 3, 3, false
leccion:
La fila del primer tiro coincide con el turno del segundo, pero las casillas son distintas.
caso 021-impacto-fantasma:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "impacto fantasma"
etiqueta: falso_verde
sintoma:
El disparo se declara impacto sin barco rival en la casilla.
como_se_detecto: persona
medida: naval.veracidad_impacto_positivo
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, true
celda_barco: id, jugador, fila, columna
"a", "cpu", 2, 4
leccion:
El disparo se declara impacto sin barco rival en la casilla.
caso 022-impacto-real:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "impacto real"
etiqueta: verde_correcto
sintoma:
El impacto declarado coincide con un barco rival.
como_se_detecto: persona
medida: naval.veracidad_impacto_positivo
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, true
celda_barco: id, jugador, fila, columna
"a", "cpu", 2, 3
leccion:
El impacto declarado coincide con un barco rival.
caso 032-agua-declarada-agua:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "agua declarada agua"
etiqueta: verde_correcto
sintoma:
Una casilla de agua declarada agua no es impacto fantasma.
como_se_detecto: persona
medida: naval.veracidad_impacto_positivo
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, false
celda_barco: id, jugador, fila, columna
"a", "cpu", 2, 4
leccion:
Una casilla de agua declarada agua no es impacto fantasma.
caso 019-barco-reportado-como-agua:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "barco reportado como agua"
etiqueta: falso_verde
sintoma:
El disparo toca un barco rival y se registra como agua.
como_se_detecto: persona
medida: naval.veracidad_impacto_negativo
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, false
celda_barco: id, jugador, fila, columna
"a", "cpu", 2, 3
leccion:
El disparo toca un barco rival y se registra como agua.
caso 020-agua-reportada-como-agua:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "agua reportada como agua"
etiqueta: verde_correcto
sintoma:
El disparo cae en agua y se registra como agua.
como_se_detecto: persona
medida: naval.veracidad_impacto_negativo
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
0, "jugador", "cpu", 2, 3, false
celda_barco: id, jugador, fila, columna
"a", "cpu", 2, 4
leccion:
El disparo cae en agua y se registra como agua.
caso 013-ganador-con-dieciseis-impactos:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "ganador con dieciseis impactos"
etiqueta: falso_verde
sintoma:
El ganador sólo acumuló 16 impactos.
como_se_detecto: persona
medida: naval.ganador_legitimo
evidencia:
partida: turno_final, impactos_ganador
30, 16
leccion:
El ganador sólo acumuló 16 impactos.
caso 014-ganador-con-diecisiete-impactos:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "ganador con diecisiete impactos"
etiqueta: verde_correcto
sintoma:
El ganador acumuló los 17 impactos exigidos.
como_se_detecto: persona
medida: naval.ganador_legitimo
evidencia:
partida: turno_final, impactos_ganador
30, 17
leccion:
El ganador acumuló los 17 impactos exigidos.
caso 009-tiro-despues-del-final:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "tiro despues del final"
etiqueta: falso_verde
sintoma:
El turno del tiro supera el turno final.
como_se_detecto: persona
medida: naval.fin_de_juego_sin_tiros_posteriores
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
5, "jugador", "cpu", 1, 1, false
partida: turno_final, impactos_ganador
4, 17
leccion:
El turno del tiro supera el turno final.
caso 010-tiro-en-turno-final:
fecha: "2026-09-25"
origen:
repo: "ejemplo/batalla-naval"
commit: "sin-commit"
procedencia: construida
titulo: "tiro en turno final"
etiqueta: verde_correcto
sintoma:
El tiro ocurre en el turno final permitido.
como_se_detecto: persona
medida: naval.fin_de_juego_sin_tiros_posteriores
evidencia:
tiro: turno, tirador, receptor, fila, columna, es_impacto
4, "jugador", "cpu", 1, 1, false
partida: turno_final, impactos_ganador
4, 17
leccion:
El tiro ocurre en el turno final permitido.
Volvé a correr las pruebas en la terminal:
oracle test
UNITARIOS: salteados (sólo aplican al propio Oracle)
CORPUS OK · 33 casos · esquema, evidencia L0 y trazabilidad en regla
SINTAXIS OK · 11 medidas · 0 macros · 33 casos · 0 relaciones
catálogo: 11 medidas · corpus: 33 casos
ROJO 001-tiro-fuera-de-tablero naval.tiros_dentro_del_tablero (valor 1)
verde 002-tiro-valido naval.tiros_dentro_del_tablero (valor 0)
ROJO 003-mismo-tirador-seguido naval.alternancia_turnos (valor 1)
verde 004-tiradores-alternos naval.alternancia_turnos (valor 0)
ROJO 005-barco-fila-desbordada naval.barcos_dentro_del_tablero (valor 1)
verde 006-barco-en-borde naval.barcos_dentro_del_tablero (valor 0)
ROJO 007-barcos-superpuestos naval.barcos_sin_solapamiento (valor 1)
verde 008-barcos-separados naval.barcos_sin_solapamiento (valor 0)
ROJO 009-tiro-despues-del-final naval.fin_de_juego_sin_tiros_posteriores (valor 1)
verde 010-tiro-en-turno-final naval.fin_de_juego_sin_tiros_posteriores (valor 0)
ROJO 011-flota-de-dieciseis naval.flota_reglamentaria (valor 1)
verde 012-flota-de-diecisiete naval.flota_reglamentaria (valor 0)
ROJO 013-ganador-con-dieciseis-impactos naval.ganador_legitimo (valor 1)
verde 014-ganador-con-diecisiete-impactos naval.ganador_legitimo (valor 0)
ROJO 015-tiro-repetido naval.tiros_sin_repeticion (valor 1)
verde 016-tiros-distintos naval.tiros_sin_repeticion (valor 0)
ROJO 017-turno-saltado naval.turnos_sin_huecos (valor 1)
verde 018-turnos-contiguos naval.turnos_sin_huecos (valor 0)
ROJO 019-barco-reportado-como-agua naval.veracidad_impacto_negativo (valor 1)
verde 020-agua-reportada-como-agua naval.veracidad_impacto_negativo (valor 0)
ROJO 021-impacto-fantasma naval.veracidad_impacto_positivo (valor 1)
verde 022-impacto-real naval.veracidad_impacto_positivo (valor 0)
ROJO 023-columna-desbordada naval.tiros_dentro_del_tablero (valor 1)
ROJO 024-fila-negativa naval.tiros_dentro_del_tablero (valor 1)
ROJO 025-columna-negativa naval.tiros_dentro_del_tablero (valor 1)
ROJO 026-barco-columna-desbordada naval.barcos_dentro_del_tablero (valor 1)
ROJO 027-barco-fila-negativa naval.barcos_dentro_del_tablero (valor 1)
ROJO 028-barco-columna-negativa naval.barcos_dentro_del_tablero (valor 1)
SIN EVIDENCIA 029-flota-sin-celdas naval.flota_reglamentaria («celda_barco» vacía)
SIN EVIDENCIA 030-sin-tiros-registrados naval.turnos_sin_huecos («tiro» vacía)
verde 031-distinta-fila-igual-turno-ajeno naval.tiros_sin_repeticion (valor 0)
verde 032-agua-declarada-agua naval.veracidad_impacto_positivo (valor 0)
verde 033-fila-confundida-con-turno naval.tiros_sin_repeticion (valor 0)
defectos que se pusieron rojos: 17 · verdes correctos: 14 · sin evidencia esperada: 2 · huecos declarados: 0
nivel meta — el marco medido con sus propias medidas:
ACEPTACIÓN ✓ — 17 defectos en rojo, 2 sin evidencia esperada, 14 verdes correctos, 0 huecos declarados sin tapar
DIFERENCIAL: salteado (el proyecto no tiene fixtures en diferencial/ todavía)
mutantes de medida (medida × mutador): 209 · murieron 209 · sobrevivieron 0
con 30 mutadores: 6 de quien escribió el lenguaje y 24 de otro autor (ver https://github.com/Segtem/oracle/blob/main/docs/decisiones/DECISION-011-LOS-MUTADORES-TIENEN-AUTOR.md)
de los muertos: 158 por conducta (invirtió el veredicto, cambió testigos o cambió el valor) · 51 rechazados por el álgebra sin evaluar
respaldo real: 0 de 209 muertos los mata al menos un caso observado; 209 sólo los sostiene evidencia construida, generada, sin procedencia o diferencial
detecciones evaluadas (mutante × caso): 634
sin políticas meta activas — se informa sólo el resultado operativo
MUTACIÓN DE CÓDIGO: salteada (sólo aplica al propio Oracle)
ALCANCE: verificación de medidas contra casos guardados del corpus.
PRODUCTO: sin nueva medición; la aceptación no reejecuta los comandos de origen ni el producto. El resultado no certifica su estado actual.
VEREDICTO: VERDE (todas las verificaciones aplicables en regla)
Lo que tenés que ver. Fijate al final de la pantalla que aparezca VEREDICTO: VERDE (todas las verificaciones aplicables en regla): eso te confirma que las once reglas y los 33 casos de prueba se ejecutaron impecablemente, y que ningún mutante quedó vivo.
Si ves otra cosa. Si la salida muestra MEDIDAS SIN CASOS o termina en ROJO, leé atentamente los renglones que tienen un punto ·: te van a marcar cuál es la medida o caso que falta o si hubo un error de tipeo en algún nombre de archivo.
Paso 8 · Juzgar una partida de verdad#
Las reglas ya están probadas en el laboratorio de casos. Ahora vamos a juzgar una partida real jugada por vos: abrí index.html en tu navegador, jugá una partida entera contra la computadora, abrí el panel de auditoría (abajo a la derecha) y hacé clic en el botón para descargar hechos_partida.json.
El navegador web va a guardar el archivo en tu carpeta de Descargas. Andá a tu carpeta de Descargas, hacé clic derecho sobre hechos_partida.json, elegí «Cortar», andá a tu carpeta del proyecto batalla-naval y poné «Pegar» (el archivo tiene que quedar en la carpeta principal de batalla-naval, al lado de oracle.json, que es donde lo busca el comando).
(Si todavía no jugaste o querés probar con una partida real ya lista, podés bajar la partida del ejemplo. Como se descarga con el nombre partida_real.json, andá a Descargas, hacé clic derecho sobre el archivo, elegí «Cambiar nombre», escribí exactamente hechos_partida.json y luego pegalo en tu carpeta batalla-naval).
Si preferís crearlo a mano en vez de descargarlo, abrí el desplegable de abajo. Es un archivo muy extenso (más de 1500 líneas) porque contiene el registro detallado de cada tiro y posición de una partida completa:
Ver la partida del ejemplo (hechos_partida.json)
{
"celda_barco": [
{
"id": 0,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 0,
"fila": 3,
"columna": 0
},
{
"id": 1,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 1,
"fila": 4,
"columna": 0
},
{
"id": 2,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 2,
"fila": 5,
"columna": 0
},
{
"id": 3,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 3,
"fila": 6,
"columna": 0
},
{
"id": 4,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 4,
"fila": 7,
"columna": 0
},
{
"id": 5,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 0,
"fila": 0,
"columna": 9
},
{
"id": 6,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 1,
"fila": 1,
"columna": 9
},
{
"id": 7,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 2,
"fila": 2,
"columna": 9
},
{
"id": 8,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 3,
"fila": 3,
"columna": 9
},
{
"id": 9,
"jugador": "cpu",
"barco_id": "crucero",
"segmento_idx": 0,
"fila": 3,
"columna": 7
},
{
"id": 10,
"jugador": "cpu",
"barco_id": "crucero",
"segmento_idx": 1,
"fila": 4,
"columna": 7
},
{
"id": 11,
"jugador": "cpu",
"barco_id": "crucero",
"segmento_idx": 2,
"fila": 5,
"columna": 7
},
{
"id": 12,
"jugador": "cpu",
"barco_id": "submarino",
"segmento_idx": 0,
"fila": 1,
"columna": 2
},
{
"id": 13,
"jugador": "cpu",
"barco_id": "submarino",
"segmento_idx": 1,
"fila": 1,
"columna": 3
},
{
"id": 14,
"jugador": "cpu",
"barco_id": "submarino",
"segmento_idx": 2,
"fila": 1,
"columna": 4
},
{
"id": 15,
"jugador": "cpu",
"barco_id": "destructor",
"segmento_idx": 0,
"fila": 1,
"columna": 6
},
{
"id": 16,
"jugador": "cpu",
"barco_id": "destructor",
"segmento_idx": 1,
"fila": 2,
"columna": 6
},
{
"id": 17,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 0,
"fila": 9,
"columna": 2
},
{
"id": 18,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 1,
"fila": 9,
"columna": 3
},
{
"id": 19,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 2,
"fila": 9,
"columna": 4
},
{
"id": 20,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 3,
"fila": 9,
"columna": 5
},
{
"id": 21,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 4,
"fila": 9,
"columna": 6
},
{
"id": 22,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 0,
"fila": 7,
"columna": 5
},
{
"id": 23,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 1,
"fila": 7,
"columna": 6
},
{
"id": 24,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 2,
"fila": 7,
"columna": 7
},
{
"id": 25,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 3,
"fila": 7,
"columna": 8
},
{
"id": 26,
"jugador": "jugador",
"barco_id": "crucero",
"segmento_idx": 0,
"fila": 8,
"columna": 1
},
{
"id": 27,
"jugador": "jugador",
"barco_id": "crucero",
"segmento_idx": 1,
"fila": 8,
"columna": 2
},
{
"id": 28,
"jugador": "jugador",
"barco_id": "crucero",
"segmento_idx": 2,
"fila": 8,
"columna": 3
},
{
"id": 29,
"jugador": "jugador",
"barco_id": "submarino",
"segmento_idx": 0,
"fila": 4,
"columna": 4
},
{
"id": 30,
"jugador": "jugador",
"barco_id": "submarino",
"segmento_idx": 1,
"fila": 4,
"columna": 5
},
{
"id": 31,
"jugador": "jugador",
"barco_id": "submarino",
"segmento_idx": 2,
"fila": 4,
"columna": 6
},
{
"id": 32,
"jugador": "jugador",
"barco_id": "destructor",
"segmento_idx": 0,
"fila": 1,
"columna": 8
},
{
"id": 33,
"jugador": "jugador",
"barco_id": "destructor",
"segmento_idx": 1,
"fila": 2,
"columna": 8
}
],
"tiro": [
{
"turno": 0,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 1,
"tirador": "cpu",
"receptor": "jugador",
"fila": 5,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 2,
"tirador": "jugador",
"receptor": "cpu",
"fila": 3,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 3,
"tirador": "cpu",
"receptor": "jugador",
"fila": 3,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 4,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 6,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 5,
"tirador": "cpu",
"receptor": "jugador",
"fila": 6,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 6,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 7,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 8,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 9,
"tirador": "cpu",
"receptor": "jugador",
"fila": 1,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 10,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 11,
"tirador": "cpu",
"receptor": "jugador",
"fila": 0,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 12,
"tirador": "jugador",
"receptor": "cpu",
"fila": 5,
"columna": 0,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 13,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 14,
"tirador": "jugador",
"receptor": "cpu",
"fila": 3,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 15,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 4,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 16,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 17,
"tirador": "cpu",
"receptor": "jugador",
"fila": 3,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 18,
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 19,
"tirador": "cpu",
"receptor": "jugador",
"fila": 5,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 20,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 21,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 22,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 23,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 5,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 24,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 25,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 6,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "submarino"
},
{
"turno": 26,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 3,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 27,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 28,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 29,
"tirador": "cpu",
"receptor": "jugador",
"fila": 5,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 30,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 31,
"tirador": "cpu",
"receptor": "jugador",
"fila": 6,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 32,
"tirador": "jugador",
"receptor": "cpu",
"fila": 5,
"columna": 7,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 33,
"tirador": "cpu",
"receptor": "jugador",
"fila": 0,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 34,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 35,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 7,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 36,
"tirador": "jugador",
"receptor": "cpu",
"fila": 3,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 37,
"tirador": "cpu",
"receptor": "jugador",
"fila": 6,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 38,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 4,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 39,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 40,
"tirador": "jugador",
"receptor": "cpu",
"fila": 7,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 41,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 6,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 42,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 6,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "destructor"
},
{
"turno": 43,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 5,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 44,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 45,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 46,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 9,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 47,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 8,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "acorazado"
},
{
"turno": 48,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 49,
"tirador": "cpu",
"receptor": "jugador",
"fila": 3,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 50,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 51,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 52,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 53,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 54,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 55,
"tirador": "cpu",
"receptor": "jugador",
"fila": 2,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 56,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 57,
"tirador": "cpu",
"receptor": "jugador",
"fila": 3,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 58,
"tirador": "jugador",
"receptor": "cpu",
"fila": 3,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 59,
"tirador": "cpu",
"receptor": "jugador",
"fila": 2,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 60,
"tirador": "jugador",
"receptor": "cpu",
"fila": 5,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 61,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 62,
"tirador": "jugador",
"receptor": "cpu",
"fila": 5,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 63,
"tirador": "cpu",
"receptor": "jugador",
"fila": 0,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 64,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 65,
"tirador": "cpu",
"receptor": "jugador",
"fila": 1,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 66,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 67,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 2,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 68,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 9,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 69,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 70,
"tirador": "jugador",
"receptor": "cpu",
"fila": 9,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 71,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 2,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 72,
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 73,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 1,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 74,
"tirador": "jugador",
"receptor": "cpu",
"fila": 7,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 75,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 3,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "crucero"
},
{
"turno": 76,
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 77,
"tirador": "cpu",
"receptor": "jugador",
"fila": 3,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 78,
"tirador": "jugador",
"receptor": "cpu",
"fila": 2,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 79,
"tirador": "cpu",
"receptor": "jugador",
"fila": 5,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 80,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 81,
"tirador": "cpu",
"receptor": "jugador",
"fila": 6,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 82,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 0,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 83,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 84,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 85,
"tirador": "cpu",
"receptor": "jugador",
"fila": 1,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 86,
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 87,
"tirador": "cpu",
"receptor": "jugador",
"fila": 8,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 88,
"tirador": "jugador",
"receptor": "cpu",
"fila": 9,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 89,
"tirador": "cpu",
"receptor": "jugador",
"fila": 2,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 90,
"tirador": "jugador",
"receptor": "cpu",
"fila": 7,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 91,
"tirador": "cpu",
"receptor": "jugador",
"fila": 1,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 92,
"tirador": "jugador",
"receptor": "cpu",
"fila": 9,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 93,
"tirador": "cpu",
"receptor": "jugador",
"fila": 6,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 94,
"tirador": "jugador",
"receptor": "cpu",
"fila": 5,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 95,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 96,
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 97,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 7,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 98,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 7,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 99,
"tirador": "cpu",
"receptor": "jugador",
"fila": 0,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 100,
"tirador": "jugador",
"receptor": "cpu",
"fila": 5,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 101,
"tirador": "cpu",
"receptor": "jugador",
"fila": 2,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 102,
"tirador": "jugador",
"receptor": "cpu",
"fila": 9,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 103,
"tirador": "cpu",
"receptor": "jugador",
"fila": 3,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 104,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 8,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 105,
"tirador": "cpu",
"receptor": "jugador",
"fila": 4,
"columna": 0,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 106,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 107,
"tirador": "cpu",
"receptor": "jugador",
"fila": 1,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 108,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 0,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 109,
"tirador": "cpu",
"receptor": "jugador",
"fila": 5,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 110,
"tirador": "jugador",
"receptor": "cpu",
"fila": 7,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 111,
"tirador": "cpu",
"receptor": "jugador",
"fila": 6,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 112,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 113,
"tirador": "cpu",
"receptor": "jugador",
"fila": 7,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 114,
"tirador": "jugador",
"receptor": "cpu",
"fila": 1,
"columna": 2,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "submarino"
},
{
"turno": 115,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 3,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 116,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 9,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 117,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 4,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 118,
"tirador": "jugador",
"receptor": "cpu",
"fila": 7,
"columna": 4,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 119,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 5,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 120,
"tirador": "jugador",
"receptor": "cpu",
"fila": 3,
"columna": 3,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 121,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 6,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "portaaviones"
},
{
"turno": 122,
"tirador": "jugador",
"receptor": "cpu",
"fila": 6,
"columna": 2,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 123,
"tirador": "cpu",
"receptor": "jugador",
"fila": 9,
"columna": 1,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 124,
"tirador": "jugador",
"receptor": "cpu",
"fila": 8,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 125,
"tirador": "cpu",
"receptor": "jugador",
"fila": 0,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 126,
"tirador": "jugador",
"receptor": "cpu",
"fila": 4,
"columna": 6,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 127,
"tirador": "cpu",
"receptor": "jugador",
"fila": 2,
"columna": 8,
"es_impacto": true,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 128,
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 5,
"es_impacto": false,
"hundio_barco": false,
"barco_hundido": "ninguno"
},
{
"turno": 129,
"tirador": "cpu",
"receptor": "jugador",
"fila": 1,
"columna": 8,
"es_impacto": true,
"hundio_barco": true,
"barco_hundido": "destructor"
}
],
"partida": [
{
"estado": "terminada",
"ganador": "cpu",
"perdedor": "jugador",
"turno_final": 129,
"total_tiros": 130,
"impactos_ganador": 17,
"impactos_perdedor": 12
}
]
}
En la terminal, escribí oracle juzgar --con hechos_partida.json y presioná Enter. El parámetro --con le indica a Oracle qué archivo de hechos debe evaluar contra las once medidas del catálogo:
oracle juzgar --con hechos_partida.json
✓ naval.alternancia_turnos 0 (<= 0)
✓ naval.barcos_dentro_del_tablero 0 (<= 0)
✓ naval.barcos_sin_solapamiento 0 (<= 0)
✓ naval.fin_de_juego_sin_tiros_posteriores 0 (<= 0)
✓ naval.flota_reglamentaria 0 (<= 0)
✓ naval.ganador_legitimo 0 (<= 0)
✓ naval.tiros_dentro_del_tablero 0 (<= 0)
✓ naval.tiros_sin_repeticion 0 (<= 0)
✓ naval.turnos_sin_huecos 0 (<= 0)
✓ naval.veracidad_impacto_negativo 0 (<= 0)
✓ naval.veracidad_impacto_positivo 0 (<= 0)
VEREDICTO: verde en 11 medidas. SIN MIRAR:
· naval.alternancia_turnos: compara el tirador del turno k con el tirador del turno k+1. No valida la ausencia de saltos si falta un turno completo.
· naval.barcos_dentro_del_tablero: revisa los límites de cada celda de barco reportada. No verifica solapamientos ni continuidad.
· naval.barcos_sin_solapamiento: detecta pares de celdas distintas del mismo jugador con id1 < id2 que colisionan en la misma coordenada. No comprueba barcos de jugadores opuestos.
· naval.fin_de_juego_sin_tiros_posteriores: compara el turno de cada tiro contra el turno final registrado en el estado de la partida. No valida si la partida debió terminar antes.
· naval.flota_reglamentaria: agrupa las casillas de barco por jugador y exige exactamente 17 casillas por flota. No comprueba la orientación lineal de los barcos.
· naval.ganador_legitimo: comprueba que los impactos acumulados por el ganador sean exactamente 17. No valida si los tiros fueron asignados al tirador correcto.
· naval.tiros_dentro_del_tablero: comprueba que las coordenadas de cada disparo estén en el rango [0, 9]. No comprueba si el casillero ya fue disparado previamente ni la validez del turno.
· naval.tiros_sin_repeticion: detecta disparos repetidos del mismo jugador a la misma celda con diferente número de turno. No evalúa si el tiro fue agua o impacto.
· naval.turnos_sin_huecos: compara la cantidad total de tiros registrados contra el turno máximo reportado más uno. No valida la alternancia de tiradores.
· naval.veracidad_impacto_negativo: cruza cada disparo contra las celdas de la flota rival y detecta impactos omitidos registrados falsamente como agua. No verifica la secuencia de turnos.
· naval.veracidad_impacto_positivo: detecta disparos registrados como impacto que no corresponden a ninguna celda ocupada por la flota del receptor. No verifica si el barco ya estaba completamente hundido.
Lo que tenés que ver. Vas a ver que las once reglas tienen un tilde ✓ y dice VEREDICTO: verde en 11 medidas. Abajo aparece una lista titulada SIN MIRAR:: no te asustes, no son errores. Oracle es totalmente honesto y transparente: te avisa qué cosas comprobó y te declara abiertamente cuáles detalles del juego quedaron fuera de su vigilancia (por ejemplo, que los barcos tengan forma recta). Esa lista no es letra chica: es la otra mitad del veredicto.
Si ves otra cosa. Si la terminal te dice que no encuentra el archivo hechos_partida.json, revisá que esté adentro de la carpeta principal batalla-naval (no adentro de corpus ni de js) y que no tenga una extensión oculta como .json.txt.
Pensalo. Todo verde. ¿Quiere decir que el juego está bien?
Ver la respuesta
Quiere decir que esas once reglas se cumplieron en esta partida. La lista SIN MIRAR dice lo que ninguna regla garantiza, por ejemplo que cada barco sea una línea recta. Esa lista no es letra chica: es la mitad de la respuesta.
Las reglas tienen nombres técnicos, pero conviene escribir también las promesas generales que querés hacer sobre el juego. En desarrollo de software, un requisito es una promesa formal sobre el comportamiento que debe tener el sistema. En Oracle, cada archivo con extensión .requisito dice qué prometés, qué medidas técnicas lo comprueban y qué parte sigue sin medir.
Primero, creá una carpeta llamada requisitos adentro de batalla-naval (en la terminal podés escribir mkdir requisitos o crearla desde tu explorador de archivos). Después abrí el desplegable de abajo y creá adentro de esa carpeta estos seis archivos con extensión .requisito: cinco agrupan las once medidas y el sexto deja visible una deuda técnica:
Ver las promesas del juego
requisito naval.flota:
texto "Las flotas ocupan 17 casillas por bando, dentro del tablero y sin solaparse"
medido_por naval.barcos_dentro_del_tablero, naval.barcos_sin_solapamiento, naval.flota_reglamentaria
sin_medir "la forma recta y continua de cada barco"
requisito naval.disparos:
texto "Cada tirador dispara a casillas válidas y no repite una coordenada"
medido_por naval.tiros_dentro_del_tablero, naval.tiros_sin_repeticion
requisito naval.turnos:
texto "Los tiros se registran sin huecos y alternan entre los jugadores"
medido_por naval.alternancia_turnos, naval.turnos_sin_huecos
requisito naval.impactos:
texto "Un impacto reportado corresponde a un barco rival y un tiro al barco no figura como agua"
medido_por naval.veracidad_impacto_negativo, naval.veracidad_impacto_positivo
requisito naval.final:
texto "El ganador registra 17 impactos y no se dispara después del turno final"
medido_por naval.fin_de_juego_sin_tiros_posteriores, naval.ganador_legitimo
requisito naval.barcos_rectos:
texto "Cada barco ocupa una línea recta y continua"
sin_medir "todavía no hay una medida para comprobar la forma de cada barco"
Pensalo. ¿Tener una medida para una promesa quiere decir que se cumplió en esta partida?
Ver la respuesta
Todavía no. oracle cobertura muestra qué promesas tienen medidas; con --con juzga los hechos de esta partida y dice cuáles se cumplieron. Una promesa puede quedar sólo en parte medida.
En Oracle, la cobertura muestra cuántas de tus promesas o requisitos están respaldados por reglas y cuáles se cumplieron efectivamente en tu partida. Ejecutá estos dos comandos en la terminal:
oracle cobertura
oracle cobertura --con hechos_partida.json
· naval.barcos_rectos SIN MEDIR: todavía no hay una medida para comprobar la forma de cada barco
✓ naval.disparos naval.tiros_dentro_del_tablero, naval.tiros_sin_repeticion
✓ naval.final naval.fin_de_juego_sin_tiros_posteriores, naval.ganador_legitimo
◐ naval.flota naval.barcos_dentro_del_tablero, naval.barcos_sin_solapamiento, naval.flota_reglamentaria · SIN MEDIR: la forma recta y continua de cada barco
✓ naval.impactos naval.veracidad_impacto_negativo, naval.veracidad_impacto_positivo
✓ naval.turnos naval.alternancia_turnos, naval.turnos_sin_huecos
6 requisitos: 4 medidos · 1 en parte · 1 sin medir · 0 con medidas inexistentes
· naval.barcos_rectos sin medir · SIN MEDIR: todavía no hay una medida para comprobar la forma de cada barco
✓ naval.disparos cumple · naval.tiros_dentro_del_tablero cumple, naval.tiros_sin_repeticion cumple
✓ naval.final cumple · naval.fin_de_juego_sin_tiros_posteriores cumple, naval.ganador_legitimo cumple
◐ naval.flota cumple · naval.barcos_dentro_del_tablero cumple, naval.barcos_sin_solapamiento cumple, naval.flota_reglamentaria cumple · SIN MEDIR: la forma recta y continua de cada barco
✓ naval.impactos cumple · naval.veracidad_impacto_negativo cumple, naval.veracidad_impacto_positivo cumple
✓ naval.turnos cumple · naval.alternancia_turnos cumple, naval.turnos_sin_huecos cumple
6 requisitos: 5 se cumplen (1 sólo en lo medido) · 0 no se cumplen · 0 sin juicio · 1 sin medir · 0 con medidas inexistentes
Lo que tenés que ver. La terminal te muestra el estado de tus promesas con tres símbolos claros:
- El tilde
✓indica un requisito completamente vigilado que se cumplió en esta partida. - El círculo medio lleno
◐significa que se cumple sólo en parte (la flota tiene 17 casillas, pero todavía no sabemos si los barcos son rectos). - El punto
·señala un requisito que estásin medir, sin ninguna regla que lo controle. Oracle no lo disfraza de verde.
Si más adelante tus promesas vienen de una especificación formal de requisitos (como OpenSpec), vas a poder usar oracle requisito importar para crear esos archivos automáticamente. Acá los escribimos a mano para que veas qué significa cada línea.
Pensalo. ¿Y si el juego se olvidara de anotar los tiros?
Ver la respuesta
Las reglas de tiros no tendrían nada que mirar. Un sistema descuidado las daría por buenas; Oracle las lista aparte, en NO SE APLICARON, y falla la corrida completa. Si ejecutás sólo una parte del catálogo a propósito, --parcial permite esa omisión. Probalo:
Guardá este archivo como sin-tiros.json: contiene la misma partida, con celda_barco y partida, pero sin la relación tiro.
{
"celda_barco": [
{
"id": 0,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 0,
"fila": 3,
"columna": 0
},
{
"id": 1,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 1,
"fila": 4,
"columna": 0
},
{
"id": 2,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 2,
"fila": 5,
"columna": 0
},
{
"id": 3,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 3,
"fila": 6,
"columna": 0
},
{
"id": 4,
"jugador": "cpu",
"barco_id": "portaaviones",
"segmento_idx": 4,
"fila": 7,
"columna": 0
},
{
"id": 5,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 0,
"fila": 0,
"columna": 9
},
{
"id": 6,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 1,
"fila": 1,
"columna": 9
},
{
"id": 7,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 2,
"fila": 2,
"columna": 9
},
{
"id": 8,
"jugador": "cpu",
"barco_id": "acorazado",
"segmento_idx": 3,
"fila": 3,
"columna": 9
},
{
"id": 9,
"jugador": "cpu",
"barco_id": "crucero",
"segmento_idx": 0,
"fila": 3,
"columna": 7
},
{
"id": 10,
"jugador": "cpu",
"barco_id": "crucero",
"segmento_idx": 1,
"fila": 4,
"columna": 7
},
{
"id": 11,
"jugador": "cpu",
"barco_id": "crucero",
"segmento_idx": 2,
"fila": 5,
"columna": 7
},
{
"id": 12,
"jugador": "cpu",
"barco_id": "submarino",
"segmento_idx": 0,
"fila": 1,
"columna": 2
},
{
"id": 13,
"jugador": "cpu",
"barco_id": "submarino",
"segmento_idx": 1,
"fila": 1,
"columna": 3
},
{
"id": 14,
"jugador": "cpu",
"barco_id": "submarino",
"segmento_idx": 2,
"fila": 1,
"columna": 4
},
{
"id": 15,
"jugador": "cpu",
"barco_id": "destructor",
"segmento_idx": 0,
"fila": 1,
"columna": 6
},
{
"id": 16,
"jugador": "cpu",
"barco_id": "destructor",
"segmento_idx": 1,
"fila": 2,
"columna": 6
},
{
"id": 17,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 0,
"fila": 9,
"columna": 2
},
{
"id": 18,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 1,
"fila": 9,
"columna": 3
},
{
"id": 19,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 2,
"fila": 9,
"columna": 4
},
{
"id": 20,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 3,
"fila": 9,
"columna": 5
},
{
"id": 21,
"jugador": "jugador",
"barco_id": "portaaviones",
"segmento_idx": 4,
"fila": 9,
"columna": 6
},
{
"id": 22,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 0,
"fila": 7,
"columna": 5
},
{
"id": 23,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 1,
"fila": 7,
"columna": 6
},
{
"id": 24,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 2,
"fila": 7,
"columna": 7
},
{
"id": 25,
"jugador": "jugador",
"barco_id": "acorazado",
"segmento_idx": 3,
"fila": 7,
"columna": 8
},
{
"id": 26,
"jugador": "jugador",
"barco_id": "crucero",
"segmento_idx": 0,
"fila": 8,
"columna": 1
},
{
"id": 27,
"jugador": "jugador",
"barco_id": "crucero",
"segmento_idx": 1,
"fila": 8,
"columna": 2
},
{
"id": 28,
"jugador": "jugador",
"barco_id": "crucero",
"segmento_idx": 2,
"fila": 8,
"columna": 3
},
{
"id": 29,
"jugador": "jugador",
"barco_id": "submarino",
"segmento_idx": 0,
"fila": 4,
"columna": 4
},
{
"id": 30,
"jugador": "jugador",
"barco_id": "submarino",
"segmento_idx": 1,
"fila": 4,
"columna": 5
},
{
"id": 31,
"jugador": "jugador",
"barco_id": "submarino",
"segmento_idx": 2,
"fila": 4,
"columna": 6
},
{
"id": 32,
"jugador": "jugador",
"barco_id": "destructor",
"segmento_idx": 0,
"fila": 1,
"columna": 8
},
{
"id": 33,
"jugador": "jugador",
"barco_id": "destructor",
"segmento_idx": 1,
"fila": 2,
"columna": 8
}
],
"partida": [
{
"estado": "terminada",
"ganador": "cpu",
"perdedor": "jugador",
"turno_final": 129,
"total_tiros": 130,
"impactos_ganador": 17,
"impactos_perdedor": 12
}
]
}
oracle juzgar --con sin-tiros.json
✓ naval.barcos_dentro_del_tablero 0 (<= 0)
✓ naval.barcos_sin_solapamiento 0 (<= 0)
✓ naval.flota_reglamentaria 0 (<= 0)
✓ naval.ganador_legitimo 0 (<= 0)
NO SE APLICARON (7) — su relación no vino en la evidencia:
· naval.alternancia_turnos: falta tiro
· naval.fin_de_juego_sin_tiros_posteriores: falta tiro
· naval.tiros_dentro_del_tablero: falta tiro
· naval.tiros_sin_repeticion: falta tiro
· naval.turnos_sin_huecos: falta tiro
· naval.veracidad_impacto_negativo: falta tiro
· naval.veracidad_impacto_positivo: falta tiro
VEREDICTO: 7 medidas propias sin aplicar (usá --parcial para una corrida deliberadamente parcial)
Lo que tenés que ver. Tenés que ver que Oracle se niega a dar un aprobado a ciegas. Siete reglas no tenían qué mirar porque faltaban los datos de los disparos, y aparecen listadas bajo NO SE APLICARON. Como faltaba información esencial, la corrida falla. (Si alguna vez querés evaluar deliberadamente una corrida parcial sin que falle, podés agregarle la opción --parcial al comando).
Si una regla sale roja en una partida, el veredicto trae los testigos: son las filas exactas de datos que cometieron la infracción (mostrándote en qué turno o coordenada ocurrió la trampa). Con esa evidencia puntual le volvés a hablar al modelo de lenguaje con total precisión: «el tiro del turno 14 se marcó como impacto y no había barco en esa casilla; corregilo».
Paso 9 · Qué le falta a este juego#
Antes de agregar otra regla, guardá esta versión en Git. Git es un sistema de control de versiones que saca fotos del estado de tus archivos: a cada punto de control guardado lo llama commit. El comando oracle cambios compara las reglas actuales con ese punto guardado en Git para vigilar que nadie haya aflojado una exigencia por error.
Vamos a aflojar a propósito el límite de una regla: permitir un disparo repetido a la misma casilla (pasando el umbral de <= 0 a <= 1), sin cambiar la frase porque que defiende el límite original.
Pensalo. Si los casos siguen pasando, ¿cómo te enterás de que alguien aflojó esa promesa?
Ver la respuesta
Git guarda el antes; oracle cambios revisa el después. Un límite más permisivo con el mismo porque es un error aunque el resto del catálogo siga igual.
Copiá y pegá estas líneas en la terminal para inicializar Git (git init), registrar los archivos (git add .) y guardar el commit base (git commit -m base). Después vas a cambiar la regla con tu editor:
git init
git add .
git commit -m base
Git confirma la creación del repositorio y el commit; la rama y el hash varían según tu máquina. Si Git pide nombre y correo, configurá git config user.name "Tu nombre" y git config user.email "tu@correo", y repetí el commit.
Editá catalogos/naval/naval.tiros_sin_repeticion.oracle: reemplazá el archivo entero por este bloque, que permite un tiro repetido.
medida naval.tiros_sin_repeticion:
de tiro t1
unir tiro t2
donde t1.tirador == t2.tirador y t1.turno < t2.turno y t1.fila == t2.fila y t1.columna == t2.columna
resumen contar(1)
umbral <= 1 segun contrato porque "un jugador no puede disparar dos veces a la misma casilla en una partida"
ambito universal
alcance "detecta disparos repetidos del mismo jugador a la misma celda con diferente número de turno. No evalúa si el tiro fue agua o impacto."
En Git, HEAD es el nombre que recibe tu último commit guardado (la versión de referencia de tu proyecto). Ahora ejecutá oracle cambios para comparar las reglas modificadas contra HEAD:
oracle cambios
cambios desde HEAD:
✗ umbral aflojado sin nueva defensa naval.tiros_sin_repeticion <= 0 → <= 1 (el `porque` no cambió)
1 errores · 0 avisos
Lo que tenés que ver. Oracle señala naval.tiros_sin_repeticion y el paso de <= 0 a <= 1: detecta que aflojaste el umbral de la regla pero el texto explicativo porque sigue defendiendo que no haya disparos repetidos.
Si ves otra cosa. Si la terminal dice git: command not found, asegurate de haber instalado Git como indicamos en «Antes de empezar» y de reiniciar la terminal para que reconozca el comando.
Para devolver el umbral a cero, podés abrir catalogos/naval/naval.tiros_sin_repeticion.oracle en tu editor y volver a escribir umbral <= 0 segun contrato, o ejecutar este bloque en la terminal:
medida naval.tiros_sin_repeticion:
de tiro t1
unir tiro t2
donde t1.tirador == t2.tirador y t1.turno < t2.turno y t1.fila == t2.fila y t1.columna == t2.columna
resumen contar(1)
umbral <= 0 segun contrato porque "un jugador no puede disparar dos veces a la misma casilla en una partida"
ambito universal
alcance "detecta disparos repetidos del mismo jugador a la misma celda con diferente número de turno. No evalúa si el tiro fue agua o impacto."
oracle cambios
cambios desde HEAD:
nada se aflojó
0 errores · 0 avisos
Lo que tenés que ver. Al correrlo, tenés que ver que dice cambios desde HEAD: nada se aflojó y 0 errores.
El ejemplo también incluye una explicación y un script (un programa que se ejecuta de corrido para hacer pruebas automáticas) para probar infracciones deliberadas. Copialos para tener la carpeta completa adentro de batalla-naval: un archivo de texto explicativo en formato Markdown (README.md) y el script de Python (verificar_oraculo.py). El recorrido de arriba no necesita ejecutar ese script.
Ver los archivos complementarios
# Batalla Naval en HTML5 con Auditoría Formal
Juego táctico de **Batalla Naval (Battleship)** en HTML5/CSS3/JavaScript con verificación formal de reglas mediante **Oracle** (`oracle-metalenguaje`).
---
## 1. Cómo jugar
Podés abrir [`index.html`](index.html) directamente en cualquier navegador moderno:
```bash
# Podés abrirlo con xdg-open o con un servidor web local:
xdg-open index.html
# o
python3 -m http.server 8080
```
### Características del juego:
- **Tableros 10x10** reglamentarios (filas A–J, columnas 1–10).
- **Flota estándar de 5 navíos** (17 casillas en total):
- Portaaviones (5 casillas)
- Acorazado (4 casillas)
- Crucero (3 casillas)
- Submarino (3 casillas)
- Destructor (2 casillas)
- **Fase de Despliegue**: colocación interactiva de buques, rotación horizontal/vertical con tecla `R` o botón, o botón **"🎲 Auto-Desplegar"** para despliegue rápido.
- **Fase de Batalla**: radar hostil interactivo con retícula táctica, efectos sonoros sintetizados proceduralmente vía **Web Audio API** (cañonazos, salpicaduras de agua, explosiones, sirenas navales), animaciones de impacto y bitácora cronológica de combate.
- **Inteligencia Artificial**: CPU con estrategia de Caza y Blanco (*Hunt & Target*) y búsqueda con paridad de tablero.
- **Panel de Auditoría integrado**: botón **"⚖ Auditoría & Traza Oracle"** para ver el estado de las reglas en vivo, inspeccionar el JSON de hechos relacionales y descargarlo para auditarlo en terminal.
---
## 2. ¿Cómo comprobamos que las reglas se cumplen de verdad?
Para no depender de afirmaciones de confianza ("funciona porque yo lo digo") ni de aserciones atrapadas dentro del mismo código que ejecuta el juego (que podrían compartir los mismos sesgos y errores lógicos), implementamos una **separación estricta entre ejecución y juicio**:
1. **El juego actúa como sensor fáctico puro**: Durante la partida no se valida a sí mismo; en cambio, emite una **traza relacional inmutable de hechos objetivos**:
- `celda_barco`: qué casillas `(fila, columna)` ocupa cada buque de cada jugador.
- `tiro`: la secuencia ordenada de disparos `(turno, tirador, receptor, fila, columna, es_impacto, hundio_barco)`.
- `partida`: el resultado final `(estado, ganador, perdedor, turno_final, total_tiros, impactos_ganador, impactos_perdedor)`.
2. **Oracle actúa como juez formal externo**: Un catálogo declarativo de 11 medidas en lenguaje Oracle evalúa la evidencia relacional de forma independiente.
---
## 3. ¿Por qué usamos `oracle-metalenguaje`?
Elegimos usar `oracle-metalenguaje` por cuatro razones metodológicas fundamentales:
1. **Independencia del evaluador (Desacoplamiento)**:
Si las pruebas se limitan a `assert` dentro de JavaScript, cualquier error conceptual en el diseño del modelo suele propagarse al verificador. Oracle corre fuera del runtime de JavaScript, evaluando únicamente la evidencia relacional sin conocer las estructuras de datos internas del juego.
2. **Declaratividad y álgebra relacional**:
En vez de escribir bucles imperativos con estados mutables, las reglas se declaran mediante álgebra relacional pura (`de`, `unir`, `sin`, `donde`, `agrupar`, `resumen contar(1)`, `umbral <= 0`).
3. **Honestidad epistémica (`alcance` y `porque`)**:
En Oracle cada medida declara explícitamente su justificación contractual (`porque`) y lo que **NO** mira (`alcance`). Un veredicto verde no oculta supuestos invisibles.
4. **Auditabilidad directa para el usuario**:
Cualquier jugador puede descargar el archivo `hechos_partida.json` desde la interfaz web o desde el script y ejecutar directamente:
```bash
oracle juzgar --con hechos_partida.json
```
obteniendo un informe matemático con veredicto, umbrales y testigos.
---
## 4. Las 11 Reglas Formalizadas en el Catálogo
Las medidas se encuentran en el directorio [`catalogos/naval/`](catalogos/naval/):
| Medida | Qué verifica |
| :--- | :--- |
| `naval.tiros_dentro_del_tablero` | Ningún disparo tiene coordenadas fuera de $[0, 9] \times [0, 9]$. |
| `naval.tiros_sin_repeticion` | Ningún jugador dispara más de una vez a la misma casilla. |
| `naval.alternancia_turnos` | Los turnos consecutivos alternan estrictamente entre `jugador` y `cpu`. |
| `naval.turnos_sin_huecos` | La secuencia de turnos es monótona y continua desde $0$ hasta $N-1$ sin saltos ni pérdidas. |
| `naval.barcos_dentro_del_tablero` | Todas las casillas ocupadas por buques pertenecen a la cuadrícula de $10 \times 10$. |
| `naval.barcos_sin_solapamiento` | Dos buques del mismo bando no pueden compartir casillas. |
| `naval.flota_reglamentaria` | Cada bando cuenta con exactamente 17 casillas de barco ($5 + 4 + 3 + 3 + 2$). |
| `naval.veracidad_impacto_positivo` | Un disparo reportado como impacto debe tener un barco rival en esa coordenada (prohíbe impactos fantasma). |
| `naval.veracidad_impacto_negativo` | Un disparo en una celda donde hay un barco rival no puede ser reportado como agua (prohíbe impactos omitidos). |
| `naval.fin_de_juego_sin_tiros_posteriores` | No se registran disparos con turno posterior a la finalización de la partida. |
| `naval.ganador_legitimo` | El bando declarado ganador acumula exactamente 17 impactos (destrucción total de la flota rival). |
---
## 5. Demostración práctica y Falsabilidad
Para comprobar que el oráculo funciona y **no es complaciente** (es decir, que sabe ponerse en **ROJO** cuando se rompen las reglas), creamos el script [`verificar_oraculo.py`](verificar_oraculo.py):
```bash
python3 verificar_oraculo.py
```
En Windows, si `python3` no se reconoce, usá `python verificar_oraculo.py`.
El script busca `partida_real.json` (incluido en el ejemplo del repositorio). Si no lo encuentra,
lee `hechos_partida.json`, el nombre que usa la guía y que descarga el juego. No hace falta renombrarlo.
El script realiza:
1. **Partida real guardada**: lee el archivo de partida y ejecuta `oracle juzgar`, confirmando **VEREDICTO: verde en 11 medidas**.
2. **Inyección de infracciones (contraejemplos)**:
- Inyecta un disparo duplicado $\rightarrow$ Atrapado por `naval.tiros_sin_repeticion` (ROJO).
- Inyecta un disparo en fila 14 $\rightarrow$ Atrapado por `naval.tiros_dentro_del_tablero` (ROJO).
- Inyecta un impacto fantasma en agua $\rightarrow$ Atrapado por `naval.veracidad_impacto_positivo` (ROJO).
- Inyecta turnos consecutivos del mismo tirador $\rightarrow$ Atrapado por `naval.alternancia_turnos` (ROJO).
### Cómo juzgar cualquier partida manualmente:
```bash
oracle juzgar --con partida_real.json
```
Salida esperada:
```
✓ naval.alternancia_turnos 0 (<= 0)
✓ naval.barcos_dentro_del_tablero 0 (<= 0)
✓ naval.barcos_sin_solapamiento 0 (<= 0)
✓ naval.fin_de_juego_sin_tiros_posteriores 0 (<= 0)
✓ naval.flota_reglamentaria 0 (<= 0)
✓ naval.ganador_legitimo 0 (<= 0)
✓ naval.tiros_dentro_del_tablero 0 (<= 0)
✓ naval.tiros_sin_repeticion 0 (<= 0)
✓ naval.turnos_sin_huecos 0 (<= 0)
✓ naval.veracidad_impacto_negativo 0 (<= 0)
✓ naval.veracidad_impacto_positivo 0 (<= 0)
VEREDICTO: verde en 11 medidas.
```
#!/usr/bin/env python3
"""
verificar_oraculo.py - Demostración de verificación rigurosa de reglas con Oracle
Este script:
1. Lee 'partida_real.json' o, en la carpeta armada con la guía, 'hechos_partida.json'.
2. Ejecuta `oracle juzgar --con <partida>` para comprobar que las 11 reglas se cumplen (VERDE).
4. Genera variantes con infracciones deliberadas (trampas/errores) para demostrar
que el oráculo es falsable y no un sello complaciente:
a) Disparo duplicado a la misma celda.
b) Disparo fuera de los límites del tablero (fila 10).
c) Impacto fantasma (mentir diciendo que dio en un barco cuando era agua).
d) Turnos consecutivos del mismo tirador sin alternar.
e) Disparo posterior al fin de la partida.
f) Ganador ilegítimo (con menos de 17 impactos).
"""
import copy
import json
import subprocess
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
CLI = BASE_DIR.parents[1] / "tools" / "cli.py"
def simular_partida():
"""Ejecuta una partida automática usando el motor del juego en Node.js."""
script = """
const { TraceRecorder } = require("./js/trace.js");
const { NavalGame } = require("./js/engine.js");
const trace = new TraceRecorder();
const game = new NavalGame(trace);
game.newGame();
game.playerBoard.autoPlaceFleet();
game.startBattle();
while (game.phase === "battle") {
if (game.currentTurn === "jugador") {
const unshot = [];
for (let r = 0; r < 10; r++) {
for (let c = 0; c < 10; c++) {
if (!game.cpuBoard.grid[r][c].shot) unshot.push({ r, c });
}
}
const target = unshot[Math.floor(Math.random() * unshot.length)];
game.firePlayerShot(target.r, target.c);
} else {
game.executeCpuTurn();
}
}
console.log(trace.toJSON(2));
"""
res = subprocess.run(
["node", "-e", script],
cwd=BASE_DIR,
capture_output=True,
text=True,
check=True
)
return json.loads(res.stdout)
def ejecutar_oracle_juzgar(ruta_json):
"""Ejecuta `oracle juzgar --con <ruta_json>` y retorna (codigo, salida)."""
comando = [sys.executable, str(CLI)] if CLI.is_file() else ["oracle"]
res = subprocess.run(
[*comando, "juzgar", "--proyecto", str(BASE_DIR),
"--con", str(ruta_json)],
cwd=BASE_DIR,
capture_output=True,
text=True
)
return res.returncode, res.stdout.strip() or res.stderr.strip()
def main():
print("=" * 70)
print("DEMOSTRACIÓN DE VERIFICACIÓN FORMAL DE BATALLA NAVAL")
print("Herramienta: Oracle Metalenguaje (oracle juzgar)")
print("=" * 70)
# 1. Partida real legítima
print("\n[1/3] Leyendo partida real guardada...")
ruta_real = BASE_DIR / "partida_real.json"
if not ruta_real.is_file():
ruta_real = BASE_DIR / "hechos_partida.json"
evidencia_real = json.loads(ruta_real.read_text(encoding="utf-8"))
print(f" -> Leída de {ruta_real.name}")
print(f" - Celdas de barco: {len(evidencia_real['celda_barco'])}")
print(f" - Tiros jugados: {len(evidencia_real['tiro'])}")
print(f" - Ganador: {evidencia_real['partida'][0]['ganador']}")
print("\n[2/3] Juzgando la partida legítima contra el catálogo de Oracle...")
rc, salida = ejecutar_oracle_juzgar(ruta_real)
print(salida)
if rc == 0 and "VEREDICTO: verde en 11 medidas" in salida:
print("\n>>> RESULTADO: ÉXITO. Todas las 11 reglas se cumplen formalmente.")
else:
print("\n>>> RESULTADO: Error en la evaluación de la partida real.")
sys.exit(1)
# 2. Pruebas de falsabilidad (contraejemplos / inyección de faltas)
print("\n" + "=" * 70)
print("[3/3] DEMOSTRACIÓN DE FALSIBILIDAD: Inyectando infracciones a propósito")
print(" (Probamos que el oráculo no aprueba cualquier cosa)")
print("=" * 70)
escenarios_infraccion = [
(
"Disparo duplicado a la misma casilla (fila 3, col 3)",
"naval.tiros_sin_repeticion",
lambda e: e["tiro"].append({
"turno": len(e["tiro"]),
"tirador": e["tiro"][0]["tirador"],
"receptor": e["tiro"][0]["receptor"],
"fila": e["tiro"][0]["fila"],
"columna": e["tiro"][0]["columna"],
"es_impacto": False,
"hundio_barco": False,
"barco_hundido": "ninguno"
})
),
(
"Disparo fuera del tablero reglamentario (fila 14, col 2)",
"naval.tiros_dentro_del_tablero",
lambda e: e["tiro"].append({
"turno": len(e["tiro"]),
"tirador": "jugador",
"receptor": "cpu",
"fila": 14,
"columna": 2,
"es_impacto": False,
"hundio_barco": False,
"barco_hundido": "ninguno"
})
),
(
"Impacto fantasma (afirmar impacto en casilla donde no había barco)",
"naval.veracidad_impacto_positivo",
lambda e: e["tiro"].append({
"turno": len(e["tiro"]),
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 9, # Supongamos agua
"es_impacto": True,
"hundio_barco": False,
"barco_hundido": "ninguno"
}) if not any(c["jugador"] == "cpu" and c["fila"] == 0 and c["columna"] == 9 for c in e["celda_barco"])
else e["tiro"].append({
"turno": len(e["tiro"]),
"tirador": "jugador",
"receptor": "cpu",
"fila": 0,
"columna": 8,
"es_impacto": True,
"hundio_barco": False,
"barco_hundido": "ninguno"
})
),
(
"Turnos consecutivos del mismo tirador sin alternar",
"naval.alternancia_turnos",
lambda e: e["tiro"].insert(1, {
"turno": 1,
"tirador": e["tiro"][0]["tirador"], # Mismo tirador consecutivo
"receptor": e["tiro"][0]["receptor"],
"fila": 8,
"columna": 8,
"es_impacto": False,
"hundio_barco": False,
"barco_hundido": "ninguno"
}) or [t.__setitem__("turno", i) for i, t in enumerate(e["tiro"])]
)
]
fallas = 0
for titulo, medida_esperada, inyector in escenarios_infraccion:
evidencia_rota = copy.deepcopy(evidencia_real)
inyector(evidencia_rota)
ruta_rota = BASE_DIR / "partida_con_infraccion.json"
ruta_rota.write_text(json.dumps(evidencia_rota, indent=2), encoding="utf-8")
rc, salida = ejecutar_oracle_juzgar(ruta_rota)
ruta_rota.unlink(missing_ok=True)
if medida_esperada in salida and ("✗" in salida or "en rojo" in salida):
print(f" ✓ {titulo}")
print(f" -> Atrapado con éxito por medida: {medida_esperada}")
else:
fallas += 1
print(f" ✗ Falló detección para: {titulo}")
print(salida)
if fallas:
sys.exit(1)
print("\n" + "=" * 70)
print("VERIFICACIÓN COMPLETA FINALIZADA CON ÉXITO")
print("=" * 70)
if __name__ == "__main__":
main()
batalla-naval/
├── catalogos/
│ └── naval/
│ ├── naval.alternancia_turnos.oracle
│ ├── naval.barcos_dentro_del_tablero.oracle
│ ├── naval.barcos_sin_solapamiento.oracle
│ ├── naval.fin_de_juego_sin_tiros_posteriores.oracle
│ ├── naval.flota_reglamentaria.oracle
│ ├── naval.ganador_legitimo.oracle
│ ├── naval.tiros_dentro_del_tablero.oracle
│ ├── naval.tiros_sin_repeticion.oracle
│ ├── naval.turnos_sin_huecos.oracle
│ ├── naval.veracidad_impacto_negativo.oracle
│ └── naval.veracidad_impacto_positivo.oracle
├── corpus/
│ └── naval/
│ ├── 001-tiro-fuera-de-tablero.caso
│ ├── 002-tiro-valido.caso
│ ├── 003-mismo-tirador-seguido.caso
│ ├── 004-tiradores-alternos.caso
│ ├── 005-barco-fila-desbordada.caso
│ ├── 006-barco-en-borde.caso
│ ├── 007-barcos-superpuestos.caso
│ ├── 008-barcos-separados.caso
│ ├── 009-tiro-despues-del-final.caso
│ ├── 010-tiro-en-turno-final.caso
│ ├── 011-flota-de-dieciseis.caso
│ ├── 012-flota-de-diecisiete.caso
│ ├── 013-ganador-con-dieciseis-impactos.caso
│ ├── 014-ganador-con-diecisiete-impactos.caso
│ ├── 015-tiro-repetido.caso
│ ├── 016-tiros-distintos.caso
│ ├── 017-turno-saltado.caso
│ ├── 018-turnos-contiguos.caso
│ ├── 019-barco-reportado-como-agua.caso
│ ├── 020-agua-reportada-como-agua.caso
│ ├── 021-impacto-fantasma.caso
│ ├── 022-impacto-real.caso
│ ├── 023-columna-desbordada.caso
│ ├── 024-fila-negativa.caso
│ ├── 025-columna-negativa.caso
│ ├── 026-barco-columna-desbordada.caso
│ ├── 027-barco-fila-negativa.caso
│ ├── 028-barco-columna-negativa.caso
│ ├── 029-flota-sin-celdas.caso
│ ├── 030-sin-tiros-registrados.caso
│ ├── 031-distinta-fila-igual-turno-ajeno.caso
│ ├── 032-agua-declarada-agua.caso
│ └── 033-fila-confundida-con-turno.caso
├── css/
│ └── style.css
├── diferencial/
├── js/
│ ├── audio.js
│ ├── engine.js
│ ├── trace.js
│ └── ui.js
├── relaciones/
├── requisitos/
│ ├── naval.barcos_rectos.requisito
│ ├── naval.disparos.requisito
│ ├── naval.final.requisito
│ ├── naval.flota.requisito
│ ├── naval.impactos.requisito
│ └── naval.turnos.requisito
├── README.md
├── hechos_partida.json
├── index.html
├── oracle.json
├── sin-tiros.json
└── verificar_oraculo.py
Para volver a abrir el juego cuando quieras, podés hacerle doble clic a index.html desde tu explorador de carpetas (o escribir start index.html en Windows, open index.html en Mac o xdg-open index.html en Linux).
Once reglas miran lo principal, no todo. Ninguna mira hoy:
- que cada barco sea una línea recta y continua (se cuentan casillas, no la forma);
- que un barco hundido tenga todas sus casillas impactadas;
- que la computadora no sepa dónde están tus barcos antes de dispararles.
Pensalo. Elegí una. ¿Qué hecho tendría que anotar el juego, qué fila ofendería y qué caso rojo la fijaría?
Ver una respuesta, para la de la línea recta
Con celda_barco alcanza: las casillas de un mismo barco_id tienen que compartir la fila (y tener columnas consecutivas) o compartir la columna (y tener filas consecutivas). Una fila ofende si su barco tiene casillas en dos filas y dos columnas distintas a la vez. El caso rojo: un destructor con una casilla en (2,3) y otra en (3,4), en diagonal.
Y el catálogo base que apagaste en el paso 1: volvé a abrir el archivo oracle.json en tu editor de texto, cambiá "catalogo_base": false por "catalogo_base": true, guardalo y corré oracle test en la terminal. Te va a pedir lo que falta para que estas reglas valgan fuera de esta guía: declarar las unidades de los campos y sostener cada regla con partidas observadas (jugadas por personas reales), no sólo con casos construidos.
Si trabajás con un modelo de inteligencia artificial#
- Pedile que el juego cuente lo que pasa, en filas planas y formato JSON, sin opiniones ni conclusiones propias.
- No le pidas las reglas y el código en la misma pasada. Escribí o revisá vos las reglas de forma independiente.
- Pedile primero el caso rojo, y recién después la regla. Si te da una regla sin caso que la pruebe, desconfiá.
- Leé el
alcancede cada regla: es lo que el veredicto verde no te promete. - Cuando algo salga rojo, pasale los testigos exactos (los turnos o casillas que ofendieron), no una descripción vaga.
- Si te dice «ya está todo verificado», preguntale qué mutantes sobrevivieron y qué dice la lista
SIN MIRAR.
Después de esto, podés consultar la documentación en el sitio web de Oracle: La primera medida real muestra el mismo recorrido con menos explicación, y Escribir una medida es la referencia completa del lenguaje.
Esta página se genera desde docs/de-cero.md.