Files
ai-coding-workflow-labs/cowork-complete-guide/pikachu-connect.html
T

668 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>