Merging dev into main
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Pacman game module.
|
||||
* Handles game logic, rendering, and user interaction.
|
||||
*/
|
||||
|
||||
// ===== Game Constants =====
|
||||
const PACMAN_SPEED = 40;
|
||||
const ENEMY_SPEED = 20;
|
||||
const CELL_SIZE = 40;
|
||||
const DOT_SIZE = 5;
|
||||
|
||||
// ===== Game State =====
|
||||
let canvas, ctx, pacman, enemy, walls, dots, score;
|
||||
let cols, rows, randSeed, gameInterval;
|
||||
|
||||
// ===== Public Interface =====
|
||||
export function setupGame() {
|
||||
console.log('[PacMan] Game module loaded.');
|
||||
window.startPacman = startPacman;
|
||||
window.exitGame = exitGame;
|
||||
}
|
||||
|
||||
export function startPacman() {
|
||||
// Scroll to the Pacman section
|
||||
const pacmanSection = document.getElementById("pacman-section");
|
||||
if (pacmanSection) {
|
||||
pacmanSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Initialize game state
|
||||
initializeGame();
|
||||
setupGameLoop();
|
||||
}
|
||||
|
||||
export function stopPacman() {
|
||||
clearInterval(gameInterval);
|
||||
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Restore scrolling
|
||||
document.body.style.overflow = '';
|
||||
document.removeEventListener('wheel', preventScroll);
|
||||
document.removeEventListener('touchmove', preventScroll);
|
||||
}
|
||||
|
||||
export function resetGame() {
|
||||
stopPacman();
|
||||
startPacman();
|
||||
}
|
||||
|
||||
export function exitGame() {
|
||||
stopPacman();
|
||||
document.getElementById("input-text").value = "";
|
||||
document.getElementById("pacman-section").style.display = "none";
|
||||
document.getElementById("encoding-section").style.display = "block";
|
||||
}
|
||||
|
||||
// ===== Game Initialization =====
|
||||
function initializeGame() {
|
||||
canvas = document.getElementById("pacmanCanvas");
|
||||
ctx = canvas.getContext("2d");
|
||||
|
||||
cols = Math.floor(canvas.width / CELL_SIZE);
|
||||
rows = Math.floor(canvas.height / CELL_SIZE);
|
||||
walls = [];
|
||||
dots = [];
|
||||
score = 0;
|
||||
|
||||
clearInterval(gameInterval);
|
||||
|
||||
// Get seed from generated password or use default
|
||||
const passwordField = document.getElementById("generated-password");
|
||||
const seedSource = passwordField?.value || "pacman";
|
||||
randSeed = [...seedSource].reduce((s, c) => s + c.charCodeAt(0), 0);
|
||||
|
||||
generateWalls();
|
||||
generateDots();
|
||||
|
||||
pacman = spawn();
|
||||
do { enemy = spawn(); } while (enemy.x === pacman.x && enemy.y === pacman.y);
|
||||
|
||||
pacman.dx = pacman.dy = 0;
|
||||
document.addEventListener("keydown", movePacman);
|
||||
|
||||
// Prevent scrolling
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.addEventListener('wheel', preventScroll, { passive: false });
|
||||
document.addEventListener('touchmove', preventScroll, { passive: false });
|
||||
|
||||
// Add touch controls
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
const touchEndX = e.changedTouches[0].clientX;
|
||||
const touchEndY = e.changedTouches[0].clientY;
|
||||
|
||||
const dx = touchEndX - touchStartX;
|
||||
const dy = touchEndY - touchStartY;
|
||||
|
||||
// Determine swipe direction based on the larger movement
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
// Horizontal swipe
|
||||
if (dx > 0) {
|
||||
pacman.dx = PACMAN_SPEED;
|
||||
pacman.dy = 0;
|
||||
} else {
|
||||
pacman.dx = -PACMAN_SPEED;
|
||||
pacman.dy = 0;
|
||||
}
|
||||
} else {
|
||||
// Vertical swipe
|
||||
if (dy > 0) {
|
||||
pacman.dx = 0;
|
||||
pacman.dy = PACMAN_SPEED;
|
||||
} else {
|
||||
pacman.dx = 0;
|
||||
pacman.dy = -PACMAN_SPEED;
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
function setupGameLoop() {
|
||||
gameInterval = setInterval(gameLoop, 150);
|
||||
}
|
||||
|
||||
// ===== Game Setup Helpers =====
|
||||
function spawn() {
|
||||
const options = [];
|
||||
for (let c = 1; c < cols - 1; c++) {
|
||||
for (let r = 1; r < rows - 1; r++) {
|
||||
if (!walls.some(w => w.c === c && w.r === r)) {
|
||||
const neighbors = [
|
||||
{ c: c + 1, r }, { c: c - 1, r },
|
||||
{ c, r: r + 1 }, { c, r: r - 1 }
|
||||
];
|
||||
if (neighbors.some(n => !walls.some(w => w.c === n.c && w.r === n.r))) {
|
||||
options.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const s = options[Math.floor(rand() * options.length)];
|
||||
return {
|
||||
x: s.c * CELL_SIZE + CELL_SIZE / 2,
|
||||
y: s.r * CELL_SIZE + CELL_SIZE / 2,
|
||||
size: CELL_SIZE / 2 - 5,
|
||||
dx: 0,
|
||||
dy: 0
|
||||
};
|
||||
}
|
||||
|
||||
function rand() {
|
||||
const x = Math.sin(randSeed++) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
function generateWalls() {
|
||||
// First pass: generate initial walls
|
||||
for (let c = 0; c < cols; c++) {
|
||||
for (let r = 0; r < rows; r++) {
|
||||
if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) {
|
||||
walls.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: check for enclosed spaces
|
||||
for (let c = 1; c < cols - 1; c++) {
|
||||
for (let r = 1; r < rows - 1; r++) {
|
||||
// Skip if already a wall
|
||||
if (walls.some(w => w.c === c && w.r === r)) continue;
|
||||
|
||||
// Check all four sides
|
||||
const hasWallAbove = walls.some(w => w.c === c && w.r === r - 1);
|
||||
const hasWallBelow = walls.some(w => w.c === c && w.r === r + 1);
|
||||
const hasWallLeft = walls.some(w => w.c === c - 1 && w.r === r);
|
||||
const hasWallRight = walls.some(w => w.c === c + 1 && w.r === r);
|
||||
|
||||
// If all sides are walls, make this spot a wall too
|
||||
if (hasWallAbove && hasWallBelow && hasWallLeft && hasWallRight) {
|
||||
walls.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateDots() {
|
||||
dots = [];
|
||||
for (let c = 1; c < cols - 1; c++) {
|
||||
for (let r = 1; r < rows - 1; r++) {
|
||||
if (walls.some(w => w.c === c && w.r === r)) continue;
|
||||
|
||||
const isEnclosed =
|
||||
walls.some(w => w.c === c + 1 && w.r === r) &&
|
||||
walls.some(w => w.c === c - 1 && w.r === r) &&
|
||||
walls.some(w => w.c === c && w.r === r + 1) &&
|
||||
walls.some(w => w.c === c && w.r === r - 1);
|
||||
|
||||
if (!isEnclosed) dots.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Game Loop & Rendering =====
|
||||
function gameLoop() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drawWalls();
|
||||
moveChar(pacman);
|
||||
moveEnemy();
|
||||
drawChar(pacman, "yellow");
|
||||
drawChar(enemy, "red");
|
||||
eatDots();
|
||||
drawScore();
|
||||
checkGameOver();
|
||||
}
|
||||
|
||||
function drawWalls() {
|
||||
ctx.fillStyle = "blue";
|
||||
walls.forEach(w => {
|
||||
ctx.fillRect(w.c * CELL_SIZE, w.r * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||
});
|
||||
}
|
||||
|
||||
function drawChar(ch, color) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(ch.x, ch.y, ch.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawScore() {
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "20px Poppins";
|
||||
ctx.textAlign = "left";
|
||||
// Add padding to prevent clipping
|
||||
const padding = 10;
|
||||
ctx.fillText("Score: " + score, padding, 25);
|
||||
}
|
||||
|
||||
function checkGameOver() {
|
||||
if (
|
||||
Math.abs(pacman.x - enemy.x) < pacman.size &&
|
||||
Math.abs(pacman.y - enemy.y) < pacman.size
|
||||
) {
|
||||
ctx.fillStyle = "#00ff99";
|
||||
ctx.font = "40px Poppins";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2);
|
||||
clearInterval(gameInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Movement Logic =====
|
||||
function movePacman(e) {
|
||||
const k = e.key;
|
||||
if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return;
|
||||
e.preventDefault();
|
||||
|
||||
if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -PACMAN_SPEED; }
|
||||
if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = PACMAN_SPEED; }
|
||||
if (k === "ArrowLeft") { pacman.dx = -PACMAN_SPEED; pacman.dy = 0; }
|
||||
if (k === "ArrowRight") { pacman.dx = PACMAN_SPEED; pacman.dy = 0; }
|
||||
}
|
||||
|
||||
function moveChar(ch) {
|
||||
const nx = ch.x + ch.dx;
|
||||
const ny = ch.y + ch.dy;
|
||||
if (!willCollide(nx, ny, ch.size)) {
|
||||
ch.x = nx;
|
||||
ch.y = ny;
|
||||
}
|
||||
}
|
||||
|
||||
function moveEnemy() {
|
||||
const options = [];
|
||||
const moves = [[ENEMY_SPEED, 0], [-ENEMY_SPEED, 0], [0, ENEMY_SPEED], [0, -ENEMY_SPEED]];
|
||||
|
||||
moves.forEach(([dx, dy]) => {
|
||||
const nx = enemy.x + dx;
|
||||
const ny = enemy.y + dy;
|
||||
if (!willCollide(nx, ny, enemy.size)) options.push({ dx, dy });
|
||||
});
|
||||
|
||||
if (!options.length) return;
|
||||
|
||||
let best = options[0];
|
||||
let bestDist = dist(enemy.x + best.dx, enemy.y + best.dy, pacman.x, pacman.y);
|
||||
|
||||
for (const opt of options) {
|
||||
const d = dist(enemy.x + opt.dx, enemy.y + opt.dy, pacman.x, pacman.y);
|
||||
if (d < bestDist) {
|
||||
best = opt;
|
||||
bestDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
enemy.x += best.dx;
|
||||
enemy.y += best.dy;
|
||||
}
|
||||
|
||||
function dist(x1, y1, x2, y2) {
|
||||
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
|
||||
}
|
||||
|
||||
function willCollide(x, y, size) {
|
||||
const left = x - size, right = x + size;
|
||||
const top = y - size, bottom = y + size;
|
||||
|
||||
return walls.some(w => {
|
||||
const wx1 = w.c * CELL_SIZE, wy1 = w.r * CELL_SIZE;
|
||||
const wx2 = wx1 + CELL_SIZE, wy2 = wy1 + CELL_SIZE;
|
||||
return right > wx1 && left < wx2 && bottom > wy1 && top < wy2;
|
||||
});
|
||||
}
|
||||
|
||||
function eatDots() {
|
||||
const chompSound = document.getElementById("chomp-sound");
|
||||
|
||||
dots = dots.filter(d => {
|
||||
const dx = d.c * CELL_SIZE + CELL_SIZE / 2;
|
||||
const dy = d.r * CELL_SIZE + CELL_SIZE / 2;
|
||||
|
||||
if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) {
|
||||
score++;
|
||||
if (chompSound) {
|
||||
chompSound.currentTime = 0;
|
||||
chompSound.volume = 0.4;
|
||||
chompSound.play();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Check if all dots are eaten
|
||||
if (dots.length === 0) {
|
||||
// Trigger password generator for new random map
|
||||
const generateBtn = document.getElementById("generate-btn");
|
||||
if (generateBtn) {
|
||||
generateBtn.click();
|
||||
}
|
||||
|
||||
// Auto-restart the game after a short delay
|
||||
setTimeout(() => {
|
||||
resetGame();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ctx.fillStyle = "white";
|
||||
dots.forEach(d => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(d.c * CELL_SIZE + CELL_SIZE / 2, d.r * CELL_SIZE + CELL_SIZE / 2, DOT_SIZE, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Global Functions =====
|
||||
window.resetGame = resetGame;
|
||||
window.exitGame = exitGame;
|
||||
|
||||
// Add scroll prevention function
|
||||
function preventScroll(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
Reference in New Issue
Block a user