<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>ManyArcade</title>
<style>
:root {
--primary: #4f46e5;
--primary-dark: #4338ca;
--background: #f9fafb;
--foreground: #1f2937;
--gray-light: #e5e7eb;
--gray-med: #9ca3af;
--gray-dark: #4b5563;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--radius: 1.1rem;
--box-shadow: 0 4px 16px rgba(0,0,0,0.10);
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--btn-padding: .85rem 2.2rem;
--transition: all .19s;
--square-width: 240px;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: var(--font);}
body {
background: var(--background);
color: var(--foreground);
min-height: 100vh;
min-height: 100svh;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.fade {
transition: opacity 0.4s;
opacity: 1;
}
.fade.fade-out {
opacity: 0;
pointer-events: none;
}
.title-container {
text-align: center;
margin-bottom: 2.7rem;
animation: fadein 0.8s;
}
@keyframes fadein {
from { opacity: 0; transform: translateY(-26px);}
to { opacity: 1; transform: none;}
}
.main-menu-square {
margin: 0 auto;
width: var(--square-width);
height: var(--square-width);
background: white;
border-radius: 1.2rem;
box-shadow: var(--box-shadow);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 2.1rem;
font-weight: 700;
color: var(--primary);
cursor: pointer;
user-select: none;
border: 3.5px solid var(--gray-light);
transition: box-shadow 0.23s, border-color 0.19s;
}
.main-menu-square:active, .main-menu-square:focus {
border-color: var(--primary);
box-shadow: 0 6px 24px rgba(79,70,229,0.15);
}
.main-menu-label {
font-size: 1.1rem;
font-weight: 400;
color: var(--gray-med);
margin-top: 0.7rem;
letter-spacing: 0.05em;
}
.back-btn {
background: var(--gray-light);
color: var(--foreground);
border: none;
border-radius: var(--radius);
padding: var(--btn-padding);
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
margin-bottom: 2.2rem;
margin-left: 1.2rem;
margin-top: 0.6rem;
transition: var(--transition);
}
.back-btn:hover {
background: var(--primary);
color: #fff;
}
.dots-game-container {
max-width: 920px;
margin: 0 auto;
margin-top: 2rem;
background: white;
border-radius: var(--radius);
box-shadow: var(--box-shadow);
padding: 2.2rem 2.5rem 2.5rem 2.5rem;
min-height: 540px;
animation: fadein 0.7s;
}
@media (max-width: 600px) {
.main-menu-square, .dots-game-container {
width: 98vw !important;
min-width: unset !important;
max-width: 98vw !important;
min-height: 270px;
padding: 1.2rem;
}
.dots-game-container { padding: 0.7rem; }
}
#loading-overlay {
position: fixed;
z-index: 10001;
inset: 0;
width: 100vw;
height: 100vh;
min-height: 100svh;
background: rgba(249,250,251,0.94);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
opacity: 0;
pointer-events: none;
transition: opacity 0.35s;
}
#loading-overlay.active {
opacity: 1;
pointer-events: auto;
transition: opacity 0.23s;
}
.loading-spinner {
width: 66px;
height: 66px;
border-radius: 50%;
border: 0.17em dotted var(--primary);
border-top: 0.19em solid var(--primary-dark);
border-bottom: 0.19em solid var(--gray-light);
background: transparent;
animation: spinner-spin 1.1s linear infinite;
margin-bottom: 1.2rem;
box-shadow: 0 2px 12px rgba(79,70,229,0.07);
display: block;
}
@keyframes spinner-spin { 100% { transform: rotate(360deg); } }
.loading-text {
font-size: 1.18rem;
color: var(--primary-dark);
font-weight: 600;
letter-spacing: 0.03em;
text-align: center;
text-shadow: 0 1px 0 #fff;
margin-bottom: 0.2rem;
}
/* ManyDots Game Styles */
.game-container {
background: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,.1);
padding: 2rem;
width: 100%;
max-width: 900px;
margin: 2rem auto;
min-height: 530px;
}
.dark-mode .game-container { background: #111827; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;}
h1 { color: var(--primary);}
h2, h3 { color: var(--primary); margin-bottom:.7rem;}
button { cursor:pointer; border:none; border-radius:var(--radius); font-weight:500; transition:var(--transition);}
button:focus { outline:2px solid var(--primary); outline-offset:2px;}
.btn-primary { background: var(--primary); color:#fff; padding:var(--btn-padding);}
.btn-primary:hover { background: var(--primary-dark);}
.btn-secondary { background: var(--gray-light); color: var(--foreground); padding:var(--btn-padding);}
.btn-secondary:hover { background: var(--gray-med);}
.tabs { display: flex; border-bottom:1px solid var(--gray-light); margin-bottom:1.5rem; overflow-x: auto; gap: 0.1rem; }
.tabs::-webkit-scrollbar { height: 6px; background: var(--gray-light);}
.tabs::-webkit-scrollbar-thumb { background: var(--primary);}
.tab { flex:0 0 auto; padding:.75rem 1.5rem; border-bottom:2px solid transparent; background: none; cursor:pointer; white-space:nowrap;}
.tab.active { border-bottom-color:var(--primary); color:var(--primary);}
.score-display { display:flex; justify-content:space-between; gap:1rem; margin-bottom:2rem;}
.score-box { background:var(--gray-light); border-radius:var(--radius); padding:1rem; flex:1; text-align:center;}
.score-value { font-size:2rem; font-weight:bold; color:var(--primary);}
.message { text-align:center; font-size:1.2rem; margin:1rem 0; min-height:2rem;}
.controls { display:flex; justify-content:center; gap:1rem; margin-top:2rem;}
.game-buttons-container { position:relative; width:300px; height:300px; margin:0 auto;}
.game-button { position:absolute; width:80px; height:80px; border-radius:50%; box-shadow:0 4px 8px rgba(0,0,0,0.2);}
.game-button.active { filter:brightness(0.8); box-shadow:0 0 15px rgba(255,255,255,0.7);}
.red { background:#ef4444; top:0; left:50%; transform:translateX(-50%);}
.green { background:#10b981; right:0; top:50%; transform:translateY(-50%);}
.blue { background:#3b82f6; bottom:0; left:50%; transform:translateX(-50%);}
.yellow { background:#f59e0b; left:0; top:50%; transform:translateY(-50%);}
.pattern-dots { display:flex; justify-content:center; gap:.5rem; margin:1.5rem 0;}
.pattern-dot { width:12px; height:12px; border-radius:50%; background:var(--gray-light);}
.pattern-dot.active { background:var(--primary);}
.stats-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:1rem; margin-bottom:2rem;}
.stat-card { background:var(--gray-light); border-radius:var(--radius); padding:1.5rem; text-align:center;}
.stat-value { font-size:2.5rem; font-weight:bold; color:var(--primary);}
.stat-label { color:var(--gray-dark);}
.chart-container { margin-top:2rem; background: white; border-radius: var(--radius); padding:1.5rem;}
.dark-mode .chart-container { background: #111827;}
.chart { height:200px; display:flex; align-items:flex-end; gap:.5rem; padding-bottom:1.5rem; position:relative;}
.chart::after { content:''; position:absolute; left:0; right:0; bottom:0; height:1px; background-color:var(--gray-light);}
.chart-bar { flex:1; background:var(--primary); border:2px solid var(--primary-dark); border-radius:4px 4px 0 0; min-width:20px; transition:height .5s;}
.chart-labels { display:flex; justify-content:space-around; margin-top:.5rem; color:var(--gray-dark); font-size:.75rem;}
#badges-container { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:1rem; margin-top:1.5rem;}
.badge { background:white; border-radius:var(--radius); padding:1rem; text-align:center; box-shadow:0 1px 3px rgba(0,0,0,0.1);}
.dark-mode .badge { background: #111827;}
.badge-icon { width:60px; height:60px; border-radius:50%; background:var(--primary); color:#fff; display:flex; justify-content:center; align-items:center; margin:0 auto 1rem; font-size:1.5rem;}
.locked .badge-icon { background:var(--gray-med); opacity:0.5;}
.badge-name { font-weight:600; margin-bottom:.5rem;}
.badge-desc { font-size:.75rem; color:var(--gray-dark);}
.locked .badge-name, .locked .badge-desc { opacity:0.6;}
.code-content { background: var(--gray-light); border-radius: var(--radius); padding:1.5rem; font-size: 0.96rem; font-family: "Fira Mono", "Consolas", "monospace"; overflow-x: auto; margin: 1.5rem 0;}
.dark-mode .code-content { background: #23272e; color: #f9fafb; }
.save-load-row { display:flex; gap:0.5rem; margin:1rem 0; flex-wrap:wrap; align-items: center; }
.save-load-input { font-size:1rem; padding:0.5rem 0.75rem; border-radius:var(--radius); border:1px solid var(--gray-med); min-width: 220px; }
.save-load-modal { position:fixed; left:0; top:0; width:100vw; height:100vh; background:rgba(31,41,55,0.7); display:flex; align-items:center; justify-content:center; z-index:1000; }
.modal-box { background:white; color:var(--foreground); padding:2rem 2rem 1.5rem 2rem; border-radius:var(--radius); box-shadow:0 8px 32px rgba(0,0,0,0.22); min-width:320px; max-width:95vw;}
.dark-mode .modal-box { background:#23272e; color: #f9fafb; }
.modal-actions { margin-top:1rem; display:flex; gap:0.5rem; justify-content:flex-end; }
.settings-form { display: flex; flex-direction: column; gap: 1.1rem; max-width: 420px; margin: 0 auto 1.5rem auto; background: var(--gray-light); border-radius: var(--radius); padding: 1.2rem 2rem;}
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem;}
.settings-label { flex: 1 1 60%; font-weight: 500; font-size: 1rem; color: var(--gray-dark);}
.settings-control { flex: 1 1 40%; display: flex; gap: 0.5rem; justify-content: flex-end; align-items: center;}
.settings-group-title { font-size: 1.04rem; font-weight: 600; color: var(--primary); margin: 0.7rem 0 0.3rem 0;}
.settings-divider { height: 1px; background: var(--gray-med); border: none; margin: 1rem 0; opacity: 0.25;}
@media (max-width: 768px) {
.game-container { padding:1rem; margin:1rem auto;}
.header { flex-direction:column; gap:1rem; text-align:center;}
.header h1 { font-size:1.5rem;}
.game-buttons-container { width:240px; height:240px;}
.game-button { width:56px; height:56px;}
.controls { flex-direction:column;}
.stats-grid { grid-template-columns:1fr 1fr;}
.score-display { flex-direction:column;}
.tabs { gap: 0.1rem; }
.code-content { font-size:0.8rem; }
.settings-form { padding:1rem; }
}
@media (min-width: 1024px) {
.game-container { max-width:1000px;}
.game-buttons-container { width:340px; height:340px;}
.game-button { width:88px; height:88px;}
}
</style>
</head>
<body>
<div id="arcade-root">
<!-- Title Screen -->
<div id="title-screen" class="fade">
<div class="title-container">
<h1 style="font-size:3.2rem; color:var(--primary); letter-spacing:0.03em; margin-bottom:0.7rem;">ManyArcade</h1>
<div class="main-menu-label">A mini collection by manywebby</div>
</div>
<div
class="main-menu-square"
id="manydots-btn"
tabindex="0"
aria-label="Play ManyDots"
style="width:var(--square-width);height:var(--square-width);"
>
ManyDots
</div>
</div>
<!-- Game screen: always present, just hidden/shown -->
<div id="game-screen" class="fade" style="display:none;">
<button class="back-btn" id="back-to-menu">← Back</button>
<div class="dots-game-container">
<div id="app" class="app-container desktop-mode">
<div id="game-outer-container">
<div id="game-container" class="game-container">
<div class="header">
<h1>Pattern Memory Game</h1>
<h2>Memory Pattern Game</h2>
</div>
<div class="tabs" tabindex="0" aria-label="Main navigation tabs">
<div class="tab active" data-tab="play">Play</div>
<div class="tab" data-tab="howto">How to Play</div>
<div class="tab" data-tab="stats">Stats</div>
<div class="tab" data-tab="badges">Badges</div>
<div class="tab" data-tab="time">Time</div>
<div class="tab" data-tab="code">Code</div>
<div class="tab" data-tab="settings">Settings</div>
</div>
<div id="howto-content" class="tab-content" style="display: none;">
<h2>How to Play</h2>
<ol style="margin-bottom: 1.5rem; line-height: 1.7;">
<li>Press <strong>Start Game</strong> to begin.</li>
<li>Watch the color pattern that lights up.</li>
<li>Repeat the pattern by clicking the colored buttons, or use arrow keys (or <b>WASD</b>).</li>
<li>Each round, the pattern grows by one color. Try to remember as long as you can!</li>
<li>Score points for each level. Earn badges for reaching milestones.</li>
<li>View your stats, unlock achievements, and try toggling game settings!</li>
</ol>
<div>
<b>Tips:</b>
<ul>
<li>Use <u>arrow keys/WASD</u> for faster play.</li>
<li>Switch between <b>Mobile/ Desktop Mode</b> for best experience.</li>
<li>Adjust difficulty, dark mode, sound, and more in Settings.</li>
</ul>
</div>
</div>
<div id="play-content" class="tab-content">
<div class="score-display">
<div class="score-box"><div class="text-sm text-gray-dark mb-1">Current Score</div><div id="current-score" class="score-value">0</div></div>
<div class="score-box"><div class="text-sm text-gray-dark mb-1">High Score</div><div id="high-score" class="score-value">0</div></div>
<div class="score-box"><div class="text-sm text-gray-dark mb-1">Level</div><div id="level" class="score-value">1</div></div>
</div>
<div id="message" class="message" aria-live="polite">Press Start to play</div>
<div id="pattern-dots" class="pattern-dots"></div>
<div class="game-buttons-container">
<button id="red-btn" class="game-button red" disabled aria-label="Red (Up or W)"></button>
<button id="green-btn" class="game-button green" disabled aria-label="Green (Right or D)"></button>
<button id="blue-btn" class="game-button blue" disabled aria-label="Blue (Down or S)"></button>
<button id="yellow-btn" class="game-button yellow" disabled aria-label="Yellow (Left or A)"></button>
</div>
<div class="controls">
<button id="start-btn" class="btn-primary">Start Game</button>
<button id="reset-btn" class="btn-secondary">Reset</button>
</div>
</div>
<div id="stats-content" class="tab-content" style="display: none;">
<h2>Your Statistics</h2>
<div class="stats-grid">
<div class="stat-card"><div id="games-played" class="stat-value">0</div><div class="stat-label">Games Played</div></div>
<div class="stat-card"><div id="highest-score" class="stat-value">0</div><div class="stat-label">Highest Score</div></div>
<div class="stat-card"><div id="max-level" class="stat-value">0</div><div class="stat-label">Max Level</div></div>
<div class="stat-card"><div id="total-patterns" class="stat-value">0</div><div class="stat-label">Total Patterns</div></div>
</div>
<div class="chart-container">
<h3>Score History</h3>
<div id="score-chart" class="chart"></div>
<div id="chart-labels" class="chart-labels"></div>
</div>
</div>
<div id="badges-content" class="tab-content" style="display: none;">
<h2>Your Achievements</h2>
<p class="text-gray-dark mb-6">
Unlock badges by completing challenges and improving your memory skills!
</p>
<div id="badges-container"></div>
</div>
<div id="time-content" class="tab-content" style="display: none;">
<h2>Current Time & Date</h2>
<div style="text-align:center; margin:2rem 0;">
<div id="clock" style="font-size:2.5rem; font-weight:bold; margin-bottom: 1rem;">--:--:--</div>
<div id="date" style="font-size:1.25rem; color: var(--gray-dark);">--/--/----</div>
</div>
</div>
<div id="code-content" class="tab-content" style="display: none;">
<h2>Source Code</h2>
<div class="code-content">
<pre style="margin-bottom: 0.7rem;"><code id="source-code-block"></code></pre>
<button id="copy-code-btn" class="btn-secondary" style="float:right;">Copy to Clipboard</button>
</div>
</div>
<div id="settings-content" class="tab-content" style="display: none;">
<h2>Game Settings</h2>
<form class="settings-form" autocomplete="off" onsubmit="return false;">
<div class="settings-group-title">Appearance</div>
<div class="settings-row">
<div class="settings-label">Dark Mode</div>
<div class="settings-control">
<button type="button" id="settings-darkmode" class="btn-secondary" aria-pressed="false">OFF</button>
</div>
</div>
<div class="settings-row">
<div class="settings-label">Layout</div>
<div class="settings-control">
<button type="button" id="settings-mobilemode" class="btn-secondary" aria-pressed="false">Mobile</button>
<button type="button" id="settings-desktopmode" class="btn-primary" aria-pressed="true">Desktop</button>
</div>
</div>
<hr class="settings-divider" />
<div class="settings-group-title">Audio</div>
<div class="settings-row">
<div class="settings-label">Sound Effects</div>
<div class="settings-control">
<button type="button" id="settings-sound" class="btn-primary" aria-pressed="true">ON</button>
</div>
</div>
<div class="settings-row">
<div class="settings-label">Background Music</div>
<div class="settings-control">
<button type="button" id="settings-music" class="btn-secondary" aria-pressed="false">OFF</button>
</div>
</div>
<hr class="settings-divider" />
<div class="settings-group-title">Game</div>
<div class="settings-row">
<div class="settings-label">Difficulty</div>
<div class="settings-control">
<select id="settings-difficulty" class="save-load-input">
<option value="easy">Easy</option>
<option value="medium" selected>Medium</option>
<option value="hard">Hard</option>
</select>
</div>
</div>
</form>
<div class="save-load-row">
<button id="save-data-btn" class="btn-secondary" title="Save progress to text">Save Code</button>
<button id="load-data-btn" class="btn-secondary" title="Load progress from code">Load Data</button>
<span style="font-size:0.95rem; color:var(--gray-dark);">Use these to transfer or backup your progress.</span>
</div>
</div>
</div>
</div>
<div id="save-load-modal" class="save-load-modal" style="display:none;">
<div class="modal-box">
<div id="modal-content"></div>
<div class="modal-actions" id="modal-actions"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="loading-overlay">
<span class="loading-spinner"></span>
<span class="loading-text">Loading...</span>
</div>
<script>
// (Paste the full JS from the previous 'main.js' message here, unchanged)
// All logic for ManyArcade and ManyDots goes here.
// Sorry, the JS is too long for this box. If you want the _entire_ code ready to copy (including every line of JS), let me know and I will generate it in a downloadable Gist or split it over multiple messages.
// --- Screen switching logic ---
const titleScreen = document.getElementById('title-screen');
const gameScreen = document.getElementById('game-screen');
const loadingOverlay = document.getElementById('loading-overlay');
function showLoading() { loadingOverlay.classList.add('active'); }
function hideLoading() { loadingOverlay.classList.remove('active'); }
function showScreen(screen) {
showLoading();
setTimeout(() => {
if (screen === "title") {
titleScreen.style.display = '';
gameScreen.style.display = 'none';
hideLoading();
} else if (screen === "manydots") {
gameScreen.style.display = '';
titleScreen.style.display = 'none';
hideLoading();
}
}, 500);
}
document.getElementById('manydots-btn').addEventListener('click', () => showScreen("manydots"));
document.getElementById('manydots-btn').addEventListener('keydown', e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
showScreen("manydots");
}
});
document.getElementById('back-to-menu').addEventListener('click', () => showScreen("title"));
window.addEventListener('DOMContentLoaded', hideLoading);
// --- ManyDots Game Logic (original, no function wrappers) ---
// Utility for Base64 Unicode safe encode/decode with checksum
function base64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function base64DecodeUnicode(str) {
return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
function encodeSaveData(obj) {
const json = JSON.stringify(obj);
let base = base64EncodeUnicode(json);
let sum = 0;
for (let i = 0; i < base.length; i++) sum = (sum + base.charCodeAt(i)) % 9973;
return "PMEMV1." + base.replace(/=+$/, '') + "." + sum.toString(36).padStart(3,'0');
}
function decodeSaveData(str) {
if (!str.startsWith("PMEMV1.")) throw new Error("Invalid save data: wrong header");
let rest = str.slice(7);
let [base, sumStr] = rest.split(".");
if (!base || !sumStr) throw new Error("Invalid save data: missing parts");
while (base.length % 4 !== 0) base += "=";
let sum = 0;
for (let i = 0; i < base.length; i++) sum = (sum + base.charCodeAt(i)) % 9973;
if (sum.toString(36).padStart(3,'0') !== sumStr) throw new Error("Corrupt save data: checksum mismatch");
let json = base64DecodeUnicode(base);
return JSON.parse(json);
}
// Game state and settings
const COLORS = ["red", "green", "blue", "yellow"];
const DIFFICULTY_SETTINGS = {
easy: { patternDisplayTime: 800, patternPauseTime: 400 },
medium: { patternDisplayTime: 600, patternPauseTime: 300 },
hard: { patternDisplayTime: 400, patternPauseTime: 200 }
};
let audioContext = null, oscillator = null, backgroundMusic = null, musicGainNode = null;
const gameState = {
isPlaying: false, isShowingPattern: false,
currentScore: 0, highScore: 0, level: 1, message: "Press Start to play",
currentPattern: [], userPattern: [], patternIndex: -1, consecutiveCorrect: 0,
settings: { darkMode: false, soundEnabled: true, musicEnabled: false, difficulty: "medium", layout: "desktop" },
stats: { gamesPlayed: 0, highestScore: 0, maxLevel: 0, totalPatterns: 0, scoreHistory: [] },
badges: [
{ name: "First Win", description: "Complete your first game", icon: "🎯", unlocked: false, condition: s=>s.gamesPlayed>=1 },
{ name: "Memory Master", description: "Reach level 10", icon: "🧠", unlocked: false, condition: s=>s.maxLevel>=10 },
{ name: "Champion", description: "Score 20+ points", icon: "🏆", unlocked: false, condition: s=>s.highestScore>=20 },
{ name: "Pattern Expert", description: "Complete 50 total patterns", icon: "📊", unlocked: false, condition: s=>s.totalPatterns>=50 },
{ name: "Dedicated Player", description: "Play 10 or more games", icon: "🎮", unlocked: false, condition: s=>s.gamesPlayed>=10 }
]
};
function $(id) { return document.getElementById(id); }
function $q(sel) { return document.querySelector(sel); }
function $qa(sel) { return document.querySelectorAll(sel); }
const elements = {
app: $("app"),
tabs: $qa('.tab'),
howtoContent: $("howto-content"),
playContent: $("play-content"),
statsContent: $("stats-content"),
badgesContent: $("badges-content"),
timeContent: $("time-content"),
codeContent: $("code-content"),
settingsContent: $("settings-content"),
currentScore: $("current-score"),
highScore: $("high-score"),
level: $("level"),
message: $("message"),
patternDots: $("pattern-dots"),
redBtn: $("red-btn"),
greenBtn: $("green-btn"),
blueBtn: $("blue-btn"),
yellowBtn: $("yellow-btn"),
startBtn: $("start-btn"),
resetBtn: $("reset-btn"),
gamesPlayed: $("games-played"),
highestScore: $("highest-score"),
maxLevel: $("max-level"),
totalPatterns: $("total-patterns"),
scoreChart: $("score-chart"),
chartLabels: $("chart-labels"),
badgesContainer: $("badges-container"),
clock: $("clock"),
date: $("date"),
settingsDarkmode: $("settings-darkmode"),
settingsSound: $("settings-sound"),
settingsMusic: $("settings-music"),
settingsDifficulty: $("settings-difficulty"),
settingsMobilemode: $("settings-mobilemode"),
settingsDesktopmode: $("settings-desktopmode"),
saveDataBtn: $("save-data-btn"),
loadDataBtn: $("load-data-btn"),
saveLoadModal: $("save-load-modal"),
modalContent: $("modal-content"),
modalActions: $("modal-actions"),
sourceCodeBlock: $("source-code-block"),
copyCodeBtn: $("copy-code-btn")
};
function getSaveObj() {
return {
settings: gameState.settings,
stats: gameState.stats,
badges: gameState.badges.map(b => ({ ...b })),
highScore: gameState.highScore
};
}
function applySaveObj(saved) {
if (saved.settings) gameState.settings = { ...gameState.settings, ...saved.settings };
if (saved.stats) gameState.stats = { ...gameState.stats, ...saved.stats };
if (Array.isArray(saved.badges)) {
gameState.badges.forEach((b, i) => {
if (saved.badges[i]) b.unlocked = saved.badges[i].unlocked;
});
}
if (typeof saved.highScore === "number") gameState.highScore = saved.highScore;
updateGameDisplay();
updateStatsDisplay();
updateBadgesDisplay();
applySettings();
}
function saveGameState() {
try {
localStorage.setItem('pattern-memory-game', JSON.stringify(getSaveObj()));
} catch (e) {}
}
function loadGameState() {
try {
const data = localStorage.getItem('pattern-memory-game');
if (data) applySaveObj(JSON.parse(data));
} catch (e) {}
}
function updateStatsDisplay() {
elements.gamesPlayed.textContent = gameState.stats.gamesPlayed;
elements.highestScore.textContent = gameState.stats.highestScore;
elements.maxLevel.textContent = gameState.stats.maxLevel;
elements.totalPatterns.textContent = gameState.stats.totalPatterns;
elements.scoreChart.innerHTML = '';
elements.chartLabels.innerHTML = '';
if (gameState.stats.scoreHistory.length === 0) {
const emptyMessage = document.createElement('div');
emptyMessage.textContent = 'No score history yet. Play some games!';
emptyMessage.style.textAlign = 'center';
emptyMessage.style.width = '100%';
emptyMessage.style.padding = '2rem 0';
emptyMessage.style.color = 'var(--gray-dark)';
elements.scoreChart.appendChild(emptyMessage);
return;
}
const maxScore = Math.max(...gameState.stats.scoreHistory, 1);
gameState.stats.scoreHistory.forEach((score, index) => {
const bar = document.createElement('div');
bar.className = 'chart-bar';
bar.style.height = `${(score / maxScore) * 100}%`;
if (index === gameState.stats.scoreHistory.length - 1) {
bar.style.backgroundColor = "var(--success)";
}
elements.scoreChart.appendChild(bar);
const label = document.createElement('span');
label.textContent = `Game ${index + 1}`;
elements.chartLabels.appendChild(label);
});
saveGameState();
}
function updateBadgesDisplay() {
elements.badgesContainer.innerHTML = '';
gameState.badges.forEach((badge) => {
const badgeElement = document.createElement('div');
badgeElement.className = `badge ${badge.unlocked ? '' : 'locked'}`;
badgeElement.innerHTML = `
<div class="badge-icon">${badge.icon}</div>
<div class="badge-name">${badge.name}</div>
<div class="badge-desc">${badge.description}</div>
`;
elements.badgesContainer.appendChild(badgeElement);
});
saveGameState();
}
function updateGameDisplay() {
elements.currentScore.textContent = gameState.currentScore;
elements.highScore.textContent = gameState.highScore;
elements.level.textContent = gameState.level;
elements.message.textContent = gameState.message;
updatePatternDots();
const buttonsDisabled = gameState.isShowingPattern || !gameState.isPlaying;
elements.redBtn.disabled = buttonsDisabled;
elements.greenBtn.disabled = buttonsDisabled;
elements.blueBtn.disabled = buttonsDisabled;
elements.yellowBtn.disabled = buttonsDisabled;
elements.startBtn.disabled = gameState.isPlaying || gameState.isShowingPattern;
elements.resetBtn.disabled = gameState.isShowingPattern;
elements.redBtn.classList.toggle('active', gameState.isShowingPattern && gameState.currentPattern[gameState.patternIndex] === 'red');
elements.greenBtn.classList.toggle('active', gameState.isShowingPattern && gameState.currentPattern[gameState.patternIndex] === 'green');
elements.blueBtn.classList.toggle('active', gameState.isShowingPattern && gameState.currentPattern[gameState.patternIndex] === 'blue');
elements.yellowBtn.classList.toggle('active', gameState.isShowingPattern && gameState.currentPattern[gameState.patternIndex] === 'yellow');
saveGameState();
}
function updatePatternDots() {
elements.patternDots.innerHTML = '';
for (let i = 0; i < gameState.currentPattern.length; i++) {
const dot = document.createElement('div');
dot.className = `pattern-dot ${i === gameState.patternIndex && gameState.isShowingPattern ? 'active' : ''}`;
elements.patternDots.appendChild(dot);
}
}
function getRandomColor() { return COLORS[Math.floor(Math.random() * COLORS.length)]; }
function showPattern(pattern) {
const { patternDisplayTime, patternPauseTime } = DIFFICULTY_SETTINGS[gameState.settings.difficulty];
gameState.isShowingPattern = true;
gameState.message = "Watch the pattern...";
gameState.patternIndex = 0;
updateGameDisplay();
let index = 0;
const intervalId = setInterval(() => {
gameState.patternIndex = index;
playSound(pattern[index]);
updateGameDisplay();
setTimeout(() => {
if (index < pattern.length - 1) { index++; }
else {
clearInterval(intervalId);
gameState.isShowingPattern = false;
gameState.patternIndex = -1;
gameState.message = "Your turn! Repeat the pattern.";
updateGameDisplay();
}
}, patternPauseTime);
}, patternDisplayTime);
}
function startGame() {
initAudio();
gameState.isPlaying = true;
gameState.currentScore = 0; gameState.level = 1; gameState.userPattern = [];
gameState.consecutiveCorrect = 0;
const newPattern = [getRandomColor()];
gameState.currentPattern = newPattern;
gameState.message = "Get ready...";
gameState.stats.gamesPlayed += 1;
updateGameDisplay();
playSound('success');
setTimeout(() => { showPattern(newPattern); }, 1000);
saveGameState();
}
function resetGame() {
gameState.isPlaying = false; gameState.isShowingPattern = false;
gameState.currentScore = 0; gameState.level = 1;
gameState.message = "Press Start to play";
gameState.currentPattern = []; gameState.userPattern = [];
gameState.patternIndex = -1; gameState.consecutiveCorrect = 0;
updateGameDisplay();
saveGameState();
}
function handleLevelComplete() {
gameState.stats.totalPatterns += 1;
gameState.stats.maxLevel = Math.max(gameState.stats.maxLevel, gameState.level + 1);
checkBadges(gameState.stats);
gameState.currentScore += gameState.level;
if (gameState.currentScore > gameState.highScore) { gameState.highScore = gameState.currentScore; }
gameState.consecutiveCorrect += 1;
gameState.level += 1;
playSound('levelUp');
const newPattern = [...gameState.currentPattern, getRandomColor()];
gameState.currentPattern = newPattern;
gameState.userPattern = [];
gameState.message = `Level ${gameState.level - 1} complete! Next level: ${gameState.level}`;
updateGameDisplay();
setTimeout(() => { showPattern(newPattern); }, 1500);
saveGameState();
}
function handleGameOver() {
gameState.isPlaying = false; gameState.isShowingPattern = false;
gameState.message = `Game Over! Final Score: ${gameState.currentScore}`;
playSound('error');
gameState.stats.highestScore = Math.max(gameState.stats.highestScore, gameState.currentScore);
gameState.stats.scoreHistory.push(gameState.currentScore);
if (gameState.stats.scoreHistory.length > 10) {
gameState.stats.scoreHistory = gameState.stats.scoreHistory.slice(-10);
}
checkBadges(gameState.stats);
updateGameDisplay();
updateStatsDisplay();
saveGameState();
}
function handleButtonClick(color) {
if (!gameState.isPlaying || gameState.isShowingPattern) return;
gameState.userPattern.push(color);
playSound(color);
const currentIndex = gameState.userPattern.length - 1;
if (color !== gameState.currentPattern[currentIndex]) {
gameState.consecutiveCorrect = 0;
setTimeout(() => { handleGameOver(); }, 100);
return;
}
if (gameState.userPattern.length === gameState.currentPattern.length) {
handleLevelComplete();
}
}
function activateButton(button, color) {
button.classList.add('active'); playSound(color);
setTimeout(() => { button.classList.remove('active'); }, 300);
}
function checkBadges(updatedStats) {
let newBadgeUnlocked = false, unlockedBadge = null;
gameState.badges.forEach((badge, index) => {
if (!badge.unlocked && badge.condition(updatedStats)) {
gameState.badges[index].unlocked = true;
newBadgeUnlocked = true;
unlockedBadge = badge;
}
});
if (newBadgeUnlocked && unlockedBadge) {
updateBadgesDisplay();
alert(`🎉 Achievement Unlocked: ${unlockedBadge.name}\n${unlockedBadge.description}`);
}
saveGameState();
}
function applySettings() {
if (gameState.settings.darkMode) {
document.body.classList.add('dark-mode'); elements.app.classList.add('dark-mode');
elements.settingsDarkmode.textContent = 'ON';
elements.settingsDarkmode.classList.add('bg-primary', 'text-white');
elements.settingsDarkmode.classList.remove('bg-gray-light');
} else {
document.body.classList.remove('dark-mode'); elements.app.classList.remove('dark-mode');
elements.settingsDarkmode.textContent = 'OFF';
elements.settingsDarkmode.classList.remove('bg-primary', 'text-white');
elements.settingsDarkmode.classList.add('bg-gray-light');
}
if (gameState.settings.soundEnabled) {
elements.settingsSound.textContent = 'ON';
elements.settingsSound.classList.add('bg-primary', 'text-white');
elements.settingsSound.classList.remove('bg-gray-light');
} else {
elements.settingsSound.textContent = 'OFF';
elements.settingsSound.classList.remove('bg-primary', 'text-white');
elements.settingsSound.classList.add('bg-gray-light');
}
if (gameState.settings.musicEnabled) {
elements.settingsMusic.textContent = 'ON';
elements.settingsMusic.classList.add('bg-primary', 'text-white');
elements.settingsMusic.classList.remove('bg-gray-light');
startBackgroundMusic();
} else {
elements.settingsMusic.textContent = 'OFF';
elements.settingsMusic.classList.remove('bg-primary', 'text-white');
elements.settingsMusic.classList.add('bg-gray-light');
stopBackgroundMusic();
}
elements.settingsDifficulty.value = gameState.settings.difficulty;
if (gameState.settings.layout === "mobile") {
elements.app.classList.add('mobile-mode');
elements.app.classList.remove('desktop-mode');
elements.settingsMobilemode.classList.add('bg-primary', 'text-white');
elements.settingsDesktopmode.classList.remove('bg-primary', 'text-white');
elements.settingsMobilemode.classList.remove('bg-gray-light');
elements.settingsDesktopmode.classList.add('bg-gray-light');
} else {
elements.app.classList.remove('mobile-mode');
elements.app.classList.add('desktop-mode');
elements.settingsDesktopmode.classList.add('bg-primary', 'text-white');
elements.settingsMobilemode.classList.remove('bg-primary', 'text-white');
elements.settingsDesktopmode.classList.remove('bg-gray-light');
elements.settingsMobilemode.classList.add('bg-gray-light');
}
saveGameState();
}
function toggleDarkMode() { gameState.settings.darkMode = !gameState.settings.darkMode; applySettings();}
function toggleSound() { gameState.settings.soundEnabled = !gameState.settings.soundEnabled; applySettings();}
function toggleMusic() { gameState.settings.musicEnabled = !gameState.settings.musicEnabled; applySettings();}
function setDifficulty(difficulty) { gameState.settings.difficulty = difficulty; applySettings(); }
function setMobileMode() { gameState.settings.layout = "mobile"; applySettings(); }
function setDesktopMode() { gameState.settings.layout = "desktop"; applySettings(); }
function initAudio() {
if (!audioContext) {
try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); }
catch (error) { console.error('Web Audio API is not supported in this browser:', error);}
}
}
function playSound(color) {
if (!gameState.settings.soundEnabled || !audioContext) return;
try {
if (oscillator) { oscillator.stop(); oscillator.disconnect(); }
oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
switch (color) {
case 'red': oscillator.frequency.value = 261.63; break;
case 'green': oscillator.frequency.value = 329.63; break;
case 'blue': oscillator.frequency.value = 392.00; break;
case 'yellow': oscillator.frequency.value = 493.88; break;
case 'success': oscillator.frequency.value = 523.25; break;
case 'error': oscillator.frequency.value = 220.00; break;
case 'levelUp': oscillator.frequency.value = 659.25; break;
default: oscillator.frequency.value = 440.00;
}
oscillator.connect(gainNode); gainNode.connect(audioContext.destination);
oscillator.start(); gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
setTimeout(() => {
if (oscillator) {
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1);
setTimeout(() => {
oscillator.stop(); oscillator.disconnect(); oscillator = null;
}, 100);
}
}, 150);
} catch (error) { console.error('Error playing sound:', error);}
}
function startBackgroundMusic() {
if (!gameState.settings.musicEnabled || !audioContext) return;
try {
stopBackgroundMusic();
backgroundMusic = audioContext.createOscillator();
musicGainNode = audioContext.createGain();
backgroundMusic.type = 'sine';
backgroundMusic.frequency.value = 146.83;
backgroundMusic.connect(musicGainNode); musicGainNode.connect(audioContext.destination);
musicGainNode.gain.setValueAtTime(0, audioContext.currentTime);
musicGainNode.gain.linearRampToValueAtTime(0.03, audioContext.currentTime + 2);
backgroundMusic.start();
const lfo = audioContext.createOscillator();
const lfoGain = audioContext.createGain();
lfo.frequency.value = 0.1;
lfoGain.gain.value = 5;
lfo.connect(lfoGain); lfoGain.connect(backgroundMusic.frequency);
lfo.start();
} catch (error) { console.error('Error starting background music:', error);}
}
function stopBackgroundMusic() {
if (backgroundMusic) {
try {
musicGainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
setTimeout(() => {
backgroundMusic.stop(); backgroundMusic.disconnect(); backgroundMusic = null; musicGainNode = null;
}, 1000);
} catch (error) { console.error('Error stopping background music:', error);}
}
}
function updateClockAndDate() {
const now = new Date();
const pad = n => n.toString().padStart(2, '0');
const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const dateStr = now.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
elements.clock.textContent = timeStr;
elements.date.textContent = dateStr;
}
setInterval(updateClockAndDate, 1000);
updateClockAndDate();
function handleTabClick(tabName) {
elements.howtoContent.style.display = 'none';
elements.playContent.style.display = 'none';
elements.statsContent.style.display = 'none';
elements.badgesContent.style.display = 'none';
elements.timeContent.style.display = 'none';
elements.codeContent.style.display = 'none';
elements.settingsContent.style.display = 'none';
elements.tabs.forEach(tab => tab.classList.remove('active'));
switch (tabName) {
case 'howto':
elements.howtoContent.style.display = 'block';
document.querySelector('[data-tab="howto"]').classList.add('active');
break;
case 'play':
elements.playContent.style.display = 'block';
document.querySelector('[data-tab="play"]').classList.add('active');
break;
case 'stats':
elements.statsContent.style.display = 'block';
document.querySelector('[data-tab="stats"]').classList.add('active');
updateStatsDisplay();
break;
case 'badges':
elements.badgesContent.style.display = 'block';
document.querySelector('[data-tab="badges"]').classList.add('active');
updateBadgesDisplay();
break;
case 'time':
elements.timeContent.style.display = 'block';
document.querySelector('[data-tab="time"]').classList.add('active');
break;
case 'code':
elements.codeContent.style.display = 'block';
document.querySelector('[data-tab="code"]').classList.add('active');
showSourceCode();
break;
case 'settings':
elements.settingsContent.style.display = 'block';
document.querySelector('[data-tab="settings"]').classList.add('active');
break;
}
}
// Source code tab
function showSourceCode() {
elements.sourceCodeBlock.textContent = document.documentElement.outerHTML;
}
elements.copyCodeBtn.addEventListener('click', () => {
const codeText = elements.sourceCodeBlock.textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(codeText)
.then(() => elements.copyCodeBtn.textContent = "Copied!")
.catch(() => elements.copyCodeBtn.textContent = "Failed to copy!");
} else {
try {
const temp = document.createElement('textarea');
temp.value = codeText;
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
temp.remove();
elements.copyCodeBtn.textContent = "Copied!";
} catch {
elements.copyCodeBtn.textContent = "Failed to copy!";
}
}
setTimeout(()=>elements.copyCodeBtn.textContent="Copy to Clipboard", 1400);
});
function showModal(contentHTML, actions) {
elements.modalContent.innerHTML = contentHTML;
elements.modalActions.innerHTML = '';
actions.forEach(({ label, onClick, type }) => {
const btn = document.createElement('button');
btn.className = 'btn-primary';
btn.textContent = label;
if (type === 'secondary') btn.className = 'btn-secondary';
btn.onclick = onClick;
elements.modalActions.appendChild(btn);
});
elements.saveLoadModal.style.display = 'flex';
}
function closeModal() {
elements.saveLoadModal.style.display = 'none';
elements.modalContent.innerHTML = '';
elements.modalActions.innerHTML = '';
}
elements.saveDataBtn.addEventListener('click', () => {
const code = encodeSaveData(getSaveObj());
showModal(
`<div>
<div style="margin-bottom:1rem;">Copy your save code below. Keep it secret, keep it safe!</div>
<input class="save-load-input" style="width:100%;" readonly value="${code}" id="save-code-output"/>
</div>`,
[
{
label: "Copy",
onClick: () => {
const inp = $("save-code-output");
if (navigator.clipboard) {
navigator.clipboard.writeText(inp.value);
} else {
inp.select(); inp.setSelectionRange(0, 99999);
document.execCommand('copy');
}
}
},
{
label: "Close",
type: "secondary",
onClick: closeModal
}
]
);
setTimeout(() => {
$("save-code-output").select();
}, 150);
});
elements.loadDataBtn.addEventListener('click', () => {
showModal(
`<div>
<div style="margin-bottom:0.7rem;">Paste your encrypted save code below:</div>
<input class="save-load-input" style="width:100%;" placeholder="Paste code here..." id="load-code-input" autocomplete="off"/>
<div id="load-error" style="color:#ef4444; font-size:0.93rem; margin-top:0.6rem;"></div>
</div>`,
[
{
label: "Load",
onClick: () => {
const code = $("load-code-input").value.trim();
try {
const obj = decodeSaveData(code);
applySaveObj(obj);
saveGameState();
closeModal();
} catch (err) {
$("load-error").textContent = err.message || "Invalid code!";
}
}
},
{
label: "Cancel",
type: "secondary",
onClick: closeModal
}
]
);
setTimeout(() => {
$("load-code-input").focus();
}, 150);
});
elements.saveLoadModal.addEventListener('click', (e) => {
if (e.target === elements.saveLoadModal) closeModal();
});
// Game initialization
function setup() {
loadGameState();
updateGameDisplay();
updateStatsDisplay();
updateBadgesDisplay();
elements.redBtn.addEventListener('click', () => { activateButton(elements.redBtn, 'red'); handleButtonClick('red'); });
elements.greenBtn.addEventListener('click', () => { activateButton(elements.greenBtn, 'green'); handleButtonClick('green'); });
elements.blueBtn.addEventListener('click', () => { activateButton(elements.blueBtn, 'blue'); handleButtonClick('blue'); });
elements.yellowBtn.addEventListener('click', () => { activateButton(elements.yellowBtn, 'yellow'); handleButtonClick('yellow'); });
elements.startBtn.addEventListener('click', startGame);
elements.resetBtn.addEventListener('click', resetGame);
elements.tabs.forEach(tab => {
tab.addEventListener('click', () => { handleTabClick(tab.getAttribute('data-tab')); });
});
elements.settingsDarkmode.addEventListener('click', toggleDarkMode);
elements.settingsSound.addEventListener('click', toggleSound);
elements.settingsMusic.addEventListener('click', toggleMusic);
elements.settingsDifficulty.addEventListener('change', (e) => { setDifficulty(e.target.value); });
elements.settingsMobilemode.addEventListener('click', setMobileMode);
elements.settingsDesktopmode.addEventListener('click', setDesktopMode);
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (!gameState.isPlaying || gameState.isShowingPattern) return;
switch (e.key) {
case 'ArrowUp': case 'w': activateButton(elements.redBtn, 'red'); handleButtonClick('red'); break;
case 'ArrowRight': case 'd': activateButton(elements.greenBtn, 'green'); handleButtonClick('green'); break;
case 'ArrowDown': case 's': activateButton(elements.blueBtn, 'blue'); handleButtonClick('blue'); break;
case 'ArrowLeft': case 'a': activateButton(elements.yellowBtn, 'yellow'); handleButtonClick('yellow'); break;
}
});
// Audio unlock on first interaction
document.addEventListener('click', () => {
if (!audioContext) {
initAudio();
if (gameState.settings.musicEnabled) { startBackgroundMusic(); }
}
}, { once: true });
applySettings();
handleTabClick("play");
}
setup();
</script>
</body>
</html>