Saltar al contenido
oracle
Empezar De cero a un rojo: la primera medida con veredicto.

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:

Cómo moverte entre carpetas desde la terminal:

Qué instalar y cómo#

Necesitás tener instalados estos cuatro programas gratuitos:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

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 los bloques de esta guía#

En cada misión vas a ver estos tipos de bloques:

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-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">&times;</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ónUna fila por…Campos
celda_barcocada casilla ocupada por un barcoid, jugador, barco_id, segmento_idx, fila, columna
tirocada disparoturno, tirador, receptor, fila, columna, es_impacto, hundio_barco, barco_hundido
partidael finalestado, 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:

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:

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:

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#

  1. Pedile que el juego cuente lo que pasa, en filas planas y formato JSON, sin opiniones ni conclusiones propias.
  2. No le pidas las reglas y el código en la misma pasada. Escribí o revisá vos las reglas de forma independiente.
  3. Pedile primero el caso rojo, y recién después la regla. Si te da una regla sin caso que la pruebe, desconfiá.
  4. Leé el alcance de cada regla: es lo que el veredicto verde no te promete.
  5. Cuando algo salga rojo, pasale los testigos exactos (los turnos o casillas que ofendieron), no una descripción vaga.
  6. 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.

Oracle · navegación y medida continua

Esta página se genera desde docs/de-cero.md.