mirror of
https://github.com/tiennm99/ai-coding-workflow-labs.git
synced 2026-09-17 18:21:33 +00:00
feat(outputs): add Pikachu Connect HTML page
This commit is contained in:
@@ -0,0 +1,667 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>⚡ Pikachu Connect</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #0d0d2b 0%, #1a1a4e 50%, #0d0d2b 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Stars background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.6) 0%, transparent 100%),
|
||||
radial-gradient(1px 1px at 80% 10%, rgba(255,255,255,0.5) 0%, transparent 100%),
|
||||
radial-gradient(1px 1px at 50% 80%, rgba(255,255,255,0.4) 0%, transparent 100%),
|
||||
radial-gradient(1px 1px at 10% 60%, rgba(255,255,255,0.5) 0%, transparent 100%),
|
||||
radial-gradient(1px 1px at 90% 70%, rgba(255,255,255,0.4) 0%, transparent 100%),
|
||||
radial-gradient(2px 2px at 40% 20%, rgba(255,215,0,0.4) 0%, transparent 100%),
|
||||
radial-gradient(2px 2px at 70% 50%, rgba(255,215,0,0.3) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
color: #FFD700;
|
||||
text-shadow: 0 0 20px rgba(255,215,0,0.8), 0 0 40px rgba(255,215,0,0.4);
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stat {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,215,0,0.3);
|
||||
padding: 6px 18px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.95rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.stat span {
|
||||
color: #FFD700;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.timer-warning { color: #ff4444 !important; animation: pulse 0.5s infinite alternate; }
|
||||
@keyframes pulse { from { opacity: 1; } to { opacity: 0.5; } }
|
||||
|
||||
/* Game wrapper with canvas overlay */
|
||||
.game-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
canvas#pathCanvas {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 58px);
|
||||
grid-template-rows: repeat(8, 58px);
|
||||
gap: 4px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,215,0,0.25);
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.6), inset 0 0 30px rgba(0,0,100,0.3);
|
||||
}
|
||||
|
||||
.cell {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
border-radius: 10px;
|
||||
transition: transform 0.12s, background 0.12s, box-shadow 0.12s;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cell.filled {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cell.filled:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: scale(1.06);
|
||||
border-color: rgba(255,255,255,0.35);
|
||||
}
|
||||
|
||||
.cell.selected {
|
||||
background: rgba(255,215,0,0.25) !important;
|
||||
border: 2px solid #FFD700 !important;
|
||||
transform: scale(1.1) !important;
|
||||
box-shadow: 0 0 18px rgba(255,215,0,0.7), 0 0 6px rgba(255,215,0,0.4);
|
||||
}
|
||||
|
||||
.cell.empty {
|
||||
cursor: default;
|
||||
background: rgba(0,0,0,0.15);
|
||||
border: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.cell.matched {
|
||||
animation: matched-anim 0.45s ease forwards;
|
||||
}
|
||||
@keyframes matched-anim {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
40% { transform: scale(1.3) rotate(15deg); opacity: 0.8; }
|
||||
100% { transform: scale(0) rotate(30deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.cell.wrong {
|
||||
animation: wrong-anim 0.35s ease;
|
||||
}
|
||||
@keyframes wrong-anim {
|
||||
0%,100% { transform: translateX(0); background: rgba(255,255,255,0.1); }
|
||||
20% { transform: translateX(-6px); background: rgba(255,60,60,0.3); }
|
||||
60% { transform: translateX(6px); background: rgba(255,60,60,0.3); }
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 9px 26px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.btn-primary:hover { transform: scale(1.06); box-shadow: 0 4px 18px rgba(255,165,0,0.5); }
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
}
|
||||
.btn-secondary:hover { background: rgba(255,255,255,0.18); transform: scale(1.04); }
|
||||
|
||||
/* Overlay */
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.75);
|
||||
backdrop-filter: blur(6px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
z-index: 100;
|
||||
}
|
||||
.overlay.show { display: flex; }
|
||||
|
||||
.overlay-box {
|
||||
background: rgba(20,20,50,0.95);
|
||||
border: 1px solid rgba(255,215,0,0.4);
|
||||
border-radius: 20px;
|
||||
padding: 40px 60px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 60px rgba(255,215,0,0.15);
|
||||
}
|
||||
|
||||
.overlay-box h2 { font-size: 3rem; color: #FFD700; margin-bottom: 10px; }
|
||||
.overlay-box p { font-size: 1.2rem; color: rgba(255,255,255,0.8); margin-bottom: 20px; }
|
||||
|
||||
/* Hint flash */
|
||||
.cell.hint {
|
||||
animation: hint-anim 0.6s ease 3;
|
||||
}
|
||||
@keyframes hint-anim {
|
||||
0%,100% { box-shadow: none; }
|
||||
50% { box-shadow: 0 0 20px rgba(0,200,255,0.9); background: rgba(0,200,255,0.2); border-color: rgba(0,200,255,0.8) !important; }
|
||||
}
|
||||
|
||||
/* Combo popup */
|
||||
.combo-popup {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
color: #FFD700;
|
||||
text-shadow: 0 0 10px rgba(255,215,0,0.8);
|
||||
animation: combo-fly 0.9s ease forwards;
|
||||
z-index: 200;
|
||||
}
|
||||
@keyframes combo-fly {
|
||||
0% { transform: translateY(0) scale(1); opacity: 1; }
|
||||
100% { transform: translateY(-70px) scale(1.4); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>⚡ Pikachu Connect ⚡</h1>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">Score: <span id="score">0</span></div>
|
||||
<div class="stat">Time: <span id="timer">180</span>s</div>
|
||||
<div class="stat">Pairs: <span id="pairs">0</span> / 32</div>
|
||||
<div class="stat">Combo: <span id="combo">x1</span></div>
|
||||
</div>
|
||||
|
||||
<div class="game-wrapper">
|
||||
<canvas id="pathCanvas"></canvas>
|
||||
<div class="grid" id="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="btn btn-primary" onclick="newGame()">New Game</button>
|
||||
<button class="btn btn-secondary" onclick="showHint()">Hint 💡</button>
|
||||
</div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div class="overlay" id="overlay">
|
||||
<div class="overlay-box">
|
||||
<h2 id="overlayTitle">🎉</h2>
|
||||
<p id="overlayMsg"></p>
|
||||
<button class="btn btn-primary" onclick="newGame()">Play Again</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ROWS = 8, COLS = 8;
|
||||
const TOTAL_PAIRS = 32;
|
||||
|
||||
const POKEMON = [
|
||||
'⚡','🔥','💧','🌿','🎵','🔮','😴','👻',
|
||||
'🌊','🍃','❄️','🌈','💫','🌙','⭐','🎯'
|
||||
];
|
||||
|
||||
// Colors per pokemon type for path drawing
|
||||
const TYPE_COLORS = [
|
||||
'#FFD700','#FF6B35','#4FC3F7','#81C784','#CE93D8','#B39DDB','#A5D6A7','#9575CD',
|
||||
'#29B6F6','#66BB6A','#80DEEA','#F48FB1','#FFF176','#7986CB','#FFB74D','#EF9A9A'
|
||||
];
|
||||
|
||||
let grid = [];
|
||||
let selected = null;
|
||||
let score = 0;
|
||||
let pairsFound = 0;
|
||||
let timerInterval = null;
|
||||
let timeLeft = 180;
|
||||
let combo = 1;
|
||||
let lastMatchTime = 0;
|
||||
let hintsLeft = 3;
|
||||
|
||||
// ── Grid helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function shuffle(arr) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function newGame() {
|
||||
document.getElementById('overlay').classList.remove('show');
|
||||
clearInterval(timerInterval);
|
||||
|
||||
timeLeft = 180;
|
||||
score = 0;
|
||||
pairsFound = 0;
|
||||
combo = 1;
|
||||
hintsLeft = 3;
|
||||
selected = null;
|
||||
lastMatchTime = 0;
|
||||
|
||||
updateStats();
|
||||
clearCanvas();
|
||||
|
||||
// 16 types × 4 tiles each = 64 tiles, giving 32 pairs
|
||||
let tiles = [];
|
||||
for (let i = 0; i < 16; i++) {
|
||||
for (let j = 0; j < 4; j++) tiles.push(i + 1); // 1-indexed; 0 = empty
|
||||
}
|
||||
shuffle(tiles);
|
||||
|
||||
grid = [];
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
grid[r] = [];
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
grid[r][c] = tiles[r * COLS + c];
|
||||
}
|
||||
}
|
||||
|
||||
renderGrid();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
timerInterval = setInterval(() => {
|
||||
timeLeft--;
|
||||
const el = document.getElementById('timer');
|
||||
el.textContent = timeLeft;
|
||||
if (timeLeft <= 30) el.classList.add('timer-warning');
|
||||
else el.classList.remove('timer-warning');
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(timerInterval);
|
||||
endGame(false);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('timer').textContent = timeLeft;
|
||||
document.getElementById('pairs').textContent = pairsFound;
|
||||
document.getElementById('combo').textContent = 'x' + combo;
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const gridEl = document.getElementById('grid');
|
||||
gridEl.innerHTML = '';
|
||||
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const cell = document.createElement('div');
|
||||
const val = grid[r][c];
|
||||
cell.className = 'cell ' + (val ? 'filled' : 'empty');
|
||||
cell.dataset.r = r;
|
||||
cell.dataset.c = c;
|
||||
if (val) {
|
||||
cell.textContent = POKEMON[val - 1];
|
||||
cell.addEventListener('click', onCellClick);
|
||||
}
|
||||
gridEl.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync canvas size
|
||||
const gridDOM = gridEl.getBoundingClientRect();
|
||||
const canvas = document.getElementById('pathCanvas');
|
||||
canvas.width = gridEl.offsetWidth;
|
||||
canvas.height = gridEl.offsetHeight;
|
||||
canvas.style.width = gridEl.offsetWidth + 'px';
|
||||
canvas.style.height = gridEl.offsetHeight + 'px';
|
||||
}
|
||||
|
||||
function getCell(r, c) {
|
||||
return document.querySelector(`.cell[data-r="${r}"][data-c="${c}"]`);
|
||||
}
|
||||
|
||||
// ── Click handler ────────────────────────────────────────────────────────────
|
||||
|
||||
function onCellClick(e) {
|
||||
const r = +e.currentTarget.dataset.r;
|
||||
const c = +e.currentTarget.dataset.c;
|
||||
if (!grid[r][c]) return;
|
||||
|
||||
// Deselect same cell
|
||||
if (selected && selected.r === r && selected.c === c) {
|
||||
e.currentTarget.classList.remove('selected');
|
||||
selected = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
selected = { r, c };
|
||||
e.currentTarget.classList.add('selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const { r: r1, c: c1 } = selected;
|
||||
const r2 = r, c2 = c;
|
||||
getCell(r1, c1)?.classList.remove('selected');
|
||||
|
||||
if (grid[r1][c1] !== grid[r2][c2]) {
|
||||
flashWrong(r1, c1, r2, c2);
|
||||
selected = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const path = findPath(r1, c1, r2, c2);
|
||||
if (path) {
|
||||
handleMatch(r1, c1, r2, c2, path);
|
||||
} else {
|
||||
flashWrong(r1, c1, r2, c2);
|
||||
selected = null;
|
||||
}
|
||||
}
|
||||
|
||||
function flashWrong(r1, c1, r2, c2) {
|
||||
combo = 1;
|
||||
updateStats();
|
||||
[getCell(r1, c1), getCell(r2, c2)].forEach(el => {
|
||||
if (!el) return;
|
||||
el.classList.add('wrong');
|
||||
setTimeout(() => el?.classList.remove('wrong'), 400);
|
||||
});
|
||||
}
|
||||
|
||||
function handleMatch(r1, c1, r2, c2, path) {
|
||||
const now = Date.now();
|
||||
if (now - lastMatchTime < 2500) combo = Math.min(combo + 1, 8);
|
||||
else combo = 1;
|
||||
lastMatchTime = now;
|
||||
|
||||
const typeIdx = grid[r1][c1] - 1;
|
||||
const points = 10 * combo;
|
||||
|
||||
// Draw path briefly
|
||||
drawPath(path, TYPE_COLORS[typeIdx]);
|
||||
|
||||
// Animate cells
|
||||
getCell(r1, c1)?.classList.add('matched');
|
||||
getCell(r2, c2)?.classList.add('matched');
|
||||
|
||||
showComboPopup(r2, c2, points, combo);
|
||||
|
||||
setTimeout(() => {
|
||||
clearCanvas();
|
||||
grid[r1][c1] = 0;
|
||||
grid[r2][c2] = 0;
|
||||
renderGrid();
|
||||
|
||||
score += points;
|
||||
pairsFound++;
|
||||
updateStats();
|
||||
|
||||
if (pairsFound === TOTAL_PAIRS) {
|
||||
clearInterval(timerInterval);
|
||||
endGame(true);
|
||||
}
|
||||
}, 420);
|
||||
|
||||
selected = null;
|
||||
}
|
||||
|
||||
// ── Path drawing ─────────────────────────────────────────────────────────────
|
||||
|
||||
const CELL_SIZE = 58;
|
||||
const CELL_GAP = 4;
|
||||
const GRID_PAD = 12;
|
||||
|
||||
function cellCenter(r, c) {
|
||||
const x = GRID_PAD + c * (CELL_SIZE + CELL_GAP) + CELL_SIZE / 2;
|
||||
const y = GRID_PAD + r * (CELL_SIZE + CELL_GAP) + CELL_SIZE / 2;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function drawPath(path, color) {
|
||||
const canvas = document.getElementById('pathCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 3.5;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.globalAlpha = 0.85;
|
||||
|
||||
ctx.beginPath();
|
||||
const start = cellCenter(path[0][0], path[0][1]);
|
||||
ctx.moveTo(start.x, start.y);
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const pt = cellCenter(path[i][0], path[i][1]);
|
||||
ctx.lineTo(pt.x, pt.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Dots at endpoints
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.shadowBlur = 15;
|
||||
[path[0], path[path.length - 1]].forEach(([r, c]) => {
|
||||
const { x, y } = cellCenter(r, c);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 7, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function clearCanvas() {
|
||||
const canvas = document.getElementById('pathCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// ── Combo popup ──────────────────────────────────────────────────────────────
|
||||
|
||||
function showComboPopup(r, c, points, combo) {
|
||||
const { x, y } = cellCenter(r, c);
|
||||
const gridEl = document.getElementById('grid');
|
||||
const rect = gridEl.getBoundingClientRect();
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'combo-popup';
|
||||
el.textContent = combo > 1 ? `+${points} ×${combo}!` : `+${points}`;
|
||||
el.style.left = (rect.left + x - 20) + 'px';
|
||||
el.style.top = (rect.top + y - 20) + 'px';
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => el.remove(), 950);
|
||||
}
|
||||
|
||||
// ── Connection algorithm ─────────────────────────────────────────────────────
|
||||
|
||||
// Returns true if cell (r,c) can be passed through,
|
||||
// treating the two selected endpoints as transparent.
|
||||
function passable(r, c, er1, ec1, er2, ec2) {
|
||||
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return true; // outside grid = open
|
||||
if ((r === er1 && c === ec1) || (r === er2 && c === ec2)) return true;
|
||||
return grid[r][c] === 0;
|
||||
}
|
||||
|
||||
// Returns true if a straight horizontal/vertical line between the two points
|
||||
// is blocked (any cell along the way is occupied).
|
||||
function lineBlocked(r1, c1, r2, c2, er1, ec1, er2, ec2) {
|
||||
if (r1 !== r2 && c1 !== c2) return true; // not axis-aligned
|
||||
if (r1 === r2) {
|
||||
const a = Math.min(c1, c2), b = Math.max(c1, c2);
|
||||
for (let c = a; c <= b; c++) if (!passable(r1, c, er1, ec1, er2, ec2)) return true;
|
||||
} else {
|
||||
const a = Math.min(r1, r2), b = Math.max(r1, r2);
|
||||
for (let r = a; r <= b; r++) if (!passable(r, c1, er1, ec1, er2, ec2)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function findPath(r1, c1, r2, c2) {
|
||||
const er1 = r1, ec1 = c1, er2 = r2, ec2 = c2;
|
||||
|
||||
// 0 turns ─ direct straight line
|
||||
if (!lineBlocked(r1, c1, r2, c2, er1, ec1, er2, ec2))
|
||||
return [[r1,c1],[r2,c2]];
|
||||
|
||||
// 1 turn ─ via corner (r1,c2)
|
||||
if (passable(r1, c2, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r1, c1, r1, c2, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r1, c2, r2, c2, er1, ec1, er2, ec2))
|
||||
return [[r1,c1],[r1,c2],[r2,c2]];
|
||||
|
||||
// 1 turn ─ via corner (r2,c1)
|
||||
if (passable(r2, c1, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r1, c1, r2, c1, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r2, c1, r2, c2, er1, ec1, er2, ec2))
|
||||
return [[r1,c1],[r2,c1],[r2,c2]];
|
||||
|
||||
// 2 turns ─ scan intermediate columns (including virtual -1 and COLS)
|
||||
for (let c = -1; c <= COLS; c++) {
|
||||
if (c === c1 || c === c2) continue;
|
||||
if (passable(r1, c, er1, ec1, er2, ec2) && passable(r2, c, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r1, c1, r1, c, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r1, c, r2, c, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r2, c, r2, c2, er1, ec1, er2, ec2))
|
||||
return [[r1,c1],[r1,c],[r2,c],[r2,c2]];
|
||||
}
|
||||
|
||||
// 2 turns ─ scan intermediate rows (including virtual -1 and ROWS)
|
||||
for (let r = -1; r <= ROWS; r++) {
|
||||
if (r === r1 || r === r2) continue;
|
||||
if (passable(r, c1, er1, ec1, er2, ec2) && passable(r, c2, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r1, c1, r, c1, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r, c1, r, c2, er1, ec1, er2, ec2)
|
||||
&& !lineBlocked(r, c2, r2, c2, er1, ec1, er2, ec2))
|
||||
return [[r1,c1],[r,c1],[r,c2],[r2,c2]];
|
||||
}
|
||||
|
||||
return null; // no valid path
|
||||
}
|
||||
|
||||
// ── Hint system ──────────────────────────────────────────────────────────────
|
||||
|
||||
function showHint() {
|
||||
if (hintsLeft <= 0) { showToast('No hints left!'); return; }
|
||||
|
||||
for (let r1 = 0; r1 < ROWS; r1++) {
|
||||
for (let c1 = 0; c1 < COLS; c1++) {
|
||||
if (!grid[r1][c1]) continue;
|
||||
for (let r2 = 0; r2 < ROWS; r2++) {
|
||||
for (let c2 = 0; c2 < COLS; c2++) {
|
||||
if (r1 === r2 && c1 === c2) continue;
|
||||
if (!grid[r2][c2]) continue;
|
||||
if (grid[r1][c1] !== grid[r2][c2]) continue;
|
||||
if (findPath(r1, c1, r2, c2)) {
|
||||
hintsLeft--;
|
||||
[getCell(r1,c1), getCell(r2,c2)].forEach(el => {
|
||||
if (!el) return;
|
||||
el.classList.add('hint');
|
||||
setTimeout(() => el?.classList.remove('hint'), 2200);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showToast('No moves available — try reshuffling!');
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'combo-popup';
|
||||
el.textContent = msg;
|
||||
el.style.left = '50%';
|
||||
el.style.top = '50%';
|
||||
el.style.transform = 'translateX(-50%)';
|
||||
el.style.fontSize = '1rem';
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => el.remove(), 1100);
|
||||
}
|
||||
|
||||
// ── End game ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function endGame(win) {
|
||||
const title = win ? '🎉 You Win!' : '⏰ Time Up!';
|
||||
const msg = win
|
||||
? `Amazing! Score: ${score} | Time left: ${timeLeft}s`
|
||||
: `Score: ${score} | Pairs matched: ${pairsFound} / ${TOTAL_PAIRS}`;
|
||||
document.getElementById('overlayTitle').textContent = title;
|
||||
document.getElementById('overlayMsg').textContent = msg;
|
||||
document.getElementById('overlay').classList.add('show');
|
||||
}
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────────
|
||||
newGame();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user