implement four distinct ghost archetypes

Replace the generic hunter/random split with the classic four: Blinky
chases directly, Pinky ambushes 4 tiles ahead of Pac-Man, Inky flanks
using Blinky reflected over a pivot 2 tiles ahead, and Clyde chases
when far and retreats to its corner when within 8 tiles.
This commit is contained in:
luriasb
2026-10-17 00:53:13 -06:00
parent a08eb8af32
commit d150e4bebc
2 changed files with 64 additions and 20 deletions
+47 -8
View File
@@ -110,9 +110,52 @@ function movePacman( game ) {
wrapTunnel( p, width );
}
// Celda situada `n` casillas por delante de Pac-Man segun su direccion.
// Las columnas se mantienen dentro del laberinto con clamp; si el offset
// se sale lateralmente por el tunel, la columna se ancla a la de Pac-Man.
function aheadOfPacman( game, n ) {
const p = game.pacman;
const W = game.grid[ 0 ].length;
const d = DIRS[ p.dir ] || { x: 0, y: 0 };
const px = Math.round( p.x );
const py = Math.round( p.y );
const ty = py + d.y * n;
if ( ty === TUNNEL_ROW ) return { x: px, y: ty };
return { x: Math.max( 0, Math.min( W - 1, px + d.x * n ) ), y: ty };
}
// Objetivo de cada arquetipo. Las cuatro ramas comparten el filtro de
// candidatos y el desempate; solo cambia la celda contra la que se compara.
function ghostTarget( game, g ) {
const p = game.pacman;
const px = Math.round( p.x );
const py = Math.round( p.y );
// Blinky: perseguir sin mas.
if ( g.kind === 'blinky' ) return { x: px, y: py };
// Pinky: emboscarse 4 casillas por delante.
if ( g.kind === 'pinky' ) return aheadOfPacman( game, 4 );
// Inky: flanquear, reflejando a Blinky sobre el pivot a 2 casillas delante.
if ( g.kind === 'inky' ) {
const pivot = aheadOfPacman( game, 2 );
const blinky = game.ghosts.find( ( o ) => o.kind === 'blinky' );
// Sin Blinky (defensivo, no ocurre hoy) Pinky hace de suplente.
if ( !blinky ) return aheadOfPacman( game, 4 );
return {
x: 2 * pivot.x - Math.round( blinky.x ),
y: 2 * pivot.y - Math.round( blinky.y ),
};
}
// Clyde: persigue mientras este lejos y se retira a su esquina si se acerca.
if ( Math.abs( g.x - px ) + Math.abs( g.y - py ) > 8 ) return { x: px, y: py };
return { x: 0, y: 30 };
}
function decideGhost( game, g ) {
const grid = game.grid;
const p = game.pacman;
const options = Object.keys( DIRS ).filter(
( dir ) => dir !== OPPOSITE[ g.dir ] && canMove( grid, g.x, g.y, dir, 'ghost' )
@@ -120,25 +163,21 @@ function decideGhost( game, g ) {
// Sin salida (callejon): permitir el giro de 180.
const choices = options.length ? options : [ '' + OPPOSITE[ g.dir ] ];
if ( g.kind === 'hunter' ) {
const px = Math.round( p.x );
const py = Math.round( p.y );
const target = ghostTarget( game, g );
let best = choices[ 0 ];
let bestDist = Infinity;
for ( const dir of choices ) {
const d = DIRS[ dir ];
const nx = g.x + d.x;
const ny = g.y + d.y;
const dist = Math.abs( nx - px ) + Math.abs( ny - py );
const dist = Math.abs( nx - target.x ) + Math.abs( ny - target.y );
if ( dist < bestDist ) {
bestDist = dist;
best = dir;
}
}
g.dir = best;
} else {
g.dir = choices[ Math.floor( Math.random() * choices.length ) ];
}
}
function moveGhost( game, g ) {
+7 -2
View File
@@ -51,9 +51,14 @@ const MAZE = MAZE_STR.map( ( row ) => row.split( '' ).map( parseTile ) );
const TUNNEL_ROW = 14;
const PACMAN_START = { x: 13, y: 23 };
// Los cuatro arquetipos clasicos, dentro de la pen. Simetricos respecto al
// eje vertical (cols 11<->16, 13<->14) y en el orden de GHOST_COLORS de
// render.js (blinky rojo, pinky cyan, inky rosa, clyde naranja).
const GHOST_STARTS = [
{ x: 13, y: 14, kind: 'hunter' }, // dentro de la pen
{ x: 14, y: 14, kind: 'random' }, // dentro de la pen
{ x: 11, y: 14, kind: 'blinky' },
{ x: 13, y: 14, kind: 'pinky' },
{ x: 14, y: 14, kind: 'inky' },
{ x: 16, y: 14, kind: 'clyde' },
];
window.MAZE = MAZE;