334 lines
11 KiB
JavaScript
Executable File
334 lines
11 KiB
JavaScript
Executable File
#!/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();
|