Files
2026-04-30 02:14:25 +00:00

344 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Memory Match</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #667eea; touch-action: none; font-family: Arial, sans-serif; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
let musicEnabled = true;
let sfxEnabled = true;
let musicPlaying = false;
function initAudio() {
if (!audioCtx) audioCtx = new AudioContext();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
// Gentle puzzle melody
const melody = [
{n: 392, d: 0.3}, {n: 440, d: 0.3}, {n: 494, d: 0.3}, {n: 523, d: 0.6},
{n: 494, d: 0.3}, {n: 440, d: 0.3}, {n: 392, d: 0.6},
{n: 349, d: 0.3}, {n: 392, d: 0.3}, {n: 440, d: 0.3}, {n: 494, d: 0.6},
{n: 523, d: 0.3}, {n: 587, d: 0.3}, {n: 523, d: 0.6}, {n: 0, d: 0.3}
];
const bass = [
{n: 98, d: 0.6}, {n: 110, d: 0.6}, {n: 131, d: 0.6}, {n: 110, d: 0.6},
{n: 87, d: 0.6}, {n: 98, d: 0.6}, {n: 110, d: 0.6}, {n: 131, d: 0.6}
];
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
function playTone(freq, dur, type = 'square', vol = 0.1) {
if (!audioCtx || !musicEnabled || freq === 0) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.value = freq;
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + dur);
}
function playSfx(type) {
if (!audioCtx || !sfxEnabled) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
if (type === 'flip') {
osc.frequency.value = 600;
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
} else if (type === 'match') {
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.1);
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
} else if (type === 'wrong') {
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.setValueAtTime(200, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
} else if (type === 'win') {
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.15);
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.3);
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.45);
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
}
osc.type = 'square';
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.7);
}
function updateMusic() {
if (!audioCtx || !musicEnabled || !musicPlaying) return;
const now = audioCtx.currentTime;
if (now >= nextMelody) {
const note = melody[melodyIdx];
playTone(note.n, note.d, 'triangle', 0.08);
nextMelody = now + note.d;
melodyIdx = (melodyIdx + 1) % melody.length;
}
if (now >= nextBass) {
const note = bass[bassIdx];
playTone(note.n, note.d, 'triangle', 0.12);
nextBass = now + note.d;
bassIdx = (bassIdx + 1) % bass.length;
}
}
function startMusic() {
if (!musicPlaying && musicEnabled) {
initAudio();
musicPlaying = true;
nextMelody = audioCtx.currentTime;
nextBass = audioCtx.currentTime;
}
}
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W, H, safeTop, safeBottom;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
safeTop = H * 0.1;
safeBottom = H * 0.9;
}
resize();
window.addEventListener('resize', resize);
const symbols = ['🌟', '🎈', '🍎', '🚗', '🐱', '🌈'];
let cards = [];
let flipped = [];
let matched = [];
let moves = 0;
let canFlip = true;
let gameOver = false;
let gameRunning = false;
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 initCards() {
const pairs = shuffle([...symbols, ...symbols]);
const cols = 4, rows = 3;
const gameH = safeBottom - safeTop - 60;
const gameW = W - 40;
const cardW = Math.min((gameW - (cols - 1) * 10) / cols, 90);
const cardH = Math.min((gameH - (rows - 1) * 10) / rows, 110);
const totalW = cols * cardW + (cols - 1) * 10;
const totalH = rows * cardH + (rows - 1) * 10;
const startX = (W - totalW) / 2;
const startY = safeTop + 40 + (gameH - totalH) / 2;
cards = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
cards.push({
x: startX + c * (cardW + 10),
y: startY + r * (cardH + 10),
w: cardW,
h: cardH,
symbol: pairs[r * cols + c],
flipped: false,
matched: false
});
}
}
}
function init() {
initCards();
flipped = [];
matched = [];
moves = 0;
canFlip = true;
gameOver = false;
gameRunning = true;
startMusic();
}
function handleClick(x, y) {
if (gameOver) { init(); return; }
if (!canFlip) return;
for (let i = 0; i < cards.length; i++) {
const c = cards[i];
if (x >= c.x && x <= c.x + c.w && y >= c.y && y <= c.y + c.h) {
if (c.matched || c.flipped) continue;
c.flipped = true;
flipped.push(i);
playSfx('flip');
if (flipped.length === 2) {
moves++;
canFlip = false;
const [a, b] = flipped;
if (cards[a].symbol === cards[b].symbol) {
cards[a].matched = true;
cards[b].matched = true;
matched.push(a, b);
flipped = [];
canFlip = true;
playSfx('match');
if (matched.length === cards.length) {
gameOver = true;
gameRunning = false;
playSfx('win');
}
} else {
setTimeout(() => {
cards[a].flipped = false;
cards[b].flipped = false;
flipped = [];
canFlip = true;
playSfx('wrong');
}, 1200);
}
}
break;
}
}
}
document.addEventListener('keydown', e => {
initAudio();
if ((e.key === ' ' || e.key === 'Enter') && gameOver) init();
});
canvas.addEventListener('click', e => { initAudio(); handleClick(e.clientX, e.clientY); });
canvas.addEventListener('touchstart', e => { e.preventDefault(); initAudio(); handleClick(e.touches[0].clientX, e.touches[0].clientY); });
function update() {
updateMusic();
}
function draw() {
// Background gradient
const grad = ctx.createLinearGradient(0, 0, W, H);
grad.addColorStop(0, '#667eea');
grad.addColorStop(1, '#764ba2');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Safe zones
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(0, 0, W, safeTop);
ctx.fillRect(0, safeBottom, W, H - safeBottom);
// Header
ctx.fillStyle = '#fff';
ctx.font = 'bold 22px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Moves: ${moves}`, 20, 35);
ctx.textAlign = 'right';
ctx.fillText(`Pairs: ${matched.length / 2}/${cards.length / 2}`, W - 20, 35);
ctx.textAlign = 'center';
ctx.fillText('MEMORY MATCH', W / 2, 35);
// Cards
cards.forEach(c => {
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.roundRect(c.x + 3, c.y + 3, c.w, c.h, 10);
ctx.fill();
if (c.flipped || c.matched) {
// Face up
ctx.fillStyle = c.matched ? '#4ade80' : '#fff';
ctx.beginPath();
ctx.roundRect(c.x, c.y, c.w, c.h, 10);
ctx.fill();
ctx.font = `${c.w * 0.5}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.symbol, c.x + c.w / 2, c.y + c.h / 2);
} else {
// Face down
const cardGrad = ctx.createLinearGradient(c.x, c.y, c.x + c.w, c.y + c.h);
cardGrad.addColorStop(0, '#4facfe');
cardGrad.addColorStop(1, '#00f2fe');
ctx.fillStyle = cardGrad;
ctx.beginPath();
ctx.roundRect(c.x, c.y, c.w, c.h, 10);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = `${c.w * 0.4}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('?', c.x + c.w / 2, c.y + c.h / 2);
}
});
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff';
ctx.font = 'bold 40px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🎉 YOU WIN! 🎉', W / 2, H / 2 - 50);
ctx.font = '24px Arial';
ctx.fillText(`Completed in ${moves} moves`, W / 2, H / 2);
ctx.fillStyle = '#4ade80';
ctx.beginPath();
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
ctx.fill();
ctx.fillStyle = '#000';
ctx.font = 'bold 18px Arial';
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 55);
}
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
window.gameScore = 0;
Object.defineProperty(window, 'gameScore', { get: () => Math.max(0, 1000 - moves * 10) });
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
window.setSfxEnabled = (v) => { sfxEnabled = v; };
init();
gameLoop();
</script>
</body>
</html>