From d150e4bebc81f027f0e5cd01053f7825dca6818c Mon Sep 17 00:00:00 2001 From: luriasb Date: Sat, 17 Oct 2026 00:53:13 -0600 Subject: [PATCH] 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. --- src/js/game.js | 75 ++++++++++++++++++++++++++++++++++++++------------ src/js/maze.js | 9 ++++-- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/js/game.js b/src/js/game.js index ca3f440..cb1b4a8 100644 --- a/src/js/game.js +++ b/src/js/game.js @@ -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 ); - 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 ); - if ( dist < bestDist ) { - bestDist = dist; - best = dir; - } + 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 - 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 ) ]; } + g.dir = best; } function moveGhost( game, g ) { diff --git a/src/js/maze.js b/src/js/maze.js index 9bcaf2c..5f9bb54 100644 --- a/src/js/maze.js +++ b/src/js/maze.js @@ -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;