725 lines
21 KiB
JavaScript
725 lines
21 KiB
JavaScript
require('dotenv').config();
|
|
const { pool } = require('../src/models/db');
|
|
const { generateGame } = require('../src/utils/claude');
|
|
|
|
const games = [
|
|
{
|
|
id: 14,
|
|
title: "Snake",
|
|
prompt: `Classic Snake game - VERY EASY for 8-year-olds. Must be playable for 1+ minute before getting hard.
|
|
|
|
IMPORTANT - NO AUDIO TOGGLE BUTTONS:
|
|
- Do NOT add any sound toggle buttons to the game
|
|
- Parent page controls audio via window.setMusicEnabled() and window.setSfxEnabled()
|
|
- Just expose these functions, don't create UI for them
|
|
|
|
CRITICAL - CANVAS SETUP:
|
|
\`\`\`javascript
|
|
const canvas = document.createElement('canvas');
|
|
document.body.appendChild(canvas);
|
|
document.body.style.margin = '0';
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.background = '#111';
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
const ctx = canvas.getContext('2d');
|
|
\`\`\`
|
|
|
|
GAME SPEED - VERY IMPORTANT:
|
|
- Initial update interval: 280ms (VERY SLOW - easy for kids)
|
|
- Only speed up after score >= 100 (about 10 apples)
|
|
- Minimum interval: 150ms (never faster)
|
|
- Speed formula: Math.max(150, 280 - Math.floor(score / 20) * 10)
|
|
|
|
GAME MECHANICS:
|
|
- Grid: 15x15 cells (large cells, easy to see)
|
|
- Snake starts with 3 segments in center, moving RIGHT
|
|
- WRAP AROUND edges (no wall death - forgiving)
|
|
- Game over ONLY on self-collision
|
|
- Apple = +10 points
|
|
|
|
CONTROLS:
|
|
- Arrow keys for desktop
|
|
- Swipe detection (touchstart/touchend position difference)
|
|
- Semi-transparent D-pad at bottom (70px buttons)
|
|
|
|
PAUSE SUPPORT:
|
|
\`\`\`javascript
|
|
let paused = false;
|
|
window.pauseGame = () => { paused = true; };
|
|
window.resumeGame = () => { paused = false; };
|
|
// In game loop: if (paused) return;
|
|
\`\`\`
|
|
|
|
NES-STYLE MUSIC - Multi-channel chiptune:
|
|
\`\`\`javascript
|
|
let audioCtx, musicEnabled = true, sfxEnabled = true;
|
|
let musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (audioCtx) return;
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
|
|
// Two-channel NES music: melody + bass
|
|
const melody = [
|
|
{note: 392, dur: 0.15}, {note: 440, dur: 0.15}, {note: 494, dur: 0.15}, {note: 523, dur: 0.3},
|
|
{note: 494, dur: 0.15}, {note: 440, dur: 0.15}, {note: 392, dur: 0.3}, {note: 330, dur: 0.3}
|
|
];
|
|
const bass = [196, 196, 220, 220, 196, 196, 165, 165];
|
|
let melodyIdx = 0, bassIdx = 0;
|
|
|
|
function playMelody() {
|
|
if (!musicEnabled || !audioCtx || paused) return;
|
|
const n = melody[melodyIdx];
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = 'square';
|
|
osc.frequency.value = n.note;
|
|
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + n.dur);
|
|
osc.connect(gain).connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + n.dur);
|
|
melodyIdx = (melodyIdx + 1) % melody.length;
|
|
}
|
|
|
|
function playBass() {
|
|
if (!musicEnabled || !audioCtx || paused) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = 'triangle';
|
|
osc.frequency.value = bass[bassIdx];
|
|
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
|
|
osc.connect(gain).connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.2);
|
|
bassIdx = (bassIdx + 1) % bass.length;
|
|
}
|
|
|
|
let melodyInt, bassInt;
|
|
function startMusic() {
|
|
if (musicPlaying) return;
|
|
musicPlaying = true;
|
|
melodyInt = setInterval(playMelody, 200);
|
|
bassInt = setInterval(playBass, 400);
|
|
}
|
|
function stopMusic() {
|
|
musicPlaying = false;
|
|
clearInterval(melodyInt);
|
|
clearInterval(bassInt);
|
|
}
|
|
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; v ? startMusic() : stopMusic(); };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
\`\`\`
|
|
|
|
SOUND EFFECTS (when sfxEnabled):
|
|
- Eat: 880Hz square, 50ms
|
|
- Game over: 220Hz descending to 110Hz, 400ms
|
|
|
|
VISUALS:
|
|
- Dark background (#111)
|
|
- Bright green snake (#22c55e) with darker head (#16a34a)
|
|
- Red apple (#ef4444)
|
|
- White score at top center
|
|
- D-pad: semi-transparent white arrows on dark circles`
|
|
},
|
|
{
|
|
id: 18,
|
|
title: "Space Invaders",
|
|
prompt: `Space Invaders - VERY EASY for 8-year-olds. Must be playable for 1+ minute.
|
|
|
|
IMPORTANT - NO AUDIO TOGGLE BUTTONS:
|
|
- Do NOT add any sound toggle buttons to the game
|
|
- Parent page controls audio via window.setMusicEnabled() and window.setSfxEnabled()
|
|
- Just expose these functions, don't create UI for them
|
|
|
|
CRITICAL - CANVAS SETUP:
|
|
\`\`\`javascript
|
|
const canvas = document.createElement('canvas');
|
|
document.body.appendChild(canvas);
|
|
document.body.style.margin = '0';
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.background = '#000';
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
const ctx = canvas.getContext('2d');
|
|
\`\`\`
|
|
|
|
ALIEN INITIALIZATION - CRITICAL (do this AFTER canvas is sized):
|
|
\`\`\`javascript
|
|
let aliens = [];
|
|
let player = { x: 0, y: 0, width: 50, height: 30 };
|
|
let gameStarted = false;
|
|
|
|
function initGame() {
|
|
// Player at bottom
|
|
player.x = canvas.width / 2;
|
|
player.y = canvas.height - 70;
|
|
|
|
// Create aliens - 3 rows x 6 columns
|
|
aliens = [];
|
|
const startX = canvas.width * 0.2;
|
|
const startY = 80;
|
|
for (let row = 0; row < 3; row++) {
|
|
for (let col = 0; col < 6; col++) {
|
|
aliens.push({
|
|
x: startX + col * 60,
|
|
y: startY + row * 50,
|
|
alive: true,
|
|
width: 40,
|
|
height: 30
|
|
});
|
|
}
|
|
}
|
|
alienDir = 1;
|
|
alienSpeed = 0.3; // VERY slow
|
|
gameStarted = true;
|
|
}
|
|
|
|
// Call initGame when first interaction happens
|
|
canvas.addEventListener('click', () => { if (!gameStarted) { initAudio(); initGame(); } });
|
|
canvas.addEventListener('touchstart', () => { if (!gameStarted) { initAudio(); initGame(); } });
|
|
\`\`\`
|
|
|
|
ALIEN MOVEMENT - MUST move sideways, NOT fall:
|
|
\`\`\`javascript
|
|
let alienDir = 1;
|
|
let alienSpeed = 0.3;
|
|
|
|
function updateAliens() {
|
|
if (!gameStarted || paused) return;
|
|
|
|
let hitEdge = false;
|
|
let minX = canvas.width, maxX = 0;
|
|
|
|
aliens.forEach(a => {
|
|
if (!a.alive) return;
|
|
a.x += alienDir * alienSpeed;
|
|
minX = Math.min(minX, a.x);
|
|
maxX = Math.max(maxX, a.x + a.width);
|
|
});
|
|
|
|
if (minX < 20 || maxX > canvas.width - 20) {
|
|
hitEdge = true;
|
|
}
|
|
|
|
if (hitEdge) {
|
|
alienDir *= -1;
|
|
aliens.forEach(a => { if (a.alive) a.y += 20; });
|
|
}
|
|
}
|
|
\`\`\`
|
|
|
|
MOBILE CONTROLS - Slide to move, tap to fire:
|
|
\`\`\`javascript
|
|
let touchStartX = 0;
|
|
let touchMoved = false;
|
|
let lastTouchX = 0;
|
|
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
if (!gameStarted) { initAudio(); initGame(); return; }
|
|
touchStartX = e.touches[0].clientX;
|
|
lastTouchX = touchStartX;
|
|
touchMoved = false;
|
|
});
|
|
|
|
canvas.addEventListener('touchmove', (e) => {
|
|
e.preventDefault();
|
|
const touchX = e.touches[0].clientX;
|
|
const dx = touchX - lastTouchX;
|
|
if (Math.abs(touchX - touchStartX) > 10) touchMoved = true;
|
|
player.x = Math.max(25, Math.min(canvas.width - 25, player.x + dx));
|
|
lastTouchX = touchX;
|
|
});
|
|
|
|
canvas.addEventListener('touchend', (e) => {
|
|
e.preventDefault();
|
|
if (!touchMoved && gameStarted) {
|
|
fireBullet();
|
|
}
|
|
});
|
|
\`\`\`
|
|
|
|
DESKTOP: Arrow keys to move, Space to fire
|
|
|
|
PAUSE SUPPORT:
|
|
\`\`\`javascript
|
|
let paused = false;
|
|
window.pauseGame = () => { paused = true; };
|
|
window.resumeGame = () => { paused = false; };
|
|
\`\`\`
|
|
|
|
DIFFICULTY:
|
|
- Aliens shoot VERY rarely: Math.random() < 0.001 per frame per alien
|
|
- Player has 5 lives
|
|
- Bullets move slowly (4px/frame)
|
|
- 10 points per alien
|
|
|
|
NES-STYLE MUSIC - Space march:
|
|
\`\`\`javascript
|
|
let audioCtx, musicEnabled = true, sfxEnabled = true, musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (audioCtx) return;
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
|
|
const marchNotes = [82, 98, 82, 98]; // E2, G2 pattern
|
|
let marchIdx = 0, marchInt;
|
|
|
|
function playMarch() {
|
|
if (!musicEnabled || !audioCtx || paused) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = 'square';
|
|
osc.frequency.value = marchNotes[marchIdx];
|
|
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.08);
|
|
osc.connect(gain).connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.08);
|
|
marchIdx = (marchIdx + 1) % marchNotes.length;
|
|
|
|
// Also play higher accent every 4 notes
|
|
if (marchIdx === 0) {
|
|
const osc2 = audioCtx.createOscillator();
|
|
const gain2 = audioCtx.createGain();
|
|
osc2.type = 'square';
|
|
osc2.frequency.value = 165;
|
|
gain2.gain.setValueAtTime(0.06, audioCtx.currentTime);
|
|
gain2.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.05);
|
|
osc2.connect(gain2).connect(audioCtx.destination);
|
|
osc2.start();
|
|
osc2.stop(audioCtx.currentTime + 0.05);
|
|
}
|
|
}
|
|
|
|
function startMusic() {
|
|
if (musicPlaying) return;
|
|
musicPlaying = true;
|
|
marchInt = setInterval(playMarch, 500);
|
|
}
|
|
function stopMusic() {
|
|
musicPlaying = false;
|
|
clearInterval(marchInt);
|
|
}
|
|
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; v ? startMusic() : stopMusic(); };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
\`\`\`
|
|
|
|
SOUND EFFECTS:
|
|
- Fire: 1200Hz, 30ms square
|
|
- Alien hit: noise burst 50ms
|
|
- Player hit: 80Hz, 200ms
|
|
|
|
VISUALS:
|
|
- Black background
|
|
- Green triangular ship at bottom
|
|
- Colorful aliens (red/yellow/cyan by row)
|
|
- Show "TAP TO START" before game begins
|
|
- Lives as small ships bottom-left
|
|
- Score top-left`
|
|
},
|
|
{
|
|
id: 20,
|
|
title: "Flappy Bird",
|
|
prompt: `Flappy Bird - VERY EASY for 8-year-olds. Playable for 1+ minute before hard.
|
|
|
|
IMPORTANT - NO AUDIO TOGGLE BUTTONS:
|
|
- Do NOT add any sound toggle buttons to the game
|
|
- Parent page controls audio via window.setMusicEnabled() and window.setSfxEnabled()
|
|
- Just expose these functions, don't create UI for them
|
|
|
|
CRITICAL - CANVAS SETUP:
|
|
\`\`\`javascript
|
|
const canvas = document.createElement('canvas');
|
|
document.body.appendChild(canvas);
|
|
document.body.style.margin = '0';
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
const ctx = canvas.getContext('2d');
|
|
\`\`\`
|
|
|
|
PROGRESSIVE DIFFICULTY - VERY GRADUAL:
|
|
\`\`\`javascript
|
|
function getGapSize() {
|
|
// Start HUGE (50% of screen), shrink SLOWLY
|
|
const baseGap = canvas.height * 0.5;
|
|
const minGap = canvas.height * 0.25;
|
|
// Only shrink after 60 seconds worth of points (about 30 pipes)
|
|
const reduction = Math.min(score, 30) * 5;
|
|
return Math.max(minGap, baseGap - reduction);
|
|
}
|
|
|
|
function getPipeSpeed() {
|
|
// Start slow (2), max out at 4
|
|
return 2 + Math.min(score * 0.05, 2);
|
|
}
|
|
|
|
function getGravity() {
|
|
return 0.35; // Gentle gravity throughout
|
|
}
|
|
\`\`\`
|
|
|
|
GAME MECHANICS:
|
|
- Bird at x = canvas.width * 0.3
|
|
- Gravity: 0.35 per frame (gentle)
|
|
- Flap: velocity = -7 (strong boost)
|
|
- Pipes spawn every 180 frames
|
|
- Gap position: random but never too close to top/bottom
|
|
- Forgiving hitbox: 5px smaller than visual on each side
|
|
|
|
CONTROLS:
|
|
\`\`\`javascript
|
|
let gameOver = false, gameStarted = false;
|
|
let bird = { x: 0, y: 0, vy: 0, radius: 18 };
|
|
|
|
function flap() {
|
|
initAudio();
|
|
if (gameOver) { resetGame(); return; }
|
|
if (!gameStarted) { gameStarted = true; startMusic(); }
|
|
bird.vy = -7;
|
|
if (sfxEnabled) playFlap();
|
|
}
|
|
|
|
canvas.addEventListener('click', flap);
|
|
canvas.addEventListener('touchstart', (e) => { e.preventDefault(); flap(); });
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.code === 'Space' || e.key === ' ') { e.preventDefault(); flap(); }
|
|
});
|
|
\`\`\`
|
|
|
|
PAUSE SUPPORT:
|
|
\`\`\`javascript
|
|
let paused = false;
|
|
window.pauseGame = () => { paused = true; stopMusic(); };
|
|
window.resumeGame = () => { paused = false; if (gameStarted && !gameOver) startMusic(); };
|
|
\`\`\`
|
|
|
|
NES-STYLE MUSIC - Happy chiptune:
|
|
\`\`\`javascript
|
|
let audioCtx, musicEnabled = true, sfxEnabled = true, musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (audioCtx) return;
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
|
|
// Happy melody in C major
|
|
const melody = [
|
|
{n: 523, d: 0.12}, {n: 587, d: 0.12}, {n: 659, d: 0.12}, {n: 784, d: 0.24},
|
|
{n: 659, d: 0.12}, {n: 587, d: 0.12}, {n: 523, d: 0.24}, {n: 440, d: 0.12},
|
|
{n: 523, d: 0.12}, {n: 587, d: 0.12}, {n: 523, d: 0.24}
|
|
];
|
|
const bass = [262, 262, 330, 330, 262, 262, 220, 220, 262, 262, 262];
|
|
let mIdx = 0, melodyInt, bassInt;
|
|
|
|
function playMelodyNote() {
|
|
if (!musicEnabled || !audioCtx || paused) return;
|
|
const m = melody[mIdx];
|
|
const osc = audioCtx.createOscillator();
|
|
const g = audioCtx.createGain();
|
|
osc.type = 'square';
|
|
osc.frequency.value = m.n;
|
|
g.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
|
g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + m.d * 0.9);
|
|
osc.connect(g).connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + m.d);
|
|
mIdx = (mIdx + 1) % melody.length;
|
|
}
|
|
|
|
function playBassNote() {
|
|
if (!musicEnabled || !audioCtx || paused) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const g = audioCtx.createGain();
|
|
osc.type = 'triangle';
|
|
osc.frequency.value = bass[mIdx % bass.length];
|
|
g.gain.setValueAtTime(0.06, audioCtx.currentTime);
|
|
g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
|
osc.connect(g).connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.15);
|
|
}
|
|
|
|
function startMusic() {
|
|
if (musicPlaying) return;
|
|
musicPlaying = true;
|
|
melodyInt = setInterval(playMelodyNote, 150);
|
|
bassInt = setInterval(playBassNote, 300);
|
|
}
|
|
function stopMusic() {
|
|
musicPlaying = false;
|
|
clearInterval(melodyInt);
|
|
clearInterval(bassInt);
|
|
}
|
|
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; if(gameStarted && !gameOver) v ? startMusic() : stopMusic(); };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
|
|
function playFlap() {
|
|
if (!sfxEnabled || !audioCtx) return;
|
|
// Quick noise whoosh
|
|
const bufferSize = audioCtx.sampleRate * 0.03;
|
|
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
const data = buffer.getChannelData(0);
|
|
for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1) * (1 - i/bufferSize);
|
|
const src = audioCtx.createBufferSource();
|
|
src.buffer = buffer;
|
|
const g = audioCtx.createGain();
|
|
g.gain.value = 0.15;
|
|
src.connect(g).connect(audioCtx.destination);
|
|
src.start();
|
|
}
|
|
\`\`\`
|
|
|
|
VISUALS:
|
|
- Sky blue gradient (#87CEEB to #E0F4FF)
|
|
- Yellow bird (circle) with orange beak, white eye
|
|
- Green pipes (#22c55e) with darker caps (#16a34a)
|
|
- Brown ground (#8B4513) at bottom, 40px tall
|
|
- Large white score at top center
|
|
- "Tap to Start" text before game
|
|
- "Game Over - Tap to Retry" when dead`
|
|
},
|
|
{
|
|
id: 22,
|
|
title: "Minesweeper",
|
|
prompt: `Minesweeper - EASY for 8-year-olds. DOM-based (not canvas).
|
|
|
|
IMPORTANT - NO AUDIO TOGGLE BUTTONS:
|
|
- Do NOT add any sound toggle buttons to the game
|
|
- Parent page controls audio via window.setMusicEnabled() and window.setSfxEnabled()
|
|
- Just expose these functions, don't create UI for them
|
|
|
|
LAYOUT SETUP:
|
|
\`\`\`javascript
|
|
document.body.style.cssText = 'margin:0;background:#1e293b;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:15px;box-sizing:border-box;font-family:system-ui,sans-serif;';
|
|
|
|
const GRID_SIZE = 8;
|
|
const MINES = 8;
|
|
const cellSize = Math.min(48, Math.floor((Math.min(window.innerWidth, window.innerHeight) - 60) / GRID_SIZE));
|
|
|
|
// Header
|
|
const header = document.createElement('div');
|
|
header.style.cssText = 'display:flex;gap:15px;margin-bottom:12px;align-items:center;color:white;';
|
|
document.body.appendChild(header);
|
|
|
|
// Mine counter
|
|
const mineCount = document.createElement('span');
|
|
mineCount.style.cssText = 'font-size:1.2rem;font-weight:bold;min-width:40px;';
|
|
mineCount.textContent = '💣 ' + MINES;
|
|
header.appendChild(mineCount);
|
|
|
|
// Reset button
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.style.cssText = 'font-size:1.5rem;padding:5px 15px;border:none;border-radius:8px;cursor:pointer;background:#fbbf24;';
|
|
resetBtn.textContent = '😊';
|
|
resetBtn.onclick = resetGame;
|
|
header.appendChild(resetBtn);
|
|
|
|
// Timer
|
|
const timerEl = document.createElement('span');
|
|
timerEl.style.cssText = 'font-size:1.2rem;font-weight:bold;min-width:50px;';
|
|
timerEl.textContent = '⏱ 0';
|
|
header.appendChild(timerEl);
|
|
|
|
// Grid
|
|
const grid = document.createElement('div');
|
|
grid.style.cssText = \`display:grid;grid-template-columns:repeat(\${GRID_SIZE},\${cellSize}px);gap:3px;background:#334155;padding:10px;border-radius:10px;\`;
|
|
document.body.appendChild(grid);
|
|
\`\`\`
|
|
|
|
CELL CREATION:
|
|
\`\`\`javascript
|
|
const cells = [];
|
|
const cellData = []; // {mine, revealed, flagged, adjacentMines}
|
|
|
|
function createCells() {
|
|
grid.innerHTML = '';
|
|
cells.length = 0;
|
|
cellData.length = 0;
|
|
|
|
for (let i = 0; i < GRID_SIZE * GRID_SIZE; i++) {
|
|
const cell = document.createElement('div');
|
|
cell.style.cssText = \`
|
|
width:\${cellSize}px;height:\${cellSize}px;
|
|
background:#64748b;border-radius:6px;
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-size:\${cellSize * 0.55}px;font-weight:bold;
|
|
cursor:pointer;user-select:none;
|
|
transition:background 0.1s;
|
|
\`;
|
|
|
|
cellData.push({ mine: false, revealed: false, flagged: false, adjacentMines: 0 });
|
|
|
|
// MOBILE LONG PRESS FOR FLAG
|
|
let pressTimer = null;
|
|
let longPressed = false;
|
|
|
|
cell.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
longPressed = false;
|
|
cell.style.background = '#475569';
|
|
pressTimer = setTimeout(() => {
|
|
longPressed = true;
|
|
toggleFlag(i);
|
|
cell.style.background = cellData[i].revealed ? '#e2e8f0' : '#64748b';
|
|
}, 400);
|
|
});
|
|
|
|
cell.addEventListener('touchend', (e) => {
|
|
e.preventDefault();
|
|
clearTimeout(pressTimer);
|
|
cell.style.background = cellData[i].revealed ? '#e2e8f0' : '#64748b';
|
|
if (!longPressed && !gameOver) {
|
|
revealCell(i);
|
|
}
|
|
});
|
|
|
|
cell.addEventListener('touchmove', () => {
|
|
clearTimeout(pressTimer);
|
|
cell.style.background = cellData[i].revealed ? '#e2e8f0' : '#64748b';
|
|
});
|
|
|
|
// Desktop
|
|
cell.addEventListener('click', () => revealCell(i));
|
|
cell.addEventListener('contextmenu', (e) => { e.preventDefault(); toggleFlag(i); });
|
|
|
|
cells.push(cell);
|
|
grid.appendChild(cell);
|
|
}
|
|
}
|
|
\`\`\`
|
|
|
|
GAME LOGIC:
|
|
- First click + 8 neighbors always safe (place mines after)
|
|
- Flood fill for empty cells
|
|
- Numbers colored: 1=blue, 2=green, 3=red, 4=purple, 5=maroon, 6=teal
|
|
|
|
PAUSE SUPPORT:
|
|
\`\`\`javascript
|
|
let paused = false;
|
|
window.pauseGame = () => { paused = true; stopMusic(); };
|
|
window.resumeGame = () => { paused = false; startMusic(); };
|
|
\`\`\`
|
|
|
|
NES-STYLE MUSIC - Calm puzzle:
|
|
\`\`\`javascript
|
|
let audioCtx, musicEnabled = true, sfxEnabled = true, musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (audioCtx) return;
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
|
|
// Calm arpeggio pattern
|
|
const notes = [262, 330, 392, 330, 262, 294, 349, 294];
|
|
let noteIdx = 0, musicInt;
|
|
|
|
function playNote() {
|
|
if (!musicEnabled || !audioCtx || paused || gameOver) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const g = audioCtx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.value = notes[noteIdx];
|
|
g.gain.setValueAtTime(0.07, audioCtx.currentTime);
|
|
g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.25);
|
|
osc.connect(g).connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.25);
|
|
noteIdx = (noteIdx + 1) % notes.length;
|
|
}
|
|
|
|
function startMusic() {
|
|
if (musicPlaying) return;
|
|
musicPlaying = true;
|
|
musicInt = setInterval(playNote, 350);
|
|
}
|
|
function stopMusic() {
|
|
musicPlaying = false;
|
|
clearInterval(musicInt);
|
|
}
|
|
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; v ? startMusic() : stopMusic(); };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
\`\`\`
|
|
|
|
SOUND EFFECTS:
|
|
- Reveal: 600Hz click, 20ms
|
|
- Flag: 300Hz, 40ms
|
|
- Mine: noise explosion 200ms
|
|
- Win: ascending C-E-G-C arpeggio
|
|
|
|
VISUALS:
|
|
- Dark slate background
|
|
- Gray unrevealed (#64748b)
|
|
- Light revealed (#e2e8f0)
|
|
- 🚩 for flags, 💣 for mines
|
|
- 😊 normal, 😎 win, 😵 lose for reset button`
|
|
}
|
|
];
|
|
|
|
async function regenerateGame(g) {
|
|
console.log(`\nRegenerating: ${g.title}...`);
|
|
try {
|
|
const result = await generateGame(g.prompt);
|
|
if (!result.success) {
|
|
console.log(` FAILED: ${result.error}`);
|
|
return false;
|
|
}
|
|
await pool.query('UPDATE games SET game_code = $1, prompt = $2 WHERE id = $3', [result.code, g.prompt, g.id]);
|
|
console.log(` SUCCESS`);
|
|
return true;
|
|
} catch (e) {
|
|
console.log(` ERROR: ${e.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('='.repeat(50));
|
|
console.log('REGENERATING 4 GAMES - v3');
|
|
console.log('Slow snake, fixed space invaders, better music');
|
|
console.log('='.repeat(50));
|
|
|
|
let ok = 0;
|
|
for (const g of games) {
|
|
if (await regenerateGame(g)) ok++;
|
|
await new Promise(r => setTimeout(r, 3000));
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(50));
|
|
console.log(`COMPLETE: ${ok}/4 successful`);
|
|
console.log('='.repeat(50));
|
|
|
|
await pool.end();
|
|
process.exit(ok === 4 ? 0 : 1);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Fatal:', err);
|
|
process.exit(1);
|
|
});
|