Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cascading Grid Mechanic</title>
<style>
body {
background-color: #1a1a1a;
color: #ffffff;
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
canvas {
border: 4px solid #ffd700;
background-color: #111;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
}
.controls {
margin-top: 15px;
text-align: center;
}
button {
background: linear-gradient(#ffd700, #ffae00);
border: none;
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
color: #111;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
button:hover { background: #ffd700; }
.score-board {
font-size: 20px;
margin-bottom: 10px;
font-weight: bold;
color: #ffd700;
}
</style>
</head>
<body>
<div class="score-board">Score: <span id="score">0</span> | Combo: x<span id="multiplier">1</span></div>
<canvas id="gameCanvas" width="400" height="500"></canvas>
<div class="controls">
<button id="actionBtn">Check Matches / Cascade</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const multEl = document.getElementById('multiplier');
const actionBtn = document.getElementById('actionBtn');
// Grid Configuration (Mobile portrait aspect ratio)
const ROWS = 5;
const COLS = 4;
const TILE_SIZE = 100;
// Symbol pool with visual definitions
const SYMBOLS = {
1: { name: 'A', color: '#ff4d4d', textColor: '#fff' }, // Red
2: { name: 'B', color: '#4da6ff', textColor: '#fff' }, // Blue
3: { name: 'C', color: '#5cd65c', textColor: '#111' }, // Green
4: { name: 'D', color: '#e6b800', textColor: '#111' } // Yellow
};
let grid = [];
let score = 0;
let multiplier = 1;
let isAnimating = false;
// Initialize grid with random symbols
function initGrid() {
grid = [];
for (let r = 0; r < ROWS; r++) {
grid[r] = [];
for (let c = 0; c < COLS; c++) {
grid[r][c] = getRandomSymbol();
}
}
}
function getRandomSymbol() {
const keys = Object.keys(SYMBOLS);
return parseInt(keys[Math.floor(Math.random() * keys.length)]);
}
// Render the grid to the Canvas
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const symbolId = grid[r][c];
const x = c * TILE_SIZE;
const y = r * TILE_SIZE;
if (symbolId !== 0) { // 0 means empty space
const sym = SYMBOLS[symbolId];
// Draw tile background
ctx.fillStyle = sym.color;
ctx.fillRect(x + 4, y + 4, TILE_SIZE - 8, TILE_SIZE - 8);
// Draw symbol character text
ctx.fillStyle = sym.textColor;
ctx.font = 'bold 32px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(sym.name, x + TILE_SIZE / 2, y + TILE_SIZE / 2);
} else {
// Empty block background style
ctx.fillStyle = '#222';
ctx.fillRect(x + 4, y + 4, TILE_SIZE - 8, TILE_SIZE - 8);
}
}
}
}
// Phase 1: Scan grid for 3-in-a-row matches horizontally
function scanMatches() {
let matched = [];
// Initialize blank tracking array mirroring layout
for (let r = 0; r < ROWS; r++) {
matched[r] = new Array(COLS).fill(false);
}
let hasMatches = false;
// Check Horizontal matches
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS - 2; c++) {
let id1 = grid[r][c];
let id2 = grid[r][c+1];
let id3 = grid[r][c+2];
if (id1 !== 0 && id1 === id2 && id1 === id3) {
matched[r][c] = true;
matched[r][c+1] = true;
matched[r][c+2] = true;
hasMatches = true;
}
}
}
// Apply match deletions (set to 0) and reward calculation
if (hasMatches) {
let matchCount = 0;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (matched[r][c]) {
grid[r][c] = 0; // Cleared status
matchCount++;
}
}
}
score += matchCount * 10 * multiplier;
multiplier += 1; // Step up combo mechanics multiplier
scoreEl.innerText = score;
multEl.innerText = multiplier;
}
return hasMatches;
}
// Phase 2: Shift columns down (Gravity processing) and drop new elements down
function applyGravity() {
for (let c = 0; c < COLS; c++) {
// Collect all surviving blocks in this column from bottom to top
let survivalArray = [];
for (let r = ROWS - 1; r >= 0; r--) {
if (grid[r][c] !== 0) {
survivalArray.push(grid[r][c]);
}
}
// Fill up remaining missing spaces with fresh random tiles
while (survivalArray.length < ROWS) {
survivalArray.push(getRandomSymbol());
}
// Write the packed items back into the column structure
let index = 0;
for (let r = ROWS - 1; r >= 0; r--) {
grid[r][c] = survivalArray[index];
index++;
}
}
}
// Logic sequencer processing loop
function processTurn() {
if (isAnimating) return;
isAnimating = true;
actionBtn.disabled = true;
// 1. Scan for matching layouts
const foundMatches = scanMatches();
drawGrid();
if (foundMatches) {
actionBtn.innerText = "Cascading...";
// Pause briefly so user can visualize matching elements vanishing
setTimeout(() => {
// 2. Drop pieces down via gravity simulation logic
applyGravity();
drawGrid();
isAnimating = false;
actionBtn.disabled = false;
actionBtn.innerText = "Check Next Cascade";
}, 600);
} else {
// Reset state mechanics if sequence breaks without wins
multiplier = 1;
multEl.innerText = multiplier;
isAnimating = false;
actionBtn.disabled = false;
actionBtn.innerText = "Spin Again / Check";
initGrid(); // Refresh layout to continue testing demo loop seamlessly
setTimeout(drawGrid, 200);
}
}
// Initialization execute trigger
actionBtn.addEventListener('click', processTurn);
initGrid();
drawGrid();
</script>
</body>
</html>
1
1
8KB
8KB
73.0ms
132.0ms
73.0ms