Initial commit

This commit is contained in:
Allen
2026-04-30 02:14:25 +00:00
commit 7090e6f01d
243 changed files with 115122 additions and 0 deletions
+414
View File
@@ -0,0 +1,414 @@
require('dotenv').config();
const { pool } = require('../src/models/db');
const { generateGame } = require('../src/utils/claude');
const ADMIN_USER_ID = 2; // arcade_master
const classicGames = [
{
title: "Snake",
prompt: `Create a classic Snake game where the player controls a snake that grows longer each time it eats food.
GAMEPLAY MECHANICS:
- The snake moves continuously in the current direction
- Arrow keys (desktop) or swipe gestures (mobile) change direction
- A food item (apple or dot) appears randomly on the screen
- When the snake eats food, it grows by one segment and score increases by 10
- The game ends if the snake hits the wall or its own body
- Snake should start with 3 segments moving to the right
- Speed should gradually increase every 50 points
VISUAL DESIGN:
- Use a grid-based playing field with a subtle grid pattern
- Snake head should be slightly different color than body (dark green head, lighter green body)
- Food should be red and visually distinct (like an apple)
- Display score prominently in the top-left corner
- Show a "Game Over" screen with final score and "Play Again" button
CONTROLS:
- Desktop: Arrow keys for direction
- Mobile: Swipe in any direction to change snake direction
- Prevent 180-degree turns (can't go directly backward)`
},
{
title: "Tetris",
prompt: `Create a Tetris-style falling block puzzle game.
GAMEPLAY MECHANICS:
- Seven different tetromino shapes (I, O, T, S, Z, J, L) fall from the top
- Player can move pieces left/right and rotate them
- Pieces fall automatically at a steady pace
- When a horizontal line is completely filled, it clears and awards points
- Clearing multiple lines at once awards bonus points (1 line=100, 2=300, 3=500, 4=800)
- Game ends when pieces stack to the top
- Show the next piece that will appear
VISUAL DESIGN:
- 10 columns wide, 20 rows tall playing field
- Each tetromino type has a distinct bright color
- Ghost piece shows where current piece will land
- Clear lines with a satisfying flash animation
- Display score, level, and lines cleared
- Level increases every 10 lines, increasing fall speed
CONTROLS:
- Left/Right arrows or A/D: Move piece horizontally
- Up arrow or W: Rotate piece clockwise
- Down arrow or S: Soft drop (faster fall)
- Space: Hard drop (instant drop)
- Mobile: On-screen buttons for all actions`
},
{
title: "Pong",
prompt: `Create a classic Pong game - the original video game tennis.
GAMEPLAY MECHANICS:
- Two paddles on opposite sides of the screen (left and right)
- A ball bounces between them
- Players score when the ball passes the opponent's paddle
- Ball speed increases slightly after each paddle hit
- Ball angle changes based on where it hits the paddle (edges = sharper angles)
- First player to 11 points wins
- Ball resets to center after each point, launching toward the player who was scored on
VISUAL DESIGN:
- Black background with white elements (classic arcade look)
- Dotted line down the center of the screen
- Paddles are simple white rectangles
- Ball is a small white square
- Large score display at top for both players
- "Player 1 Wins!" or "Player 2 Wins!" message at game end
CONTROLS:
- Player 1 (left): W/S keys to move up/down
- Player 2 (right): Up/Down arrow keys
- Mobile: Touch left side to control left paddle, right side for right paddle
- Alternatively, make Player 2 a simple AI opponent`
},
{
title: "Breakout",
prompt: `Create a Breakout/Brick Breaker arcade game.
GAMEPLAY MECHANICS:
- Rows of colored bricks at the top of the screen
- Player controls a paddle at the bottom
- Ball bounces around, breaking bricks on contact
- Different colored bricks may take multiple hits (green=1, yellow=2, orange=3, red=4)
- Some bricks drop power-ups: wider paddle, multi-ball, slow ball
- Player has 3 lives, loses one if ball falls below paddle
- Level complete when all bricks are destroyed
- Ball angle changes based on where it hits the paddle
VISUAL DESIGN:
- Colorful brick arrangement (rainbow pattern from top to bottom)
- Bricks should visibly crack or change color when damaged
- Particle effects when bricks break
- Paddle has a metallic/shiny appearance
- Display score at top, lives as small ball icons
- Power-ups are falling capsules with distinct colors
CONTROLS:
- Mouse movement or Left/Right arrows move the paddle
- Click or Space to launch the ball from paddle at game start
- Mobile: Touch and drag to move paddle`
},
{
title: "Space Invaders",
prompt: `Create the classic Space Invaders alien shooting game.
GAMEPLAY MECHANICS:
- Grid of alien invaders (5 rows of 11) march left and right, dropping down each time they hit an edge
- Player ship at bottom can move left/right and shoot upward
- Aliens occasionally drop bombs downward
- Player has 3 lives, loses one if hit by bomb
- Killing aliens awards points (bottom row=10, increasing by 10 per row up)
- Mystery UFO occasionally flies across the top (bonus 50-300 points)
- Aliens speed up as fewer remain
- Game over if aliens reach the bottom
VISUAL DESIGN:
- Classic pixel-art style aliens (3 different designs for variety)
- Aliens animate between 2 frames as they move
- Green player ship with white bullets
- Four destructible shields/bunkers that erode when hit
- Black space background with stars
- Score and high score at top, lives shown as ship icons
CONTROLS:
- Left/Right arrows or A/D to move ship
- Space bar to shoot (limit: one bullet on screen at a time for authenticity, or allow 3)
- Mobile: Touch left/right sides to move, tap center to shoot`
},
{
title: "Pac-Man",
prompt: `Create a Pac-Man style maze chase game.
GAMEPLAY MECHANICS:
- Player controls Pac-Man through a maze eating dots
- Four ghosts chase Pac-Man with different behaviors
- Small dots = 10 points, large power pellets = 50 points
- Power pellets make ghosts vulnerable (blue) for 8 seconds - eating them = 200, 400, 800, 1600 points
- Player has 3 lives, loses one if touched by a non-vulnerable ghost
- Level complete when all dots are eaten
- Tunnels on sides wrap around to opposite side
- Ghosts get faster with each level
VISUAL DESIGN:
- Classic maze layout with blue walls
- Pac-Man is yellow circle with animated chomping mouth
- Four colorful ghosts: red (Blinky), pink (Pinky), cyan (Inky), orange (Clyde)
- Ghosts have cute eyes that look in their direction of travel
- Dots are small, power pellets are large and flashing
- Display score, high score, and lives
CONTROLS:
- Arrow keys for direction (Pac-Man turns at next intersection)
- Mobile: Swipe in direction to turn
- Pac-Man continues moving in current direction until wall`
},
{
title: "Flappy Bird",
prompt: `Create a Flappy Bird-style endless flying game.
GAMEPLAY MECHANICS:
- Bird automatically falls due to gravity
- Tapping/clicking makes the bird flap and rise
- Pipes (or obstacles) scroll from right to left with gaps to fly through
- Each pipe passed = 1 point
- Game over if bird hits pipe or ground/ceiling
- Pipe gap size stays constant, but spacing between pipes can vary
- Bird rotates based on velocity (nose up when rising, nose down when falling)
- High score is saved and displayed
VISUAL DESIGN:
- Cute, simple bird with flapping wing animation
- Colorful green pipes coming from top and bottom
- Scrolling background with clouds and simple scenery
- Ground at bottom with scrolling texture
- Large score display in center-top during play
- Game over screen shows score vs best score with "Tap to Retry"
CONTROLS:
- Spacebar, click, or tap anywhere to flap
- That's it - one-button gameplay!
- Bird should feel floaty with satisfying physics`
},
{
title: "2048",
prompt: `Create the 2048 sliding number puzzle game.
GAMEPLAY MECHANICS:
- 4x4 grid of tiles
- Tiles contain powers of 2 (2, 4, 8, 16... up to 2048+)
- Swiping/arrow key slides ALL tiles in that direction
- When two tiles with same number collide, they merge into their sum
- After each move, a new tile (2 or 4) appears in a random empty spot
- Goal: Create a 2048 tile (though game continues after)
- Game over when no more moves are possible
- Score increases by the value of each merged tile
VISUAL DESIGN:
- Clean, modern design with rounded tile corners
- Each number has a distinct background color (2=light, higher=darker/warmer)
- Smooth sliding and merging animations
- Tiles should pop/scale when they appear and merge
- Display current score and best score
- Large, clear numbers on tiles
- "You Win!" celebration when reaching 2048, with option to continue
CONTROLS:
- Arrow keys to slide all tiles in a direction
- Mobile: Swipe gestures in any direction
- Consider adding "Undo" button for last move`
},
{
title: "Minesweeper",
prompt: `Create the classic Minesweeper puzzle game.
GAMEPLAY MECHANICS:
- Grid of covered cells (start with 10x10 with 15 mines for beginner)
- Left-click reveals a cell
- Numbers show how many adjacent cells (including diagonals) contain mines
- Empty cells auto-reveal adjacent empty cells (flood fill)
- Right-click places a flag to mark suspected mines
- First click is always safe (never a mine)
- Win by revealing all non-mine cells
- Lose by clicking a mine (reveal all mines, show X on wrong flags)
- Timer starts on first click
VISUAL DESIGN:
- Clean grid with beveled 3D-style unrevealed cells
- Revealed cells are flat with clear numbers
- Numbers colored by value (1=blue, 2=green, 3=red, etc.)
- Mines shown as black circles/bombs when revealed
- Flags are red
- Smiley face button to restart (changes expression: smile, surprised on click, dead on loss, cool on win)
- Display mine count remaining and timer
CONTROLS:
- Left-click to reveal cell
- Right-click to toggle flag
- Mobile: Tap to reveal, long-press to flag
- Include toggle button for flag mode on mobile`
},
{
title: "Asteroids",
prompt: `Create the classic Asteroids space shooter game.
GAMEPLAY MECHANICS:
- Player ship in center of screen can rotate and thrust
- Ship wraps around screen edges (appears on opposite side)
- Shoot bullets to destroy asteroids
- Large asteroids split into 2 medium, medium split into 2 small
- Points: Large=20, Medium=50, Small=100
- Player has 3 lives, loses one if hit by asteroid
- Ship has brief invincibility after respawning
- UFOs occasionally appear and shoot at player (Small=1000pts, Large=200pts)
- Hyperspace: random teleport as escape (risky - might teleport into asteroid)
VISUAL DESIGN:
- Vector graphics style (white lines on black background)
- Ship is a simple triangle that rotates smoothly
- Asteroids are irregular polygons that rotate as they drift
- Bullets are small dots/lines
- Thrust shows small particles behind ship
- Ship explosion is satisfying particle burst
- Display score and lives (ship icons)
CONTROLS:
- Left/Right arrows or A/D to rotate ship
- Up arrow or W to thrust forward
- Space to shoot
- Shift or X for hyperspace
- Mobile: On-screen joystick for rotation/thrust, fire button`
},
{
title: "Frogger",
prompt: `Create a Frogger-style road and river crossing game.
GAMEPLAY MECHANICS:
- Frog starts at bottom, must reach one of 5 home bases at top
- Bottom half: Cross road with cars/trucks moving at different speeds
- Top half: Cross river by jumping on logs and turtles
- Falling in water or getting hit by vehicle = lose a life
- Turtles occasionally submerge (warning animation first)
- Bonus items appear on logs (flies for points, female frog for bonus)
- Time limit per frog (30 seconds) - bonus points for time remaining
- Fill all 5 home bases to complete level
- Each level, traffic and logs move faster
VISUAL DESIGN:
- Colorful top-down view
- Cute pixel-art frog that faces direction of last move
- Road section: gray with lane markings, colorful vehicles
- River section: blue water, brown logs, green turtles
- Safe zones: green grass/lily pads
- Home bases at top with flowers when empty, frog when filled
- Display score, time bar, and lives
CONTROLS:
- Arrow keys for hopping in four directions
- Each press = one hop (grid-based movement)
- Mobile: Swipe in direction to hop, or on-screen d-pad`
},
{
title: "Simon Says",
prompt: `Create a Simon Says memory pattern game.
GAMEPLAY MECHANICS:
- Four colored buttons arranged in quadrants (red, blue, green, yellow)
- Computer plays a sequence by lighting up buttons with sounds
- Player must repeat the sequence by clicking buttons in same order
- Sequence grows by one each successful round
- Wrong button = game over, show final score (rounds completed)
- Speed increases gradually as sequence gets longer
- Each color has a distinct tone/sound
- High score is saved and displayed
VISUAL DESIGN:
- Large circular or rounded square device
- Four bright colored quadrants that light up when active (glow effect)
- Center shows current round number
- Buttons dim when inactive, bright when pressed/shown
- "Simon Says" title at top
- Satisfying button press animations (press down effect)
- Game over screen with score and "Play Again"
- Consider a "strict mode" toggle (one mistake = restart from beginning vs. one mistake = retry same sequence)
CONTROLS:
- Click/tap on colored buttons to repeat sequence
- Each button should have hover effect on desktop
- Consider keyboard shortcuts (1,2,3,4 or Q,W,A,S for the four colors)
- "Start" button to begin new game`
}
];
async function createGame(gameData, index) {
console.log(`\n[${index + 1}/12] Generating: ${gameData.title}...`);
try {
const result = await generateGame(gameData.prompt);
if (!result.success) {
console.error(` Failed to generate ${gameData.title}: ${result.error}`);
return null;
}
// Insert into database with prompt
const dbResult = await pool.query(
`INSERT INTO games (creator_id, title, description, game_code, prompt, status, published_at)
VALUES ($1, $2, $3, $4, $5, 'published', NOW())
RETURNING id`,
[
ADMIN_USER_ID,
gameData.title,
gameData.prompt.split('\n\n')[0], // Use first paragraph as description
result.code,
gameData.prompt
]
);
console.log(` Created: ${gameData.title} (ID: ${dbResult.rows[0].id})`);
return dbResult.rows[0].id;
} catch (error) {
console.error(` Error creating ${gameData.title}:`, error.message);
return null;
}
}
async function main() {
console.log('Creating 12 classic games with detailed prompts...\n');
console.log('Using Claude Sonnet 4 for high-quality game generation.');
console.log('This will take several minutes...\n');
const createdGames = [];
for (let i = 0; i < classicGames.length; i++) {
const gameId = await createGame(classicGames[i], i);
if (gameId) {
createdGames.push({ title: classicGames[i].title, id: gameId });
}
// Small delay between API calls
if (i < classicGames.length - 1) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
console.log('\n========================================');
console.log('SUMMARY');
console.log('========================================');
console.log(`Successfully created: ${createdGames.length}/12 games\n`);
createdGames.forEach(g => {
console.log(` - ${g.title} (ID: ${g.id})`);
});
await pool.end();
process.exit(0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
+724
View File
@@ -0,0 +1,724 @@
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);
});
+462
View File
@@ -0,0 +1,462 @@
require('dotenv').config();
const { pool } = require('../src/models/db');
const { generateGame } = require('../src/utils/claude');
// Games that need fixing (Pong ID:16 and Minesweeper ID:22 work well)
const gamesToFix = [
{
id: 14,
title: "Snake",
prompt: `Create a classic Snake game. The snake starts in the center moving right.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Snake moves continuously in current direction
- Game starts immediately when page loads
- Use requestAnimationFrame for smooth animation
CONTROLS (implement ALL):
- Desktop: Arrow keys change direction (prevent 180° turns)
- Mobile: Touch/swipe detection - swipe in any direction to turn the snake
- Add visible on-screen arrow buttons for mobile (semi-transparent, positioned at bottom)
GAMEPLAY:
- Snake is green segments on a dark grid background
- Red apple appears randomly, eating it grows snake by 1 and adds 10 points
- Game over when hitting walls or self
- Show score in top-left corner
- Game over screen shows "Game Over! Score: X" with "Tap to Restart" button
- Speed increases slightly every 50 points
MOBILE ON-SCREEN CONTROLS:
Add a d-pad style control at the bottom of the screen:
- Four arrow buttons (up, down, left, right) arranged in a cross pattern
- Buttons should be 50px, semi-transparent white with dark arrows
- Position fixed at bottom center of game area
Keep game area square and centered. Use canvas for rendering.`
},
{
id: 15,
title: "Tetris",
prompt: `Create a Tetris falling block puzzle game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Pieces fall automatically at steady pace
- Game starts immediately when page loads
- Use requestAnimationFrame for game loop
CONTROLS (implement ALL):
- Desktop: Left/Right arrows move piece, Up arrow rotates, Down arrow soft drop, Space hard drop
- Mobile: Add visible touch buttons at bottom of screen
MOBILE CONTROLS (REQUIRED):
Create a control panel at bottom with these buttons:
- Left arrow button (move left)
- Right arrow button (move right)
- Rotate button (clockwise rotation)
- Down button (soft drop)
- Drop button (hard drop)
Buttons: 45px size, semi-transparent, arranged in a row
GAMEPLAY:
- 10 columns, 20 rows playing field
- 7 tetromino shapes (I, O, T, S, Z, J, L) with different colors
- Completed lines clear with flash effect
- Score: 1 line=100, 2=300, 3=500, 4=800 points
- Show next piece preview on the side
- Game over when pieces reach top
- Speed increases every level (10 lines = 1 level)
Use canvas rendering. Game area should be centered with controls below.`
},
{
id: 17,
title: "Breakout",
prompt: `Create a Breakout/Brick Breaker arcade game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Ball bounces continuously once launched
- Paddle follows mouse/touch position smoothly
- Game starts with ball on paddle, click/tap to launch
CONTROLS:
- Desktop: Mouse movement controls paddle position, click to launch ball
- Mobile: Touch and drag anywhere to move paddle, tap to launch ball
- Paddle should follow finger/mouse X position instantly
GAMEPLAY:
- 8 rows of colored bricks at top (rainbow colors)
- White paddle at bottom, ball is white circle
- Ball bounces off walls, paddle, and bricks
- Each brick hit = 10 points, brick disappears
- Ball angle changes based on where it hits paddle (edges = sharper angle)
- Lose a life if ball falls below paddle (3 lives total)
- Win when all bricks destroyed
- Show score and lives at top
VISUAL:
- Black background
- Colorful bricks in rows (red, orange, yellow, green, blue, purple)
- Paddle is white rectangle, rounded corners
- Ball leaves a short trail effect
- Bricks flash when hit before disappearing
Use canvas. No on-screen buttons needed - paddle follows touch/mouse directly.`
},
{
id: 18,
title: "Space Invaders",
prompt: `Create a Space Invaders shooting game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Aliens move and player can shoot immediately
- Smooth 60fps animation with requestAnimationFrame
CONTROLS (implement ALL):
- Desktop: Left/Right arrows move ship, Space to shoot
- Mobile: Touch left half of screen to move left, right half to move right
- Mobile: Tap center area or double-tap anywhere to shoot
- Add visible Left/Right buttons at bottom corners and Fire button in center
MOBILE CONTROLS (REQUIRED):
- Left button: bottom-left corner, 60px, arrow pointing left
- Right button: bottom-right corner, 60px, arrow pointing right
- Fire button: bottom-center, 70px, red circle with "FIRE" text
All buttons semi-transparent
GAMEPLAY:
- 5 rows of 8 aliens that move side-to-side and drop down
- Player ship at bottom can move and shoot upward
- One bullet on screen at a time
- Aliens occasionally drop bombs
- 3 lives, hit by bomb = lose life
- Bottom row aliens = 10pts, increasing 10pts per row up
- Aliens speed up as fewer remain
- Wave complete when all aliens destroyed
VISUAL:
- Black space background with twinkling stars
- Green player ship (triangle)
- Aliens are simple pixel-art style (different shapes per row)
- White bullets, red alien bombs
Use canvas rendering.`
},
{
id: 19,
title: "Pac-Man",
prompt: `Create a simplified Pac-Man maze game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Pac-Man moves continuously in set direction
- Simple maze (not full Pac-Man complexity)
- Smooth animation at 60fps
CONTROLS (implement ALL):
- Desktop: Arrow keys set movement direction
- Mobile: Swipe in direction to change Pac-Man's direction
- Add on-screen d-pad for mobile (arrow buttons in cross pattern)
MOBILE CONTROLS (REQUIRED):
D-pad at bottom center:
- Up, Down, Left, Right buttons arranged in cross
- Each button 50px, semi-transparent
- Pac-Man turns at next opportunity when button pressed
GAMEPLAY:
- Simple maze with blue walls (use a basic grid maze, 15x15)
- Yellow dots throughout maze (10 points each)
- 4 large power pellets in corners (50 points, makes ghosts vulnerable)
- 4 colored ghosts that chase Pac-Man
- When powered up (8 seconds), ghosts turn blue and can be eaten (200 pts)
- Lose life if touched by non-blue ghost (3 lives)
- Level complete when all dots eaten
VISUAL:
- Black background, blue maze walls
- Yellow Pac-Man with chomping animation
- Ghosts: Red, Pink, Cyan, Orange with googly eyes
- Dots are small white circles, power pellets are large and flashing
Use canvas. Keep maze simple for playability.`
},
{
id: 20,
title: "Flappy Bird",
prompt: `Create a Flappy Bird style game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Bird flaps on ANY input (tap, click, spacebar, any key)
- Gravity pulls bird down constantly
- Game starts with "Tap to Start" message
CONTROLS (SIMPLE):
- ANY tap, click, touch, or key press = bird flaps upward
- That's the only control needed
- Make sure touchstart event is captured on the entire game canvas
GAMEPLAY:
- Bird on left side of screen, pipes scroll from right
- Pipes have gap in middle to fly through
- Each pipe passed = 1 point
- Game over if bird hits pipe, ground, or ceiling
- Bird rotates based on velocity (nose up when rising, down when falling)
- After game over, show score and "Tap to Restart"
PHYSICS:
- Gravity: bird falls at accelerating rate
- Flap: gives instant upward velocity
- Bird should feel "floaty" but responsive
- Pipe gap should be generous (bird height * 4)
VISUAL:
- Light blue sky background with simple clouds
- Green pipes from top and bottom
- Yellow/orange bird with wing flap animation
- Ground at bottom with scrolling grass texture
- Large score number at top center
Use canvas. Entire canvas should respond to touch events.`
},
{
id: 21,
title: "2048",
prompt: `Create the 2048 sliding puzzle game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Tiles slide and merge correctly
- Touch swipe detection must work reliably
- Game should feel smooth and responsive
CONTROLS:
- Desktop: Arrow keys slide all tiles in that direction
- Mobile: Swipe gestures (detect swipe direction on touchend)
- Swipe detection: track touchstart and touchend positions, determine direction
SWIPE IMPLEMENTATION:
- On touchstart: record start X,Y
- On touchend: calculate delta X,Y
- If |deltaX| > |deltaY| and |deltaX| > 30px: horizontal swipe
- If |deltaY| > |deltaX| and |deltaY| > 30px: vertical swipe
- Determine direction based on positive/negative delta
GAMEPLAY:
- 4x4 grid of tiles
- Start with two "2" tiles in random positions
- Sliding merges matching adjacent tiles (2+2=4, 4+4=8, etc)
- After each move, new tile (90% chance of 2, 10% chance of 4) appears
- Score increases by merged tile value
- Game over when no moves possible
- Win message at 2048 but allow continuing
VISUAL:
- Clean, modern design with rounded tiles
- Each number has distinct background color:
- 2: #eee4da, 4: #ede0c8, 8: #f2b179, 16: #f59563
- 32: #f67c5f, 64: #f65e3b, 128+: #edcf72 to #edc22e
- Tiles animate when sliding and merging (CSS transitions)
- Large, bold numbers on tiles
- Score display at top
Use div-based grid with CSS transitions for smooth animations.`
},
{
id: 23,
title: "Asteroids",
prompt: `Create a classic Asteroids space shooter.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Ship rotates and thrusts with momentum-based physics
- Screen wrapping (go off one edge, appear on opposite)
- Smooth 60fps gameplay
CONTROLS (implement ALL):
- Desktop: Left/Right rotate ship, Up thrusts forward, Space shoots
- Mobile: On-screen virtual joystick or buttons
MOBILE CONTROLS (REQUIRED):
Left side: Rotation buttons (two buttons: rotate left, rotate right)
Right side: Action buttons (Thrust button, Fire button)
All buttons 50-60px, semi-transparent, positioned at bottom
GAMEPLAY:
- Ship starts in center, 3 lives
- Large asteroids split into 2 medium, medium into 2 small
- Points: Large=20, Medium=50, Small=100
- Ship has inertia - keeps drifting after thrust stops
- Bullets travel straight and wrap around screen
- Collision with asteroid = lose life, brief invincibility after respawn
- Clear all asteroids = next wave with more asteroids
PHYSICS:
- Ship rotation is instant
- Thrust adds velocity in facing direction
- Small friction to eventually slow down
- Asteroids drift at random angles and speeds
VISUAL:
- Black background with small white stars
- Ship is white triangle outline (vector style)
- Asteroids are irregular polygon outlines
- Bullets are small white dots
- Explosion particles when things are destroyed
Use canvas with vector-style graphics (lines, not filled shapes).`
},
{
id: 24,
title: "Frogger",
prompt: `Create a Frogger road-crossing game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Grid-based hop movement (one square per input)
- Cars and logs move continuously
- Simple but playable level design
CONTROLS:
- Desktop: Arrow keys to hop in that direction
- Mobile: On-screen d-pad buttons OR swipe to hop
MOBILE CONTROLS (REQUIRED):
D-pad at bottom:
- Up/Down/Left/Right buttons in cross pattern
- Each button 55px, semi-transparent
- Each tap = one hop in that direction
GAMEPLAY:
- Frog starts at bottom, goal is to reach top
- Bottom half: Road with cars/trucks moving left and right
- Top half: River with logs and turtles moving left and right
- Hit by car = lose life
- Fall in water (miss a log) = lose life
- Ride logs/turtles to cross river
- 5 home spots at top to fill
- Fill all 5 = level complete
- 3 lives, timer countdown (30 seconds per frog)
LEVEL DESIGN:
- Row 1 (bottom): Safe starting grass
- Rows 2-4: Roads with vehicles (alternate directions)
- Row 5: Safe middle grass
- Rows 6-8: River with logs/turtles
- Row 9 (top): Home bases with lily pads
VISUAL:
- Green frog (top-down view)
- Gray roads with lane markings
- Blue water
- Brown logs, green turtles
- Colorful cars/trucks
- Green grass safe zones
Use canvas, grid-based movement.`
},
{
id: 25,
title: "Simon Says",
prompt: `Create a Simon Says memory pattern game.
CRITICAL REQUIREMENTS:
- Game MUST work on both desktop and mobile
- Buttons must be large and easy to tap on mobile
- Clear visual and audio-like feedback when buttons activate
- Pattern plays automatically, then waits for player input
CONTROLS:
- Desktop: Click the colored buttons OR use keys 1,2,3,4
- Mobile: Tap the colored buttons (make them big!)
GAMEPLAY:
- Four large colored buttons (Red, Blue, Green, Yellow)
- Arranged in 2x2 grid or quadrants of a circle
- Computer shows pattern by lighting up buttons in sequence
- Player must repeat the pattern by pressing buttons
- Each round adds one more to the sequence
- Wrong button = game over, show final score (rounds completed)
- Display current round number
BUTTON BEHAVIOR:
- When showing pattern: button lights up brightly for 0.5s, 0.3s gap between
- When player presses: button lights up while pressed
- Each button should have distinct appearance when lit vs unlit
- Consider adding different tones/sounds for each button (optional)
VISUAL:
- Large, touchable buttons (at least 120px each on mobile)
- Distinct colors: Red (#ff4444), Blue (#4444ff), Green (#44ff44), Yellow (#ffff44)
- Buttons dim when inactive, bright when active
- Center area shows round number and "Watch..." or "Your turn!" message
- Dark background to make colors pop
- "Start Game" button initially, "Play Again" after game over
LAYOUT:
- Buttons should fill most of the screen
- 2x2 grid with small gaps
- Score/round display above or below buttons
- Make sure buttons are finger-friendly size
Use div-based buttons with CSS for the glow effects. No canvas needed.`
}
];
async function fixGame(gameData) {
console.log(`\nRegenerating: ${gameData.title} (ID: ${gameData.id})...`);
try {
const result = await generateGame(gameData.prompt);
if (!result.success) {
console.error(` Failed: ${result.error}`);
return false;
}
// Update the game in database
await pool.query(
`UPDATE games SET game_code = $1, prompt = $2 WHERE id = $3`,
[result.code, gameData.prompt, gameData.id]
);
console.log(` Success: ${gameData.title} updated`);
return true;
} catch (error) {
console.error(` Error: ${error.message}`);
return false;
}
}
async function main() {
console.log('Fixing games with improved controls...');
console.log(`Games to fix: ${gamesToFix.length}`);
console.log('Using Claude Sonnet 4 for generation.\n');
let successCount = 0;
for (const game of gamesToFix) {
const success = await fixGame(game);
if (success) successCount++;
// Delay between API calls
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log('\n========================================');
console.log(`COMPLETE: ${successCount}/${gamesToFix.length} games fixed`);
console.log('========================================');
await pool.end();
process.exit(0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
+197
View File
@@ -0,0 +1,197 @@
require('dotenv').config();
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
const snakeCode = `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Snake</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:100vh;font-family:Arial,sans-serif}#score{color:#4CAF50;font-size:24px;margin-bottom:10px}canvas{background:#16213e;border-radius:10px;touch-action:none}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);color:#fff;padding:30px;border-radius:10px;text-align:center;display:none;z-index:100}#gameOver button{margin-top:15px;padding:10px 30px;font-size:18px;background:#4CAF50;color:#fff;border:none;border-radius:5px;cursor:pointer}#controls{margin-top:10px;color:rgba(255,255,255,0.6);font-size:12px}#startBtn{margin-top:15px;padding:15px 40px;font-size:20px;background:#4CAF50;color:#fff;border:none;border-radius:10px;cursor:pointer;display:none}</style></head>
<body><div id="score">Score: 0</div><canvas id="game"></canvas><button id="startBtn">TAP TO START</button><div id="controls">Arrow keys or swipe to move</div><div id="gameOver"><h2>Game Over!</h2><p>Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div>
<script>
const canvas=document.getElementById('game'),ctx=canvas.getContext('2d'),scoreEl=document.getElementById('score'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore'),startBtn=document.getElementById('startBtn');
let size=Math.min(400,window.innerWidth-20),gridSize=20,tileCount=size/gridSize;
canvas.width=canvas.height=size;
let snake=[{x:10,y:10}],food={x:15,y:15},dx=0,dy=0,score=0,gameActive=false,speed=120,lastMove=0;
window.gameScore=0;
function placeFood(){
food.x=Math.floor(Math.random()*tileCount);
food.y=Math.floor(Math.random()*tileCount);
// Make sure food doesn't spawn on snake
for(let seg of snake){
if(seg.x===food.x&&seg.y===food.y){
placeFood();
return;
}
}
}
function draw(){
ctx.fillStyle='#16213e';
ctx.fillRect(0,0,size,size);
// Draw grid
ctx.strokeStyle='#1e2a4a';
for(let i=0;i<tileCount;i++){
ctx.beginPath();
ctx.moveTo(i*gridSize,0);
ctx.lineTo(i*gridSize,size);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0,i*gridSize);
ctx.lineTo(size,i*gridSize);
ctx.stroke();
}
// Draw snake
snake.forEach((seg,i)=>{
ctx.fillStyle=i===0?'#8BC34A':'#4CAF50';
ctx.fillRect(seg.x*gridSize+1,seg.y*gridSize+1,gridSize-2,gridSize-2);
if(i===0){
// Draw eyes
ctx.fillStyle='#fff';
ctx.fillRect(seg.x*gridSize+5,seg.y*gridSize+5,4,4);
ctx.fillRect(seg.x*gridSize+gridSize-9,seg.y*gridSize+5,4,4);
}
});
// Draw food
ctx.fillStyle='#e74c3c';
ctx.beginPath();
ctx.arc(food.x*gridSize+gridSize/2,food.y*gridSize+gridSize/2,gridSize/2-2,0,Math.PI*2);
ctx.fill();
// Draw start message if not started
if(!gameActive&&dx===0&&dy===0){
ctx.fillStyle='rgba(0,0,0,0.7)';
ctx.fillRect(0,size/2-40,size,80);
ctx.fillStyle='#fff';
ctx.font='20px Arial';
ctx.textAlign='center';
ctx.fillText('Press arrow key or swipe to start!',size/2,size/2+7);
}
}
function update(timestamp){
if(!gameActive){
draw();
requestAnimationFrame(update);
return;
}
if(timestamp-lastMove<speed){
requestAnimationFrame(update);
return;
}
lastMove=timestamp;
if(dx===0&&dy===0){
draw();
requestAnimationFrame(update);
return;
}
const head={x:snake[0].x+dx,y:snake[0].y+dy};
// Wall collision
if(head.x<0||head.x>=tileCount||head.y<0||head.y>=tileCount){
endGame();
return;
}
// Self collision
for(let seg of snake){
if(seg.x===head.x&&seg.y===head.y){
endGame();
return;
}
}
snake.unshift(head);
// Food collision
if(head.x===food.x&&head.y===food.y){
score+=10;
window.gameScore=score;
scoreEl.textContent='Score: '+score;
placeFood();
speed=Math.max(60,speed-3);
}else{
snake.pop();
}
draw();
requestAnimationFrame(update);
}
function endGame(){
gameActive=false;
finalScoreEl.textContent=score;
gameOverEl.style.display='block';
}
function restart(){
snake=[{x:10,y:10}];
placeFood();
dx=dy=0;
score=0;
speed=120;
window.gameScore=0;
scoreEl.textContent='Score: 0';
gameOverEl.style.display='none';
gameActive=true;
}
function setDirection(newDx,newDy){
// Prevent reversing
if(snake.length>1){
if(newDx===-dx&&dx!==0)return;
if(newDy===-dy&&dy!==0)return;
}
dx=newDx;
dy=newDy;
if(!gameActive){
gameActive=true;
}
}
document.addEventListener('keydown',e=>{
e.preventDefault();
if(e.key==='ArrowUp')setDirection(0,-1);
if(e.key==='ArrowDown')setDirection(0,1);
if(e.key==='ArrowLeft')setDirection(-1,0);
if(e.key==='ArrowRight')setDirection(1,0);
});
let touchStartX=0,touchStartY=0;
canvas.addEventListener('touchstart',e=>{
e.preventDefault();
touchStartX=e.touches[0].clientX;
touchStartY=e.touches[0].clientY;
},{passive:false});
canvas.addEventListener('touchend',e=>{
e.preventDefault();
const diffX=e.changedTouches[0].clientX-touchStartX;
const diffY=e.changedTouches[0].clientY-touchStartY;
if(Math.abs(diffX)>Math.abs(diffY)){
if(diffX>30)setDirection(1,0);
else if(diffX<-30)setDirection(-1,0);
}else{
if(diffY>30)setDirection(0,1);
else if(diffY<-30)setDirection(0,-1);
}
},{passive:false});
// Initial draw
draw();
requestAnimationFrame(update);
</script></body></html>`;
async function fix() {
try {
await pool.query(
"UPDATE games SET game_code = $1 WHERE title = 'Snake Classic'",
[snakeCode]
);
console.log('Snake game fixed!');
} finally {
await pool.end();
}
}
fix().catch(console.error);
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env node
/**
* Fix touch handling in all existing games
*
* Problem: Games restart on every touch during gameplay because the canvas
* has a touchstart/click listener that calls startGame() unconditionally.
*
* Solution: Modify touch/click handlers to only restart when gameState === 'gameOver'
*
* Usage:
* node fix-touch-handling.js # Fix all games
* node fix-touch-handling.js --dry-run # Preview changes without saving
* node fix-touch-handling.js --game 14 # Fix specific game by ID
*/
require('dotenv').config({ path: require('path').join(__dirname, '../.env') });
const { Pool } = require('pg');
const Anthropic = require('@anthropic-ai/sdk');
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'game_arcade',
user: process.env.DB_USER || 'gamearc',
password: process.env.DB_PASSWORD
});
const anthropic = new Anthropic({
apiKey: process.env.CLAUDE_API_KEY,
timeout: 120 * 1000 // 120 seconds for larger games
});
const HAIKU_MODEL = 'claude-haiku-4-5-20251001';
const FIX_PROMPT = `You are fixing touch handling in an HTML5 canvas game for mobile. The issues are:
**Problems**:
1. Game may restart on every touch during gameplay (unplayable on mobile)
2. After game over, tapping immediately starts AND plays (no pause to see score)
3. Both touchstart and click events may fire (double-tap issue)
**Required Changes**:
1. **Use a 3-state system**: 'ready' → 'playing' → 'gameOver' → 'ready'
- 'ready': Shows "Tap to Start", waits for tap
- 'playing': Game is active, taps control the game
- 'gameOver': Shows score + "Tap to Play Again", tap goes to 'ready' (NOT directly to playing)
2. **Fix touch handler to prevent double-fire**:
\`\`\`javascript
let lastTapTime = 0;
function handleTap(e) {
if (e.type === 'touchstart') e.preventDefault();
const now = Date.now();
if (now - lastTapTime < 100) return; // Debounce
lastTapTime = now;
// ... rest of tap logic
}
canvas.addEventListener('touchstart', handleTap, { passive: false });
canvas.addEventListener('click', handleTap);
\`\`\`
3. **State transitions on tap**:
- If gameState === 'ready': set gameState = 'playing', start game
- If gameState === 'playing': perform game action (flap, jump, etc.)
- If gameState === 'gameOver': set gameState = 'ready', reset game (but DON'T start yet)
4. **Update draw function** to show appropriate messages:
- ready: "Tap to Start!"
- gameOver: "Game Over! Score: X" + "Tap to Continue"
**IMPORTANT**:
- Keep all existing game mechanics intact
- Keep all existing visual styles
- Only change the state management and tap handling
- Return the complete fixed HTML code
Return ONLY the complete HTML code, no explanations.`;
async function fixGameCode(gameCode, gameTitle) {
try {
const response = await anthropic.messages.create({
model: HAIKU_MODEL,
max_tokens: 16000,
system: FIX_PROMPT,
messages: [{
role: 'user',
content: `Fix this game "${gameTitle}":\n\n${gameCode}`
}]
});
let fixedCode = response.content[0].text.trim();
// Remove markdown code blocks if present
if (fixedCode.startsWith('```html')) {
fixedCode = fixedCode.slice(7);
} else if (fixedCode.startsWith('```')) {
fixedCode = fixedCode.slice(3);
}
if (fixedCode.endsWith('```')) {
fixedCode = fixedCode.slice(0, -3);
}
return fixedCode.trim();
} catch (error) {
console.error(` Error fixing game: ${error.message}`);
return null;
}
}
async function saveGameVersion(gameId, oldCode, newCode, changeDescription) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Get current version number
const versionResult = await client.query(
'SELECT COALESCE(MAX(version_number), 0) as max_version FROM game_versions WHERE game_id = $1',
[gameId]
);
const newVersion = versionResult.rows[0].max_version + 1;
// Save version history
await client.query(
`INSERT INTO game_versions (game_id, version_number, game_code, change_type, change_description)
VALUES ($1, $2, $3, $4, $5)`,
[gameId, newVersion, oldCode, 'fix', changeDescription]
);
// Update game code
await client.query(
'UPDATE games SET game_code = $1, code_updated_at = NOW() WHERE id = $2',
[newCode, gameId]
);
await client.query('COMMIT');
return newVersion;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async function analyzeGame(gameCode) {
// Thorough heuristic check for touch issues
const issues = [];
// Look for canvas touch/click handlers
const canvasClickMatch = gameCode.match(/canvas\.(addEventListener\s*\(\s*['"](?:click|touchstart|pointerdown)['"]|onclick\s*=|ontouchstart\s*=)/gi);
const hasCanvasTouch = canvasClickMatch && canvasClickMatch.length > 0;
// Check for proper 3-state system (ready/playing/gameOver)
const hasThreeStates = /gameState\s*===?\s*['"]ready['"]/i.test(gameCode);
// Check for gameState or gameOver variable
const hasGameState = /(?:let|var|const)\s+gameState\s*=/i.test(gameCode);
const hasGameOver = /(?:let|var|const)\s+(?:gameOver|isGameOver|game_over)\s*=/i.test(gameCode);
// Check for tap debouncing
const hasTapDebounce = /lastTapTime|tapDebounce|Date\.now\(\)\s*-/i.test(gameCode);
// Check if restart/reset functions check game state before executing
const hasConditionalRestart = /if\s*\(\s*(?:gameState\s*===?\s*['"](?:gameOver|over|ended)['"]|gameOver\s*(?:===?\s*true)?|isGameOver)/i.test(gameCode);
// For games where canvas touch is the game mechanic (like Flappy Bird),
// check if game over transitions to 'ready' (not directly back to playing)
const hasFlap = /function\s+flap\s*\(/i.test(gameCode);
const flapChecksGameOver = /function\s+flap[\s\S]{0,200}if\s*\(\s*gameOver\s*\)/i.test(gameCode);
// Check if resetGame goes to 'ready' state instead of immediately allowing play
const resetGoesToReady = /resetGame[\s\S]{0,300}gameState\s*=\s*['"]ready['"]/i.test(gameCode);
// Games using d-pad buttons should NOT have canvas click for restart during play
const hasDpad = /class\s*=\s*['"].*dpad/i.test(gameCode);
const hasButtonControls = /getElementById\s*\(\s*['"](?:leftBtn|rightBtn|upBtn|downBtn|actionBtn|shootBtn)/i.test(gameCode);
// Check for problematic patterns
if (hasCanvasTouch) {
// Missing 3-state system means game may feel "restarty"
if (!hasThreeStates && hasFlap) {
issues.push('Flappy-style game missing 3-state system (ready/playing/gameOver)');
}
// No tap debouncing
if (!hasTapDebounce) {
issues.push('No tap debouncing (may double-fire on mobile)');
}
// If using d-pad/buttons AND canvas touch handler exists, check if it's properly guarded
if ((hasDpad || hasButtonControls) && !hasConditionalRestart) {
issues.push('Uses button controls but canvas touch handler may not check game state');
}
// If game uses flap mechanic, check it's guarded
if (hasFlap && !flapChecksGameOver) {
issues.push('Flap function may not check gameOver state');
}
}
// If no game state tracking at all, that's a problem
if (hasCanvasTouch && !hasGameState && !hasGameOver) {
issues.push('Has canvas touch but no gameState/gameOver tracking');
}
return {
needsFix: issues.length > 0,
issues,
details: { hasCanvasTouch, hasGameState, hasGameOver, hasThreeStates, hasTapDebounce, hasConditionalRestart, hasDpad, hasButtonControls }
};
}
async function main() {
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const gameIdArg = args.find(a => a.startsWith('--game'));
const specificGameId = gameIdArg ? parseInt(args[args.indexOf(gameIdArg) + 1] || args[args.indexOf('--game') + 1]) : null;
console.log('='.repeat(60));
console.log(' Touch Handling Fix Script');
console.log('='.repeat(60));
console.log(`Mode: ${dryRun ? 'DRY RUN (no changes will be saved)' : 'LIVE (changes will be saved)'}`);
console.log();
try {
// Get games to fix
let query = `
SELECT id, title, game_code, status
FROM games
WHERE status IN ('published', 'testing', 'pending_review')
AND game_code IS NOT NULL
AND LENGTH(game_code) > 0
`;
const params = [];
if (specificGameId) {
query += ' AND id = $1';
params.push(specificGameId);
}
query += ' ORDER BY id';
const result = await pool.query(query, params);
const games = result.rows;
console.log(`Found ${games.length} games to process\n`);
let fixed = 0;
let skipped = 0;
let errors = 0;
for (const game of games) {
console.log(`[${game.id}] ${game.title} (${game.status})`);
// Analyze if fix is needed
const analysis = await analyzeGame(game.game_code);
// Debug output
if (args.includes('--verbose')) {
console.log(' Details:', JSON.stringify(analysis.details, null, 2));
}
if (!analysis.needsFix) {
console.log(' ✓ Already has proper touch handling');
skipped++;
continue;
}
console.log(' Issues found:');
analysis.issues.forEach(issue => console.log(` - ${issue}`));
// Fix the game
console.log(' Fixing with Haiku...');
const fixedCode = await fixGameCode(game.game_code, game.title);
if (!fixedCode) {
console.log(' ✗ Failed to fix');
errors++;
continue;
}
// Check if code actually changed
if (fixedCode === game.game_code) {
console.log(' ⊘ No changes needed');
skipped++;
continue;
}
const sizeDiff = fixedCode.length - game.game_code.length;
console.log(` Code size: ${game.game_code.length}${fixedCode.length} (${sizeDiff >= 0 ? '+' : ''}${sizeDiff} bytes)`);
if (dryRun) {
console.log(' [DRY RUN] Would save changes');
fixed++;
} else {
// Save the fix
const version = await saveGameVersion(
game.id,
game.game_code,
fixedCode,
'Fixed touch handling: game only restarts on tap after game over'
);
console.log(` ✓ Fixed and saved as version ${version}`);
fixed++;
}
// Small delay to avoid rate limiting
await new Promise(r => setTimeout(r, 500));
}
console.log('\n' + '='.repeat(60));
console.log(' Summary');
console.log('='.repeat(60));
console.log(` Fixed: ${fixed}`);
console.log(` Skipped: ${skipped}`);
console.log(` Errors: ${errors}`);
console.log(` Total: ${games.length}`);
if (dryRun && fixed > 0) {
console.log('\nRun without --dry-run to apply fixes.');
}
} catch (error) {
console.error('Script failed:', error);
process.exit(1);
} finally {
await pool.end();
}
}
main();
+127
View File
@@ -0,0 +1,127 @@
-- Users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(30) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255),
display_name VARCHAR(50),
is_guest BOOLEAN DEFAULT false,
device_fingerprint VARCHAR(255),
tokens_balance INTEGER DEFAULT 5,
total_tokens_earned INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
last_login TIMESTAMP,
is_banned BOOLEAN DEFAULT false,
role VARCHAR(20) DEFAULT 'player'
);
-- Games table
CREATE TABLE IF NOT EXISTS games (
id SERIAL PRIMARY KEY,
creator_id INTEGER REFERENCES users(id),
title VARCHAR(100) NOT NULL,
description TEXT,
game_code TEXT NOT NULL,
thumbnail_url VARCHAR(500),
status VARCHAR(20) DEFAULT 'draft',
play_count INTEGER DEFAULT 0,
total_ratings INTEGER DEFAULT 0,
rating_sum INTEGER DEFAULT 0,
tokens_earned INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
published_at TIMESTAMP,
last_played TIMESTAMP,
is_featured BOOLEAN DEFAULT false,
rejection_reason TEXT
);
-- Play sessions
CREATE TABLE IF NOT EXISTS plays (
id SERIAL PRIMARY KEY,
player_id INTEGER REFERENCES users(id),
game_id INTEGER REFERENCES games(id),
started_at TIMESTAMP DEFAULT NOW(),
ended_at TIMESTAMP,
score INTEGER,
duration_seconds INTEGER,
token_spent BOOLEAN DEFAULT false,
device_fingerprint VARCHAR(255),
ip_address INET
);
-- High scores
CREATE TABLE IF NOT EXISTS high_scores (
id SERIAL PRIMARY KEY,
game_id INTEGER REFERENCES games(id),
player_id INTEGER REFERENCES users(id),
score INTEGER NOT NULL,
achieved_at TIMESTAMP DEFAULT NOW(),
UNIQUE(game_id, player_id)
);
-- Developer earnings
CREATE TABLE IF NOT EXISTS developer_earnings (
id SERIAL PRIMARY KEY,
developer_id INTEGER REFERENCES users(id),
game_id INTEGER REFERENCES games(id),
tokens_earned INTEGER NOT NULL,
play_id INTEGER REFERENCES plays(id),
created_at TIMESTAMP DEFAULT NOW()
);
-- Guest sessions (for fingerprint tracking)
CREATE TABLE IF NOT EXISTS guest_sessions (
id SERIAL PRIMARY KEY,
device_fingerprint VARCHAR(255) NOT NULL,
user_agent TEXT,
ip_address INET,
tokens_granted_today INTEGER DEFAULT 5,
last_token_grant DATE DEFAULT CURRENT_DATE,
total_plays INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
last_seen TIMESTAMP DEFAULT NOW()
);
-- Game reports (moderation)
CREATE TABLE IF NOT EXISTS game_reports (
id SERIAL PRIMARY KEY,
game_id INTEGER REFERENCES games(id),
reporter_id INTEGER REFERENCES users(id),
reason VARCHAR(50),
description TEXT,
status VARCHAR(20) DEFAULT 'pending',
reviewed_by INTEGER REFERENCES users(id),
reviewed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- Suspicious activity log
CREATE TABLE IF NOT EXISTS suspicious_activity_log (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
activity_type VARCHAR(50),
details JSONB,
ip_address INET,
device_fingerprint VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
-- Ratings table (to track one rating per user per game)
CREATE TABLE IF NOT EXISTS ratings (
id SERIAL PRIMARY KEY,
game_id INTEGER REFERENCES games(id),
user_id INTEGER REFERENCES users(id),
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(game_id, user_id)
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_games_status ON games(status);
CREATE INDEX IF NOT EXISTS idx_games_creator ON games(creator_id);
CREATE INDEX IF NOT EXISTS idx_plays_player ON plays(player_id);
CREATE INDEX IF NOT EXISTS idx_plays_game ON plays(game_id);
CREATE INDEX IF NOT EXISTS idx_high_scores_game ON high_scores(game_id);
CREATE INDEX IF NOT EXISTS idx_guest_fingerprint ON guest_sessions(device_fingerprint);
CREATE INDEX IF NOT EXISTS idx_games_published ON games(published_at) WHERE status = 'published';
CREATE INDEX IF NOT EXISTS idx_games_play_count ON games(play_count DESC) WHERE status = 'published';
@@ -0,0 +1,314 @@
-- GamerComp Full Platform Migration
-- Run with: psql -U gamearc -d game_arcade -f 002_full_platform.sql
BEGIN;
-- ============================================
-- UPDATE EXISTING TABLES
-- ============================================
-- Add missing columns to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS xp_total INTEGER DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS creator_level INTEGER DEFAULT 1;
ALTER TABLE users ADD COLUMN IF NOT EXISTS creator_badge VARCHAR(50) DEFAULT 'Newbie';
ALTER TABLE users ADD COLUMN IF NOT EXISTS daily_login_streak INTEGER DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS weekly_creation_streak INTEGER DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_date DATE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_creation_date DATE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_verified BOOLEAN DEFAULT false;
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_data JSONB;
-- Add missing columns to games table
ALTER TABLE games ADD COLUMN IF NOT EXISTS creation_prompt TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS prompt_answers JSONB;
ALTER TABLE games ADD COLUMN IF NOT EXISTS game_style VARCHAR(50);
ALTER TABLE games ADD COLUMN IF NOT EXISTS difficulty_tags VARCHAR(50)[];
ALTER TABLE games ADD COLUMN IF NOT EXISTS theme_tags VARCHAR(50)[];
ALTER TABLE games ADD COLUMN IF NOT EXISTS accessibility_features VARCHAR(50)[];
ALTER TABLE games ADD COLUMN IF NOT EXISTS is_mobile_friendly BOOLEAN DEFAULT true;
ALTER TABLE games ADD COLUMN IF NOT EXISTS favorite_count INTEGER DEFAULT 0;
ALTER TABLE games ADD COLUMN IF NOT EXISTS remix_count INTEGER DEFAULT 0;
ALTER TABLE games ADD COLUMN IF NOT EXISTS remixed_from_id INTEGER REFERENCES games(id);
ALTER TABLE games ADD COLUMN IF NOT EXISTS allow_remix BOOLEAN DEFAULT true;
ALTER TABLE games ADD COLUMN IF NOT EXISTS creator_played BOOLEAN DEFAULT false;
ALTER TABLE games ADD COLUMN IF NOT EXISTS creator_play_duration INTEGER DEFAULT 0;
ALTER TABLE games ADD COLUMN IF NOT EXISTS ai_review_passed BOOLEAN DEFAULT false;
ALTER TABLE games ADD COLUMN IF NOT EXISTS ai_review_notes TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS license VARCHAR(20) DEFAULT 'GPL-3.0';
ALTER TABLE games ADD COLUMN IF NOT EXISTS thumbnail_data TEXT;
-- Add missing columns to plays table
ALTER TABLE plays ADD COLUMN IF NOT EXISTS levels_completed INTEGER DEFAULT 0;
ALTER TABLE plays ADD COLUMN IF NOT EXISTS difficulty_played VARCHAR(20);
ALTER TABLE plays ADD COLUMN IF NOT EXISTS is_creator_play BOOLEAN DEFAULT false;
-- Update high_scores table
ALTER TABLE high_scores ADD COLUMN IF NOT EXISTS difficulty VARCHAR(20);
-- ============================================
-- CREATE NEW TABLES
-- ============================================
-- FAVORITES
CREATE TABLE IF NOT EXISTS favorites (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
game_id INTEGER REFERENCES games(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, game_id)
);
-- FOLLOWS (follow creators)
CREATE TABLE IF NOT EXISTS follows (
id SERIAL PRIMARY KEY,
follower_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
following_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(follower_id, following_id),
CHECK (follower_id != following_id)
);
-- NOTIFICATIONS
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
title VARCHAR(100),
message TEXT,
link VARCHAR(255),
is_read BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
-- ACHIEVEMENTS
CREATE TABLE IF NOT EXISTS achievements (
id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
icon VARCHAR(50),
xp_reward INTEGER DEFAULT 0,
category VARCHAR(50)
);
-- USER ACHIEVEMENTS
CREATE TABLE IF NOT EXISTS user_achievements (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
achievement_id INTEGER REFERENCES achievements(id) ON DELETE CASCADE,
earned_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, achievement_id)
);
-- COLLECTIONS (user playlists)
CREATE TABLE IF NOT EXISTS collections (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT,
is_public BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);
-- COLLECTION GAMES
CREATE TABLE IF NOT EXISTS collection_games (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
game_id INTEGER REFERENCES games(id) ON DELETE CASCADE,
added_at TIMESTAMP DEFAULT NOW(),
UNIQUE(collection_id, game_id)
);
-- XP TRANSACTIONS (track all XP gains)
CREATE TABLE IF NOT EXISTS xp_transactions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
amount INTEGER NOT NULL,
reason VARCHAR(100),
reference_id INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);
-- WEEKLY THEMES
CREATE TABLE IF NOT EXISTS weekly_themes (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
bonus_xp_multiplier DECIMAL(3,2) DEFAULT 1.5,
is_active BOOLEAN DEFAULT false
);
-- CLASSROOMS (teacher mode)
CREATE TABLE IF NOT EXISTS classrooms (
id SERIAL PRIMARY KEY,
teacher_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
join_code VARCHAR(10) UNIQUE NOT NULL,
is_public_publishing_allowed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
-- CLASSROOM STUDENTS
CREATE TABLE IF NOT EXISTS classroom_students (
id SERIAL PRIMARY KEY,
classroom_id INTEGER REFERENCES classrooms(id) ON DELETE CASCADE,
student_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP DEFAULT NOW(),
UNIQUE(classroom_id, student_id)
);
-- CLASSROOM ASSIGNMENTS
CREATE TABLE IF NOT EXISTS classroom_assignments (
id SERIAL PRIMARY KEY,
classroom_id INTEGER REFERENCES classrooms(id) ON DELETE CASCADE,
title VARCHAR(100) NOT NULL,
description TEXT,
required_game_style VARCHAR(50),
due_date TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- ============================================
-- CREATE INDEXES
-- ============================================
CREATE INDEX IF NOT EXISTS idx_games_style ON games(game_style);
CREATE INDEX IF NOT EXISTS idx_games_difficulty ON games USING GIN(difficulty_tags);
CREATE INDEX IF NOT EXISTS idx_games_themes ON games USING GIN(theme_tags);
CREATE INDEX IF NOT EXISTS idx_favorites_user ON favorites(user_id);
CREATE INDEX IF NOT EXISTS idx_favorites_game ON favorites(game_id);
CREATE INDEX IF NOT EXISTS idx_follows_follower ON follows(follower_id);
CREATE INDEX IF NOT EXISTS idx_follows_following ON follows(following_id);
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(user_id) WHERE is_read = false;
CREATE INDEX IF NOT EXISTS idx_user_achievements_user ON user_achievements(user_id);
CREATE INDEX IF NOT EXISTS idx_collections_user ON collections(user_id);
CREATE INDEX IF NOT EXISTS idx_xp_transactions_user ON xp_transactions(user_id);
CREATE INDEX IF NOT EXISTS idx_classrooms_teacher ON classrooms(teacher_id);
CREATE INDEX IF NOT EXISTS idx_classroom_students_classroom ON classroom_students(classroom_id);
CREATE INDEX IF NOT EXISTS idx_classroom_students_student ON classroom_students(student_id);
-- ============================================
-- SEED ACHIEVEMENTS
-- ============================================
INSERT INTO achievements (code, name, description, icon, xp_reward, category) VALUES
-- Creator achievements
('first_game', 'First Game', 'Published your first game', '🎮', 100, 'creator'),
('ten_games', 'Prolific Creator', 'Published 10 games', '🏭', 500, 'creator'),
('hundred_plays', 'Getting Popular', 'One of your games reached 100 plays', '📈', 200, 'creator'),
('thousand_plays', 'Viral Hit', 'One of your games reached 1000 plays', '🔥', 1000, 'creator'),
('five_stars', 'Five Star Developer', 'Got a 5-star average rating on a game', '', 300, 'creator'),
('remixed', 'Inspirational', 'Someone remixed your game', '🔄', 150, 'creator'),
('ten_remixes', 'Remix Master', '10 games were remixed from yours', '👑', 750, 'creator'),
-- Player achievements
('first_play', 'Player One', 'Played your first game', '🕹️', 25, 'player'),
('fifty_plays', 'Dedicated Gamer', 'Played 50 games', '🎯', 200, 'player'),
('high_scorer', 'High Scorer', 'Got #1 on any leaderboard', '🏆', 300, 'player'),
('all_styles', 'Genre Explorer', 'Played every game style', '🗺️', 400, 'player'),
-- Social achievements
('first_follow', 'Fan Club', 'Followed your first creator', '👥', 25, 'social'),
('ten_favorites', 'Collector', 'Favorited 10 games', '❤️', 100, 'social'),
('first_collection', 'Curator', 'Created your first collection', '📚', 50, 'social'),
-- Prompt engineering achievements
('no_edits', 'Prompt Pro', 'Created a game that needed zero edits', '', 250, 'creator'),
('detailed_prompt', 'Detail Detective', 'Used 5+ specific details in your prompt', '🔍', 100, 'creator'),
('accessibility', 'Inclusive Designer', 'Added accessibility features to a game', '', 200, 'creator'),
-- Streak achievements
('week_streak', 'Week Warrior', '7-day login streak', '📅', 150, 'special'),
('month_streak', 'Monthly Master', '30-day login streak', '🗓️', 500, 'special'),
('creation_streak', 'Creative Machine', 'Created games 4 weeks in a row', '🏃', 400, 'special'),
-- Special
('beta_tester', 'Beta Tester', 'Joined during beta', '🧪', 500, 'special'),
('bug_reporter', 'Bug Hunter', 'Reported 5 bugs that were fixed', '🐛', 200, 'special')
ON CONFLICT (code) DO NOTHING;
-- ============================================
-- CREATE HELPER FUNCTIONS
-- ============================================
-- Function to calculate creator level from XP
CREATE OR REPLACE FUNCTION get_creator_level(xp INTEGER)
RETURNS INTEGER AS $$
BEGIN
RETURN CASE
WHEN xp >= 10000 THEN 10
WHEN xp >= 6000 THEN 9
WHEN xp >= 4000 THEN 8
WHEN xp >= 2500 THEN 7
WHEN xp >= 1500 THEN 6
WHEN xp >= 1000 THEN 5
WHEN xp >= 600 THEN 4
WHEN xp >= 300 THEN 3
WHEN xp >= 100 THEN 2
ELSE 1
END;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Function to get creator badge from level
CREATE OR REPLACE FUNCTION get_creator_badge(level INTEGER)
RETURNS VARCHAR AS $$
BEGIN
RETURN CASE level
WHEN 10 THEN 'Grandmaster'
WHEN 9 THEN 'Legend'
WHEN 8 THEN 'Master'
WHEN 7 THEN 'Expert'
WHEN 6 THEN 'Pro'
WHEN 5 THEN 'Game Dev'
WHEN 4 THEN 'Creator'
WHEN 3 THEN 'Apprentice'
WHEN 2 THEN 'Beginner'
ELSE 'Newbie'
END;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Function to award XP and update level
CREATE OR REPLACE FUNCTION award_xp(p_user_id INTEGER, p_amount INTEGER, p_reason VARCHAR, p_reference_id INTEGER DEFAULT NULL)
RETURNS TABLE(new_xp INTEGER, new_level INTEGER, new_badge VARCHAR, leveled_up BOOLEAN) AS $$
DECLARE
old_level INTEGER;
current_xp INTEGER;
calculated_level INTEGER;
calculated_badge VARCHAR;
BEGIN
-- Get current XP and level
SELECT xp_total, creator_level INTO current_xp, old_level FROM users WHERE id = p_user_id;
-- Calculate new XP
current_xp := COALESCE(current_xp, 0) + p_amount;
-- Calculate new level and badge
calculated_level := get_creator_level(current_xp);
calculated_badge := get_creator_badge(calculated_level);
-- Update user
UPDATE users
SET xp_total = current_xp,
creator_level = calculated_level,
creator_badge = calculated_badge
WHERE id = p_user_id;
-- Log XP transaction
INSERT INTO xp_transactions (user_id, amount, reason, reference_id)
VALUES (p_user_id, p_amount, p_reason, p_reference_id);
-- Return results
RETURN QUERY SELECT current_xp, calculated_level, calculated_badge, (calculated_level > COALESCE(old_level, 1));
END;
$$ LANGUAGE plpgsql;
COMMIT;
-- Show summary
SELECT 'Migration complete!' as status;
SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;
@@ -0,0 +1,21 @@
-- Migration 003: Onboarding Flow
-- Adds onboarding tracking to users and new onboarding achievements
-- Add onboarding columns to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS onboarding_complete BOOLEAN DEFAULT false;
ALTER TABLE users ADD COLUMN IF NOT EXISTS onboarding_step INTEGER DEFAULT 0;
-- Create index for finding users who haven't completed onboarding
CREATE INDEX IF NOT EXISTS idx_users_onboarding ON users(onboarding_complete) WHERE onboarding_complete = false;
-- Insert onboarding achievements
INSERT INTO achievements (code, name, description, icon, xp_reward, category) VALUES
('welcome_tour', 'Welcome Tour', 'Completed the welcome tutorial', '🎉', 50, 'onboarding'),
('first_arcade_visit', 'Explorer', 'Visited the arcade for the first time', '🗺️', 25, 'onboarding'),
('profile_customized', 'Personal Touch', 'Customized your profile', '', 50, 'onboarding'),
('onboarding_complete', 'Ready to Play!', 'Completed all getting started steps', '🚀', 100, 'onboarding')
ON CONFLICT (code) DO NOTHING;
-- Grant permissions
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gamearc;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO gamearc;
@@ -0,0 +1,24 @@
-- Migration 004: Admin Audit Logs
-- Tracks all admin actions for accountability and auditing
-- Create audit_logs table
CREATE TABLE IF NOT EXISTS audit_logs (
id SERIAL PRIMARY KEY,
admin_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(100) NOT NULL,
target_type VARCHAR(50) NOT NULL, -- 'game', 'user', 'report'
target_id INTEGER NOT NULL,
details JSONB,
ip_address VARCHAR(45), -- Supports IPv6
created_at TIMESTAMP DEFAULT NOW()
);
-- Create indexes for common queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_admin_id ON audit_logs(admin_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_target ON audit_logs(target_type, target_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
-- Grant permissions
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gamearc;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO gamearc;
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env node
/**
* Publish sample games under the "Arcade Master" account
* Run with: node scripts/publish-sample-games.js
*/
require('dotenv').config();
const { Pool } = require('pg');
const bcrypt = require('bcrypt');
const fs = require('fs');
const path = require('path');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
const ARCADE_MASTER = {
username: 'ArcadeMaster',
email: 'arcademaster@gamercomp.com',
password: 'ArcadeMaster2024!',
displayName: 'Arcade Master'
};
const GAMES_DIR = path.join(__dirname, '../../games/samples');
// Game metadata (title, description, prompt for each game)
const GAME_METADATA = {
'brick-breaker.html': {
title: 'Brick Breaker',
description: 'Classic paddle and ball game! Move your paddle to bounce the ball and break all the bricks. Easy to learn, fun to play!',
prompt: 'Create a classic brick breaker game with a wide paddle, slow ball, and 5 lives. Make it easy for kids to play.'
},
'pong.html': {
title: 'Pong',
description: 'The classic 2-player game! Play against the computer and try to score 7 points first. The computer makes mistakes so you can win!',
prompt: 'Create a single-player Pong game vs AI. The AI should make mistakes sometimes so kids can win.'
},
'dino-runner.html': {
title: 'Dino Runner',
description: 'Run as far as you can! Tap or press space to jump over cacti. The game starts slow so you can learn the timing.',
prompt: 'Create a Chrome Dino style endless runner. Start very slow with obstacles far apart.'
},
'asteroids.html': {
title: 'Asteroids',
description: 'Classic space shooter! Rotate your ship and blast the asteroids. You have shields that protect you when you get hit.',
prompt: 'Create an Asteroids game with a large ship, slow asteroids, and invincibility after being hit.'
},
'whack-a-mole.html': {
title: 'Whack-a-Mole',
description: 'Tap the moles when they pop up! They stay visible for a long time so you can catch them. 60 seconds to get the high score!',
prompt: 'Create a whack-a-mole game with a 3x3 grid. Moles should stay up for 2+ seconds.'
},
'block-drop.html': {
title: 'Block Drop',
description: 'Stack falling blocks to complete lines! Pieces fall slowly so you have time to think. A ghost piece shows where blocks will land.',
prompt: 'Create a Tetris-style game with very slow falling pieces and helpful ghost piece preview.'
},
'twenty-forty-eight.html': {
title: '2048',
description: 'Swipe to combine numbers! 2+2=4, 4+4=8, and so on. No time limit - take as long as you need to plan your moves.',
prompt: 'Create the 2048 puzzle game with no time pressure and clear directional arrows.'
},
'memory-match.html': {
title: 'Memory Match',
description: 'Find all the matching pairs! Flip cards to reveal pictures and remember where they are. Only 6 pairs to start.',
prompt: 'Create a memory matching game with a small 3x4 grid (6 pairs) and cards that stay flipped for 1.5 seconds.'
},
'connect-four.html': {
title: 'Connect Four',
description: 'Drop your pieces to connect 4 in a row! Play against a friendly computer that lets you learn the game.',
prompt: 'Create Connect Four vs AI. The AI should play randomly most of the time so kids can win.'
},
'lights-out.html': {
title: 'Lights Out',
description: 'Turn off all the lights to win! Clicking a light toggles it and its neighbors. Includes a helpful tutorial!',
prompt: 'Create Lights Out puzzle with an easy 3x3 grid, interactive tutorial, and hover highlights.'
},
'geometry-runner.html': {
title: 'Geometry Runner',
description: 'Jump over obstacles in this rhythm-style runner! Starts very slow with obstacles far apart so you can learn the timing.',
prompt: 'Create a Geometry Dash style runner that starts very slow with generous jump timing.'
},
'lunar-lander.html': {
title: 'Lunar Lander',
description: 'Land your spaceship safely on the moon! You have lots of fuel and the landing pad is big. Low gravity makes it easier.',
prompt: 'Create a Lunar Lander with generous fuel, low gravity, and a large landing pad.'
},
'fruit-slicer.html': {
title: 'Fruit Slicer',
description: 'Swipe through the fruit to slice it! Fruit moves slowly and stays on screen longer. Watch out for bombs!',
prompt: 'Create a Fruit Ninja style game with big slow fruit and rare bombs at the start.'
},
'maze-chomper.html': {
title: 'Maze Chomper',
description: 'Eat all the dots while avoiding ghosts! You move much faster than the ghosts, and power pellets last a long time.',
prompt: 'Create a Pac-Man style game where the player is 3x faster than ghosts and power-ups last 10 seconds.'
},
'platformer.html': {
title: 'Platformer',
description: 'Jump and run to reach the flag! 5 lives, slow enemies, and big platforms make this adventure fun for everyone.',
prompt: 'Create a simple platformer with 5 lives, slow enemies, and generous jump mechanics.'
},
'bridge-builder.html': {
title: 'Bridge Builder',
description: 'Build a bridge to get the car across! Strong beams and a light car make building easy. Includes a helpful tutorial.',
prompt: 'Create a bridge building physics game with strong materials, light car, and visual tutorial.'
}
};
async function getOrCreateArcadeMaster() {
// Check if user exists
let result = await pool.query(
'SELECT * FROM users WHERE username = $1',
[ARCADE_MASTER.username]
);
if (result.rows.length > 0) {
console.log(`Found existing Arcade Master account (ID: ${result.rows[0].id})`);
return result.rows[0];
}
// Create new user
const passwordHash = await bcrypt.hash(ARCADE_MASTER.password, 10);
result = await pool.query(
`INSERT INTO users (username, email, password_hash, display_name, tokens_balance, role)
VALUES ($1, $2, $3, $4, 9999, 'developer')
RETURNING *`,
[ARCADE_MASTER.username, ARCADE_MASTER.email, passwordHash, ARCADE_MASTER.displayName]
);
console.log(`Created Arcade Master account (ID: ${result.rows[0].id})`);
return result.rows[0];
}
async function publishGame(creatorId, filename, gameCode) {
const metadata = GAME_METADATA[filename];
if (!metadata) {
console.log(` Skipping ${filename} - no metadata defined`);
return null;
}
// Check if game already exists
const existing = await pool.query(
'SELECT id FROM games WHERE title = $1 AND creator_id = $2',
[metadata.title, creatorId]
);
if (existing.rows.length > 0) {
// Update existing game
await pool.query(
`UPDATE games SET game_code = $1, description = $2, prompt = $3 WHERE id = $4`,
[gameCode, metadata.description, metadata.prompt, existing.rows[0].id]
);
console.log(` Updated: ${metadata.title} (ID: ${existing.rows[0].id})`);
return existing.rows[0].id;
}
// Insert new game as published
const result = await pool.query(
`INSERT INTO games (creator_id, title, description, game_code, prompt, status, published_at)
VALUES ($1, $2, $3, $4, $5, 'published', NOW())
RETURNING id`,
[creatorId, metadata.title, metadata.description, gameCode, metadata.prompt]
);
console.log(` Published: ${metadata.title} (ID: ${result.rows[0].id})`);
return result.rows[0].id;
}
async function main() {
try {
console.log('=== Publishing Sample Games ===\n');
// Get or create Arcade Master account
const arcadeMaster = await getOrCreateArcadeMaster();
console.log('');
// Read all game files
const files = fs.readdirSync(GAMES_DIR).filter(f => f.endsWith('.html'));
console.log(`Found ${files.length} game files in ${GAMES_DIR}\n`);
let published = 0;
for (const filename of files) {
const filepath = path.join(GAMES_DIR, filename);
const gameCode = fs.readFileSync(filepath, 'utf8');
const gameId = await publishGame(arcadeMaster.id, filename, gameCode);
if (gameId) published++;
}
console.log(`\n=== Complete! ===`);
console.log(`Published ${published} games under ${ARCADE_MASTER.username}`);
console.log(`\nArcade Master credentials:`);
console.log(` Username: ${ARCADE_MASTER.username}`);
console.log(` Password: ${ARCADE_MASTER.password}`);
} catch (error) {
console.error('Error:', error);
process.exit(1);
} finally {
await pool.end();
}
}
main();
+278
View File
@@ -0,0 +1,278 @@
require('dotenv').config();
const { pool } = require('../src/models/db');
const { generateGame } = require('../src/utils/claude');
const allGames = [
{
id: 14,
title: "Snake",
prompt: `Classic Snake game - mobile-first with sound.
MOBILE CONTROLS:
- Swipe anywhere to change direction
- Also add semi-transparent d-pad buttons at bottom (up/down/left/right)
GAMEPLAY:
- Snake starts moving right automatically
- Eating apple grows snake, adds 10 points
- Game over on wall/self collision
- Speed increases every 5 apples
SOUNDS: Eat sound (high beep), game over (low tone), background beat`
},
{
id: 15,
title: "Tetris",
prompt: `Tetris falling blocks - mobile-first with sound.
MOBILE CONTROLS:
- Tap left half to move left, right half to move right
- Swipe down for drop
- Tap piece or swipe up to rotate
- Add visible buttons at bottom: [←] [→] [↻] [↓]
GAMEPLAY:
- 7 tetromino shapes, auto-falling
- Clear lines for points (100/300/500/800)
- Show next piece
- Speed increases each level
SOUNDS: Move (click), rotate (whoosh), line clear (ding), game over`
},
{
id: 16,
title: "Pong",
prompt: `Pong paddle game - mobile-first with sound.
MOBILE CONTROLS:
- Touch and drag on YOUR side to move your paddle
- Single player vs AI opponent
GAMEPLAY:
- Ball bounces between paddles
- First to 5 points wins
- Ball speeds up each hit
- AI difficulty increases with score
SOUNDS: Paddle hit (pop), wall bounce (tick), score (ding), win/lose`
},
{
id: 17,
title: "Breakout",
prompt: `Breakout brick breaker - mobile-first with sound.
MOBILE CONTROLS:
- Touch anywhere and drag to move paddle
- Paddle follows finger X position
- Tap to launch ball from paddle
GAMEPLAY:
- Colorful brick rows at top
- Ball bounces off paddle to break bricks
- Each brick = 10 points
- 3 lives, lose one when ball falls
SOUNDS: Brick break (pop), paddle hit (thud), ball launch (whoosh), life lost`
},
{
id: 18,
title: "Space Invaders",
prompt: `Space Invaders shooter - mobile-first with sound.
MOBILE CONTROLS:
- Large [←] and [→] buttons at bottom corners
- Large [FIRE] button at bottom center
- Touch zones: left third moves left, right third moves right, center fires
GAMEPLAY:
- Alien grid moves and descends
- Shoot aliens for points (10-50 per row)
- Aliens shoot back randomly
- 3 lives, clear all aliens to win wave
SOUNDS: Shoot (pew), alien hit (explosion), player hit (boom), march beat`
},
{
id: 19,
title: "Pac-Man",
prompt: `Pac-Man maze game - mobile-first with sound.
MOBILE CONTROLS:
- Swipe in direction to change Pac-Man direction
- Also add d-pad overlay at bottom of screen
GAMEPLAY:
- Simple 15x15 maze
- Eat dots (10pts) and power pellets (50pts)
- 4 ghosts chase, turn blue when powered
- Eat blue ghosts for bonus (200pts)
- 3 lives
SOUNDS: Wakka-wakka eating, power pellet sound, ghost eaten, death`
},
{
id: 20,
title: "Flappy Bird",
prompt: `Flappy Bird - mobile-first with sound.
MOBILE CONTROLS:
- TAP ANYWHERE on screen to flap
- That's the only control - dead simple
- Entire game canvas is the tap zone
GAMEPLAY:
- Bird falls with gravity
- Tap to flap up
- Pass through pipe gaps
- Each pipe = 1 point
- Game over on any collision
SOUNDS: Flap (whoosh), score (ding), hit (thud), background wing beat`
},
{
id: 21,
title: "2048",
prompt: `2048 number puzzle - mobile-first with sound.
MOBILE CONTROLS:
- SWIPE in any direction to slide tiles
- Must detect swipe vs tap
- Minimum swipe distance: 30px
GAMEPLAY:
- 4x4 grid
- Swipe merges matching numbers
- New tile after each move
- Win at 2048, game over when stuck
SOUNDS: Slide (swoosh), merge (pop - higher pitch for bigger numbers), game over`
},
{
id: 22,
title: "Minesweeper",
prompt: `Minesweeper - mobile-first with sound.
MOBILE CONTROLS:
- TAP to reveal cell
- LONG PRESS (0.5s) to flag/unflag
- Cells must be large enough to tap (40px minimum)
GAMEPLAY:
- 9x9 grid, 10 mines
- Numbers show adjacent mines
- First tap always safe
- Flag all mines to win
SOUNDS: Reveal (click), flag (flag sound), mine (explosion), win (fanfare)`
},
{
id: 23,
title: "Asteroids",
prompt: `Asteroids space shooter - mobile-first with sound.
MOBILE CONTROLS:
- Left side: two buttons for rotate left/right
- Right side: THRUST button and FIRE button
- Virtual joystick alternative: drag to rotate, tap to thrust
GAMEPLAY:
- Ship in center with momentum physics
- Shoot asteroids to break them
- Large→Medium→Small
- Screen wrapping
- 3 lives
SOUNDS: Thrust (rumble), shoot (pew), asteroid break (boom), ship explode`
},
{
id: 24,
title: "Frogger",
prompt: `Frogger road crossing - mobile-first with sound.
MOBILE CONTROLS:
- D-pad at bottom: up/down/left/right buttons
- Or SWIPE in direction to hop
- Each input = one grid hop
GAMEPLAY:
- Cross road (avoid cars) then river (ride logs)
- Grid-based movement
- 5 home spots to fill
- 30 second timer per frog
- 3 lives
SOUNDS: Hop (boing), car hit (splat), water splash, home safe (ding)`
},
{
id: 25,
title: "Simon Says",
prompt: `Simon Says memory game - mobile-first with sound.
MOBILE CONTROLS:
- Four LARGE colored buttons (2x2 grid)
- Each button at least 100px
- Tap to select color
GAMEPLAY:
- Watch pattern light up
- Repeat by tapping buttons
- Pattern grows each round
- Wrong tap = game over
SOUNDS: Each color has unique tone (do, re, mi, fa), error (buzz), round complete (fanfare)`
}
];
async function regenerateGame(gameData) {
console.log(`\nRegenerating: ${gameData.title} (ID: ${gameData.id})...`);
try {
const result = await generateGame(gameData.prompt);
if (!result.success) {
console.error(` FAILED: ${result.error}`);
return false;
}
await pool.query(
`UPDATE games SET game_code = $1, prompt = $2 WHERE id = $3`,
[result.code, gameData.prompt, gameData.id]
);
console.log(` SUCCESS: ${gameData.title}`);
return true;
} catch (error) {
console.error(` ERROR: ${error.message}`);
return false;
}
}
async function main() {
console.log('='.repeat(50));
console.log('REGENERATING ALL GAMES');
console.log('Mobile-first + Sound Effects + Music');
console.log('='.repeat(50));
let success = 0;
let failed = 0;
for (const game of allGames) {
const ok = await regenerateGame(game);
if (ok) success++; else failed++;
// Delay between API calls
await new Promise(r => setTimeout(r, 2000));
}
console.log('\n' + '='.repeat(50));
console.log(`COMPLETE: ${success} success, ${failed} failed`);
console.log('='.repeat(50));
await pool.end();
process.exit(failed > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal:', err);
process.exit(1);
});
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Rollback the touch handling fixes - restore original game code
*/
require('dotenv').config({ path: require('path').join(__dirname, '../.env') });
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'game_arcade',
user: process.env.DB_USER || 'gamearc',
password: process.env.DB_PASSWORD
});
async function main() {
console.log('Rolling back touch handling fixes...\n');
// Find all games that were "fixed" and have a version to restore from
const result = await pool.query(`
SELECT gv.game_id, gv.game_code, g.title
FROM game_versions gv
JOIN games g ON g.id = gv.game_id
WHERE gv.change_type = 'fix'
AND gv.change_description LIKE '%touch handling%'
ORDER BY gv.game_id
`);
console.log(`Found ${result.rows.length} games to rollback\n`);
for (const row of result.rows) {
console.log(`[${row.game_id}] ${row.title}`);
// Restore the original code
await pool.query(
'UPDATE games SET game_code = $1, code_updated_at = NOW() WHERE id = $2',
[row.game_code, row.game_id]
);
// Delete the fix version from history
await pool.query(
`DELETE FROM game_versions
WHERE game_id = $1
AND change_type = 'fix'
AND change_description LIKE '%touch handling%'`,
[row.game_id]
);
console.log(' ✓ Restored original code');
}
console.log('\nRollback complete!');
await pool.end();
}
main().catch(err => {
console.error('Error:', err);
process.exit(1);
});
+142
View File
@@ -0,0 +1,142 @@
require('dotenv').config();
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
const games = [
{
title: "Star Catcher",
description: "Catch falling stars while avoiding meteors! Move left and right to collect as many stars as you can.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Star Catcher</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;font-family:Arial,sans-serif}#game{position:relative;background:linear-gradient(to bottom,#16213e,#0f3460);border-radius:10px;overflow:hidden;touch-action:none}#score{position:absolute;top:10px;left:10px;color:#fff;font-size:20px;z-index:10}#player{position:absolute;bottom:20px;width:60px;height:60px;font-size:50px;text-align:center;transition:left 0.1s}.item{position:absolute;font-size:30px;text-align:center}#gameOver{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.8);color:#fff;padding:30px;border-radius:10px;text-align:center;display:none}#gameOver button{margin-top:15px;padding:10px 30px;font-size:18px;background:#4CAF50;color:#fff;border:none;border-radius:5px;cursor:pointer}#instructions{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.7);font-size:12px}</style></head>
<body><div id="game"><div id="score">Score: 0</div><div id="player">🧺</div><div id="gameOver"><h2>Game Over!</h2><p>Final Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div><div id="instructions">← → Arrow keys or touch to move</div></div>
<script>const game=document.getElementById('game'),player=document.getElementById('player'),scoreEl=document.getElementById('score'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore');let width=Math.min(400,window.innerWidth-20),height=500,playerX=width/2-30,score=0,gameActive=true,items=[],speed=3;window.gameScore=0;game.style.width=width+'px';game.style.height=height+'px';player.style.left=playerX+'px';function createItem(){if(!gameActive)return;const item=document.createElement('div');item.className='item';item.textContent=Math.random()>0.3?'⭐':'☄️';item.dataset.type=item.textContent==='⭐'?'star':'meteor';item.style.left=Math.random()*(width-30)+'px';item.style.top='-30px';game.appendChild(item);items.push({el:item,y:-30,x:parseFloat(item.style.left)})}function update(){if(!gameActive)return;items.forEach((item,i)=>{item.y+=speed;item.el.style.top=item.y+'px';if(item.y>height){item.el.remove();items.splice(i,1)}else if(item.y>height-100&&item.y<height-20){const itemX=item.x,itemW=30;if(playerX<itemX+itemW&&playerX+60>itemX){if(item.el.dataset.type==='star'){score+=10;window.gameScore=score;scoreEl.textContent='Score: '+score;item.el.remove();items.splice(i,1)}else{endGame()}}}});speed=3+Math.floor(score/50)*0.5;requestAnimationFrame(update)}function endGame(){gameActive=false;finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){items.forEach(i=>i.el.remove());items=[];score=0;window.gameScore=0;speed=3;scoreEl.textContent='Score: 0';gameOverEl.style.display='none';gameActive=true;update()}document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')playerX=Math.max(0,playerX-20);if(e.key==='ArrowRight')playerX=Math.min(width-60,playerX+20);player.style.left=playerX+'px'});game.addEventListener('touchmove',e=>{e.preventDefault();const touch=e.touches[0],rect=game.getBoundingClientRect();playerX=Math.max(0,Math.min(width-60,touch.clientX-rect.left-30));player.style.left=playerX+'px'});setInterval(createItem,1000);update();</script></body></html>`
},
{
title: "Balloon Pop Frenzy",
description: "Pop as many balloons as you can before they float away! Click or tap the balloons to pop them and score points.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Balloon Pop</title>
<style>*{margin:0;padding:0}body{background:linear-gradient(to bottom,#87CEEB,#98FB98);min-height:100vh;font-family:Arial,sans-serif;overflow:hidden}#game{position:relative;width:100%;height:100vh;max-width:500px;margin:0 auto}#score{position:fixed;top:10px;left:50%;transform:translateX(-50%);background:rgba(255,255,255,0.9);padding:10px 20px;border-radius:20px;font-size:20px;font-weight:bold;color:#333;z-index:100}.balloon{position:absolute;font-size:50px;cursor:pointer;transition:transform 0.1s;user-select:none;animation:float 0.5s ease-out}.balloon:hover{transform:scale(1.1)}.balloon:active{transform:scale(0.9)}@keyframes float{from{opacity:0;transform:scale(0.5)}to{opacity:1;transform:scale(1)}}.pop{animation:pop 0.3s ease-out forwards}@keyframes pop{to{transform:scale(1.5);opacity:0}}#timer{position:fixed;top:10px;right:20px;background:rgba(255,255,255,0.9);padding:10px 15px;border-radius:20px;font-size:18px;color:#e74c3c}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:40px;border-radius:20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,0.2);display:none;z-index:200}#gameOver h2{color:#333;margin-bottom:10px}#gameOver p{font-size:24px;color:#4CAF50;margin-bottom:20px}#gameOver button{padding:15px 40px;font-size:18px;background:#4CAF50;color:white;border:none;border-radius:10px;cursor:pointer}</style></head>
<body><div id="game"><div id="score">Score: 0</div><div id="timer">Time: 30</div><div id="gameOver"><h2>Time's Up!</h2><p>Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div></div>
<script>const game=document.getElementById('game'),scoreEl=document.getElementById('score'),timerEl=document.getElementById('timer'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore');let score=0,timeLeft=30,gameActive=true,balloonInterval,timerInterval;window.gameScore=0;const colors=['🎈','🔴','🟡','🟢','🔵','🟣','🟠'];function createBalloon(){if(!gameActive)return;const balloon=document.createElement('div');balloon.className='balloon';balloon.textContent=colors[Math.floor(Math.random()*colors.length)];balloon.style.left=Math.random()*(window.innerWidth-60)+'px';balloon.style.bottom='-60px';game.appendChild(balloon);let y=-60,speed=2+Math.random()*2;function rise(){if(!gameActive){balloon.remove();return}y+=speed;balloon.style.bottom=y+'px';if(y>window.innerHeight+60){balloon.remove()}else{requestAnimationFrame(rise)}}rise();balloon.onclick=()=>{if(!gameActive)return;balloon.classList.add('pop');score+=10;window.gameScore=score;scoreEl.textContent='Score: '+score;setTimeout(()=>balloon.remove(),300)}}function startGame(){gameActive=true;score=0;timeLeft=30;window.gameScore=0;scoreEl.textContent='Score: 0';timerEl.textContent='Time: 30';gameOverEl.style.display='none';balloonInterval=setInterval(createBalloon,600);timerInterval=setInterval(()=>{timeLeft--;timerEl.textContent='Time: '+timeLeft;if(timeLeft<=0)endGame()},1000)}function endGame(){gameActive=false;clearInterval(balloonInterval);clearInterval(timerInterval);finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){document.querySelectorAll('.balloon').forEach(b=>b.remove());startGame()}startGame();</script></body></html>`
},
{
title: "Snake Classic",
description: "The classic snake game! Eat the apples to grow longer but don't hit the walls or yourself. Use arrow keys or swipe to control.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Snake</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:100vh;font-family:Arial,sans-serif}#score{color:#4CAF50;font-size:24px;margin-bottom:10px}canvas{background:#16213e;border-radius:10px;touch-action:none}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);color:#fff;padding:30px;border-radius:10px;text-align:center;display:none}#gameOver button{margin-top:15px;padding:10px 30px;font-size:18px;background:#4CAF50;color:#fff;border:none;border-radius:5px;cursor:pointer}#controls{margin-top:10px;color:rgba(255,255,255,0.6);font-size:12px}</style></head>
<body><div id="score">Score: 0</div><canvas id="game"></canvas><div id="controls">Arrow keys or swipe to move</div><div id="gameOver"><h2>Game Over!</h2><p>Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div>
<script>const canvas=document.getElementById('game'),ctx=canvas.getContext('2d'),scoreEl=document.getElementById('score'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore');let size=Math.min(400,window.innerWidth-20),gridSize=20,tileCount=size/gridSize;canvas.width=canvas.height=size;let snake=[{x:10,y:10}],food={x:15,y:15},dx=0,dy=0,score=0,gameActive=true,speed=100;window.gameScore=0;function draw(){if(!gameActive)return;ctx.fillStyle='#16213e';ctx.fillRect(0,0,size,size);ctx.fillStyle='#4CAF50';snake.forEach((seg,i)=>{ctx.fillStyle=i===0?'#8BC34A':'#4CAF50';ctx.fillRect(seg.x*gridSize+1,seg.y*gridSize+1,gridSize-2,gridSize-2)});ctx.fillStyle='#e74c3c';ctx.font=gridSize-4+'px Arial';ctx.fillText('🍎',food.x*gridSize+2,food.y*gridSize+gridSize-4)}function update(){if(!gameActive||dx===0&&dy===0)return;const head={x:snake[0].x+dx,y:snake[0].y+dy};if(head.x<0||head.x>=tileCount||head.y<0||head.y>=tileCount||snake.some(s=>s.x===head.x&&s.y===head.y)){endGame();return}snake.unshift(head);if(head.x===food.x&&head.y===food.y){score+=10;window.gameScore=score;scoreEl.textContent='Score: '+score;food={x:Math.floor(Math.random()*tileCount),y:Math.floor(Math.random()*tileCount)};speed=Math.max(50,speed-2)}else{snake.pop()}}function endGame(){gameActive=false;finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){snake=[{x:10,y:10}];food={x:15,y:15};dx=dy=0;score=0;speed=100;window.gameScore=0;scoreEl.textContent='Score: 0';gameOverEl.style.display='none';gameActive=true}function gameLoop(){update();draw();setTimeout(()=>requestAnimationFrame(gameLoop),speed)}document.addEventListener('keydown',e=>{if(e.key==='ArrowUp'&&dy!==1){dx=0;dy=-1}if(e.key==='ArrowDown'&&dy!==-1){dx=0;dy=1}if(e.key==='ArrowLeft'&&dx!==1){dx=-1;dy=0}if(e.key==='ArrowRight'&&dx!==-1){dx=1;dy=0}});let touchStartX,touchStartY;canvas.addEventListener('touchstart',e=>{touchStartX=e.touches[0].clientX;touchStartY=e.touches[0].clientY});canvas.addEventListener('touchend',e=>{const diffX=e.changedTouches[0].clientX-touchStartX,diffY=e.changedTouches[0].clientY-touchStartY;if(Math.abs(diffX)>Math.abs(diffY)){if(diffX>0&&dx!==1){dx=1;dy=0}else if(diffX<0&&dx!==1){dx=-1;dy=0}}else{if(diffY>0&&dy!==-1){dx=0;dy=1}else if(diffY<0&&dy!==1){dx=0;dy=-1}}});gameLoop();</script></body></html>`
},
{
title: "Memory Match",
description: "Test your memory! Flip cards to find matching pairs. Complete the game with as few moves as possible.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Memory Match</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:linear-gradient(135deg,#667eea,#764ba2);min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px;font-family:Arial,sans-serif}h1{color:white;margin-bottom:10px}#stats{color:white;margin-bottom:20px;font-size:18px}#grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;max-width:400px}.card{width:80px;height:80px;background:white;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:40px;cursor:pointer;transition:transform 0.3s,background 0.3s;user-select:none}.card:hover{transform:scale(1.05)}.card.hidden{background:linear-gradient(135deg,#4CAF50,#45a049)}.card.hidden::after{content:'?';font-size:30px;color:white}.card.matched{background:#98FB98;transform:scale(0.95)}.card.wrong{animation:shake 0.3s}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:40px;border-radius:20px;text-align:center;display:none;z-index:100}#gameOver h2{color:#4CAF50;margin-bottom:10px}#gameOver button{margin-top:15px;padding:12px 30px;font-size:16px;background:#4CAF50;color:white;border:none;border-radius:10px;cursor:pointer}</style></head>
<body><h1>Memory Match</h1><div id="stats">Moves: 0 | Pairs: 0/8</div><div id="grid"></div><div id="gameOver"><h2>You Win!</h2><p>Completed in <span id="finalMoves">0</span> moves</p><button onclick="restart()">Play Again</button></div>
<script>const grid=document.getElementById('grid'),statsEl=document.getElementById('stats'),gameOverEl=document.getElementById('gameOver'),finalMovesEl=document.getElementById('finalMoves');const emojis=['🎮','🎯','🎪','🎨','🎭','🎪','🎸','🎺'];let cards=[],flipped=[],matched=0,moves=0,canFlip=true;window.gameScore=0;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 createCards(){const pairs=[...emojis,...emojis];shuffle(pairs);grid.innerHTML='';cards=pairs.map((emoji,i)=>{const card=document.createElement('div');card.className='card hidden';card.dataset.emoji=emoji;card.dataset.index=i;card.onclick=()=>flipCard(card);grid.appendChild(card);return card})}function flipCard(card){if(!canFlip||flipped.includes(card)||card.classList.contains('matched'))return;card.classList.remove('hidden');card.textContent=card.dataset.emoji;flipped.push(card);if(flipped.length===2){moves++;canFlip=false;checkMatch()}}function checkMatch(){const[a,b]=flipped;if(a.dataset.emoji===b.dataset.emoji){a.classList.add('matched');b.classList.add('matched');matched++;window.gameScore=matched*10;updateStats();flipped=[];canFlip=true;if(matched===8)endGame()}else{a.classList.add('wrong');b.classList.add('wrong');setTimeout(()=>{a.classList.add('hidden');a.classList.remove('wrong');a.textContent='';b.classList.add('hidden');b.classList.remove('wrong');b.textContent='';flipped=[];canFlip=true},800)}updateStats()}function updateStats(){statsEl.textContent='Moves: '+moves+' | Pairs: '+matched+'/8'}function endGame(){finalMovesEl.textContent=moves;gameOverEl.style.display='block'}function restart(){matched=moves=0;window.gameScore=0;flipped=[];canFlip=true;gameOverEl.style.display='none';updateStats();createCards()}createCards();</script></body></html>`
},
{
title: "Whack-a-Mole",
description: "Quick! Whack the moles before they hide! Test your reflexes in this classic arcade game.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Whack-a-Mole</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:linear-gradient(to bottom,#87CEEB,#90EE90);min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px;font-family:Arial,sans-serif}h1{color:#333;margin-bottom:10px}#stats{display:flex;gap:30px;margin-bottom:20px;font-size:20px;color:#333}#grid{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.hole{width:100px;height:100px;background:#8B4513;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;position:relative;box-shadow:inset 0 -10px 20px rgba(0,0,0,0.3)}.mole{font-size:60px;opacity:0;transform:translateY(20px);transition:all 0.2s}.mole.up{opacity:1;transform:translateY(-10px)}.hole:active .mole.up{transform:scale(0.8)}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:40px;border-radius:20px;text-align:center;display:none;box-shadow:0 10px 30px rgba(0,0,0,0.3)}#gameOver button{margin-top:15px;padding:12px 30px;font-size:16px;background:#4CAF50;color:white;border:none;border-radius:10px;cursor:pointer}</style></head>
<body><h1>Whack-a-Mole!</h1><div id="stats"><span>Score: <span id="score">0</span></span><span>Time: <span id="timer">30</span></span></div><div id="grid"></div><div id="gameOver"><h2>Time's Up!</h2><p>Final Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div>
<script>const grid=document.getElementById('grid'),scoreEl=document.getElementById('score'),timerEl=document.getElementById('timer'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore');let score=0,timeLeft=30,gameActive=true,moleTimeout,gameInterval;window.gameScore=0;for(let i=0;i<9;i++){const hole=document.createElement('div');hole.className='hole';hole.innerHTML='<span class="mole">🐹</span>';hole.onclick=()=>whack(hole);grid.appendChild(hole)}const holes=[...document.querySelectorAll('.hole')],moles=[...document.querySelectorAll('.mole')];function showMole(){if(!gameActive)return;moles.forEach(m=>m.classList.remove('up'));const idx=Math.floor(Math.random()*9);moles[idx].classList.add('up');moleTimeout=setTimeout(showMole,Math.max(400,1000-score*10))}function whack(hole){const mole=hole.querySelector('.mole');if(mole.classList.contains('up')){mole.classList.remove('up');score+=10;window.gameScore=score;scoreEl.textContent=score}}function startGame(){score=0;timeLeft=30;gameActive=true;window.gameScore=0;scoreEl.textContent='0';timerEl.textContent='30';gameOverEl.style.display='none';showMole();gameInterval=setInterval(()=>{timeLeft--;timerEl.textContent=timeLeft;if(timeLeft<=0)endGame()},1000)}function endGame(){gameActive=false;clearTimeout(moleTimeout);clearInterval(gameInterval);moles.forEach(m=>m.classList.remove('up'));finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){startGame()}startGame();</script></body></html>`
},
{
title: "Color Match",
description: "Match the color to the word! But be careful - the colors are tricky. How fast can you react?",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Color Match</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;font-family:Arial,sans-serif;color:white}#stats{display:flex;gap:40px;margin-bottom:30px;font-size:20px}#word{font-size:60px;font-weight:bold;margin-bottom:10px;text-transform:uppercase}#question{font-size:18px;margin-bottom:30px;color:#ccc}.buttons{display:flex;gap:15px;flex-wrap:wrap;justify-content:center}button{padding:15px 35px;font-size:18px;border:none;border-radius:10px;cursor:pointer;transition:transform 0.1s}button:hover{transform:scale(1.05)}button:active{transform:scale(0.95)}#result{margin-top:20px;font-size:24px;height:30px}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;color:#333;padding:40px;border-radius:20px;text-align:center;display:none}#gameOver h2{color:#4CAF50}#gameOver button{margin-top:15px;background:#4CAF50;color:white}</style></head>
<body><div id="stats"><span>Score: <span id="score">0</span></span><span>Time: <span id="timer">30</span></span></div><div id="word">RED</div><div id="question">Does the COLOR match the WORD?</div><div class="buttons"><button id="yesBtn" style="background:#4CAF50;color:white" onclick="answer(true)">YES</button><button id="noBtn" style="background:#e74c3c;color:white" onclick="answer(false)">NO</button></div><div id="result"></div><div id="gameOver"><h2>Game Over!</h2><p>Final Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div>
<script>const wordEl=document.getElementById('word'),scoreEl=document.getElementById('score'),timerEl=document.getElementById('timer'),resultEl=document.getElementById('result'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore');const colors=[{name:'RED',hex:'#e74c3c'},{name:'BLUE',hex:'#3498db'},{name:'GREEN',hex:'#2ecc71'},{name:'YELLOW',hex:'#f1c40f'},{name:'PURPLE',hex:'#9b59b6'},{name:'ORANGE',hex:'#e67e22'}];let score=0,timeLeft=30,currentMatch,gameActive=true;window.gameScore=0;function newRound(){const wordColor=colors[Math.floor(Math.random()*colors.length)];const textColor=colors[Math.floor(Math.random()*colors.length)];currentMatch=wordColor.name===textColor.name;wordEl.textContent=wordColor.name;wordEl.style.color=textColor.hex}function answer(yes){if(!gameActive)return;if(yes===currentMatch){score+=10;window.gameScore=score;scoreEl.textContent=score;resultEl.textContent='✓ Correct!';resultEl.style.color='#4CAF50'}else{score=Math.max(0,score-5);window.gameScore=score;scoreEl.textContent=score;resultEl.textContent='✗ Wrong!';resultEl.style.color='#e74c3c'}setTimeout(()=>{resultEl.textContent='';newRound()},500)}function startGame(){score=0;timeLeft=30;gameActive=true;window.gameScore=0;scoreEl.textContent='0';timerEl.textContent='30';gameOverEl.style.display='none';newRound();const timer=setInterval(()=>{timeLeft--;timerEl.textContent=timeLeft;if(timeLeft<=0){gameActive=false;clearInterval(timer);finalScoreEl.textContent=score;gameOverEl.style.display='block'}},1000)}function restart(){startGame()}startGame();</script></body></html>`
},
{
title: "Fruit Ninja",
description: "Slice the fruits as they fly across the screen! Avoid the bombs or it's game over.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Fruit Ninja</title>
<style>*{margin:0;padding:0}body{background:linear-gradient(to bottom,#2c3e50,#1a252f);min-height:100vh;overflow:hidden;font-family:Arial,sans-serif}#game{position:relative;width:100%;height:100vh;touch-action:none}#score{position:fixed;top:20px;left:20px;color:white;font-size:24px;z-index:100}#lives{position:fixed;top:20px;right:20px;color:white;font-size:24px}.fruit{position:absolute;font-size:50px;cursor:crosshair;user-select:none;transition:opacity 0.2s}.sliced{opacity:0;transform:scale(1.5)rotate(180deg);transition:all 0.3s}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);color:white;padding:40px;border-radius:20px;text-align:center;display:none;z-index:200}#gameOver button{margin-top:15px;padding:12px 30px;font-size:16px;background:#e74c3c;color:white;border:none;border-radius:10px;cursor:pointer}</style></head>
<body><div id="game"><div id="score">Score: 0</div><div id="lives">❤️❤️❤️</div><div id="gameOver"><h2>Game Over!</h2><p>Final Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div></div>
<script>const game=document.getElementById('game'),scoreEl=document.getElementById('score'),livesEl=document.getElementById('lives'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore');const fruits=['🍎','🍊','🍋','🍇','🍉','🍓','🥝','🍑'];let score=0,lives=3,gameActive=true;window.gameScore=0;function createFruit(){if(!gameActive)return;const fruit=document.createElement('div');fruit.className='fruit';const isBomb=Math.random()<0.15;fruit.textContent=isBomb?'💣':fruits[Math.floor(Math.random()*fruits.length)];fruit.dataset.bomb=isBomb;const startX=Math.random()*window.innerWidth;fruit.style.left=startX+'px';fruit.style.bottom='-60px';game.appendChild(fruit);const angle=(Math.random()-0.5)*0.5,speed=15+Math.random()*5;let x=startX,y=-60,vy=speed,vx=angle*10,gravity=0.3;function animate(){if(!gameActive){fruit.remove();return}vy-=gravity;y+=vy;x+=vx;fruit.style.bottom=y+'px';fruit.style.left=x+'px';if(y<-100){fruit.remove();if(!fruit.dataset.sliced&&!isBomb){lives--;updateLives();if(lives<=0)endGame()}}else{requestAnimationFrame(animate)}}animate();fruit.onmousedown=fruit.ontouchstart=(e)=>{e.preventDefault();if(!gameActive)return;fruit.dataset.sliced='true';fruit.classList.add('sliced');if(isBomb){lives=0;endGame()}else{score+=10;window.gameScore=score;scoreEl.textContent='Score: '+score}setTimeout(()=>fruit.remove(),300)}}function updateLives(){livesEl.textContent='❤️'.repeat(lives)}function endGame(){gameActive=false;finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){score=0;lives=3;gameActive=true;window.gameScore=0;scoreEl.textContent='Score: 0';updateLives();gameOverEl.style.display='none';document.querySelectorAll('.fruit').forEach(f=>f.remove())}setInterval(createFruit,800);restart();</script></body></html>`
},
{
title: "Math Blaster",
description: "Solve math problems as fast as you can! Each correct answer earns points, but wrong answers cost time.",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Math Blaster</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body{background:linear-gradient(135deg,#667eea,#764ba2);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;font-family:Arial,sans-serif;color:white}#stats{display:flex;gap:40px;margin-bottom:30px;font-size:22px}#problem{font-size:48px;margin-bottom:30px;background:rgba(255,255,255,0.1);padding:20px 40px;border-radius:15px}#answer{font-size:32px;padding:15px 30px;border:none;border-radius:10px;text-align:center;width:150px;outline:none}#result{margin-top:20px;font-size:28px;height:40px}.answers{display:flex;gap:15px;flex-wrap:wrap;justify-content:center}.answer-btn{padding:15px 30px;font-size:24px;border:none;border-radius:10px;cursor:pointer;background:rgba(255,255,255,0.9);color:#333;min-width:80px;transition:transform 0.1s}.answer-btn:hover{transform:scale(1.1)}.answer-btn.correct{background:#4CAF50;color:white}.answer-btn.wrong{background:#e74c3c;color:white}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;color:#333;padding:40px;border-radius:20px;text-align:center;display:none}#gameOver h2{color:#4CAF50}#gameOver button{margin-top:15px;padding:12px 30px;font-size:16px;background:#4CAF50;color:white;border:none;border-radius:10px;cursor:pointer}</style></head>
<body><div id="stats"><span>Score: <span id="score">0</span></span><span>Time: <span id="timer">60</span></span></div><div id="problem">5 + 3 = ?</div><div class="answers" id="answers"></div><div id="result"></div><div id="gameOver"><h2>Time's Up!</h2><p>Final Score: <span id="finalScore">0</span></p><p>Problems Solved: <span id="solved">0</span></p><button onclick="restart()">Play Again</button></div>
<script>const problemEl=document.getElementById('problem'),answersEl=document.getElementById('answers'),scoreEl=document.getElementById('score'),timerEl=document.getElementById('timer'),resultEl=document.getElementById('result'),gameOverEl=document.getElementById('gameOver'),finalScoreEl=document.getElementById('finalScore'),solvedEl=document.getElementById('solved');let score=0,timeLeft=60,solved=0,correctAnswer,gameActive=true,timer;window.gameScore=0;function newProblem(){const ops=['+','-','×'];const op=ops[Math.floor(Math.random()*ops.length)];let a,b,answer;if(op==='+'){a=Math.floor(Math.random()*20)+1;b=Math.floor(Math.random()*20)+1;answer=a+b}else if(op==='-'){a=Math.floor(Math.random()*20)+10;b=Math.floor(Math.random()*a)+1;answer=a-b}else{a=Math.floor(Math.random()*12)+1;b=Math.floor(Math.random()*12)+1;answer=a*b}correctAnswer=answer;problemEl.textContent=a+' '+op+' '+b+' = ?';const answers=[answer];while(answers.length<4){const wrong=answer+Math.floor(Math.random()*11)-5;if(wrong!==answer&&wrong>0&&!answers.includes(wrong))answers.push(wrong)}answers.sort(()=>Math.random()-0.5);answersEl.innerHTML='';answers.forEach(ans=>{const btn=document.createElement('button');btn.className='answer-btn';btn.textContent=ans;btn.onclick=()=>checkAnswer(ans,btn);answersEl.appendChild(btn)})}function checkAnswer(ans,btn){if(!gameActive)return;if(ans===correctAnswer){btn.classList.add('correct');score+=10;solved++;window.gameScore=score;scoreEl.textContent=score;resultEl.textContent='✓ Correct!';resultEl.style.color='#4CAF50'}else{btn.classList.add('wrong');timeLeft=Math.max(0,timeLeft-3);timerEl.textContent=timeLeft;resultEl.textContent='✗ -3 seconds!';resultEl.style.color='#e74c3c'}setTimeout(()=>{resultEl.textContent='';newProblem()},600)}function startGame(){score=0;timeLeft=60;solved=0;gameActive=true;window.gameScore=0;scoreEl.textContent='0';timerEl.textContent='60';gameOverEl.style.display='none';newProblem();timer=setInterval(()=>{timeLeft--;timerEl.textContent=timeLeft;if(timeLeft<=0){gameActive=false;clearInterval(timer);finalScoreEl.textContent=score;solvedEl.textContent=solved;gameOverEl.style.display='block'}},1000)}function restart(){startGame()}startGame();</script></body></html>`
},
{
title: "Brick Breaker",
description: "Classic brick breaking action! Bounce the ball to destroy all bricks. Don't let the ball fall!",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Brick Breaker</title>
<style>*{margin:0;padding:0}body{background:#1a1a2e;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;font-family:Arial,sans-serif}#score{color:white;font-size:20px;margin-bottom:10px}canvas{background:#16213e;border-radius:10px;touch-action:none}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);color:white;padding:30px;border-radius:15px;text-align:center;display:none}#gameOver button{margin-top:15px;padding:10px 30px;font-size:16px;background:#4CAF50;color:white;border:none;border-radius:5px;cursor:pointer}#controls{color:rgba(255,255,255,0.6);margin-top:10px;font-size:12px}</style></head>
<body><div id="score">Score: 0 | Lives: 3</div><canvas id="game"></canvas><div id="controls">← → or touch to move</div><div id="gameOver"><h2 id="endTitle">Game Over</h2><p>Score: <span id="finalScore">0</span></p><button onclick="restart()">Play Again</button></div>
<script>const canvas=document.getElementById('game'),ctx=canvas.getContext('2d'),scoreEl=document.getElementById('score'),gameOverEl=document.getElementById('gameOver'),endTitleEl=document.getElementById('endTitle'),finalScoreEl=document.getElementById('finalScore');let W=Math.min(400,window.innerWidth-20),H=500;canvas.width=W;canvas.height=H;const paddleW=80,paddleH=12,ballR=8,brickRows=5,brickCols=8,brickW=(W-20)/brickCols,brickH=20;let paddleX=(W-paddleW)/2,ballX=W/2,ballY=H-50,dx=4,dy=-4,score=0,lives=3,bricks=[],gameActive=true;window.gameScore=0;function createBricks(){bricks=[];const colors=['#e74c3c','#e67e22','#f1c40f','#2ecc71','#3498db'];for(let r=0;r<brickRows;r++){for(let c=0;c<brickCols;c++){bricks.push({x:10+c*brickW,y:30+r*(brickH+5),w:brickW-4,h:brickH,color:colors[r],alive:true})}}}function draw(){ctx.clearRect(0,0,W,H);ctx.fillStyle='#4CAF50';ctx.fillRect(paddleX,H-paddleH-10,paddleW,paddleH);ctx.beginPath();ctx.arc(ballX,ballY,ballR,0,Math.PI*2);ctx.fillStyle='white';ctx.fill();bricks.forEach(b=>{if(b.alive){ctx.fillStyle=b.color;ctx.fillRect(b.x,b.y,b.w,b.h)}})}function update(){if(!gameActive)return;ballX+=dx;ballY+=dy;if(ballX<ballR||ballX>W-ballR)dx=-dx;if(ballY<ballR)dy=-dy;if(ballY>H-paddleH-10-ballR&&ballX>paddleX&&ballX<paddleX+paddleW){dy=-dy;dx+=(ballX-(paddleX+paddleW/2))*0.1}if(ballY>H){lives--;if(lives<=0){endGame(false)}else{ballX=W/2;ballY=H-50;dx=4;dy=-4}updateScore()}bricks.forEach(b=>{if(b.alive&&ballX>b.x&&ballX<b.x+b.w&&ballY>b.y&&ballY<b.y+b.h){b.alive=false;dy=-dy;score+=10;window.gameScore=score;updateScore();if(bricks.every(br=>!br.alive))endGame(true)}})}function updateScore(){scoreEl.textContent='Score: '+score+' | Lives: '+lives}function endGame(won){gameActive=false;endTitleEl.textContent=won?'You Win!':'Game Over';finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){score=0;lives=3;window.gameScore=0;paddleX=(W-paddleW)/2;ballX=W/2;ballY=H-50;dx=4;dy=-4;gameActive=true;createBricks();updateScore();gameOverEl.style.display='none'}function gameLoop(){update();draw();requestAnimationFrame(gameLoop)}document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')paddleX=Math.max(0,paddleX-20);if(e.key==='ArrowRight')paddleX=Math.min(W-paddleW,paddleX+20)});canvas.addEventListener('touchmove',e=>{e.preventDefault();const rect=canvas.getBoundingClientRect();paddleX=Math.max(0,Math.min(W-paddleW,e.touches[0].clientX-rect.left-paddleW/2))});createBricks();gameLoop();</script></body></html>`
},
{
title: "Space Invaders",
description: "Defend Earth from alien invaders! Shoot them down before they reach the bottom. Classic arcade action!",
code: `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Space Invaders</title>
<style>*{margin:0;padding:0}body{background:#000;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;font-family:Arial,sans-serif}#score{color:#0f0;font-size:20px;margin-bottom:10px;font-family:monospace}canvas{background:#000;border:2px solid #0f0}#gameOver{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,50,0,0.95);color:#0f0;padding:30px;border:2px solid #0f0;text-align:center;display:none;font-family:monospace}#gameOver button{margin-top:15px;padding:10px 30px;font-size:16px;background:#0f0;color:#000;border:none;cursor:pointer;font-family:monospace}#controls{color:#0f0;margin-top:10px;font-size:12px;font-family:monospace}</style></head>
<body><div id="score">SCORE: 0 | LIVES: 3</div><canvas id="game"></canvas><div id="controls">← → MOVE | SPACE FIRE | TAP TO SHOOT</div><div id="gameOver"><h2 id="endTitle">GAME OVER</h2><p>SCORE: <span id="finalScore">0</span></p><button onclick="restart()">PLAY AGAIN</button></div>
<script>const canvas=document.getElementById('game'),ctx=canvas.getContext('2d'),scoreEl=document.getElementById('score'),gameOverEl=document.getElementById('gameOver'),endTitleEl=document.getElementById('endTitle'),finalScoreEl=document.getElementById('finalScore');let W=Math.min(400,window.innerWidth-20),H=500;canvas.width=W;canvas.height=H;const playerW=40,playerH=20;let playerX=W/2-playerW/2,bullets=[],enemies=[],enemyBullets=[],score=0,lives=3,gameActive=true,enemyDir=1,enemySpeed=1;window.gameScore=0;function createEnemies(){enemies=[];for(let r=0;r<4;r++){for(let c=0;c<8;c++){enemies.push({x:30+c*40,y:50+r*35,w:30,h:20,alive:true})}}}function draw(){ctx.fillStyle='#000';ctx.fillRect(0,0,W,H);ctx.fillStyle='#0f0';ctx.fillRect(playerX,H-playerH-10,playerW,playerH);ctx.fillStyle='#0f0';bullets.forEach(b=>{ctx.fillRect(b.x-2,b.y,4,10)});ctx.fillStyle='#fff';enemies.forEach(e=>{if(e.alive){ctx.font='25px Arial';ctx.fillText('👾',e.x,e.y+20)}});ctx.fillStyle='#f00';enemyBullets.forEach(b=>{ctx.fillRect(b.x-2,b.y,4,10)})}function update(){if(!gameActive)return;bullets.forEach((b,i)=>{b.y-=8;if(b.y<0)bullets.splice(i,1)});enemyBullets.forEach((b,i)=>{b.y+=5;if(b.y>H)enemyBullets.splice(i,1);if(b.y>H-playerH-10&&b.x>playerX&&b.x<playerX+playerW){lives--;enemyBullets.splice(i,1);if(lives<=0)endGame(false);updateScore()}});let hitEdge=false;enemies.forEach(e=>{if(e.alive){e.x+=enemyDir*enemySpeed;if(e.x<10||e.x>W-40)hitEdge=true;if(e.y>H-60)endGame(false)}});if(hitEdge){enemyDir*=-1;enemies.forEach(e=>e.y+=20)}if(Math.random()<0.01){const shooters=enemies.filter(e=>e.alive);if(shooters.length){const e=shooters[Math.floor(Math.random()*shooters.length)];enemyBullets.push({x:e.x+15,y:e.y+20})}}bullets.forEach((b,bi)=>{enemies.forEach(e=>{if(e.alive&&b.x>e.x&&b.x<e.x+e.w&&b.y>e.y&&b.y<e.y+e.h){e.alive=false;bullets.splice(bi,1);score+=10;window.gameScore=score;updateScore();if(enemies.every(en=>!en.alive))endGame(true)}})});enemySpeed=1+Math.floor(score/100)*0.2}function updateScore(){scoreEl.textContent='SCORE: '+score+' | LIVES: '+lives}function shoot(){if(gameActive)bullets.push({x:playerX+playerW/2,y:H-playerH-15})}function endGame(won){gameActive=false;endTitleEl.textContent=won?'YOU WIN!':'GAME OVER';finalScoreEl.textContent=score;gameOverEl.style.display='block'}function restart(){score=0;lives=3;window.gameScore=0;bullets=[];enemyBullets=[];enemyDir=1;enemySpeed=1;playerX=W/2-playerW/2;gameActive=true;createEnemies();updateScore();gameOverEl.style.display='none'}function gameLoop(){update();draw();requestAnimationFrame(gameLoop)}document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')playerX=Math.max(0,playerX-15);if(e.key==='ArrowRight')playerX=Math.min(W-playerW,playerX+15);if(e.key===' ')shoot()});canvas.addEventListener('touchstart',e=>{const rect=canvas.getBoundingClientRect(),tx=e.touches[0].clientX-rect.left;if(tx<W/3)playerX=Math.max(0,playerX-20);else if(tx>W*2/3)playerX=Math.min(W-playerW,playerX+20);else shoot()});createEnemies();gameLoop();</script></body></html>`
}
];
async function insertGames() {
const client = await pool.connect();
try {
// Get admin user ID
const adminResult = await client.query(
"SELECT id FROM users WHERE role = 'admin' LIMIT 1"
);
if (adminResult.rows.length === 0) {
console.error('No admin user found!');
return;
}
const adminId = adminResult.rows[0].id;
console.log('Using admin ID:', adminId);
for (const game of games) {
const result = await client.query(
`INSERT INTO games (creator_id, title, description, game_code, status, published_at, play_count)
VALUES ($1, $2, $3, $4, 'published', NOW(), $5)
ON CONFLICT DO NOTHING
RETURNING id, title`,
[adminId, game.title, game.description, game.code, Math.floor(Math.random() * 100) + 10]
);
if (result.rows[0]) {
console.log('Created game:', result.rows[0].title);
}
}
console.log('All games created successfully!');
} finally {
client.release();
await pool.end();
}
}
insertGames().catch(console.error);