console
document.addEventListener('DOMContentLoaded', () => {
const gridElement = document.getElementById('game-grid');
const victoryRibbon = document.getElementById('victory-ribbon');
const GRID_SIZE = 10;
const CANDY_TYPES = ['��', '��', '��', '��', '��', '��', '��', '��'];
const MIN_MATCH_LENGTH = 3;
let grid = []; // 2D array representing the game board [row][col]
let isAnimating = false; // Prevent clicks during animation/processing
// --- Initialization ---
function initGame() {
gridElement.innerHTML = ''; // Clear previous grid
grid = [];
isAnimating = false;
victoryRibbon.classList.add('hidden');
victoryRibbon.classList.remove('visible');
for (let r = 0; r < GRID_SIZE; r++) {
grid[r] = [];
for (let c = 0; c < GRID_SIZE; c++) {
createCandyCell(r, c);
}
}
// Ensure no initial matches (optional but good practice)
// removeInitialMatches(); // Can be complex, skipping for simplicity here
updateGridDOM(); // Initial draw
}
function getRandomCandy() {
return CANDY_TYPES[Math.floor(Math.random() * CANDY_TYPES.length)];
}
function createCandyCell(r, c) {
const cell = document.createElement('div');
cell.classList.add('candy');
cell.dataset.row = r;
cell.dataset.col = c;
grid[r][c] = getRandomCandy(); // Assign candy type to logical grid
cell.addEventListener('click', handleCandyClick);
gridElement.appendChild(cell);
}
// --- Core Gameplay Logic ---
function handleCandyClick(event) {
if (isAnimating) return; // Don't process clicks during animation
const row = parseInt(event.target.dataset.row);
const col = parseInt(event.target.dataset.col);
const candyType = grid[row][col];
if (!candyType) return; // Clicked on an empty space (shouldn't happen often)
const connectedCandies = findConnectedCandies(row, col, candyType);
if (connectedCandies.length >= MIN_MATCH_LENGTH) {
isAnimating = true; // Start animation lock
eliminateCandies(connectedCandies);
// Use setTimeout to allow elimination animation to show
setTimeout(() => {
applyGravity();
refillGrid();
updateGridDOM(); // Update DOM after falling and refilling
// Check for victory *after* everything has settled
if (checkVictory()) {
triggerVictory();
} else {
isAnimating = false; // Release lock if not victory
}
// Note: Cascading matches are not implemented here for simplicity.
}, 300); // Delay matching the elimination animation duration
}
}
function findConnectedCandies(startRow, startCol, candyType) {
const connected = [];
const queue = [{ r: startRow, c: startCol }];
const visited = new Set([`${startRow}-${startCol}`]); // Track visited cells
while (queue.length > 0) {
const { r, c } = queue.shift();
connected.push({ r, c });
// Check neighbors (Up, Down, Left, Right)
const neighbors = [
{ r: r - 1, c: c }, { r: r + 1, c: c },
{ r: r, c: c - 1 }, { r: r, c: c + 1 }
];
for (const neighbor of neighbors) {
const { r: nr, c: nc } = neighbor;
const key = `${nr}-${nc}`;
// Check bounds, if same type, and not visited
if (nr >= 0 && nr < GRID_SIZE && nc >= 0 && nc < GRID_SIZE &&
!visited.has(key) && grid[nr][nc] === candyType) {
visited.add(key);
queue.push({ r: nr, c: nc });
}
}
}
return connected;
}
function eliminateCandies(candiesToEliminate) {
candiesToEliminate.forEach(({ r, c }) => {
const cellElement = getCellElement(r, c);
if (cellElement) {
cellElement.classList.add('eliminating'); // Add class for visual effect
}
grid[r][c] = null; // Mark as empty in the logical grid
});
}
// --- Gravity and Refill ---
function applyGravity() {
for (let c = 0; c < GRID_SIZE; c++) {
let emptyRow = GRID_SIZE - 1; // Start checking from bottom
for (let r = GRID_SIZE - 1; r >= 0; r--) {
if (grid[r][c] !== null) {
// If this cell has a candy, move it down to the lowest empty spot
if (r !== emptyRow) {
grid[emptyRow][c] = grid[r][c];
grid[r][c] = null; // Clear the original spot
}
emptyRow--; // Move the target empty spot pointer up
}
}
}
}
function refillGrid() {
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
if (grid[r][c] === null) {
grid[r][c] = getRandomCandy();
}
}
}
}
// --- DOM Updates ---
function updateGridDOM() {
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
const cellElement = getCellElement(r, c);
if (cellElement) {
const candyType = grid[r][c];
if (candyType) {
cellElement.textContent = candyType;
cellElement.classList.remove('eliminating'); // Ensure class is removed
cellElement.style.opacity = 1; // Make sure it's visible
} else {
// Should ideally not happen after refill, but handles empty state visually
cellElement.textContent = '';
cellElement.style.opacity = 0; // Make empty cells invisible
}
}
}
}
}
function getCellElement(r, c) {
// Selector finds the element based on data attributes
return gridElement.querySelector(`[data-row="${r}"][data-col="${c}"]`);
}
// --- Victory Condition ---
function checkVictory() {
const lastRow = GRID_SIZE - 1;
for (let c = 0; c < GRID_SIZE; c++) {
if (grid[lastRow][c] !== null) {
return false; // Found a candy in the last row, not victory yet
}
}
return true; // All cells in the last row are null (empty)
}
function triggerVictory() {
console.log("Victory!");
isAnimating = true; // Keep interaction locked
victoryRibbon.classList.remove('hidden');
// Force reflow to ensure transition plays
void victoryRibbon.offsetWidth;
victoryRibbon.classList.add('visible');
setTimeout(() => {
victoryRibbon.classList.remove('visible');
// Optionally hide it again after animation + duration
setTimeout(() => {
victoryRibbon.classList.add('hidden');
// Maybe restart game or show a "Play Again" button here
// For now, just keep the lock
// isAnimating = false; // Or keep it locked until manual reset
}, 500); // Corresponds to the transition duration in CSS
}, 3000); // Display victory message for 3 seconds
}
// --- Start Game ---
initGame();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Candy Elimination Fun</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Simple Candy Elimination Fun</h1>
<div id="game-container">
<div id="game-grid">
<!-- Candy cells will be generated by JavaScript -->
</div>
<div id="victory-ribbon" class="hidden">
�� Victory! ��
</div>
</div>
<div id="instructions">
Click on 3 or more connected candies of the same type to eliminate them! Clear the bottom row to win!
</div>
<script src="script.js"></script>
</body>
</html>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #ffdde1, #ee9ca7); /* Cute pink gradient */
font-family: 'Arial Rounded MT Bold', 'Helvetica Rounded', Arial, sans-serif;
overflow: hidden; /* Hide scrollbars if content slightly overflows */
}
h1 {
color: #d13a5a;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
margin-bottom: 15px;
}
#game-container {
position: relative; /* Needed for absolute positioning of victory ribbon */
border: 5px solid #fff;
border-radius: 15px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.7); /* Semi-transparent white */
padding: 10px;
}
#game-grid {
display: grid;
grid-template-columns: repeat(10, 40px); /* 10 columns */
grid-template-rows: repeat(10, 40px); /* 10 rows */
gap: 3px; /* Small gap between candies */
width: 430px; /* (10 * 40px) + (9 * 3px) */
height: 430px; /* (10 * 40px) + (9 * 3px) */
}
.candy {
display: flex;
justify-content: center;
align-items: center;
font-size: 28px; /* Make emojis larger */
border-radius: 8px; /* Slightly rounded corners */
background-color: rgba(255, 255, 255, 0.5); /* Slight background for cells */
cursor: pointer;
transition: transform 0.1s ease-out, background-color 0.2s;
user-select: none; /* Prevent text selection */
box-shadow: inset 0 0 5px rgba(0,0,0,0.05);
}
.candy:hover {
transform: scale(1.1);
background-color: rgba(255, 255, 255, 0.8);
}
/* Style for eliminated candies (briefly) */
.eliminating {
transform: scale(0);
transition: transform 0.3s ease-in;
background-color: transparent !important; /* Override hover */
}
#instructions {
margin-top: 20px;
color: #555;
font-size: 0.9em;
text-align: center;
}
#victory-ribbon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8); /* Start slightly smaller */
background: linear-gradient(45deg, #ffd700, #ffa500); /* Gold gradient */
color: white;
padding: 25px 50px;
border-radius: 10px;
font-size: 2.5em;
font-weight: bold;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
pointer-events: none; /* Don't interfere with clicks */
z-index: 10;
}
#victory-ribbon.visible {
opacity: 1;
transform: translate(-50%, -50%) scale(1); /* Scale to full size */
}
.hidden {
display: none; /* Use display: none initially */
}
/* Override hidden for animation start */
#victory-ribbon:not(.hidden) {
display: block; /* Make it block to allow transitions */
}