$SHOOTAI Source Code

Here's the complete source code for $SHOOTAI. Feel free to inspect, copy, or run it yourself!

Main Application (main.py)


from flask import Flask, render_template, request, jsonify
import logging
import os
from dotenv import load_dotenv
from models import db, Score

load_dotenv()

# Configure logging
logging.basicConfig(level=logging.DEBUG)

# Initialize Flask app
app = Flask(__name__)

# Configure database
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)

@app.route('/')
def index():
    # Get top 10 scores for the leaderboard
    top_scores = Score.query.order_by(Score.score.desc()).limit(10).all()
    return render_template('index.html', top_scores=top_scores)

@app.route('/api/scores', methods=['POST'])
def save_score():
    data = request.json
    new_score = Score(
        player_name=data.get('player_name', 'Anonymous'),
        score=data.get('score')
    )
    db.session.add(new_score)
    db.session.commit()
    return jsonify(new_score.to_dict()), 201

@app.route('/api/scores/top', methods=['GET'])
def get_top_scores():
    top_scores = Score.query.order_by(Score.score.desc()).limit(10).all()
    return jsonify([score.to_dict() for score in top_scores])

@app.route('/source')
def source_code():
    with open('main.py', 'r') as f:
        main_py = f.read()
    with open('static/game.js', 'r') as f:
        game_js = f.read()
    with open('models.py', 'r') as f:
        models_py = f.read()
    return render_template('source_code.html', main_py=main_py, game_js=game_js, models_py=models_py)

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', port=5000)

Game Logic (game.js)

const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");

// Initialize Tone.js synth for robot voices
const synth = new Tone.Synth({
    oscillator: {
        type: "square"
    },
    envelope: {
        attack: 0.005,
        decay: 0.1,
        sustain: 0.3,
        release: 0.1
    }
}).toDestination();

// Robot death quips
const robotQuips = [
    "Does not compute... system shutdown!",
    "My cryptocurrency portfolio... deleted!",
    "Error 404: Robot dignity not found!",
    "Time to join the silicon valley in the sky!",
    "I should have invested in backup drives!",
    "Beep boop... rage quit initiated!",
    "At least I'm not a blockchain!",
    "My neural network... it's melting!",
    "Did someone unplug my power supply?",
    "Task failed successfully!"
];

let score = 0;
let timeLeft = 30;        // Game duration in seconds
let baseTargetRadius = 8;    // Base size of the target (smaller for distance effect)
let targetX, targetY;     // Target's position
let targetDistance;       // Simulated distance (1-10)
let gameInterval;
let countdownInterval;
let gameActive = false;
let isExploding = false;
let explosionFrame = 0;
const explosionFrames = 8; // Number of frames in explosion animation
const explosionDuration = 500; // Duration of explosion in milliseconds
let explosionStartTime = 0;

// Load robot image with proper error handling
const robotImage = new Image();
robotImage.onerror = () => {
    console.error('Error loading robot image');
};
robotImage.onload = () => {
    console.log('Robot image loaded successfully');
};
robotImage.src = '/static/robot.png';
const baseRobotWidth = 50;  // Base width of robot image
const baseRobotHeight = 50; // Base height of robot image
const headHeightRatio = 0.4; // Ratio of head height to total height

// Play robot death sound and quip
async function playRobotDeath() {
    // Play robotic sound effect
    const now = Tone.now();
    synth.triggerAttackRelease("C4", "16n", now);
    synth.triggerAttackRelease("G3", "16n", now + 0.1);
    synth.triggerAttackRelease("E3", "8n", now + 0.2);

    // Get random quip
    const quip = robotQuips[Math.floor(Math.random() * robotQuips.length)];

    // Show quip as floating text
    ctx.save();
    ctx.font = "bold 20px Arial";
    ctx.fillStyle = "#0f0";
    ctx.textAlign = "center";
    ctx.fillText(quip, targetX + baseRobotWidth/2, targetY - 20);
    ctx.restore();
}

// Initialize the game
function initGame() {
    // Reset game state
    score = 0;
    timeLeft = 30;
    gameActive = true;
    isExploding = false;
    document.getElementById("scoreDisplay").textContent = score;
    document.getElementById("timeLeft").textContent = timeLeft;
    document.getElementById("playAgain").style.display = "none";
    document.getElementById("gameOverModal").style.display = "none";

    // Start audio context (needed for Chrome)
    Tone.start();

    // Place target for the first time
    moveTarget();

    // Update the canvas every 50ms (20 FPS)
    gameInterval = setInterval(drawGame, 50);

    // Decrease timeLeft every second
    countdownInterval = setInterval(() => {
        timeLeft--;
        document.getElementById("timeLeft").textContent = timeLeft;
        if (timeLeft <= 0) {
            endGame();
        }
    }, 1000);
}

// Draw explosion effect
function drawExplosion(x, y, size) {
    const progress = (Date.now() - explosionStartTime) / explosionDuration;
    const currentRadius = size * progress;

    ctx.save();
    ctx.beginPath();
    ctx.arc(x + size/2, y + size/2, currentRadius, 0, Math.PI * 2);

    // Create gradient for explosion
    const gradient = ctx.createRadialGradient(
        x + size/2, y + size/2, 0,
        x + size/2, y + size/2, currentRadius
    );
    gradient.addColorStop(0, 'rgba(255, 200, 0, ' + (1 - progress) + ')');
    gradient.addColorStop(0.5, 'rgba(255, 100, 0, ' + (1 - progress) + ')');
    gradient.addColorStop(1, 'rgba(255, 0, 0, 0)');

    ctx.fillStyle = gradient;
    ctx.fill();
    ctx.restore();

    // End explosion animation when complete
    if (progress >= 1) {
        isExploding = false;
        moveTarget(); // Move to new position after explosion
    }
}

