created html5 implementation
This commit is contained in:
691
pacman.html
Normal file
691
pacman.html
Normal file
@@ -0,0 +1,691 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pac-Man HTML5</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; background: #000; display: flex; flex-direction: column; align-items: center; font-family: 'Courier New', monospace; color: #fff; }
|
||||
#gameContainer { position: relative; background: #000; border: 2px solid #00f; }
|
||||
#gameCanvas { display: block; image-rendering: pixelated; }
|
||||
#score, #lives { font-size: 20px; margin: 10px 0; color: #ff0; }
|
||||
#gameOver, #startScreen { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; color: #ff0; text-align: center; }
|
||||
.controls { margin-top: 20px; text-align: center; color: #0ff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PAC-MAN</h1>
|
||||
<div id="score">SCORE: 0</div>
|
||||
<div id="lives">LIVES: 3</div>
|
||||
<div id="gameContainer">
|
||||
<canvas id="gameCanvas" width="448" height="560"></canvas>
|
||||
<div id="startScreen">
|
||||
<div>PRESS SPACE TO START</div>
|
||||
<div style="font-size: 16px; margin-top: 10px;">USE ARROW KEYS TO MOVE</div>
|
||||
</div>
|
||||
<div id="gameOver" style="display: none;">
|
||||
<div>GAME OVER</div>
|
||||
<div style="font-size: 16px; margin-top: 10px;">PRESS SPACE TO RESTART</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div>↑ ↓ ← → : MOVE PAC-MAN</div>
|
||||
<div>SPACE : START/RESTART GAME</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreElement = document.getElementById('score');
|
||||
const livesElement = document.getElementById('lives');
|
||||
const gameOverElement = document.getElementById('gameOver');
|
||||
const startScreenElement = document.getElementById('startScreen');
|
||||
|
||||
const CELL_SIZE = 14;
|
||||
const MAZE_WIDTH = 32;
|
||||
const MAZE_HEIGHT = 36;
|
||||
|
||||
let gameState = 'START'; // START, PLAYING, GAME_OVER, ATTRACT, READY, INTERMISSION
|
||||
let score = 0;
|
||||
let lives = 3;
|
||||
let powerPelletActive = false;
|
||||
let powerPelletTimer = 0;
|
||||
let animationFrame = 0;
|
||||
let attractModeTimer = 0;
|
||||
let attractColorIndex = 0;
|
||||
let tunnelMask = 0xFF;
|
||||
let readyTimer = 0;
|
||||
let intermissionTimer = 0;
|
||||
let currentLevel = 1;
|
||||
|
||||
// Maze layout based on DATMAZ data from assembly code
|
||||
// 0 = wall, 1 = dot, 2 = power pellet, 3 = empty
|
||||
const mazeLayout = [
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
|
||||
[0,3,0,0,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0],
|
||||
[0,2,0,0,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,2,0,0],
|
||||
[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
|
||||
[0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0],
|
||||
[0,1,1,1,1,1,1,0,0,1,1,1,1,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,1,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
|
||||
[0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0],
|
||||
[0,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
|
||||
[0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0],
|
||||
[0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
|
||||
[0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0],
|
||||
[0,2,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,2,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0],
|
||||
[0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
||||
];
|
||||
|
||||
let maze = JSON.parse(JSON.stringify(mazeLayout));
|
||||
|
||||
// Pacman object
|
||||
const pacman = {
|
||||
x: 15 * CELL_SIZE,
|
||||
y: 23 * CELL_SIZE,
|
||||
size: CELL_SIZE - 2,
|
||||
speed: PACMAN_SPEED,
|
||||
direction: 'RIGHT',
|
||||
nextDirection: null,
|
||||
mouthOpen: true,
|
||||
mouthAngle: 0.2
|
||||
};
|
||||
|
||||
// Sound system based on assembly audio routines
|
||||
class SoundSystem {
|
||||
constructor() {
|
||||
this.audioContext = null;
|
||||
this.sounds = {};
|
||||
this.initAudio();
|
||||
}
|
||||
|
||||
initAudio() {
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.createSounds();
|
||||
} catch (e) {
|
||||
console.log('Audio not supported');
|
||||
}
|
||||
}
|
||||
|
||||
createSounds() {
|
||||
// Create basic sound effects based on assembly AUDF/AUDC values
|
||||
this.sounds.dot = () => this.playTone(200, 50);
|
||||
this.sounds.powerPellet = () => this.playTone(400, 200);
|
||||
this.sounds.ghostEat = () => this.playTone(800, 100);
|
||||
this.sounds.death = () => this.playTone(100, 1000);
|
||||
this.sounds.waka = () => this.playTone(300, 30);
|
||||
this.sounds.ready = () => this.playTone(500, 300);
|
||||
}
|
||||
|
||||
playTone(frequency, duration) {
|
||||
if (!this.audioContext) return;
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
const gainNode = this.audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(this.audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = frequency;
|
||||
oscillator.type = 'square';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration / 1000);
|
||||
|
||||
oscillator.start(this.audioContext.currentTime);
|
||||
oscillator.stop(this.audioContext.currentTime + duration / 1000);
|
||||
}
|
||||
|
||||
play(soundName) {
|
||||
if (this.sounds[soundName]) {
|
||||
this.sounds[soundName]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const soundSystem = new SoundSystem();
|
||||
// Ghosts object with enhanced AI based on assembly code
|
||||
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' }
|
||||
];
|
||||
|
||||
function drawMaze() {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Attract mode color cycling based on assembly ADLIV routine
|
||||
const attractColors = ['#0000ff', '#ff0000', '#ffff00', '#00ff00', '#ff00ff', '#00ffff'];
|
||||
const wallColor = gameState === 'ATTRACT' ? attractColors[attractColorIndex] : '#0000ff';
|
||||
|
||||
for (let row = 0; row < MAZE_HEIGHT; row++) {
|
||||
for (let col = 0; col < MAZE_WIDTH; col++) {
|
||||
const x = col * CELL_SIZE;
|
||||
const y = row * CELL_SIZE;
|
||||
const cell = maze[row][col];
|
||||
|
||||
if (cell === 0) {
|
||||
ctx.fillStyle = wallColor;
|
||||
ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE);
|
||||
} else if (cell === 1) {
|
||||
// Blinking dots in attract mode
|
||||
if (gameState === 'ATTRACT' && Math.floor(animationFrame / 30) % 2 === 0) {
|
||||
continue; // Skip drawing every other frame
|
||||
}
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + CELL_SIZE/2, y + CELL_SIZE/2, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (cell === 2) {
|
||||
// Power pellets with enhanced animation
|
||||
if (gameState === 'ATTRACT' && Math.floor(animationFrame / 15) % 2 === 0) {
|
||||
ctx.fillStyle = attractColors[(attractColorIndex + 1) % attractColors.length];
|
||||
} else {
|
||||
ctx.fillStyle = '#fff';
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + CELL_SIZE/2, y + CELL_SIZE/2, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawReadyScreen() {
|
||||
// READY1 routine - "READY!" text display
|
||||
ctx.fillStyle = '#ff0';
|
||||
ctx.font = '24px Courier New';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('READY!', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
// Player indicator based on assembly logic
|
||||
ctx.font = '16px Courier New';
|
||||
ctx.fillText(`PLAYER ${currentLevel}`, canvas.width / 2, canvas.height / 2 + 30);
|
||||
}
|
||||
|
||||
function drawTitleScreen() {
|
||||
// Title/Intro screen based on assembly INTROF logic
|
||||
ctx.fillStyle = '#ff0';
|
||||
ctx.font = '32px Courier New';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('PAC-MAN', canvas.width / 2, canvas.height / 3);
|
||||
|
||||
ctx.font = '16px Courier New';
|
||||
ctx.fillText('ATARI 1980', canvas.width / 2, canvas.height / 3 + 40);
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '20px Courier New';
|
||||
ctx.fillText('PRESS SPACE TO START', canvas.width / 2, canvas.height * 2/3);
|
||||
|
||||
// Blinking instruction
|
||||
if (Math.floor(animationFrame / 30) % 2 === 0) {
|
||||
ctx.font = '14px Courier New';
|
||||
ctx.fillText('USE ARROW KEYS TO MOVE', canvas.width / 2, canvas.height * 2/3 + 30);
|
||||
}
|
||||
}
|
||||
|
||||
function drawIntermission() {
|
||||
// INTMIS routine - between-level animations
|
||||
ctx.fillStyle = '#ff0';
|
||||
ctx.font = '24px Courier New';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
if (intermissionTimer < 180) {
|
||||
ctx.fillText('INTERMISSION', canvas.width / 2, canvas.height / 2);
|
||||
} else {
|
||||
ctx.fillText(`LEVEL ${currentLevel} COMPLETE!`, canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPacman() {
|
||||
ctx.save();
|
||||
ctx.translate(pacman.x + pacman.size/2, pacman.y + pacman.size/2);
|
||||
|
||||
let rotation = 0;
|
||||
switch(pacman.direction) {
|
||||
case 'RIGHT': rotation = 0; break;
|
||||
case 'DOWN': rotation = Math.PI/2; break;
|
||||
case 'LEFT': rotation = Math.PI; break;
|
||||
case 'UP': rotation = -Math.PI/2; break;
|
||||
}
|
||||
ctx.rotate(rotation);
|
||||
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.beginPath();
|
||||
|
||||
// Enhanced mouth animation based on assembly PACRGT/PACLFT data
|
||||
if (pacman.mouthOpen) {
|
||||
const mouthSize = 0.2 + Math.sin(animationFrame * 0.3) * 0.3;
|
||||
ctx.arc(0, 0, pacman.size/2, mouthSize, Math.PI * 2 - mouthSize);
|
||||
} else {
|
||||
ctx.arc(0, 0, pacman.size/2, 0, Math.PI * 2);
|
||||
}
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawGhosts() {
|
||||
ghosts.forEach((ghost, index) => {
|
||||
ctx.fillStyle = powerPelletActive ? (powerPelletTimer < 60 && powerPelletTimer % 20 < 10 ? '#ffffff' : '#0000ff') : ghost.color;
|
||||
|
||||
// Ghost body shape based on assembly graphics
|
||||
ctx.beginPath();
|
||||
ctx.arc(ghost.x + CELL_SIZE/2, ghost.y + CELL_SIZE/2, CELL_SIZE/2 - 2, Math.PI, 0);
|
||||
ctx.lineTo(ghost.x + CELL_SIZE - 2, ghost.y + CELL_SIZE - 2);
|
||||
|
||||
// Wavy bottom
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const waveX = ghost.x + CELL_SIZE - 2 - (i * 4);
|
||||
const waveY = ghost.y + CELL_SIZE - 2 + (i % 2 === 0 ? -2 : 0);
|
||||
ctx.lineTo(waveX, waveY);
|
||||
}
|
||||
|
||||
ctx.lineTo(ghost.x + 2, ghost.y + CELL_SIZE - 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Eyes based on assembly MONSEY data
|
||||
if (!powerPelletActive) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(ghost.x + 3, ghost.y + 4, 3, 3);
|
||||
ctx.fillRect(ghost.x + 8, ghost.y + 4, 3, 3);
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(ghost.x + 4, ghost.y + 5, 1, 1);
|
||||
ctx.fillRect(ghost.x + 9, ghost.y + 5, 1, 1);
|
||||
} else {
|
||||
// Scared eyes
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(ghost.x + 4, ghost.y + 6, 2, 1);
|
||||
ctx.fillRect(ghost.x + 8, ghost.y + 6, 2, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function canMove(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;
|
||||
}
|
||||
|
||||
return maze[row][col] !== 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const nextPos = getNextPosition(pacman.x, pacman.y, pacman.direction);
|
||||
if (canMove(nextPos.x, nextPos.y)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Animate mouth
|
||||
pacman.mouthAngle = 0.2 + Math.sin(animationFrame * 0.3) * 0.3;
|
||||
pacman.mouthOpen = Math.floor(animationFrame / 3) % 2 === 0;
|
||||
}
|
||||
|
||||
function getNextPosition(x, y, direction) {
|
||||
switch(direction) {
|
||||
case 'UP': return { x: x, y: y - pacman.speed };
|
||||
case 'DOWN': return { x: x, y: y + pacman.speed };
|
||||
case 'LEFT': return { x: x - pacman.speed, y: y };
|
||||
case 'RIGHT': return { x: x + pacman.speed, y: y };
|
||||
default: return { x: x, y: y };
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextPos = getNextPositionGhost(ghost.x, ghost.y, ghost.direction);
|
||||
if (canMove(nextPos.x, nextPos.y)) {
|
||||
ghost.x = nextPos.x;
|
||||
ghost.y = nextPos.y;
|
||||
}
|
||||
|
||||
// Tunnel wrapping
|
||||
if (ghost.x < 0) ghost.x = canvas.width - CELL_SIZE;
|
||||
if (ghost.x >= canvas.width) ghost.x = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getNextPositionGhost(x, y, direction) {
|
||||
switch(direction) {
|
||||
case 'UP': return { x: x, y: y - ghost.speed };
|
||||
case 'DOWN': return { x: x, y: y + ghost.speed };
|
||||
case 'LEFT': return { x: x - ghost.speed, y: y };
|
||||
case 'RIGHT': return { x: x + ghost.speed, y: y };
|
||||
default: return { x: x, y: y };
|
||||
}
|
||||
}
|
||||
|
||||
function chasePacman(ghost, validDirections) {
|
||||
const dx = pacman.x - ghost.x;
|
||||
const dy = pacman.y - ghost.y;
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
return dx > 0 && validDirections.includes('RIGHT') ? 'RIGHT' :
|
||||
dx < 0 && validDirections.includes('LEFT') ? 'LEFT' :
|
||||
dy > 0 && validDirections.includes('DOWN') ? 'DOWN' : 'UP';
|
||||
} else {
|
||||
return dy > 0 && validDirections.includes('DOWN') ? 'DOWN' :
|
||||
dy < 0 && validDirections.includes('UP') ? 'UP' :
|
||||
dx > 0 && validDirections.includes('RIGHT') ? 'RIGHT' : 'LEFT';
|
||||
}
|
||||
}
|
||||
|
||||
function ambushPacman(ghost, validDirections) {
|
||||
// Predict where Pacman will be based on his direction
|
||||
let targetX = pacman.x;
|
||||
let targetY = pacman.y;
|
||||
|
||||
switch(pacman.direction) {
|
||||
case 'RIGHT': targetX += CELL_SIZE * 4; break;
|
||||
case 'LEFT': targetX -= CELL_SIZE * 4; break;
|
||||
case 'UP': targetY -= CELL_SIZE * 4; break;
|
||||
case 'DOWN': targetY += CELL_SIZE * 4; break;
|
||||
}
|
||||
|
||||
const dx = targetX - ghost.x;
|
||||
const dy = targetY - ghost.y;
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
return dx > 0 && validDirections.includes('RIGHT') ? 'RIGHT' :
|
||||
dx < 0 && validDirections.includes('LEFT') ? 'LEFT' :
|
||||
dy > 0 && validDirections.includes('DOWN') ? 'DOWN' : 'UP';
|
||||
} else {
|
||||
return dy > 0 && validDirections.includes('DOWN') ? 'DOWN' :
|
||||
dy < 0 && validDirections.includes('UP') ? 'UP' :
|
||||
dx > 0 && validDirections.includes('RIGHT') ? 'RIGHT' : 'LEFT';
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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 ghost collisions with precise distance calculation
|
||||
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];
|
||||
score += 200;
|
||||
soundSystem.play('ghostEat');
|
||||
updateScore();
|
||||
} else {
|
||||
// Pacman death sequence based on assembly PMDEAD
|
||||
soundSystem.play('death');
|
||||
lives--;
|
||||
updateLives();
|
||||
if (lives <= 0) {
|
||||
gameState = 'GAME_OVER';
|
||||
gameOverElement.style.display = 'block';
|
||||
} else {
|
||||
gameState = 'READY';
|
||||
readyTimer = 180; // 3 seconds at 60fps
|
||||
resetPositions();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetPositions() {
|
||||
pacman.x = 15 * CELL_SIZE;
|
||||
pacman.y = 23 * CELL_SIZE;
|
||||
pacman.direction = 'RIGHT';
|
||||
|
||||
ghosts[0].x = 14 * CELL_SIZE; ghosts[0].y = 15 * CELL_SIZE;
|
||||
ghosts[1].x = 15 * CELL_SIZE; ghosts[1].y = 15 * CELL_SIZE;
|
||||
ghosts[2].x = 14 * CELL_SIZE; ghosts[2].y = 16 * CELL_SIZE;
|
||||
ghosts[3].x = 15 * CELL_SIZE; ghosts[3].y = 16 * CELL_SIZE;
|
||||
}
|
||||
|
||||
function updateScore() {
|
||||
scoreElement.textContent = `SCORE: ${score}`;
|
||||
}
|
||||
|
||||
function updateLives() {
|
||||
livesElement.textContent = `LIVES: ${lives}`;
|
||||
}
|
||||
|
||||
function checkWinCondition() {
|
||||
let dotsRemaining = 0;
|
||||
for (let row = 0; row < MAZE_HEIGHT; row++) {
|
||||
for (let col = 0; col < MAZE_WIDTH; col++) {
|
||||
if (maze[row][col] === 1 || maze[row][col] === 2) {
|
||||
dotsRemaining++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dotsRemaining === 0) {
|
||||
gameState = 'INTERMISSION';
|
||||
intermissionTimer = 300; // 5 seconds at 60fps
|
||||
currentLevel++;
|
||||
}
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
if (gameState === 'ATTRACT') {
|
||||
// Attract mode logic based on assembly SEQATM routine
|
||||
attractModeTimer++;
|
||||
if (attractModeTimer % 60 === 0) {
|
||||
attractColorIndex = (attractColorIndex + 1) % 6;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drawMaze();
|
||||
|
||||
// Demo gameplay in attract mode
|
||||
if (attractModeTimer % 120 === 0) {
|
||||
// Random direction changes for demo
|
||||
const directions = ['UP', 'DOWN', 'LEFT', 'RIGHT'];
|
||||
pacman.nextDirection = directions[Math.floor(Math.random() * directions.length)];
|
||||
}
|
||||
|
||||
movePacman();
|
||||
moveGhosts();
|
||||
checkCollisions();
|
||||
|
||||
animationFrame++;
|
||||
|
||||
// Auto-start game after attract mode timeout
|
||||
if (attractModeTimer > 600) { // 10 seconds at 60fps
|
||||
gameState = 'START';
|
||||
attractModeTimer = 0;
|
||||
startScreenElement.style.display = 'block';
|
||||
}
|
||||
|
||||
} else if (gameState === 'START') {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drawTitleScreen();
|
||||
animationFrame++;
|
||||
|
||||
} else if (gameState === 'READY') {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drawMaze();
|
||||
drawPacman();
|
||||
drawGhosts();
|
||||
drawReadyScreen();
|
||||
|
||||
readyTimer--;
|
||||
if (readyTimer <= 0) {
|
||||
gameState = 'PLAYING';
|
||||
soundSystem.play('ready');
|
||||
}
|
||||
animationFrame++;
|
||||
|
||||
} else if (gameState === 'INTERMISSION') {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drawIntermission();
|
||||
|
||||
intermissionTimer--;
|
||||
if (intermissionTimer <= 0) {
|
||||
maze = JSON.parse(JSON.stringify(mazeLayout));
|
||||
resetPositions();
|
||||
gameState = 'READY';
|
||||
readyTimer = 180;
|
||||
}
|
||||
animationFrame++;
|
||||
|
||||
} else if (gameState === 'PLAYING') {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
drawMaze();
|
||||
drawPacman();
|
||||
drawGhosts();
|
||||
|
||||
movePacman();
|
||||
moveGhosts();
|
||||
checkCollisions();
|
||||
checkWinCondition();
|
||||
|
||||
if (powerPelletActive) {
|
||||
powerPelletTimer--;
|
||||
if (powerPelletTimer <= 0) {
|
||||
powerPelletActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Waka-waka sound effect
|
||||
if (animationFrame % 10 === 0 && Math.random() < 0.3) {
|
||||
soundSystem.play('waka');
|
||||
}
|
||||
|
||||
animationFrame++;
|
||||
|
||||
// Auto-switch to attract mode after inactivity
|
||||
if (animationFrame % 3600 === 0) { // 1 minute of inactivity
|
||||
gameState = 'ATTRACT';
|
||||
attractModeTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Keyboard controls with attract mode support
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Reset inactivity timer on any key press
|
||||
animationFrame = 0;
|
||||
|
||||
if (gameState === 'ATTRACT') {
|
||||
if (e.code === 'Space') {
|
||||
gameState = 'START';
|
||||
attractModeTimer = 0;
|
||||
soundSystem.play('ready');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState === 'START' && e.code === 'Space') {
|
||||
gameState = 'READY';
|
||||
readyTimer = 180;
|
||||
startScreenElement.style.display = 'none';
|
||||
resetPositions();
|
||||
soundSystem.play('ready');
|
||||
} else if (gameState === 'GAME_OVER' && e.code === 'Space') {
|
||||
gameState = 'START';
|
||||
gameOverElement.style.display = 'none';
|
||||
score = 0;
|
||||
lives = 3;
|
||||
currentLevel = 1;
|
||||
updateScore();
|
||||
updateLives();
|
||||
maze = JSON.parse(JSON.stringify(mazeLayout));
|
||||
soundSystem.play('ready');
|
||||
} else if (gameState === 'PLAYING') {
|
||||
switch(e.code) {
|
||||
case 'ArrowUp': pacman.nextDirection = 'UP'; break;
|
||||
case 'ArrowDown': pacman.nextDirection = 'DOWN'; break;
|
||||
case 'ArrowLeft': pacman.nextDirection = 'LEFT'; break;
|
||||
case 'ArrowRight': pacman.nextDirection = 'RIGHT'; break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start with attract mode
|
||||
gameState = 'ATTRACT';
|
||||
attractModeTimer = 0;
|
||||
gameLoop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user