<!doctype html>
<html lang="fi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sirkus-Myyräpeli 🎪</title>
<style>
:root{
--stripe-red:#c72626;
--stripe-cream:#fff2d6;
--accent:#2b2b2b;
--gold:#f3c969;
}
html,body{height:100%;margin:0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:
repeating-linear-gradient(45deg,var(--stripe-cream) 0 28px,var(--stripe-red) 28px 56px);
color:#222;
}
.wrap{max-width:960px;margin:0 auto;padding:18px 16px 64px;}
h1{margin:0 0 10px;font-size:clamp(22px,3.5vw,36px);text-align:center;}
.board-wrap{display:grid;gap:16px;grid-template-columns:1fr;}
.hud{
background:rgba(255,255,255,.8);border:3px solid var(--gold);box-shadow:0 6px 18px rgba(0,0,0,.12);
border-radius:16px;padding:10px 12px;display:flex;flex-wrap:wrap;align-items:center;gap:10px;justify-content:space-between;
}
.scores{
background:rgba(255,255,255,.95);border:3px solid var(--gold);border-radius:16px;padding:10px;box-shadow:0 8px 20px rgba(0,0,0,.15);
}
.scores h2{margin:0 0 8px;font-size:18px;text-align:center}
table{width:100%;border-collapse:collapse;font-size:14px}
th,td{padding:6px 8px;border-bottom:1px dashed #ddd;text-align:left}
th{background:#fff8e8}
.controls{display:flex;gap:8px;flex-wrap:wrap}
button{
cursor:pointer;border:none;border-radius:12px;padding:10px 14px;font-weight:700
}
.start{background:#23b14d;color:white}
.stop{background:#ff6b6b;color:white}
.hole-grid{
aspect-ratio:1/1;display:grid;gap:12px;grid-template-columns:repeat(3,1fr);
background:radial-gradient(circle at 50% -20%, rgba(255,255,255,.9), rgba(255,255,255,.65));
border:6px solid var(--gold);border-radius:24px;padding:14px;box-shadow:inset 0 20px 30px rgba(0,0,0,.08), 0 10px 22px rgba(0,0,0,.2);
}
.hole{
background:radial-gradient(ellipse at center, #3b2d1f 0%, #1e130a 60%, #000 100%);
border-radius:50%;position:relative;overflow:hidden;box-shadow:inset 0 10px 18px rgba(255,255,255,.08), 0 8px 20px rgba(0,0,0,.35);
}
.mole{
position:absolute;left:50%;bottom:-65%;transform:translateX(-50%);
width:78%;height:78%; border-radius:50%;
background:#999;display:flex;align-items:center;justify-content:center;
transition:bottom .12s ease-out;will-change:bottom;user-select:none;pointer-events:auto
}
.mole.up{ bottom:8%; }
.mole img{width:100%;height:100%;object-fit:cover;border-radius:50%;image-rendering:auto;}
.note{font-size:12px;opacity:.8;text-align:center}
.uploader{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.badge{background:#000000c0;color:#fff;font-size:12px;border-radius:8px;padding:4px 8px}
dialog.modal{border:none;border-radius:16px;padding:0;max-width:420px;width:calc(100% - 32px)}
.modal .box{padding:18px;background:#fff;border-radius:16px}
.modal h3{margin:0 0 10px}
.modal form{display:flex;gap:8px}
input[type="text"]{flex:1;border:1px solid #ccc;border-radius:8px;padding:10px}
</style>
</head>
<body>
<div class="wrap">
<h1>🎪 Sirkus-Myyräpeli — mäiski siskot alas! 🔨</h1>
<section class="scores" id="scores">
<h2>Parhaat tulokset</h2>
<table id="scoreTable">
<thead><tr><th>#</th><th>Nimi</th><th>Pisteet</th><th>Päivä</th></tr></thead>
<tbody></tbody>
</table>
</section>
<div class="hud">
<div class="stats"><strong>Pisteet:</strong> <span id="score">0</span> <span class="badge" id="level">Taso 1</span></div>
<div class="controls">
<button class="start" id="startBtn">Aloita peli</button>
<button class="stop" id="stopBtn" disabled>Lopeta</button>
</div>
</div>
<div class="board-wrap">
<div class="uploader">
<span><strong>Kasvokuvat:</strong></span>
<input type="file" accept="image/*" id="face1">
<input type="file" accept="image/*" id="face2">
<span class="note">(lataa siskojen kuvat, rajaus hoituu automaattisesti)</span>
</div>
<div class="hole-grid" id="grid"></div>
<div class="note">Vinkki: Ohi-klik = peli päättyy ja tallennat nimen listalle.</div>
</div>
</div>
<dialog class="modal" id="nameModal">
<div class="box">
<h3>Peli ohi! Syötä nimesi tuloslistalle</h3>
<form method="dialog">
<input type="text" id="playerName" placeholder="Nimesi" maxlength="24" required>
<button type="submit" class="start">Tallenna</button>
</form>
</div>
</dialog>
<script>
// ===== Utility: localStorage high scores =====
const STORAGE_KEY = 'sirkus_whack_highscores_v1';
function getScores(){
try{ return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; }catch{ return []; }
}
function saveScore(name, points){
const arr = getScores();
arr.push({name, points, ts: Date.now()});
arr.sort((a,b)=> b.points - a.points).splice(10); // top 10
localStorage.setItem(STORAGE_KEY, JSON.stringify(arr));
}
function renderScores(){
const tbody = document.querySelector('#scoreTable tbody');
tbody.innerHTML = '';
getScores().forEach((s, i)=>{
const tr = document.createElement('tr');
const d = new Date(s.ts);
tr.innerHTML = `<td>${i+1}</td><td>${escapeHtml(s.name)}</td><td><strong>${s.points}</strong></td><td>${d.toLocaleDateString()}</td>`;
tbody.appendChild(tr);
});
}
function escapeHtml(str){ return str.replace(/[&<>\"']/g,m=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[m])); }
// ===== Setup grid =====
const grid = document.getElementById('grid');
const HOLES = 9; // 3x3
for(let i=0;i<HOLES;i++){
const hole = document.createElement('div');
hole.className = 'hole';
const mole = document.createElement('div');
mole.className = 'mole';
const img = document.createElement('img');
img.alt = 'mole';
mole.appendChild(img);
hole.appendChild(mole);
grid.appendChild(hole);
}
// ===== Face upload & circular crop via canvas =====
const faceInputs = [document.getElementById('face1'), document.getElementById('face2')];
let faceDataUris = [];
faceInputs.forEach((inp, idx)=>{
inp.addEventListener('change', async ()=>{
const file = inp.files && inp.files[0];
if(!file) return;
const dataUrl = await fileToDataURL(file);
const circ = await circularCrop(dataUrl, 512);
faceDataUris[idx] = circ;
document.querySelectorAll('.mole img').forEach((im, i)=>{
im.src = faceDataUris[i%faceDataUris.length] || placeholderFace(i);
});
});
});
function fileToDataURL(file){
return new Promise((res)=>{ const r=new FileReader(); r.onload=()=>res(r.result); r.readAsDataURL(file); });
}
function circularCrop(dataUrl, size=512){
return new Promise((resolve)=>{
const img = new Image();
img.onload = ()=>{
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
const s = Math.min(img.width, img.height);
const sx = (img.width - s)/2, sy=(img.height - s)/2;
ctx.clearRect(0,0,size,size);
ctx.save();
ctx.beginPath();
ctx.arc(size/2,size/2,size/2,0,Math.PI*2);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, sx, sy, s, s, 0,0,size,size);
ctx.restore();
resolve(canvas.toDataURL('image/png'));
};
img.src = dataUrl;
});
}
// Placeholder faces
function placeholderFace(i){
const svg = encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
<defs><radialGradient id='g' cx='.5' cy='.35'><stop offset='0' stop-color='#fff'/><stop offset='1' stop-color='#ddd'/></radialGradient></defs>
<circle cx='50' cy='50' r='48' fill='url(#g)' stroke='#222' />
<circle cx='32' cy='42' r='6'/><circle cx='68' cy='42' r='6'/>
<ellipse cx='50' cy='65' rx='22' ry='12' fill='#ff4d4d'/>
<circle cx='20' cy='25' r='10' fill='#ff4d4d'/>
<circle cx='80' cy='25' r='10' fill='#ff4d4d'/>
<path d='M20 20 C35 5,65 5,80 20' fill='none' stroke='#222' stroke-width='4'/>
</svg>`);
return `data:image/svg+xml;charset=utf-8,${svg}`;
}
document.querySelectorAll('.mole img').forEach((im, i)=> im.src = placeholderFace(i));
// ===== Game logic =====
let playing = false, score = 0, level = 1, activeIndex = -1, roundTimer = 0, speedMs = 900;
const scoreEl = document.getElementById('score');
const levelEl = document.getElementById('level');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const nameModal = document.getElementById('nameModal');
const playerNameInput = document.getElementById('playerName');
function setUIPlaying(p){
startBtn.disabled = p; stopBtn.disabled = !p;
}
function chooseFace(i){
if(faceDataUris.length === 0) return placeholderFace(i);
return faceDataUris[i % faceDataUris.length];
}
function nextMole(){
if(!playing) return;
if(activeIndex >= 0){
grid.children[activeIndex].querySelector('.mole').classList.remove('up');
}
const next = pickNewIndex(activeIndex);
activeIndex = next;
const mole = grid.children[next].querySelector('.mole');
const img = mole.querySelector('img');
img.src = chooseFace(next);
mole.classList.add('up');
roundTimer = setTimeout(nextMole, speedMs);
}
function pickNewIndex(prev){
let idx; do{ idx = Math.floor(Math.random()*HOLES); } while(idx===prev);
return idx;
}
grid.addEventListener('click', (e)=>{
if(!playing) return;
const moles = [...grid.querySelectorAll('.mole')];
const targetIsUpMole = moles.some(m => m.classList.contains('up') && (m===e.target || m.contains(e.target)));
if(targetIsUpMole){
score++; scoreEl.textContent = score; playBonk();
if(score % 5 === 0){ level++; levelEl.textContent = `Taso ${level}`; speedMs = Math.max(350, speedMs - 50); }
clearTimeout(roundTimer); nextMole();
} else {
playFail();
endGame();
}
});
startBtn.addEventListener('click', startGame);
stopBtn.addEventListener('click', endGame);
function startGame(){
if(playing) return; playing = true; score = 0; level = 1; speedMs = 900; scoreEl.textContent = '0'; levelEl.textContent = 'Taso 1';
setUIPlaying(true); startMusic();
nextMole();
}
function endGame(){
if(!playing) return; playing=false; setUIPlaying(false);
clearTimeout(roundTimer);
[...grid.querySelectorAll('.mole')].forEach(m=>m.classList.remove('up'));
activeIndex = -1; stopMusic();
playerNameInput.value = '';
nameModal.showModal();
}
nameModal.addEventListener('close', ()=>{
const name = playerNameInput.value.trim() || 'Nimetön';
saveScore(name, score); renderScores();
});
// ====== Sounds (WebAudio) ======
let audioCtx, bgGain, bgTimer;
function ctx(){ return audioCtx || (audioCtx = new (window.AudioContext||window.webkitAudioContext)()); }
function playBonk(){
const c = ctx();
const o = c.createOscillator();
const g = c.createGain();
o.type = 'square'; o.frequency.value = 180;
g.gain.setValueAtTime(0.25, c.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, c.currentTime + 0.12);
o.connect(g).connect(c.destination); o.start(); o.stop(c.currentTime + 0.13);
}
function playFail(){
const c = ctx();
const o = c.createOscillator();
const g = c.createGain();
o.type = 'sawtooth'; o.frequency.setValueAtTime(400, c.currentTime);
o.frequency.exponentialRampToValueAtTime(120, c.currentTime + 0.35);
g.gain.setValueAtTime(0.3, c.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, c.currentTime + 0.4);
o.connect(g).connect(c.destination); o.start(); o.stop(c.currentTime + 0.42);
}
function startMusic(){
const c = ctx(); bgGain = c.createGain(); bgGain.gain.value = 0.08; bgGain.connect(c.destination);
let t = c.currentTime + 0.05;
const tempo = 140; // bpm
const secPerBeat = 60/tempo;
const melody = [
523.25,523.25,392.00,440.00,493.88,523.25,392.00,392.00,
440.00,392.00,349.23,392.00,440.00,392.00,349.23,329.63
];
stopMusic();
bgTimer = setInterval(()=>{
melody.forEach((freq,i)=>{
const o = c.createOscillator(); const g = c.createGain();
o.type='triangle'; o.frequency.value=freq; g.gain.value=0.001;
g.gain.setValueAtTime(0.001, t + i*secPerBeat);
g.gain.linearRampToValueAtTime(0.08, t + i*secPerBeat + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, t + i*secPerBeat + 0.3);
o.connect(g).connect(bgGain); o.start(t + i*secPerBeat); o.stop(t + i*secPerBeat + 0.32);
});
t += melody.length * secPerBeat;
}, melody.length * secPerBeat * 1000);
}
function stopMusic(){ if(bgTimer){ clearInterval(bgTimer); bgTimer=null; } }
renderScores();
</script>
</body>
</html>