Initial commit
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
Executable
+333
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
Executable
+61
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user