Inteligencia artificial de un enemigo con una máquina de estados
El comportamiento de un enemigo de videojuego como máquina de estados finitos: patrulla entre dos puntos, persigue al jugador cuando lo ve, ataca a su lado, lo busca donde lo vio por última vez y huye a recuperarse con poca vida. Estados, transiciones con prioridad y movimiento en rejilla.
- Máquina de estados finitos
- Transiciones con prioridad
- Distancia Manhattan
- Movimiento en rejilla
- Memoria del agente (última posición vista)
- Contadores y temporizadores
Enunciado
Los enemigos de la mayoría de los videojuegos no piensan: siguen una máquina de estados. En cada momento están en un estado (patrullar, perseguir, atacar…) que decide qué hacen, y unas reglas de transición deciden cuándo cambian a otro. Es fácil de programar, de depurar y de ajustar, y por eso motores como Unity (con su Animator) y Godot la usan a todas horas.
Este guardia vigila un pasillo en un mapa de casillas. Patrulla entre dos puntos; si ve al jugador (a 5 casillas o menos y sin que el jugador vaya en sigilo) lo persigue; si lo tiene al lado, ataca; si lo pierde de vista, va al último sitio donde lo vio y espera un poco antes de volver a patrullar. Y si el jugador le baja la vida por debajo de 30, huye hasta poder recuperarse.
La simulación es por turnos: cada línea de la entrada dice dónde está el jugador en ese turno y si ha golpeado al guardia. El programa escribe cada cambio de estado con su motivo, que es exactamente el registro que se activa en un juego para depurar la IA.
Qué tiene que hacer el programa
- La primera línea es
PATRULLA x1,y1 x2,y2(si no,La primera línea debe ser PATRULLA x1,y1 x2,y2y termina). El guardia empieza en el primer punto, en estado PATRULLA, con 100 de vida. Cada línea siguiente es un turno:x,ydel jugador y, opcionalmente,SIGILOyGOLPE n. Las líneas vacías no son turnos; una línea con otra forma escribet=N: línea no válida(y cuenta como turno). - En cada turno: primero se resta el golpe (mínimo 0; si la vida llega a 0,
t=N: el enemigo cae derrotadoy la simulación termina). Después se calcula la distancia Manhattan al jugador: el guardia lo ve si es 5 o menos y no va en sigilo, y entonces recuerda su posición. - Transiciones, se aplica solo la primera que se cumpla: cualquier estado salvo HUIDA → HUIDA si la vida es 30 o menos (
vida V); PATRULLA → PERSECUCION si lo ve (te ve a distancia D); PERSECUCION → ATAQUE si la distancia es 1 o menos (a tu lado); PERSECUCION → BUSCAR si la distancia es mayor que 8 (te pierde a distancia D); ATAQUE → PERSECUCION si la distancia es mayor que 1 (te alejas a distancia D); BUSCAR → PERSECUCION si lo ve (te vuelve a ver a distancia D); HUIDA → PATRULLA si la vida es 60 o más (recuperado con vida V). Cada cambio escribet=N: ANTES → DESPUÉS (motivo). - Acción del estado (ya cambiado): PATRULLA, un paso hacia el punto de destino (al llegar a uno, el destino pasa a ser el otro); PERSECUCION, un paso hacia el jugador; ATAQUE, 10 de daño al jugador; BUSCAR, un paso hacia la última posición vista y, una vez allí, espera: al cuarto turno de espera escribe
t=N: BUSCAR → PATRULLA (no te encuentra)y vuelve a patrullar; HUIDA, un paso alejándose del jugador si está a 8 o menos, o recupera 10 de vida (hasta 100) si está más lejos. - Un paso mueve una casilla en el eje en el que queda más distancia (en horizontal si empatan); alejarse es lo mismo en sentido contrario. Al final:
Posición final: (x, y) · vida V · daño causado DyTurnos por estado: PATRULLA n, …con los estados que han tenido algún turno, en el orden PATRULLA, PERSECUCION, ATAQUE, BUSCAR, HUIDA. Cada turno cuenta para el estado en el que termina (el turno en que cae derrotado, para el estado que tenía).
Entrada
Línea 1: PATRULLA x1,y1 x2,y2. Resto: un turno por línea, x,y [SIGILO] [GOLPE n].
Datos de referencia
| Estado | Qué hace | Sale hacia |
|---|---|---|
| PATRULLA | va y vuelve entre los dos puntos | PERSECUCION (te ve) · HUIDA (vida ≤ 30) |
| PERSECUCION | un paso hacia ti | ATAQUE (distancia ≤ 1) · BUSCAR (distancia > 8) · HUIDA |
| ATAQUE | 10 de daño por turno | PERSECUCION (distancia > 1) · HUIDA |
| BUSCAR | va a donde te vio y espera 3 turnos | PERSECUCION (te ve) · PATRULLA (no te encuentra) · HUIDA |
| HUIDA | se aleja o, lejos, recupera 10 de vida | PATRULLA (vida ≥ 60) |
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Te ve, te persigue, ataca y te pierde
Entrada
PATRULLA 0,0 4,0 9,5 8,4 6,2 4,1 3,2 3,2 3,2 3,2 5,3 9,6 12,8 14,9 15,9 15,9 15,9 15,9 15,9 15,9 15,9
Salida por consola
t=4: PATRULLA → PERSECUCION (te ve a distancia 2) t=7: PERSECUCION → ATAQUE (a tu lado) t=9: ATAQUE → PERSECUCION (te alejas a distancia 4) t=10: PERSECUCION → BUSCAR (te pierde a distancia 10) t=16: BUSCAR → PATRULLA (no te encuentra) Posición final: (4, 1) · vida 100 · daño causado 20 Turnos por estado: PATRULLA 7, PERSECUCION 4, ATAQUE 2, BUSCAR 6
Sigilo, golpes y huida
Entrada
PATRULLA 0,0 0,3 2,0 SIGILO 1,0 SIGILO GOLPE 40 1,0 GOLPE 35 2,0 3,0 4,0 5,0 12,0 12,0 12,0 12,0 12,0
Salida por consola
t=3: PATRULLA → HUIDA (vida 25) t=10: HUIDA → PATRULLA (recuperado con vida 65) Posición final: (0, 2) · vida 65 · daño causado 0 Turnos por estado: PATRULLA 5, HUIDA 7
Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. El estado es una variable
Guarda el estado actual en una variable de texto y, en cada turno, haz dos cosas por separado: decidir si cambia (las transiciones) y actuar según el estado (la acción). Mezclarlas es el error más común.
2. Transiciones con prioridad
Escribe las transiciones como una cadena de if / else if en el orden del enunciado: la primera que se cumple gana. Por eso la huida va la primera: con poca vida, huir importa más que perseguir.
if (estado !== "HUIDA" && vida <= HUIR) cambiar(t, "HUIDA", `vida ${vida}`);
else if (estado === "PATRULLA" && ve) cambiar(t, "PERSECUCION", `te ve a distancia ${d}`);
else if (…)3. Moverse en una rejilla
La distancia Manhattan es |dx| + |dy|. Un paso avanza una casilla en el eje con más distancia; para huir, se da el paso con el signo cambiado. Una función paso(desde, hacia, signo) sirve para patrullar, perseguir, buscar y huir.
4. Memoria
Cuando el guardia ve al jugador, guarda su posición. En BUSCAR va hacia esa posición aunque el jugador ya no esté allí: es lo que da la sensación de que «te busca» en lugar de saber siempre dónde estás.
5. Un registro para depurar
Escribir cada cambio de estado con su motivo convierte un comportamiento difícil de seguir en una lista que se puede leer. Si el guardia hace algo raro en el juego, este registro dice por qué.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.
Ejemplo
PATRULLA 0,0 4,0 9,5 8,4 6,2 4,1 3,2 3,2 3,2 3,2 5,3 9,6 12,8 14,9 15,9 15,9 15,9 15,9 15,9 15,9 15,9
t=4: PATRULLA → PERSECUCION (te ve a distancia 2) t=7: PERSECUCION → ATAQUE (a tu lado) t=9: ATAQUE → PERSECUCION (te alejas a distancia 4) t=10: PERSECUCION → BUSCAR (te pierde a distancia 10) t=16: BUSCAR → PATRULLA (no te encuentra) Posición final: (4, 1) · vida 100 · daño causado 20 Turnos por estado: PATRULLA 7, PERSECUCION 4, ATAQUE 2, BUSCAR 6
Solución explicada
Ver la solución completa
1const lineas = require("fs").readFileSync(0, "utf8").split("\n");
2
3const VISION = 5, PERDER = 8, ESPERA = 3, VIDA_MAX = 100, HUIR = 30, RECUPERADO = 60;
4
5const distancia = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
6// Un paso en el eje con más distancia (horizontal si empatan), hacia el objetivo o (signo -1) alejándose
7function paso(desde, hacia, signo = 1) {
8 const dx = hacia.x - desde.x, dy = hacia.y - desde.y;
9 if (dx === 0 && dy === 0) return { ...desde };
10 return Math.abs(dx) >= Math.abs(dy) ? { x: desde.x + signo * Math.sign(dx || 1), y: desde.y } : { x: desde.x, y: desde.y + signo * Math.sign(dy) };
11}
12
13function main() {
14 const cab = (lineas[0] ?? "").trim().match(/^PATRULLA (-?\d+),(-?\d+) (-?\d+),(-?\d+)$/);
15 if (!cab) return console.log("La primera línea debe ser PATRULLA x1,y1 x2,y2");
16 const ruta = [{ x: +cab[1], y: +cab[2] }, { x: +cab[3], y: +cab[4] }];
17 let yo = { ...ruta[0] }, destino = 1, estado = "PATRULLA", vida = VIDA_MAX, visto = null, esperando = 0;
18 let danoHecho = 0;
19 const ticks = { PATRULLA: 0, PERSECUCION: 0, ATAQUE: 0, BUSCAR: 0, HUIDA: 0 };
20
21 const cambiar = (t, nuevo, motivo) => {
22 console.log(`t=${t}: ${estado} → ${nuevo} (${motivo})`);
23 estado = nuevo;
24 };
25
26 let t = 0;
27 for (const linea of lineas.slice(1)) {
28 const p = linea.trim().split(/\s+/);
29 if (!p[0]) continue;
30 t++;
31 const m = p[0].match(/^(-?\d+),(-?\d+)$/);
32 if (!m) { console.log(`t=${t}: línea no válida`); continue; }
33 const jugador = { x: +m[1], y: +m[2] };
34 const sigilo = p.includes("SIGILO");
35 const golpe = p.indexOf("GOLPE") >= 0 ? Number(p[p.indexOf("GOLPE") + 1]) || 0 : 0;
36 if (golpe) vida = Math.max(0, vida - golpe);
37 if (vida === 0) {
38 console.log(`t=${t}: el enemigo cae derrotado`);
39 ticks[estado]++;
40 break;
41 }
42
43 const d = distancia(yo, jugador);
44 const ve = d <= VISION && !sigilo;
45 if (ve) visto = { ...jugador };
46
47 // Transiciones: la vida baja manda sobre todo lo demás
48 if (estado !== "HUIDA" && vida <= HUIR) cambiar(t, "HUIDA", `vida ${vida}`);
49 else if (estado === "PATRULLA" && ve) cambiar(t, "PERSECUCION", `te ve a distancia ${d}`);
50 else if (estado === "PERSECUCION" && d <= 1) cambiar(t, "ATAQUE", "a tu lado");
51 else if (estado === "PERSECUCION" && d > PERDER) cambiar(t, "BUSCAR", `te pierde a distancia ${d}`);
52 else if (estado === "ATAQUE" && d > 1) cambiar(t, "PERSECUCION", `te alejas a distancia ${d}`);
53 else if (estado === "BUSCAR" && ve) cambiar(t, "PERSECUCION", `te vuelve a ver a distancia ${d}`);
54 else if (estado === "HUIDA" && vida >= RECUPERADO) cambiar(t, "PATRULLA", `recuperado con vida ${vida}`);
55
56 // Acción del estado actual
57 if (estado === "PATRULLA") {
58 if (yo.x === ruta[destino].x && yo.y === ruta[destino].y) destino = 1 - destino;
59 yo = paso(yo, ruta[destino]);
60 } else if (estado === "PERSECUCION") {
61 yo = paso(yo, jugador);
62 } else if (estado === "ATAQUE") {
63 danoHecho += 10;
64 } else if (estado === "BUSCAR") {
65 if (yo.x !== visto.x || yo.y !== visto.y) {
66 yo = paso(yo, visto);
67 } else if (++esperando > ESPERA) {
68 esperando = 0;
69 cambiar(t, "PATRULLA", "no te encuentra");
70 }
71 } else if (estado === "HUIDA") {
72 if (d <= PERDER) yo = paso(yo, jugador, -1);
73 else vida = Math.min(VIDA_MAX, vida + 10);
74 }
75 ticks[estado]++;
76 }
77
78 console.log(`Posición final: (${yo.x}, ${yo.y}) · vida ${vida} · daño causado ${danoHecho}`);
79 console.log("Turnos por estado: " + Object.entries(ticks).filter(([, n]) => n).map(([e, n]) => `${e} ${n}`).join(", "));
80}
81
82main();Una máquina de estados separa el qué hago (la acción de cada estado) del cuándo cambio (las transiciones). Cada estado es pequeño y fácil de entender por sí solo, y añadir un comportamiento nuevo es añadir un estado y sus transiciones sin tocar el resto.
La prioridad de las transiciones resuelve los conflictos: si el guardia ve al jugador pero tiene poca vida, huye. Escribirlas como una cadena de else if ordenada hace explícita esa prioridad.
El guardia tiene memoria (la última posición vista) y temporizadores (los turnos de espera), dos ingredientes que hacen que una IA sencilla parezca inteligente. Con la distancia Manhattan y pasos de una casilla, el movimiento es el de los juegos de rejilla clásicos.
En un motor real, las transiciones se comprobarían en cada fotograma y la acción movería al personaje poco a poco. Para comportamientos más complejos se usan árboles de comportamiento o planificadores, pero casi siempre combinados con máquinas de estados como esta.
Para ir más allá
- Añade un estado ALERTA intermedio: al oír al jugador (distancia ≤ 8 aunque no lo vea) gira hacia él durante dos turnos antes de patrullar.
- Pon paredes en el mapa y usa búsqueda en anchura (BFS) para el paso hacia el objetivo, en lugar de ir en línea recta.
- Reescribe la máquina con una clase por estado (patrón State) con los métodos
entrar,actualizarysalir.