#!/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 = """ Face Guess

Rate:

Score
0
Accuracy
0%
Who is this?
""" @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