new file: .env.example

new file:   .gitignore
	new file:   Dockerfile
	new file:   README.md
	new file:   app.py
	new file:   app/__init__.py
	new file:   app/game.py
	new file:   app/models.py
	new file:   app/routes.py
	new file:   app/seed.py
	new file:   data/movies.json
	new file:   docker-compose.yml
	new file:   requirements.txt
	new file:   static/css/style.css
	new file:   static/js/game.js
	new file:   static/js/player.js
	new file:   templates/index.html
This commit is contained in:
SimolZimol
2026-08-07 20:48:08 +02:00
commit 526b0353e7
17 changed files with 2722 additions and 0 deletions

202
app/game.py Normal file
View File

@@ -0,0 +1,202 @@
import hashlib
import random
from datetime import date, datetime, time, timedelta
from . import db
from .models import Movie, Player, Session, Universe
# Statusklassen fuer die Zellen
GREEN = "green"
YELLOW = "yellow"
RED = "red"
def get_max_attempts():
from flask import current_app
return current_app.config.get("MAX_ATTEMPTS", 10)
def _game_date(today=None):
"""Ermittelt den 'Spieltag' basierend auf der globalen Reset-Uhrzeit (UTC)."""
from flask import current_app
reset_hour = current_app.config.get("DAILY_RESET_UTC", 9)
now = today or datetime.utcnow()
game_date = now.date()
# Vor der Resetzeit gilt noch der Vortag
if now.time() < time(reset_hour % 24, 0, 0):
game_date = game_date - timedelta(days=1)
return game_date
def daily_target(game_date=None):
"""Bestimmt deterministisch den taeglichen Ziel-Film (fuer alle gleich)."""
game_date = game_date or _game_date()
movies = Movie.query.order_by(Movie.id).all()
if not movies:
return None
idx = int(hashlib.sha256(str(game_date).encode()).hexdigest(), 16) % len(movies)
return movies[idx]
def random_target():
movies = Movie.query.order_by(Movie.id).all()
if not movies:
return None
return random.choice(movies)
def get_or_create_player(nickname):
player = Player.query.filter_by(nickname=nickname).first()
if player is None:
player = Player(nickname=nickname)
db.session.add(player)
db.session.commit()
return player
def _series(normalized_title):
"""Erstellt eine Normalisierung fuer Titel-Vergleiche (nur Buchstaben, klein)."""
return "".join(ch for ch in normalized_title.lower() if ch.isalnum())
def match_platforms(guess_movie, target_movie):
guess = set(p.name for p in guess_movie.platforms)
target = set(p.name for p in target_movie.platforms)
if guess == target:
return GREEN
# gelb: mindestens eine gemeinsame Platform
if guess & target:
return YELLOW
return RED
def match_genres(guess_movie, target_movie):
guess = set(g.name for g in guess_movie.genres)
target = set(g.name for g in target_movie.genres)
if guess == target:
return GREEN
if guess & target:
return YELLOW
return RED
def match_year(guess_year, target_year):
if guess_year == target_year:
return GREEN, "equal"
if guess_year < target_year:
return YELLOW, "up" # Ziel liegt hoeher (spaeter)
return YELLOW, "down" # Ziel liegt tiefer (frueher)
def match_fsk(guess_fsk, target_fsk):
if guess_fsk == target_fsk:
return GREEN
if guess_fsk is None or target_fsk is None:
return YELLOW if guess_fsk == target_fsk else RED
return RED
def match_director(guess_director, target_director):
if not guess_director or not target_director:
return GREEN if guess_director == target_director else RED
if _series(guess_director) == _series(target_director):
return GREEN
return RED
def match_studio(guess_studio, target_studio):
if (guess_studio or "").lower() == (target_studio or "").lower():
return GREEN
return RED
def match_budget(guess_budget, target_budget):
if guess_budget is None or target_budget is None:
return RED if guess_budget != target_budget else GREEN
if guess_budget == target_budget:
return GREEN, "equal"
if guess_budget < target_budget:
return YELLOW, "up"
return YELLOW, "down"
def match_universe(guess_universe, target_universe):
# Fuer beide None => green (kein Universum)
if (guess_universe or "") == (target_universe or ""):
return GREEN
if not guess_universe or not target_universe:
return RED
return RED
def compare(guess_movie, target_movie):
"""Vergleicht einen Tipp mit dem Ziel-Film und liefert die 9 Zellen."""
ys, ydir = match_year(guess_movie.year, target_movie.year)
bs, bdir = match_budget(guess_movie.budget, target_movie.budget)
return {
"title": guess_movie.title,
"platforms": {"status": match_platforms(guess_movie, target_movie), "value": sorted(p.name for p in guess_movie.platforms)},
"genres": {"status": match_genres(guess_movie, target_movie), "value": sorted(g.name for g in guess_movie.genres)},
"year": {"status": ys, "value": guess_movie.year, "direction": ydir, "diff": target_movie.year - guess_movie.year},
"fsk": {"status": match_fsk(guess_movie.fsk, target_movie.fsk), "value": guess_movie.fsk},
"studio": {"status": match_studio(guess_movie.studio.name if guess_movie.studio else None, target_movie.studio.name if target_movie.studio else None), "value": guess_movie.studio.name if guess_movie.studio else None},
"director": {"status": match_director(guess_movie.director, target_movie.director), "value": guess_movie.director or None},
"budget": {"status": bs, "value": guess_movie.budget, "direction": bdir, "diff": (target_movie.budget - guess_movie.budget) if guess_movie.budget is not None and target_movie.budget is not None else None},
"universe": {"status": match_universe(guess_movie.universe.name if guess_movie.universe else None, target_movie.universe.name if target_movie.universe else None), "value": guess_movie.universe.name if guess_movie.universe else None},
}
def is_correct(compared):
"""Prueft ob alle Zellen gruen sind (== Ziel gefunden)."""
return all(cell["status"] == GREEN for cell in compared.values() if cell is not compared["title"])
def finish_session(session, won):
"""Markiert eine Runde als gewonnen/verloren und aktualisiert den Str eak."""
session.status = "won" if won else "lost"
session.won = won
db.session.commit()
def _streak_for(player, mode, won):
"""Berechnet den aktuellen Str eak (Sieg bzw. Niederlage) fuer Spieler+Modus."""
sessions = (
Session.query
.filter_by(player_id=player.id, mode=mode)
.order_by(Session.id.desc())
.all()
)
count = 0
for s in sessions:
if s.won is None:
continue
if s.won == won:
count += 1
else:
break
return count
def get_streaks(player_id):
"""Liefert getrennte Sieg-/Niederlagen-Str eaks fuer Tages- und Unbegrenzt-Modus."""
player = Player.query.get(player_id)
if player is None:
return None
return {
"daily": {
"win": _streak_for(player, "daily", True),
"loss": _streak_for(player, "daily", False),
},
"unlimited": {
"win": _streak_for(player, "unlimited", True),
"loss": _streak_for(player, "unlimited", False),
},
}
def existing_daily_session(player_id, game_date):
"""Gibt eine vorhandene, abgeschlossene Tages-Runde zurueck (falls vorhanden)."""
return Session.query.filter_by(
player_id=player_id, mode="daily", date=game_date
).order_by(Session.id.desc()).first()