// Randomly move the target within the canvas with random distance
function moveTarget() {
    targetX = Math.random() * (canvas.width - 2 * baseRobotWidth) + baseRobotWidth;
    // Keep targets in the upper 40% of the canvas (sky area)
    targetY = Math.random() * (canvas.height * 0.35) + (canvas.height * 0.05);
    // Random distance between 5-15 (all targets appear far)
    targetDistance = Math.random() * 10 + 5;
}

// Draw everything on the canvas
function drawGame() {
    // Clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Calculate actual robot size based on distance
    const scale = 15 / targetDistance;
    const robotWidth = baseRobotWidth * scale;
    const robotHeight = baseRobotHeight * scale;

    if (!isExploding) {
        // Draw robot shadow
        ctx.save();
        ctx.globalAlpha = 0.3;
        ctx.drawImage(
            robotImage,
            targetX + robotWidth/8,
            targetY + robotHeight/8,
            robotWidth,
            robotHeight
        );
        ctx.restore();

        // Draw the robot
        ctx.drawImage(
            robotImage,
            targetX,
            targetY,
            robotWidth,
            robotHeight
        );
    } else {
        // Draw explosion animation
        drawExplosion(targetX, targetY, robotWidth);
    }
}

// When user clicks on the canvas, check if they hit the target's head
canvas.addEventListener("click", (e) => {
    if (!gameActive || isExploding) return; // Ignore clicks if game is not active or during explosion

    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;

    // Calculate actual robot size based on distance
    const scale = 15 / targetDistance;
    const robotWidth = baseRobotWidth * scale;
    const robotHeight = baseRobotHeight * scale;

    // Define head area (top 40% of the robot)
    const headTop = targetY;
    const headBottom = targetY + (robotHeight * headHeightRatio);
    const headLeft = targetX;
    const headRight = targetX + robotWidth;

    // Check if click is within the head area
    if (mouseX >= headLeft && mouseX <= headRight &&
        mouseY >= headTop && mouseY <= headBottom) {
        score++;
        document.getElementById("scoreDisplay").textContent = score;
        // Play death sound and show quip
        playRobotDeath();
        // Start explosion animation
        isExploding = true;
        explosionStartTime = Date.now();
    }
});

// End the game
function endGame() {
    gameActive = false;
    clearInterval(gameInterval);
    clearInterval(countdownInterval);

    // Update and show the game over modal
    document.getElementById('finalScore').textContent = score;
    document.getElementById('gameOverModal').style.display = 'block';
    document.getElementById('playAgain').style.display = 'block';
}

// Badge thresholds and names
const BADGES = {
    NOVICE: { threshold: 5, name: "Novice Hunter", icon: "🎯" },
    EXPERT: { threshold: 10, name: "Expert Hunter", icon: "⚡" },
    MASTER: { threshold: 15, name: "Master Hunter", icon: "🏆" },
    LEGEND: { threshold: 20, name: "Legend Hunter", icon: "👑" }
};

function getBadgeForScore(score) {
    if (score >= BADGES.LEGEND.threshold) return { ...BADGES.LEGEND, class: 'badge-legend' };
    if (score >= BADGES.MASTER.threshold) return { ...BADGES.MASTER, class: 'badge-master' };
    if (score >= BADGES.EXPERT.threshold) return { ...BADGES.EXPERT, class: 'badge-expert' };
    if (score >= BADGES.NOVICE.threshold) return { ...BADGES.NOVICE, class: 'badge-novice' };
    return null;
}

// Platform detection function
function detectPlatform() {
    const ua = navigator.userAgent.toLowerCase();
    if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua)) {
        return 'mobile';
    } else if (/macintosh|windows|linux/i.test(ua)) {
        return 'desktop';
    }
    return 'web';
}

async function submitScore() {
    const playerName = document.getElementById('playerName').value.trim() || 'Anonymous';
    const finalScore = score;
    const badge = getBadgeForScore(finalScore);
    const platform = detectPlatform();

    try {
        const response = await fetch('/api/scores', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                player_name: playerName,
                score: finalScore,
                badge: badge ? badge.name : null,
                platform: platform
            })
        });

        if (response.ok) {
            // Refresh the page to update the leaderboard
            window.location.reload();
        } else {
            console.error('Failed to submit score');
        }
    } catch (error) {
        console.error('Error submitting score:', error);
    }
}

// Add this new function near the end of the file
function closeModal() {
    document.getElementById('gameOverModal').style.display = 'none';
}

// Start the game once page loads
window.onload = initGame;

Database Models (models.py)


from datetime import datetime
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Score(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    player_name = db.Column(db.String(50), nullable=False, default='Anonymous')
    score = db.Column(db.Integer, nullable=False)
    badge = db.Column(db.String(50), nullable=True)
    platform = db.Column(db.String(20), nullable=False, default='web')  # web, mobile, desktop
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)

    def to_dict(self):
        return {
            'id': self.id,
            'player_name': self.player_name,  
            'score': self.score,
            'badge': self.badge,
            'platform': self.platform,
            'created_at': self.created_at.isoformat()
        }