slavic_game_jam/src/maze.js

434 lines
15 KiB
JavaScript
Raw Normal View History

export class Maze {
constructor(cols, rows) {
this.cols = cols;
this.rows = rows;
this.grid = [];
this.dangerousSquares = new Set();
this.exitX = cols - 2;
this.exitY = rows - 2;
// Cell types
this.WALL = 0;
this.SAFE = 1;
this.DANGEROUS_VISUAL = 2; // Visually dangerous, no audio
this.DANGEROUS_AUDIO = 3; // Looks safe, has audio when near
2025-08-02 13:01:45 +02:00
this.DANGEROUS_HIDDEN = 4; // Invisible, only detectable with Shift+direction
this.EXIT = 5;
}
generate() {
// Initialize grid with safe squares
this.grid = Array(this.rows).fill().map(() => Array(this.cols).fill(this.SAFE));
this.dangerousSquares.clear();
// Create outer boundary walls only
this.createOuterWalls();
// Set exit (ensure it's not on the outer wall)
this.exitX = this.cols - 2;
this.exitY = this.rows - 2;
this.grid[this.exitY][this.exitX] = this.EXIT;
2025-08-01 17:24:25 +02:00
// Add dangerous squares while ensuring a path exists
this.addDangerousSquaresWithPathGuarantee();
}
createOuterWalls() {
// Create walls only on the outer perimeter
for (let y = 0; y < this.rows; y++) {
for (let x = 0; x < this.cols; x++) {
if (x === 0 || x === this.cols - 1 || y === 0 || y === this.rows - 1) {
this.grid[y][x] = this.WALL;
}
}
}
}
addDangerousSquares() {
const innerSafeSquares = [];
// Find all inner safe squares (excluding outer wall boundary and player start)
for (let y = 1; y < this.rows - 1; y++) {
for (let x = 1; x < this.cols - 1; x++) {
if (this.grid[y][x] === this.SAFE && !(x === 1 && y === 1) && !(x === this.exitX && y === this.exitY)) {
innerSafeSquares.push({ x, y });
}
}
}
// Make 30% of inner safe squares dangerous (15% visual, 15% audio)
const totalDangerousCount = Math.floor(innerSafeSquares.length * 0.3);
const visualDangerousCount = Math.floor(totalDangerousCount * 0.5);
const audioDangerousCount = totalDangerousCount - visualDangerousCount;
this.shuffle(innerSafeSquares);
// Add visual-only dangerous squares
for (let i = 0; i < visualDangerousCount; i++) {
const { x, y } = innerSafeSquares[i];
this.grid[y][x] = this.DANGEROUS_VISUAL;
this.dangerousSquares.add(`${x},${y}`);
}
// Add audio-only dangerous squares
for (let i = visualDangerousCount; i < visualDangerousCount + audioDangerousCount; i++) {
const { x, y } = innerSafeSquares[i];
this.grid[y][x] = this.DANGEROUS_AUDIO;
this.dangerousSquares.add(`${x},${y}`);
}
}
2025-08-01 17:24:25 +02:00
addDangerousSquaresWithPathGuarantee() {
const innerSafeSquares = [];
// Find all inner safe squares (excluding outer wall boundary, player start, and exit)
for (let y = 1; y < this.rows - 1; y++) {
for (let x = 1; x < this.cols - 1; x++) {
if (this.grid[y][x] === this.SAFE && !(x === 1 && y === 1) && !(x === this.exitX && y === this.exitY)) {
innerSafeSquares.push({ x, y });
}
}
}
2025-08-02 13:01:45 +02:00
// Calculate target number of dangerous squares (40% of inner safe squares)
const totalDangerousCount = Math.floor(innerSafeSquares.length * 0.4);
const visualDangerousCount = Math.floor(totalDangerousCount * 0.33);
const audioDangerousCount = Math.floor(totalDangerousCount * 0.33);
const hiddenDangerousCount = totalDangerousCount - visualDangerousCount - audioDangerousCount;
2025-08-01 17:24:25 +02:00
this.shuffle(innerSafeSquares);
let addedDangerous = 0;
let addedVisual = 0;
let addedAudio = 0;
2025-08-02 13:01:45 +02:00
let addedHidden = 0;
2025-08-01 17:24:25 +02:00
// Add dangerous squares one by one, checking path after each addition
for (let i = 0; i < innerSafeSquares.length && addedDangerous < totalDangerousCount; i++) {
const { x, y } = innerSafeSquares[i];
// Determine what type of dangerous square to add
let dangerType;
if (addedVisual < visualDangerousCount) {
dangerType = this.DANGEROUS_VISUAL;
} else if (addedAudio < audioDangerousCount) {
dangerType = this.DANGEROUS_AUDIO;
2025-08-02 13:01:45 +02:00
} else if (addedHidden < hiddenDangerousCount) {
dangerType = this.DANGEROUS_HIDDEN;
2025-08-01 17:24:25 +02:00
} else {
break; // We've added enough dangerous squares
}
// Temporarily place the dangerous square
const originalType = this.grid[y][x];
this.grid[y][x] = dangerType;
// Check if a path still exists from start to exit
if (this.hasPathToExit(1, 1)) {
// Path exists, keep this dangerous square
this.dangerousSquares.add(`${x},${y}`);
addedDangerous++;
if (dangerType === this.DANGEROUS_VISUAL) {
addedVisual++;
2025-08-02 13:01:45 +02:00
} else if (dangerType === this.DANGEROUS_AUDIO) {
2025-08-01 17:24:25 +02:00
addedAudio++;
2025-08-02 13:01:45 +02:00
} else {
addedHidden++;
2025-08-01 17:24:25 +02:00
}
} else {
// Path blocked, revert this square to safe
this.grid[y][x] = originalType;
}
}
}
// Breadth-first search to check if a safe path exists from start to exit
hasPathToExit(startX, startY) {
const visited = new Set();
const queue = [{ x: startX, y: startY }];
const targetKey = `${this.exitX},${this.exitY}`;
while (queue.length > 0) {
const { x, y } = queue.shift();
const key = `${x},${y}`;
if (key === targetKey) {
return true; // Found path to exit
}
if (visited.has(key)) {
continue;
}
visited.add(key);
// Check all four directions
const directions = [
{ dx: 0, dy: -1 }, // up
{ dx: 0, dy: 1 }, // down
{ dx: -1, dy: 0 }, // left
{ dx: 1, dy: 0 } // right
];
for (const { dx, dy } of directions) {
const newX = x + dx;
const newY = y + dy;
const newKey = `${newX},${newY}`;
// Check if the new position is valid and safe to walk
if (newX >= 0 && newX < this.cols &&
newY >= 0 && newY < this.rows &&
!visited.has(newKey) &&
this.isSafeToWalk(newX, newY)) {
queue.push({ x: newX, y: newY });
}
}
}
return false; // No safe path found
}
// Check if a position is safe to walk (only safe squares and exit)
isSafeToWalk(x, y) {
const cellType = this.grid[y][x];
return cellType === this.SAFE || cellType === this.EXIT;
}
// Check if a position is walkable (not a wall - for game movement)
isWalkable(x, y) {
const cellType = this.grid[y][x];
// Player can walk on all squares except walls
// Dangerous squares end the game but are still walkable for movement
return cellType !== this.WALL;
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
canMoveTo(x, y) {
if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) {
return false;
}
2025-08-01 17:24:25 +02:00
return this.isWalkable(x, y);
}
isDangerous(x, y) {
return this.grid[y][x] === this.DANGEROUS_VISUAL || this.grid[y][x] === this.DANGEROUS_AUDIO;
}
isDangerousVisual(x, y) {
return this.grid[y][x] === this.DANGEROUS_VISUAL;
}
isDangerousAudio(x, y) {
return this.grid[y][x] === this.DANGEROUS_AUDIO;
}
2025-08-02 13:01:45 +02:00
isDangerousHidden(x, y) {
return this.grid[y][x] === this.DANGEROUS_HIDDEN;
}
isNearAudioDanger(playerX, playerY, range = 1) {
// Check if player is within range of any audio danger squares
for (let dy = -range; dy <= range; dy++) {
for (let dx = -range; dx <= range; dx++) {
const checkX = playerX + dx;
const checkY = playerY + dy;
if (checkX >= 0 && checkX < this.cols && checkY >= 0 && checkY < this.rows) {
if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) {
return true;
}
}
}
}
return false;
}
getAudioDangerDirections(playerX, playerY, range = 1) {
2025-08-01 17:24:25 +02:00
// Get all directions where audio danger squares are located (cardinal directions only)
const directions = {
left: false,
right: false,
up: false,
down: false,
squares: []
};
2025-08-01 17:24:25 +02:00
// Only check cardinal directions (no corners)
const cardinalDirections = [
{ dx: -1, dy: 0, dir: 'left' }, // left
{ dx: 1, dy: 0, dir: 'right' }, // right
{ dx: 0, dy: -1, dir: 'up' }, // up
{ dx: 0, dy: 1, dir: 'down' } // down
];
for (const { dx, dy, dir } of cardinalDirections) {
for (let dist = 1; dist <= range; dist++) {
const checkX = playerX + (dx * dist);
const checkY = playerY + (dy * dist);
if (checkX >= 0 && checkX < this.cols && checkY >= 0 && checkY < this.rows) {
if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) {
2025-08-01 17:24:25 +02:00
directions.squares.push({ x: checkX, y: checkY, dx: dx * dist, dy: dy * dist });
directions[dir] = true;
}
}
}
}
return directions;
}
isExit(x, y) {
return this.grid[y][x] === this.EXIT;
}
render(ctx, cellSize, debugMode = false) {
for (let y = 0; y < this.rows; y++) {
for (let x = 0; x < this.cols; x++) {
const pixelX = x * cellSize;
const pixelY = y * cellSize;
switch (this.grid[y][x]) {
case this.WALL:
ctx.fillStyle = '#333333';
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add some texture to walls
ctx.fillStyle = '#444444';
ctx.fillRect(pixelX + 2, pixelY + 2, cellSize - 4, cellSize - 4);
break;
case this.SAFE:
// Safe squares - white
ctx.fillStyle = '#ffffff';
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add subtle grid lines
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
break;
2025-08-01 17:24:25 +02:00
case this.DANGEROUS_AUDIO:
if (debugMode) {
// In debug mode, show audio danger squares with a blue overlay
ctx.fillStyle = '#ffffff';
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add blue debug overlay with pulsing effect
const time = Date.now() * 0.004;
const pulse = (Math.sin(time + x + y) + 1) * 0.5;
const alpha = 0.3 + pulse * 0.4;
ctx.fillStyle = `rgba(0, 100, 255, ${alpha})`;
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add debug border
ctx.strokeStyle = '#0066ff';
ctx.lineWidth = 2;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
// Add audio symbol
ctx.fillStyle = '#ffffff';
ctx.font = `${cellSize * 0.4}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🔊', pixelX + cellSize / 2, pixelY + cellSize / 2);
} else {
// In normal mode, audio-danger squares look identical to safe squares (white)
ctx.fillStyle = '#ffffff';
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
2025-08-02 13:01:45 +02:00
// Add subtle grid lines
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
}
break;
case this.DANGEROUS_HIDDEN:
if (debugMode) {
// In debug mode, show hidden danger squares with a purple overlay
ctx.fillStyle = '#ffffff';
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add purple debug overlay with pulsing effect
const hiddenTime = Date.now() * 0.004;
const hiddenPulse = (Math.sin(hiddenTime + x + y) + 1) * 0.5;
const hiddenAlpha = 0.3 + hiddenPulse * 0.4;
ctx.fillStyle = `rgba(128, 0, 128, ${hiddenAlpha})`;
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add debug border
ctx.strokeStyle = '#800080';
ctx.lineWidth = 2;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
// Add hidden symbol
ctx.fillStyle = '#ffffff';
ctx.font = `${cellSize * 0.4}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('👁️', pixelX + cellSize / 2, pixelY + cellSize / 2);
} else {
// In normal mode, hidden danger squares look identical to safe squares
ctx.fillStyle = '#ffffff';
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add subtle grid lines
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
}
break;
case this.DANGEROUS_VISUAL:
// Visual-only danger squares - animated red appearance
const time = Date.now() * 0.003;
const intensity = (Math.sin(time + x + y) + 1) * 0.5;
const red = Math.floor(150 + intensity * 105);
ctx.fillStyle = `rgb(${red}, 50, 50)`;
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add warning pattern
ctx.fillStyle = `rgba(255, 255, 255, ${0.3 + intensity * 0.3})`;
ctx.fillRect(pixelX + 5, pixelY + 5, cellSize - 10, cellSize - 10);
// Add border
ctx.strokeStyle = '#ff6666';
ctx.lineWidth = 2;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
break;
case this.EXIT:
// Animated exit square
const exitTime = Date.now() * 0.005;
const exitGlow = (Math.sin(exitTime) + 1) * 0.5;
const green = Math.floor(100 + exitGlow * 155);
ctx.fillStyle = `rgb(50, ${green}, 50)`;
ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
// Add exit symbol
ctx.fillStyle = '#ffffff';
ctx.font = `${cellSize * 0.6}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🏁', pixelX + cellSize / 2, pixelY + cellSize / 2);
break;
}
}
}
}
2025-08-02 12:32:31 +02:00
cloneMaze() {
// Create a deep copy of the current maze state for replay
return {
grid: this.grid.map(row => [...row]),
dangerousSquares: new Set(this.dangerousSquares),
exitX: this.exitX,
exitY: this.exitY,
cols: this.cols,
rows: this.rows
};
}
}