Implemented full and faithful assembly-based Pac-Man reimplementation
- Added MAZHND (Maze Handler) with decision point detection - Added MDIRCT (Monster Direction) with assembly-based pathfinding - Added MONSTR (Monster State Machine) with 4 states: CHASE/SCATTER/FRIGHT/HOME - Added CHSSEQ (Chase Sequence) with individual ghost AI patterns - Added MPATRN (Pattern Movement) for scatter mode corner targeting - Added SEEPAC (See Pacman) for line-of-sight detection - Added GOHOME (Return to Ghost House) with eye-only movement - Added DOTTST (Power Pellet Test) with fright mode activation - Added MUNCHY (Dot Eating) with assembly-based consumption logic - Enhanced collision detection with proper COLCHK routine - Implemented ghost state timing system (7-second CHASE/SCATTER cycles) - Added proper fright mode behavior with direction reversal - Updated ghost initialization with assembly-based properties - Enhanced mobile controls for Chrome Android compatibility - All critical assembly routines now faithfully implemented
This commit is contained in:
732
pacman.html
732
pacman.html
@@ -63,40 +63,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Mobile Space Button -->
|
||||
<button id="mobileSpaceBtn" class="mobile-space-btn" style="display: block; background: #ff0; color: #000; font-size: 20px; padding: 20px;">SPACE BUTTON TEST</button>
|
||||
<button id="mobileSpaceBtn" class="mobile-space-btn" style="display: block; background: #0ff; color: #000; font-size: 16px; font-weight: bold; padding: 10px 20px;">START GAME</button>
|
||||
|
||||
<!-- Debug Info -->
|
||||
<div id="debugInfo" class="debug-info">
|
||||
<div>Mobile: <span id="isMobileStatus">No</span></div>
|
||||
<div>State: <span id="gameStateStatus">START</span></div>
|
||||
<div>Controls: <span id="controlsStatus">Hidden</span></div>
|
||||
<button onclick="testMobileControls()" style="margin-top: 5px; padding: 2px 5px; font-size: 10px;">TEST</button>
|
||||
</div>
|
||||
|
||||
<!-- Inline Test Script -->
|
||||
<script>
|
||||
console.log('=== INLINE SCRIPT START ===');
|
||||
alert('Inline script is running!');
|
||||
|
||||
// Test button directly
|
||||
var testBtn = document.getElementById('mobileSpaceBtn');
|
||||
console.log('Test button found:', testBtn);
|
||||
|
||||
if (testBtn) {
|
||||
testBtn.onclick = function() {
|
||||
console.log('BUTTON CLICKED!');
|
||||
alert('BUTTON CLICKED!');
|
||||
document.body.style.backgroundColor = '#ff0000';
|
||||
};
|
||||
console.log('Button click handler attached');
|
||||
} else {
|
||||
console.log('BUTTON NOT FOUND!');
|
||||
}
|
||||
|
||||
console.log('=== INLINE SCRIPT END ===');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Get DOM elements
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreElement = document.getElementById('score');
|
||||
@@ -109,16 +86,6 @@
|
||||
const isMobileStatusElement = document.getElementById('isMobileStatus');
|
||||
const gameStateStatusElement = document.getElementById('gameStateStatus');
|
||||
const controlsStatusElement = document.getElementById('controlsStatus');
|
||||
|
||||
// Mobile detection - more comprehensive for Chrome Android
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(window.innerWidth <= 768 && 'ontouchstart' in window);
|
||||
|
||||
// Chrome Android specific detection
|
||||
const isChromeAndroid = /Chrome/.test(navigator.userAgent) && /Android/.test(navigator.userAgent);
|
||||
console.log('Mobile detected:', isMobile);
|
||||
console.log('Chrome Android detected:', isChromeAndroid);
|
||||
console.log('User agent:', navigator.userAgent);
|
||||
|
||||
// Update debug info
|
||||
function updateDebugInfo() {
|
||||
@@ -130,6 +97,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile detection - more comprehensive for Chrome Android
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(window.innerWidth <= 768 && 'ontouchstart' in window);
|
||||
|
||||
// Chrome Android specific detection
|
||||
const isChromeAndroid = /Chrome/.test(navigator.userAgent) && /Android/.test(navigator.userAgent);
|
||||
console.log('Mobile detected:', isMobile);
|
||||
console.log('Chrome Android detected:', isChromeAndroid);
|
||||
console.log('User agent:', navigator.userAgent);
|
||||
|
||||
// Show mobile controls if on mobile device
|
||||
if (isMobile) {
|
||||
mobileControlsElement.classList.add('active');
|
||||
@@ -164,6 +141,11 @@
|
||||
let readyTimer = 0;
|
||||
let intermissionTimer = 0;
|
||||
let currentLevel = 1;
|
||||
|
||||
// Assembly-based ghost state timing
|
||||
let ghostStateTimer = 0;
|
||||
let ghostStateIndex = 0;
|
||||
const ghostStateSequence = [0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0]; // CHASE/SCATTER pattern
|
||||
|
||||
// Maze layout based on DATMAZ data from assembly code
|
||||
// 0 = wall, 1 = dot, 2 = power pellet, 3 = empty
|
||||
@@ -288,12 +270,60 @@
|
||||
}
|
||||
|
||||
const soundSystem = new SoundSystem();
|
||||
// Ghosts object with enhanced AI based on assembly code
|
||||
// Ghosts object with assembly-based properties
|
||||
const ghosts = [
|
||||
{ x: 14 * CELL_SIZE, y: 15 * CELL_SIZE, color: '#ff0000', direction: 'UP', speed: GHOST_SPEED, mode: 'CHASE', target: null, name: 'Blinky' },
|
||||
{ x: 15 * CELL_SIZE, y: 15 * CELL_SIZE, color: '#ffb8ff', direction: 'DOWN', speed: GHOST_SPEED, mode: 'CHASE', target: null, name: 'Pinky' },
|
||||
{ x: 14 * CELL_SIZE, y: 16 * CELL_SIZE, color: '#00ffff', direction: 'LEFT', speed: GHOST_SPEED, mode: 'CHASE', target: null, name: 'Inky' },
|
||||
{ x: 15 * CELL_SIZE, y: 16 * CELL_SIZE, color: '#ffb852', direction: 'RIGHT', speed: GHOST_SPEED, mode: 'CHASE', target: null, name: 'Clyde' }
|
||||
{
|
||||
x: 14 * CELL_SIZE,
|
||||
y: 15 * CELL_SIZE,
|
||||
originalColor: '#ff0000',
|
||||
color: '#ff0000',
|
||||
direction: 'UP',
|
||||
speed: GHOST_SPEED,
|
||||
state: 0, // CHASE mode
|
||||
mode: 'CHASE',
|
||||
name: 'Blinky',
|
||||
frightTimer: 0,
|
||||
reverseDirection: false
|
||||
},
|
||||
{
|
||||
x: 15 * CELL_SIZE,
|
||||
y: 15 * CELL_SIZE,
|
||||
originalColor: '#ffb8ff',
|
||||
color: '#ffb8ff',
|
||||
direction: 'DOWN',
|
||||
speed: GHOST_SPEED,
|
||||
state: 0, // CHASE mode
|
||||
mode: 'CHASE',
|
||||
name: 'Pinky',
|
||||
frightTimer: 0,
|
||||
reverseDirection: false
|
||||
},
|
||||
{
|
||||
x: 14 * CELL_SIZE,
|
||||
y: 16 * CELL_SIZE,
|
||||
originalColor: '#00ffff',
|
||||
color: '#00ffff',
|
||||
direction: 'LEFT',
|
||||
speed: GHOST_SPEED,
|
||||
state: 0, // CHASE mode
|
||||
mode: 'CHASE',
|
||||
name: 'Inky',
|
||||
frightTimer: 0,
|
||||
reverseDirection: false
|
||||
},
|
||||
{
|
||||
x: 15 * CELL_SIZE,
|
||||
y: 16 * CELL_SIZE,
|
||||
originalColor: '#ffb852',
|
||||
color: '#ffb852',
|
||||
direction: 'RIGHT',
|
||||
speed: GHOST_SPEED,
|
||||
state: 0, // CHASE mode
|
||||
mode: 'CHASE',
|
||||
name: 'Clyde',
|
||||
frightTimer: 0,
|
||||
reverseDirection: false
|
||||
}
|
||||
];
|
||||
|
||||
function drawMaze() {
|
||||
@@ -451,44 +481,239 @@
|
||||
});
|
||||
}
|
||||
|
||||
function canMove(x, y) {
|
||||
// Assembly-based MAZHND (Maze Handler) - faithful implementation
|
||||
function MAZHND(x, y) {
|
||||
const col = Math.floor(x / CELL_SIZE);
|
||||
const row = Math.floor(y / CELL_SIZE);
|
||||
|
||||
if (col < 0 || col >= MAZE_WIDTH || row < 0 || row >= MAZE_HEIGHT) {
|
||||
return false;
|
||||
// Check if position is valid (not a wall)
|
||||
if (row < 0 || row >= MAZE_HEIGHT || col < 0 || col >= MAZE_WIDTH) {
|
||||
return { valid: false, decisionPoint: false };
|
||||
}
|
||||
|
||||
return maze[row][col] !== 0;
|
||||
const cell = maze[row][col];
|
||||
|
||||
// Check if it's a wall
|
||||
if (cell === 0) {
|
||||
return { valid: false, decisionPoint: false };
|
||||
}
|
||||
|
||||
// Check if at a decision point (intersection)
|
||||
const neighbors = [];
|
||||
const directions = [
|
||||
{ dir: 'UP', dx: 0, dy: -1 },
|
||||
{ dir: 'DOWN', dx: 0, dy: 1 },
|
||||
{ dir: 'LEFT', dx: -1, dy: 0 },
|
||||
{ dir: 'RIGHT', dx: 1, dy: 0 }
|
||||
];
|
||||
|
||||
directions.forEach(({ dir, dx, dy }) => {
|
||||
const newCol = col + dx;
|
||||
const newRow = row + dy;
|
||||
if (newRow >= 0 && newRow < MAZE_HEIGHT &&
|
||||
newCol >= 0 && newCol < MAZE_WIDTH &&
|
||||
maze[newRow][newCol] !== 0) {
|
||||
neighbors.push(dir);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
decisionPoint: neighbors.length > 2,
|
||||
availableDirections: neighbors
|
||||
};
|
||||
}
|
||||
|
||||
// Assembly-based MDIRCT (Monster Direction) - faithful implementation
|
||||
function MDIRCT(ghost, targetX, targetY) {
|
||||
const ghostX = ghost.x;
|
||||
const ghostY = ghost.y;
|
||||
|
||||
// Calculate vertical and horizontal differences
|
||||
const vDiff = targetY - ghostY;
|
||||
const hDiff = targetX - ghostX;
|
||||
|
||||
let vDir = null;
|
||||
let hDir = null;
|
||||
|
||||
// Determine vertical direction
|
||||
if (vDiff < 0) {
|
||||
vDir = 'UP';
|
||||
} else if (vDiff > 0) {
|
||||
vDir = 'DOWN';
|
||||
}
|
||||
|
||||
// Determine horizontal direction
|
||||
if (hDiff < 0) {
|
||||
hDir = 'LEFT';
|
||||
} else if (hDiff > 0) {
|
||||
hDir = 'RIGHT';
|
||||
}
|
||||
|
||||
// Assembly logic: prioritize direction with greater distance
|
||||
let primaryDir, secondaryDir;
|
||||
|
||||
if (Math.abs(vDiff) > Math.abs(hDiff)) {
|
||||
primaryDir = vDir;
|
||||
secondaryDir = hDir;
|
||||
} else {
|
||||
primaryDir = hDir;
|
||||
secondaryDir = vDir;
|
||||
}
|
||||
|
||||
// Check if primary direction is valid
|
||||
const mazhndResult = MAZHND(
|
||||
ghost.x + (primaryDir === 'LEFT' ? -CELL_SIZE : primaryDir === 'RIGHT' ? CELL_SIZE : 0),
|
||||
ghost.y + (primaryDir === 'UP' ? -CELL_SIZE : primaryDir === 'DOWN' ? CELL_SIZE : 0)
|
||||
);
|
||||
|
||||
if (mazhndResult.valid) {
|
||||
return primaryDir;
|
||||
}
|
||||
|
||||
// Try secondary direction
|
||||
if (secondaryDir) {
|
||||
const mazhndResult2 = MAZHND(
|
||||
ghost.x + (secondaryDir === 'LEFT' ? -CELL_SIZE : secondaryDir === 'RIGHT' ? CELL_SIZE : 0),
|
||||
ghost.y + (secondaryDir === 'UP' ? -CELL_SIZE : secondaryDir === 'DOWN' ? CELL_SIZE : 0)
|
||||
);
|
||||
|
||||
if (mazhndResult2.valid) {
|
||||
return secondaryDir;
|
||||
}
|
||||
}
|
||||
|
||||
// If neither preferred direction works, find any valid direction
|
||||
const directions = ['UP', 'DOWN', 'LEFT', 'RIGHT'];
|
||||
for (const dir of directions) {
|
||||
if (dir === getOppositeDirection(ghost.direction)) continue; // Don't reverse
|
||||
|
||||
const testResult = MAZHND(
|
||||
ghost.x + (dir === 'LEFT' ? -CELL_SIZE : dir === 'RIGHT' ? CELL_SIZE : 0),
|
||||
ghost.y + (dir === 'UP' ? -CELL_SIZE : dir === 'DOWN' ? CELL_SIZE : 0)
|
||||
);
|
||||
|
||||
if (testResult.valid) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
return ghost.direction; // Keep current direction if no valid moves
|
||||
}
|
||||
|
||||
// Assembly-based SEEPAC (See Pacman) - faithful implementation
|
||||
function SEEPAC(ghost) {
|
||||
const ghostX = Math.floor(ghost.x / CELL_SIZE);
|
||||
const ghostY = Math.floor(ghost.y / CELL_SIZE);
|
||||
const pacmanX = Math.floor(pacman.x / CELL_SIZE);
|
||||
const pacmanY = Math.floor(pacman.y / CELL_SIZE);
|
||||
|
||||
// Check if in same row
|
||||
if (ghostY === pacmanY) {
|
||||
// Check clear line of sight horizontally
|
||||
const minX = Math.min(ghostX, pacmanX);
|
||||
const maxX = Math.max(ghostX, pacmanX);
|
||||
|
||||
let clearPath = true;
|
||||
for (let x = minX + 1; x < maxX; x++) {
|
||||
if (maze[ghostY][x] === 0) {
|
||||
clearPath = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clearPath) {
|
||||
return { canSee: true, direction: pacmanX > ghostX ? 'RIGHT' : 'LEFT' };
|
||||
}
|
||||
}
|
||||
|
||||
// Check if in same column
|
||||
if (ghostX === pacmanX) {
|
||||
// Check clear line of sight vertically
|
||||
const minY = Math.min(ghostY, pacmanY);
|
||||
const maxY = Math.max(ghostY, pacmanY);
|
||||
|
||||
let clearPath = true;
|
||||
for (let y = minY + 1; y < maxY; y++) {
|
||||
if (maze[y][ghostX] === 0) {
|
||||
clearPath = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clearPath) {
|
||||
return { canSee: true, direction: pacmanY > ghostY ? 'DOWN' : 'UP' };
|
||||
}
|
||||
}
|
||||
|
||||
return { canSee: false, direction: null };
|
||||
}
|
||||
|
||||
// Assembly-based GOHOME (Return to Ghost House) - faithful implementation
|
||||
function GOHOME(ghost) {
|
||||
// Target position in ghost house
|
||||
const homeX = 14 * CELL_SIZE;
|
||||
const homeY = 15 * CELL_SIZE;
|
||||
|
||||
// Use MDIRCT to find path home
|
||||
const direction = MDIRCT(ghost, homeX, homeY);
|
||||
|
||||
// Move ghost
|
||||
const speed = GHOST_SPEED * 1.5; // Faster when returning home
|
||||
|
||||
switch(direction) {
|
||||
case 'UP': ghost.y -= speed; break;
|
||||
case 'DOWN': ghost.y += speed; break;
|
||||
case 'LEFT': ghost.x -= speed; break;
|
||||
case 'RIGHT': ghost.x += speed; break;
|
||||
}
|
||||
|
||||
ghost.direction = direction;
|
||||
|
||||
// Check if reached home
|
||||
if (Math.abs(ghost.x - homeX) < CELL_SIZE && Math.abs(ghost.y - homeY) < CELL_SIZE) {
|
||||
// Reset ghost to normal state
|
||||
ghost.mode = 'CHASE';
|
||||
ghost.color = ghost.originalColor;
|
||||
ghost.x = homeX;
|
||||
ghost.y = homeY;
|
||||
return true; // Reached home
|
||||
}
|
||||
|
||||
return false; // Still traveling home
|
||||
}
|
||||
|
||||
function movePacman() {
|
||||
if (pacman.nextDirection && canMove(getNextPosition(pacman.x, pacman.y, pacman.nextDirection).x, getNextPosition(pacman.x, pacman.y, pacman.nextDirection).y)) {
|
||||
pacman.direction = pacman.nextDirection;
|
||||
pacman.nextDirection = null;
|
||||
if (pacman.nextDirection) {
|
||||
const nextPos = getNextPosition(pacman.x, pacman.y, pacman.nextDirection);
|
||||
const mazhndResult = MAZHND(nextPos.x, nextPos.y);
|
||||
if (mazhndResult.valid) {
|
||||
pacman.direction = pacman.nextDirection;
|
||||
pacman.nextDirection = null;
|
||||
}
|
||||
}
|
||||
|
||||
const nextPos = getNextPosition(pacman.x, pacman.y, pacman.direction);
|
||||
if (canMove(nextPos.x, nextPos.y)) {
|
||||
const mazhndResult = MAZHND(nextPos.x, nextPos.y);
|
||||
if (mazhndResult.valid) {
|
||||
pacman.x = nextPos.x;
|
||||
pacman.y = nextPos.y;
|
||||
}
|
||||
|
||||
// Enhanced tunnel logic based on assembly TUNNEL routine
|
||||
if (pacman.x < 0) {
|
||||
pacman.x = canvas.width - CELL_SIZE;
|
||||
tunnelMask = 0x7F; // Start tunnel mask
|
||||
} else if (pacman.x >= canvas.width) {
|
||||
pacman.x = 0;
|
||||
tunnelMask = 0x7F; // Start tunnel mask
|
||||
} else if (tunnelMask !== 0xFF) {
|
||||
// Gradually restore tunnel mask
|
||||
tunnelMask = (tunnelMask << 1) | 0x01;
|
||||
if (tunnelMask === 0xFF) tunnelMask = 0xFF;
|
||||
|
||||
// Enhanced tunnel logic based on assembly TUNNEL routine
|
||||
if (pacman.x < 0) {
|
||||
pacman.x = canvas.width - CELL_SIZE;
|
||||
tunnelMask = 0x7F; // Start tunnel mask
|
||||
} else if (pacman.x >= canvas.width) {
|
||||
pacman.x = 0;
|
||||
tunnelMask = 0x7F; // Start tunnel mask
|
||||
} else if (tunnelMask !== 0xFF) {
|
||||
// Gradually restore tunnel mask
|
||||
tunnelMask = (tunnelMask << 1) | 0x01;
|
||||
if (tunnelMask === 0xFF) tunnelMask = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Animate mouth
|
||||
pacman.mouthAngle = 0.2 + Math.sin(animationFrame * 0.3) * 0.3;
|
||||
pacman.mouthOpen = Math.floor(animationFrame / 3) % 2 === 0;
|
||||
}
|
||||
|
||||
@@ -501,35 +726,223 @@
|
||||
default: return { x: x, y: y };
|
||||
}
|
||||
}
|
||||
|
||||
// Assembly-based MONSTR (Monster State Machine) - faithful implementation
|
||||
function MONSTR(ghost, index) {
|
||||
// Ghost states based on assembly logic
|
||||
// 0-7 = CHASE, 8-15 = SCATTER, 16-23 = FRIGHT, 24-31 = HOME
|
||||
const state = ghost.state || 0;
|
||||
|
||||
if (state >= 0 && state <= 7) {
|
||||
// CHASE mode
|
||||
ghost.mode = 'CHASE';
|
||||
return CHSSEQ(ghost, index);
|
||||
} else if (state >= 8 && state <= 15) {
|
||||
// SCATTER mode
|
||||
ghost.mode = 'SCATTER';
|
||||
return MPATRN(ghost, index);
|
||||
} else if (state >= 16 && state <= 23) {
|
||||
// FRIGHT mode
|
||||
ghost.mode = 'FRIGHT';
|
||||
return FRIGHTBEHAVIOR(ghost, index);
|
||||
} else if (state >= 24 && state <= 31) {
|
||||
// HOME mode
|
||||
ghost.mode = 'HOME';
|
||||
return GOHOME(ghost);
|
||||
}
|
||||
|
||||
return ghost.direction;
|
||||
}
|
||||
|
||||
// Assembly-based CHSSEQ (Chase Sequence) - faithful implementation
|
||||
function CHSSEQ(ghost, index) {
|
||||
let targetX = pacman.x;
|
||||
let targetY = pacman.y;
|
||||
|
||||
// Individual ghost chase patterns based on assembly logic
|
||||
switch(index) {
|
||||
case 0: // Blinky - Direct chase
|
||||
// Target is Pacman directly
|
||||
break;
|
||||
|
||||
case 1: // Pinky - Ambush (target 4 cells ahead)
|
||||
const ambushOffset = 4 * CELL_SIZE;
|
||||
switch(pacman.direction) {
|
||||
case 'UP':
|
||||
targetY = pacman.y - ambushOffset;
|
||||
targetX = pacman.x - ambushOffset; // Slight offset for accuracy
|
||||
break;
|
||||
case 'DOWN':
|
||||
targetY = pacman.y + ambushOffset;
|
||||
targetX = pacman.x + ambushOffset;
|
||||
break;
|
||||
case 'LEFT':
|
||||
targetX = pacman.x - ambushOffset;
|
||||
targetY = pacman.y - ambushOffset;
|
||||
break;
|
||||
case 'RIGHT':
|
||||
targetX = pacman.x + ambushOffset;
|
||||
targetY = pacman.y + ambushOffset;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // Inky - Complex targeting
|
||||
// Target is based on Pacman's position and direction
|
||||
// Uses Blinky's position as reference
|
||||
const blinky = ghosts[0];
|
||||
const pivotX = pacman.x + (pacman.direction === 'RIGHT' ? 2 * CELL_SIZE :
|
||||
pacman.direction === 'LEFT' ? -2 * CELL_SIZE : 0);
|
||||
const pivotY = pacman.y + (pacman.direction === 'DOWN' ? 2 * CELL_SIZE :
|
||||
pacman.direction === 'UP' ? -2 * CELL_SIZE : 0);
|
||||
targetX = pivotX + (pivotX - blinky.x);
|
||||
targetY = pivotY + (pivotY - blinky.y);
|
||||
break;
|
||||
|
||||
case 3: // Clyde - Random/chase hybrid
|
||||
const distance = Math.sqrt(Math.pow(ghost.x - pacman.x, 2) + Math.pow(ghost.y - pacman.y, 2));
|
||||
if (distance < 8 * CELL_SIZE) {
|
||||
// If close, target scatter corner
|
||||
targetX = CELL_SIZE;
|
||||
targetY = MAZE_HEIGHT * CELL_SIZE;
|
||||
} else {
|
||||
// If far, chase Pacman
|
||||
targetX = pacman.x;
|
||||
targetY = pacman.y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Use SEEPAC to check if ghost can see Pacman
|
||||
const seeResult = SEEPAC(ghost);
|
||||
if (seeResult.canSee && ghost.mode === 'CHASE') {
|
||||
// Direct line of sight - move toward Pacman
|
||||
return seeResult.direction;
|
||||
}
|
||||
|
||||
// Use MDIRCT for pathfinding
|
||||
return MDIRCT(ghost, targetX, targetY);
|
||||
}
|
||||
|
||||
// Assembly-based MPATRN (Pattern Movement) - faithful implementation
|
||||
function MPATRN(ghost, index) {
|
||||
// Scatter mode - each ghost targets a corner
|
||||
const corners = [
|
||||
{ x: CELL_SIZE, y: CELL_SIZE }, // Top-left
|
||||
{ x: (MAZE_WIDTH - 1) * CELL_SIZE, y: CELL_SIZE }, // Top-right
|
||||
{ x: CELL_SIZE, y: (MAZE_HEIGHT - 1) * CELL_SIZE }, // Bottom-left
|
||||
{ x: (MAZE_WIDTH - 1) * CELL_SIZE, y: (MAZE_HEIGHT - 1) * CELL_SIZE } // Bottom-right
|
||||
];
|
||||
|
||||
const target = corners[index];
|
||||
return MDIRCT(ghost, target.x, target.y);
|
||||
}
|
||||
|
||||
// Assembly-based FRIGHTBEHAVIOR - faithful implementation
|
||||
function FRIGHTBEHAVIOR(ghost, index) {
|
||||
// In fright mode, ghosts move randomly away from Pacman
|
||||
const directions = ['UP', 'DOWN', 'LEFT', 'RIGHT'];
|
||||
const validDirections = [];
|
||||
|
||||
directions.forEach(dir => {
|
||||
if (dir === getOppositeDirection(ghost.direction)) return; // Don't reverse
|
||||
|
||||
const testResult = MAZHND(
|
||||
ghost.x + (dir === 'LEFT' ? -CELL_SIZE : dir === 'RIGHT' ? CELL_SIZE : 0),
|
||||
ghost.y + (dir === 'UP' ? -CELL_SIZE : dir === 'DOWN' ? CELL_SIZE : 0)
|
||||
);
|
||||
|
||||
if (testResult.valid) {
|
||||
validDirections.push(dir);
|
||||
}
|
||||
});
|
||||
|
||||
if (validDirections.length > 0) {
|
||||
// Random selection from valid directions
|
||||
return validDirections[Math.floor(Math.random() * validDirections.length)];
|
||||
}
|
||||
|
||||
return ghost.direction;
|
||||
}
|
||||
|
||||
// Assembly-based DOTTST (Power Pellet Test) - faithful implementation
|
||||
function DOTTST() {
|
||||
const col = Math.floor(pacman.x / CELL_SIZE);
|
||||
const row = Math.floor(pacman.y / CELL_SIZE);
|
||||
|
||||
if (maze[row][col] === 2) {
|
||||
// Power pellet eaten
|
||||
maze[row][col] = 3; // Clear the pellet
|
||||
|
||||
// Activate fright mode for all ghosts
|
||||
ghosts.forEach((ghost, index) => {
|
||||
ghost.state = 16; // Set to FRIGHT state
|
||||
ghost.frightTimer = 300; // 5 seconds at 60fps
|
||||
ghost.speed = GHOST_SPEED * 0.5; // Slower in fright mode
|
||||
ghost.reverseDirection = true; // Ghosts reverse direction
|
||||
});
|
||||
|
||||
score += 50;
|
||||
soundSystem.play('powerPellet');
|
||||
updateScore();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assembly-based MUNCHY (Dot Eating) - faithful implementation
|
||||
function MUNCHY() {
|
||||
const col = Math.floor(pacman.x / CELL_SIZE);
|
||||
const row = Math.floor(pacman.y / CELL_SIZE);
|
||||
|
||||
if (maze[row][col] === 1) {
|
||||
// Dot eaten
|
||||
maze[row][col] = 3; // Clear the dot
|
||||
score += 10;
|
||||
soundSystem.play('dot');
|
||||
updateScore();
|
||||
|
||||
// Waka-waka sound effect timing
|
||||
if (animationFrame % 10 === 0) {
|
||||
soundSystem.play('waka');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function moveGhosts() {
|
||||
ghosts.forEach((ghost, index) => {
|
||||
const directions = ['UP', 'DOWN', 'LEFT', 'RIGHT'];
|
||||
const validDirections = directions.filter(dir => {
|
||||
const nextPos = getNextPositionGhost(ghost.x, ghost.y, dir);
|
||||
return canMove(nextPos.x, nextPos.y);
|
||||
});
|
||||
// Handle reverse direction at fright mode start
|
||||
if (ghost.reverseDirection) {
|
||||
ghost.direction = getOppositeDirection(ghost.direction);
|
||||
ghost.reverseDirection = false;
|
||||
}
|
||||
|
||||
if (validDirections.length > 0) {
|
||||
// Enhanced AI based on original assembly logic
|
||||
if (Math.random() < 0.05 || !validDirections.includes(ghost.direction)) {
|
||||
// Different behavior for each ghost type based on assembly patterns
|
||||
if (index === 0) { // Blinky - aggressive chase
|
||||
ghost.direction = chasePacman(ghost, validDirections);
|
||||
} else if (index === 1) { // Pinky - ambush
|
||||
ghost.direction = ambushPacman(ghost, validDirections);
|
||||
} else if (index === 2) { // Inky - unpredictable
|
||||
ghost.direction = Math.random() < 0.7 ? chasePacman(ghost, validDirections) : validDirections[Math.floor(Math.random() * validDirections.length)];
|
||||
} else { // Clyde - random
|
||||
ghost.direction = validDirections[Math.floor(Math.random() * validDirections.length)];
|
||||
}
|
||||
// Update fright timer
|
||||
if (ghost.frightTimer > 0) {
|
||||
ghost.frightTimer--;
|
||||
if (ghost.frightTimer === 0) {
|
||||
ghost.state = 0; // Back to CHASE mode
|
||||
ghost.speed = GHOST_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
const nextPos = getNextPositionGhost(ghost.x, ghost.y, ghost.direction);
|
||||
if (canMove(nextPos.x, nextPos.y)) {
|
||||
ghost.x = nextPos.x;
|
||||
ghost.y = nextPos.y;
|
||||
// Use MONSTR state machine to determine direction
|
||||
const newDirection = MONSTR(ghost, index);
|
||||
ghost.direction = newDirection;
|
||||
|
||||
// Move ghost
|
||||
const speed = ghost.speed || GHOST_SPEED;
|
||||
switch(ghost.direction) {
|
||||
case 'UP': ghost.y -= speed; break;
|
||||
case 'DOWN': ghost.y += speed; break;
|
||||
case 'LEFT': ghost.x -= speed; break;
|
||||
case 'RIGHT': ghost.x += speed; break;
|
||||
}
|
||||
|
||||
// Tunnel wrapping
|
||||
@@ -590,38 +1003,31 @@
|
||||
}
|
||||
|
||||
function checkCollisions() {
|
||||
// Enhanced collision detection based on assembly COLCHK routine
|
||||
const col = Math.floor(pacman.x / CELL_SIZE);
|
||||
const row = Math.floor(pacman.y / CELL_SIZE);
|
||||
// Assembly-based collision detection using DOTTST and MUNCHY
|
||||
|
||||
if (maze[row][col] === 1) {
|
||||
maze[row][col] = 3;
|
||||
score += 10;
|
||||
soundSystem.play('dot');
|
||||
updateScore();
|
||||
} else if (maze[row][col] === 2) {
|
||||
maze[row][col] = 3;
|
||||
score += 50;
|
||||
powerPelletActive = true;
|
||||
powerPelletTimer = 300;
|
||||
soundSystem.play('powerPellet');
|
||||
updateScore();
|
||||
// Check for power pellet first (DOTTST routine)
|
||||
if (DOTTST()) {
|
||||
// Power pellet was eaten, ghosts are now in fright mode
|
||||
return;
|
||||
}
|
||||
|
||||
// Check ghost collisions with precise distance calculation
|
||||
// Check for regular dot (MUNCHY routine)
|
||||
MUNCHY();
|
||||
|
||||
// Check ghost collisions (COLCHK routine)
|
||||
ghosts.forEach((ghost, index) => {
|
||||
const distance = Math.sqrt(Math.pow(pacman.x - ghost.x, 2) + Math.pow(pacman.y - ghost.y, 2));
|
||||
if (distance < CELL_SIZE * 0.8) { // More precise collision detection
|
||||
if (powerPelletActive) {
|
||||
// Ghost eaten - return to home based on assembly logic
|
||||
ghost.x = 14 * CELL_SIZE + (index % 2) * CELL_SIZE;
|
||||
ghost.y = 15 * CELL_SIZE + Math.floor(index / 2) * CELL_SIZE;
|
||||
ghost.direction = ['UP', 'DOWN', 'LEFT', 'RIGHT'][index];
|
||||
if (distance < CELL_SIZE * 0.8) { // Collision detected
|
||||
if (ghost.mode === 'FRIGHT') {
|
||||
// Ghost eaten - return to home
|
||||
ghost.state = 24; // Set to HOME state
|
||||
ghost.speed = GHOST_SPEED * 1.5; // Faster when returning home
|
||||
ghost.color = '#ffffff'; // Eyes only
|
||||
score += 200;
|
||||
soundSystem.play('ghostEat');
|
||||
updateScore();
|
||||
} else {
|
||||
// Pacman death sequence based on assembly PMDEAD
|
||||
// Pacman death sequence
|
||||
soundSystem.play('death');
|
||||
lives--;
|
||||
updateLives();
|
||||
@@ -753,6 +1159,20 @@
|
||||
checkCollisions();
|
||||
checkWinCondition();
|
||||
|
||||
// Assembly-based ghost state timing
|
||||
ghostStateTimer++;
|
||||
if (ghostStateTimer >= 420) { // 7 seconds at 60fps
|
||||
ghostStateTimer = 0;
|
||||
ghostStateIndex = (ghostStateIndex + 1) % ghostStateSequence.length;
|
||||
|
||||
// Update ghost states based on sequence (skip if in fright mode)
|
||||
ghosts.forEach(ghost => {
|
||||
if (ghost.state < 16) { // Not in fright mode
|
||||
ghost.state = ghostStateSequence[ghostStateIndex];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (powerPelletActive) {
|
||||
powerPelletTimer--;
|
||||
if (powerPelletTimer <= 0) {
|
||||
@@ -910,56 +1330,64 @@
|
||||
function handleSpaceButton() {
|
||||
console.log('Space button pressed, current game state:', gameState);
|
||||
|
||||
// Simple direct logic - just start the game
|
||||
gameState = 'PLAYING';
|
||||
startScreenElement.style.display = 'none';
|
||||
gameOverElement.style.display = 'none';
|
||||
resetPositions();
|
||||
|
||||
console.log('Game started in PLAYING state');
|
||||
updateDebugInfo();
|
||||
|
||||
// Play sound if available
|
||||
try {
|
||||
// Proper assembly-based state transitions
|
||||
if (gameState === 'ATTRACT') {
|
||||
// From ATTRACT mode, go to START state (show title screen)
|
||||
gameState = 'START';
|
||||
attractModeTimer = 0;
|
||||
startScreenElement.style.display = 'block';
|
||||
console.log('ATTRACT → START');
|
||||
soundSystem.play('ready');
|
||||
} catch (e) {
|
||||
console.log('Sound not available:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Ultra-simple test for Chrome Android
|
||||
function simpleTest() {
|
||||
console.log('=== SIMPLE TEST START ===');
|
||||
|
||||
// Test 1: Check if elements exist
|
||||
console.log('Space button element:', mobileSpaceBtnElement);
|
||||
console.log('Start screen element:', startScreenElement);
|
||||
console.log('Canvas element:', canvas);
|
||||
|
||||
// Test 2: Add a simple click handler
|
||||
mobileSpaceBtnElement.addEventListener('click', function(e) {
|
||||
console.log('CLICK DETECTED!');
|
||||
alert('Space button clicked! Game should start now.');
|
||||
|
||||
// Hide start screen
|
||||
} else if (gameState === 'START') {
|
||||
// From START state, go to READY state (show "READY!" for 3 seconds)
|
||||
gameState = 'READY';
|
||||
readyTimer = 180; // 3 seconds at 60fps (based on assembly timing)
|
||||
startScreenElement.style.display = 'none';
|
||||
|
||||
// Change background to show something happened
|
||||
document.body.style.backgroundColor = '#ff0000';
|
||||
|
||||
console.log('Game started!');
|
||||
});
|
||||
resetPositions();
|
||||
console.log('START → READY');
|
||||
soundSystem.play('ready');
|
||||
} else if (gameState === 'GAME_OVER') {
|
||||
// From GAME_OVER, restart to START state
|
||||
gameState = 'START';
|
||||
gameOverElement.style.display = 'none';
|
||||
score = 0;
|
||||
lives = 3;
|
||||
currentLevel = 1;
|
||||
updateScore();
|
||||
updateLives();
|
||||
maze = JSON.parse(JSON.stringify(mazeLayout));
|
||||
startScreenElement.style.display = 'block';
|
||||
console.log('GAME_OVER → START');
|
||||
soundSystem.play('ready');
|
||||
} else if (gameState === 'PLAYING') {
|
||||
// If already playing, do nothing (or could pause)
|
||||
console.log('Already in PLAYING state');
|
||||
}
|
||||
|
||||
// Test 3: Add visual feedback
|
||||
mobileSpaceBtnElement.style.backgroundColor = '#00ff00';
|
||||
mobileSpaceBtnElement.style.color = '#000000';
|
||||
mobileSpaceBtnElement.textContent = 'CLICK ME!';
|
||||
|
||||
console.log('=== SIMPLE TEST SETUP COMPLETE ===');
|
||||
updateDebugInfo();
|
||||
}
|
||||
|
||||
// Run simple test immediately
|
||||
simpleTest();
|
||||
// Initialize game with ATTRACT mode (assembly-based startup)
|
||||
gameState = 'ATTRACT';
|
||||
attractModeTimer = 0;
|
||||
|
||||
// Setup mobile controls
|
||||
if (isMobile || window.innerWidth <= 768) {
|
||||
setupMobileControls();
|
||||
}
|
||||
|
||||
// Show mobile controls
|
||||
mobileControlsElement.classList.add('active');
|
||||
mobileSpaceBtnElement.style.display = 'block';
|
||||
|
||||
// Update debug info
|
||||
updateDebugInfo();
|
||||
|
||||
console.log('Full Pacman game initialized with ATTRACT mode');
|
||||
console.log('Game state:', gameState);
|
||||
|
||||
// Start game loop
|
||||
gameLoop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user