Here's the complete source code for $SHOOTAI. Feel free to inspect, copy, or run it yourself!
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)
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;
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()
}