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:
202
app/game.py
Normal file
202
app/game.py
Normal 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()
|
||||
Reference in New Issue
Block a user