intial commit

This commit is contained in:
Brian Fertig 2026-02-13 17:58:18 -07:00
commit 36828b276e
3 changed files with 237 additions and 0 deletions

23
index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SVG Match Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>SVG Memory Match</h1>
<div class="stats">
<span id="moves">Moves: 0</span>
<span id="timer">Time: 0s</span>
</div>
</header>
<main>
<section id="board" class="board"></section>
</main>
<script src="script.js"></script>
</body>
</html>

168
script.js Normal file
View File

@ -0,0 +1,168 @@
// Configuration
const BOARD_SIZE = 4; // 4x4 grid → 16 cards (8 pairs)
const FLIP_DELAY = 800; // ms before nonmatching cards flip back
// SVG icons eight distinct graphics (you can replace these with your own)
const SVG_ICONS = [
`<svg viewBox="0 0 64 64" fill="#ff9800"><polygon points="32,4 39,24 60,24 42,38 48,58 32,46 16,58 22,38 4,24 25,24"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#4caf50"><circle cx="32" cy="32" r="28"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#2196F3"><rect x="12" y="12" width="40" height="40"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#e91e63"><path d="M32 4 L39 24 H60 L42 38 L48 58 L32 46 L16 58 L22 38 L4 24 H25z"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#9c27b0"><ellipse cx="32" cy="32" rx="28" ry="14"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#ff5722"><line x1="8" y1="8" x2="56" y2="56" stroke-width="8"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#03a9f4"><polygon points="32,8 40,24 58,24 44,36 50,54 32,44 14,54 20,36 6,24 24,24"/></svg>`,
`<svg viewBox="0 0 64 64" fill="#8bc34a"><polygon points="32,4 39,24 60,24 42,38 48,58 32,46 16,58 22,38 4,24 25,24"/></svg>`
];
// Game state variables
let firstCard = null; // The previously flipped card
let lockBoard = false; // Prevent interaction while cards are animating
let moves = 0; // Number of attempts (pairs flipped)
let matched = 0; // Number of cards that have been matched
let timer = null; // Interval ID for the game timer
let seconds = 0; // Elapsed seconds
// Cached DOM references
const board = document.getElementById('board');
const movesEl = document.getElementById('moves');
const timerEl = document.getElementById('timer');
/* -------------------------------------------------------------------------- */
/* Utility functions */
/* -------------------------------------------------------------------------- */
function shuffle(array) {
// FisherYates shuffle inplace
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function updateMoves() {
movesEl.textContent = `Moves: ${moves}`;
}
function updateTimer() {
timerEl.textContent = `Time: ${seconds}s`;
}
function startTimer() {
if (timer) return; // already running
timer = setInterval(() => {
seconds++;
updateTimer();
}, 1000);
}
function stopTimer() {
clearInterval(timer);
timer = null;
}
/* -------------------------------------------------------------------------- */
/* Card creation */
/* -------------------------------------------------------------------------- */
function createCard(svgMarkup, id) {
const card = document.createElement('div');
card.className = 'card';
card.dataset.id = id; // same id for the two matching cards
const inner = document.createElement('div');
inner.className = 'card-inner';
const front = document.createElement('div');
front.className = 'card-face card-front';
front.innerHTML = svgMarkup; // SVG goes here
const back = document.createElement('div');
back.className = 'card-face card-back';
// back can stay empty or contain a pattern
inner.appendChild(front);
inner.appendChild(back);
card.appendChild(inner);
card.addEventListener('click', onCardClick);
return card;
}
/* -------------------------------------------------------------------------- */
/* Click handling */
/* -------------------------------------------------------------------------- */
function onCardClick(event) {
if (lockBoard) return;
const card = event.currentTarget;
if (card.classList.contains('flipped')) return; // ignore already revealed cards
// Start timer on the very first flip
if (moves === 0 && seconds === 0) startTimer();
card.classList.add('flipped');
if (!firstCard) {
// This is the first card of the pair
firstCard = card;
return;
}
// Second card of the pair
moves++;
updateMoves();
const isMatch = firstCard.dataset.id === card.dataset.id;
if (isMatch) {
// Keep both cards flipped
matched += 2;
firstCard = null;
// Check for win condition
if (matched === BOARD_SIZE * BOARD_SIZE) {
stopTimer();
setTimeout(() => {
alert(`Congratulations! You completed the game in ${moves} moves and ${seconds} seconds.`);
}, 300);
}
} else {
// Not a match flip back after a short delay
lockBoard = true;
setTimeout(() => {
firstCard.classList.remove('flipped');
card.classList.remove('flipped');
firstCard = null;
lockBoard = false;
}, FLIP_DELAY);
}
}
/* -------------------------------------------------------------------------- */
/* Game initialization */
/* -------------------------------------------------------------------------- */
function initGame() {
// Reset UI and state variables
board.innerHTML = '';
firstCard = null;
lockBoard = false;
moves = 0;
matched = 0;
seconds = 0;
updateMoves();
updateTimer();
stopTimer();
// Build a deck: duplicate each SVG, assign an id, then shuffle
const deck = [];
SVG_ICONS.forEach((svg, idx) => {
deck.push({svg, id: idx});
deck.push({svg, id: idx}); // second copy for the pair
});
shuffle(deck);
// Create card elements and append them to the board
deck.forEach(item => {
const cardEl = createCard(item.svg, item.id);
board.appendChild(cardEl);
});
}
// Kickoff the game when the script loads
initGame();

46
style.css Normal file
View File

@ -0,0 +1,46 @@
/* Basic reset */
* { box-sizing: border-box; margin:0; padding:0; }
body { font-family: Arial, sans-serif; background:#fafafa; display:flex; flex-direction:column; align-items:center; }
/* Header */
header { text-align:center; margin:20px; }
.stats { margin-top:8px; font-size:0.9rem; }
/* Board */
.board {
display: grid;
gap: 12px;
max-width: 800px;
width: 100%;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
/* Card */
.card {
perspective: 1000px;
cursor: pointer;
}
.card-inner {
position: relative;
width: 100%;
padding-top: 100%; /* square */
transform-style: preserve-3d;
transition: transform 0.6s;
}
.card.flipped .card-inner { transform: rotateY(180deg); }
.card-face {
position: absolute;
inset: 0;
backface-visibility: hidden;
border-radius: 8px;
display:flex; align-items:center; justify-content:center;
}
.card-back {
background:#2196F3;
}
.card-front {
background:#fff;
transform: rotateY(180deg);
border: 2px solid #ddd;
}