initial commit
This commit is contained in:
606
guessinggame/main.py
Normal file
606
guessinggame/main.py
Normal file
@@ -0,0 +1,606 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Face Guessing Game - Simplified version with auto-advance and no repeats
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import io
|
||||
import base64
|
||||
from flask import Flask, render_template_string, request, jsonify
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
import cv2
|
||||
import numpy as np
|
||||
from functools import lru_cache
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
class FaceGuessingGame:
|
||||
def __init__(self, images_dir="./images/"):
|
||||
self.images_dir = Path(images_dir)
|
||||
self.used_images = set()
|
||||
self.available_images = []
|
||||
self.current_image = None
|
||||
self.current_answer = None
|
||||
self.score = 0
|
||||
self.total_games = 0
|
||||
self.face_cascade = None
|
||||
self.load_face_detector()
|
||||
self.load_images()
|
||||
|
||||
def load_face_detector(self):
|
||||
"""Load face detection cascade classifier"""
|
||||
try:
|
||||
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
|
||||
if os.path.exists(cascade_path):
|
||||
self.face_cascade = cv2.CascadeClassifier(cascade_path)
|
||||
print("✅ Face detector loaded")
|
||||
else:
|
||||
print("⚠️ Using fallback cropping")
|
||||
except:
|
||||
self.face_cascade = None
|
||||
|
||||
def load_images(self):
|
||||
"""Load all image files from the images directory"""
|
||||
if not self.images_dir.exists():
|
||||
self.images_dir.mkdir(exist_ok=True)
|
||||
print(f"📁 Created images directory: {self.images_dir}")
|
||||
|
||||
image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff'}
|
||||
all_images = [
|
||||
f for f in self.images_dir.iterdir()
|
||||
if f.is_file() and f.suffix.lower() in image_extensions
|
||||
]
|
||||
|
||||
self.available_images = all_images.copy()
|
||||
self.used_images.clear()
|
||||
|
||||
if all_images:
|
||||
print(f"✅ Loaded {len(all_images)} images")
|
||||
else:
|
||||
print("⚠️ No images found - add images to ./images/ folder")
|
||||
|
||||
def filename_to_name(self, filename):
|
||||
"""Convert filename to readable name"""
|
||||
name = filename.stem
|
||||
name = re.sub(r'[_-]', ' ', name)
|
||||
name = re.sub(r'\s+', ' ', name).strip().title()
|
||||
return name
|
||||
|
||||
def get_random_image(self):
|
||||
"""Get a random unused image, reset if all used"""
|
||||
if not self.available_images:
|
||||
return None
|
||||
|
||||
if len(self.used_images) >= len(self.available_images):
|
||||
# Reset if all images have been used
|
||||
self.used_images.clear()
|
||||
print("🔄 All images used - resetting game")
|
||||
|
||||
available = [img for img in self.available_images if img not in self.used_images]
|
||||
if not available:
|
||||
return None
|
||||
|
||||
image_file = random.choice(available)
|
||||
self.used_images.add(image_file)
|
||||
return image_file
|
||||
|
||||
@lru_cache(maxsize=50)
|
||||
def crop_face_portion_cached(self, image_path_str, crop_ratio):
|
||||
"""Cached version of face cropping"""
|
||||
image_path = Path(image_path_str)
|
||||
try:
|
||||
result = self.detect_faces(image_path)
|
||||
if result is None:
|
||||
return self.crop_random_portion(image_path, crop_ratio)
|
||||
|
||||
faces, img = result
|
||||
|
||||
if len(faces) == 0:
|
||||
return self.crop_random_portion(image_path, crop_ratio)
|
||||
|
||||
# Use the largest face found
|
||||
x, y, w, h = sorted(faces, key=lambda x: x[2] * x[3], reverse=True)[0]
|
||||
|
||||
height, width = img.shape[:2]
|
||||
center_x = x + w // 2
|
||||
center_y = y + h // 2
|
||||
|
||||
# Ultra-hard cropping for higher difficulties
|
||||
if crop_ratio <= 0.08:
|
||||
crop_width = int(w * 0.4)
|
||||
crop_height = int(h * 0.3)
|
||||
center_y = y + int(h * 0.6)
|
||||
elif crop_ratio <= 0.12:
|
||||
crop_width = int(w * 0.5)
|
||||
crop_height = int(h * 0.4)
|
||||
center_y = y + int(h * random.choice([0.3, 0.5, 0.7]))
|
||||
else:
|
||||
crop_width = int(w * (0.3 + crop_ratio))
|
||||
crop_height = int(h * (0.3 + crop_ratio))
|
||||
|
||||
crop_x1 = max(0, center_x - crop_width // 2)
|
||||
crop_y1 = max(0, center_y - crop_height // 2)
|
||||
crop_x2 = min(width, center_x + crop_width // 2)
|
||||
crop_y2 = min(height, center_y + crop_height // 2)
|
||||
|
||||
if (crop_x2 - crop_x1) < 50 or (crop_y2 - crop_y1) < 50:
|
||||
crop_x1, crop_y1, crop_x2, crop_y2 = x, y, x + w, y + h
|
||||
|
||||
cropped_img = img[crop_y1:crop_y2, crop_x1:crop_x2]
|
||||
cropped_img_rgb = cv2.cvtColor(cropped_img, cv2.COLOR_BGR2RGB)
|
||||
pil_img = Image.fromarray(cropped_img_rgb)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
pil_img.save(buffer, format='JPEG', quality=85)
|
||||
img_str = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
return f"data:image/jpeg;base64,{img_str}"
|
||||
|
||||
except Exception as e:
|
||||
return self.crop_random_portion(image_path, crop_ratio)
|
||||
|
||||
def detect_faces(self, image_path):
|
||||
"""Detect faces in image"""
|
||||
try:
|
||||
img = cv2.imread(str(image_path))
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
if self.face_cascade is not None:
|
||||
faces = self.face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(30, 30))
|
||||
else:
|
||||
height, width = gray.shape
|
||||
faces = [(0, 0, width, height)]
|
||||
|
||||
return faces, img
|
||||
except:
|
||||
return None
|
||||
|
||||
def crop_random_portion(self, image_path, crop_ratio):
|
||||
"""Fallback cropping method"""
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
if img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
width, height = img.size
|
||||
crop_width = max(50, int(width * crop_ratio))
|
||||
crop_height = max(50, int(height * crop_ratio))
|
||||
|
||||
x = random.randint(0, max(0, width - crop_width))
|
||||
y = random.randint(0, max(0, height - crop_height))
|
||||
|
||||
cropped = img.crop((x, y, x + crop_width, y + crop_height))
|
||||
|
||||
buffer = io.BytesIO()
|
||||
cropped.save(buffer, format='JPEG', quality=85)
|
||||
img_str = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
return f"data:image/jpeg;base64,{img_str}"
|
||||
except:
|
||||
return None
|
||||
|
||||
def new_game(self, difficulty='normal'):
|
||||
"""Start a new game with unused image"""
|
||||
image_file = self.get_random_image()
|
||||
if not image_file:
|
||||
return None, None
|
||||
|
||||
self.current_image = image_file
|
||||
self.current_answer = self.filename_to_name(image_file)
|
||||
|
||||
crop_ratios = {
|
||||
'easy': 0.6, 'normal': 0.4, 'hard': 0.25,
|
||||
'expert': 0.15, 'insane': 0.12, 'extreme': 0.08
|
||||
}
|
||||
|
||||
crop_ratio = crop_ratios.get(difficulty, 0.4)
|
||||
cropped_image = self.crop_face_portion_cached(str(image_file), crop_ratio)
|
||||
|
||||
return cropped_image, self.current_answer
|
||||
|
||||
def check_guess(self, guess):
|
||||
"""Check if guess is correct (lenient matching)"""
|
||||
if not self.current_answer or not guess:
|
||||
return False
|
||||
|
||||
guess_clean = re.sub(r'[^a-zA-Z0-9\s]', '', guess.strip().lower())
|
||||
answer_clean = re.sub(r'[^a-zA-Z0-9\s]', '', self.current_answer.lower())
|
||||
|
||||
# Exact match
|
||||
if guess_clean == answer_clean:
|
||||
return True
|
||||
|
||||
# Partial match (at least one word matches)
|
||||
guess_words = set(guess_clean.split())
|
||||
answer_words = set(answer_clean.split())
|
||||
|
||||
return len(guess_words & answer_words) > 0
|
||||
|
||||
def update_score(self, correct):
|
||||
"""Update game statistics"""
|
||||
self.total_games += 1
|
||||
if correct:
|
||||
self.score += 1
|
||||
|
||||
# Initialize game
|
||||
game = FaceGuessingGame()
|
||||
|
||||
# Ultra-simple UI HTML Template
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Face Guess</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.face-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
border: 3px solid #444;
|
||||
border-radius: 12px;
|
||||
margin: 0 auto 20px;
|
||||
background: #2a2a2a;
|
||||
display: none;
|
||||
}
|
||||
.input-container {
|
||||
position: relative;
|
||||
max-width: 300px;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
#guessInput {
|
||||
width: 100%;
|
||||
padding: 15px 50px 15px 15px;
|
||||
font-size: 18px;
|
||||
border: 2px solid #555;
|
||||
border-radius: 8px;
|
||||
background: #2a2a2a;
|
||||
color: white;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
#guessInput:focus {
|
||||
border-color: #007acc;
|
||||
}
|
||||
#guessInput:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.skip-btn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.skip-btn:hover:not(:disabled) {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
.skip-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
color: #ccc;
|
||||
}
|
||||
.stat { text-align: center; }
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
.difficulty {
|
||||
margin: 15px 0;
|
||||
}
|
||||
select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: #2a2a2a;
|
||||
color: white;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
.message {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
display: none;
|
||||
}
|
||||
.correct { background: #2d5a2d; color: #90ee90; }
|
||||
.incorrect { background: #5a2d2d; color: #ffb6c1; }
|
||||
.skip { background: #3a3a5a; color: #aaaaff; }
|
||||
.no-images {
|
||||
padding: 40px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 12px;
|
||||
border: 2px dashed #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Rate:</h1>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div>Score</div>
|
||||
<div class="stat-value" id="score">0</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div>Accuracy</div>
|
||||
<div class="stat-value" id="accuracy">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img id="gameImage" class="face-image" alt="Who is this?">
|
||||
|
||||
<div class="difficulty">
|
||||
<select id="difficulty" onchange="newGame()">
|
||||
<option value="easy">Easy</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="hard">Hard</option>
|
||||
<option value="expert">Expert</option>
|
||||
<option value="insane">Insane</option>
|
||||
<option value="extreme">Extreme</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<input type="text" id="guessInput" placeholder="Enter name..." autocomplete="off">
|
||||
<button class="skip-btn" id="skipBtn" title="Skip this person">?</button>
|
||||
</div>
|
||||
|
||||
<div id="message" class="message"></div>
|
||||
|
||||
<div id="noImages" class="no-images" style="display: none;">
|
||||
<h3>No Images Found</h3>
|
||||
<p>Add images to ./images/ folder to start playing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let gameActive = false;
|
||||
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === 'Enter' && gameActive) {
|
||||
submitGuess();
|
||||
}
|
||||
}
|
||||
|
||||
function submitGuess() {
|
||||
if (!gameActive) return;
|
||||
|
||||
const guess = document.getElementById('guessInput').value.trim();
|
||||
if (!guess) return;
|
||||
|
||||
gameActive = false;
|
||||
document.getElementById('guessInput').disabled = true;
|
||||
document.getElementById('skipBtn').disabled = true;
|
||||
|
||||
fetch('/guess', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guess: guess })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showMessage(data.correct ? '✓ Correct!' : '✗ Wrong!', data.correct);
|
||||
updateStats(data.score, data.total_games, data.accuracy);
|
||||
|
||||
if (data.correct) {
|
||||
// Auto-advance on correct guess
|
||||
setTimeout(newGame, 800);
|
||||
} else {
|
||||
// Re-enable input for retry
|
||||
setTimeout(() => {
|
||||
document.getElementById('guessInput').disabled = false;
|
||||
document.getElementById('skipBtn').disabled = false;
|
||||
document.getElementById('guessInput').focus();
|
||||
gameActive = true;
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function skipGame() {
|
||||
if (!gameActive) return;
|
||||
|
||||
gameActive = false;
|
||||
document.getElementById('guessInput').disabled = true;
|
||||
document.getElementById('skipBtn').disabled = true;
|
||||
|
||||
fetch('/skip')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showMessage(`Skipped! It was ${data.answer}`, 'skip');
|
||||
updateStats(data.score, data.total_games, data.accuracy);
|
||||
|
||||
// Auto-advance after skip
|
||||
setTimeout(newGame, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function newGame() {
|
||||
const difficulty = document.getElementById('difficulty').value;
|
||||
|
||||
fetch('/new_game', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ difficulty: difficulty })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('gameImage').src = data.image_data;
|
||||
document.getElementById('gameImage').style.display = 'block';
|
||||
document.getElementById('noImages').style.display = 'none';
|
||||
document.getElementById('guessInput').value = '';
|
||||
document.getElementById('guessInput').disabled = false;
|
||||
document.getElementById('skipBtn').disabled = false;
|
||||
document.getElementById('guessInput').focus();
|
||||
document.getElementById('message').style.display = 'none';
|
||||
gameActive = true;
|
||||
} else {
|
||||
document.getElementById('gameImage').style.display = 'none';
|
||||
document.getElementById('noImages').style.display = 'block';
|
||||
gameActive = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const msg = document.getElementById('message');
|
||||
msg.textContent = text;
|
||||
msg.className = `message ${type}`;
|
||||
msg.style.display = 'block';
|
||||
}
|
||||
|
||||
function updateStats(score, totalGames, accuracy) {
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('accuracy').textContent = accuracy + '%';
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.getElementById('guessInput').addEventListener('keypress', handleKeyPress);
|
||||
document.getElementById('skipBtn').addEventListener('click', skipGame);
|
||||
|
||||
window.onload = () => {
|
||||
fetch('/check_images')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.has_images) {
|
||||
newGame();
|
||||
} else {
|
||||
document.getElementById('noImages').style.display = 'block';
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Main game page"""
|
||||
return render_template_string(HTML_TEMPLATE)
|
||||
|
||||
@app.route('/check_images')
|
||||
def check_images():
|
||||
"""Check if images are available"""
|
||||
return jsonify({
|
||||
'has_images': len(game.available_images) > 0,
|
||||
'count': len(game.available_images)
|
||||
})
|
||||
|
||||
@app.route('/new_game', methods=['POST'])
|
||||
def new_game():
|
||||
"""Start a new game"""
|
||||
data = request.get_json()
|
||||
difficulty = data.get('difficulty', 'normal')
|
||||
|
||||
image_data, answer = game.new_game(difficulty)
|
||||
|
||||
if image_data:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'image_data': image_data
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'No images available'
|
||||
})
|
||||
|
||||
@app.route('/guess', methods=['POST'])
|
||||
def make_guess():
|
||||
"""Submit a guess"""
|
||||
data = request.get_json()
|
||||
guess = data.get('guess', '')
|
||||
|
||||
correct = game.check_guess(guess)
|
||||
game.update_score(correct)
|
||||
|
||||
accuracy = round((game.score / game.total_games * 100) if game.total_games > 0 else 0)
|
||||
|
||||
return jsonify({
|
||||
'correct': correct,
|
||||
'answer': game.current_answer,
|
||||
'score': game.score,
|
||||
'total_games': game.total_games,
|
||||
'accuracy': accuracy
|
||||
})
|
||||
|
||||
@app.route('/skip')
|
||||
def skip_game():
|
||||
"""Skip current game"""
|
||||
game.update_score(False)
|
||||
|
||||
accuracy = round((game.score / game.total_games * 100) if game.total_games > 0 else 0)
|
||||
|
||||
return jsonify({
|
||||
'answer': game.current_answer,
|
||||
'score': game.score,
|
||||
'total_games': game.total_games,
|
||||
'accuracy': accuracy
|
||||
})
|
||||
|
||||
def open_browser():
|
||||
"""Open browser after delay"""
|
||||
time.sleep(1.5)
|
||||
webbrowser.open('http://127.0.0.1:5000')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("http://127.0.0.1:5000")
|
||||
|
||||
browser_thread = threading.Thread(target=open_browser)
|
||||
browser_thread.daemon = True
|
||||
browser_thread.start()
|
||||
|
||||
try:
|
||||
app.run(debug=False, host='127.0.0.1', port=5000, use_reloader=False)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
Reference in New Issue
Block a user