Files
gamercomp/backend/scripts/fix-touch-handling.js
T
2026-04-30 02:14:25 +00:00

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();