diff --git a/README.md b/README.md
deleted file mode 100644
index 6109ea3..0000000
--- a/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# SCOicide Squadâ·
-
-
-
-> A python Flask based role play game app đȘ
-
-â [Live Page](https://example.com/)
-
-â [Game rules](rules.md)
\ No newline at end of file
diff --git a/application.py b/application.py
deleted file mode 100644
index dafe69d..0000000
--- a/application.py
+++ /dev/null
@@ -1,653 +0,0 @@
-from gevent import monkey
-monkey.patch_all()
-
-import sqlite3
-import string
-import secrets
-import io
-import zoneinfo
-import os
-import markdown
-from datetime import datetime, timezone
-from passlib.hash import argon2
-import pyotp
-import qrcode
-import qrcode.image.svg
-from apscheduler.schedulers.gevent import GeventScheduler
-from apscheduler.triggers.cron import CronTrigger
-from apscheduler.triggers.date import DateTrigger
-from flask import Flask, request, session, redirect, render_template, url_for, abort, send_from_directory
-from flask_socketio import SocketIO, emit, join_room
-
-application = Flask(__name__)
-app = application # Fallback-Alias fĂŒr Gunicorn und Pella
-application.secret_key = secrets.token_hex(64)
-websocket_manager = SocketIO(application, manage_session=False, async_mode='gevent')
-scheduler_instance = GeventScheduler(timezone="Europe/Berlin")
-european_timezone = zoneinfo.ZoneInfo("Europe/Berlin")
-
-IDENTIFIER_LENGTH = 5
-
-def get_database_connection():
- connection = sqlite3.connect("game_database.sqlite")
- connection.row_factory = sqlite3.Row
- return connection
-
-def initialize_database():
- connection = get_database_connection()
- connection.execute("""
- CREATE TABLE IF NOT EXISTS game_configuration (
- configuration_key TEXT PRIMARY KEY,
- configuration_value TEXT
- )
- """)
- connection.execute("""
- CREATE TABLE IF NOT EXISTS player (
- player_identifier TEXT PRIMARY KEY,
- password_hash TEXT,
- display_name TEXT,
- is_alive INTEGER DEFAULT 1,
- score INTEGER DEFAULT 0,
- target_identifier TEXT,
- reset_token TEXT,
- pending_attacker_identifier TEXT,
- is_approved INTEGER DEFAULT 0,
- pending_display_name TEXT,
- late_joiner INTEGER DEFAULT 0
- )
- """)
- connection.execute("""
- CREATE TABLE IF NOT EXISTS administrator (
- administrator_identifier INTEGER PRIMARY KEY,
- password_hash TEXT,
- two_factor_secret TEXT,
- two_factor_enabled INTEGER DEFAULT 0
- )
- """)
- connection.execute("""
- CREATE TABLE IF NOT EXISTS kill_log (
- kill_id INTEGER PRIMARY KEY AUTOINCREMENT,
- killer_identifier TEXT,
- victim_identifier TEXT,
- victim_previous_target TEXT,
- kill_timestamp TEXT
- )
- """)
- try:
- connection.execute("ALTER TABLE administrator ADD COLUMN two_factor_enabled INTEGER DEFAULT 0")
- except sqlite3.OperationalError:
- pass
- try:
- connection.execute("ALTER TABLE player ADD COLUMN is_approved INTEGER DEFAULT 0")
- except sqlite3.OperationalError:
- pass
- try:
- connection.execute("ALTER TABLE player ADD COLUMN pending_display_name TEXT")
- except sqlite3.OperationalError:
- pass
- try:
- connection.execute("ALTER TABLE player ADD COLUMN late_joiner INTEGER DEFAULT 0")
- except sqlite3.OperationalError:
- pass
- cursor = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'")
- if not cursor.fetchone():
- connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('game_status', 'pending')")
- connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('start_time', '')")
- connection.commit()
- connection.close()
-
-def require_player_authentication(function):
- def wrapper(*arguments, **keyword_arguments):
- if "player_identifier" not in session:
- return redirect(url_for("handle_login"))
- return function(*arguments, **keyword_arguments)
- wrapper.__name__ = function.__name__
- return wrapper
-
-def require_administrator_authentication(function):
- def wrapper(*arguments, **keyword_arguments):
- if not session.get("administrator_authenticated"):
- return redirect(url_for("handle_administrator"))
- return function(*arguments, **keyword_arguments)
- wrapper.__name__ = function.__name__
- return wrapper
-
-def get_markdown_rules():
- if os.path.exists("rules.md"):
- with open("rules.md", "r", encoding="utf-8") as markdown_file:
- return markdown.markdown(markdown_file.read())
- return "
Regelwerk nicht gefunden "
-
-def shuffle_player_targets():
- connection = get_database_connection()
- players = connection.execute("SELECT player_identifier FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall()
- if len(players) > 1:
- identifiers = [player_record["player_identifier"] for player_record in players]
- secure_random_generator = secrets.SystemRandom()
- secure_random_generator.shuffle(identifiers)
- for index, identifier in enumerate(identifiers):
- target = identifiers[(index + 1) % len(identifiers)]
- connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target, identifier))
- connection.commit()
- connection.close()
- websocket_manager.emit("game_state_changed")
-
-def process_administrator_action(action_type, form_data):
- connection = get_database_connection()
- has_game_state_changed = False
-
- if action_type == "start_game":
- alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone()
- if alive_count_record["alive_count"] >= 2:
- connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'")
- connection.execute("UPDATE game_configuration SET configuration_value = ? WHERE configuration_key = 'start_time'", (datetime.now(european_timezone).isoformat(),))
- connection.commit()
- connection.close()
- shuffle_player_targets()
- websocket_manager.emit("admin_state_changed", to="administrator_room")
- return
-
- elif action_type == "stop_game":
- connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'")
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "pause_game":
- connection.execute("UPDATE game_configuration SET configuration_value = 'paused' WHERE configuration_key = 'game_status'")
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "resume_game":
- connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'")
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "reset_game":
- connection.execute("UPDATE game_configuration SET configuration_value = 'pending' WHERE configuration_key = 'game_status'")
- connection.execute("UPDATE game_configuration SET configuration_value = '' WHERE configuration_key = 'start_time'")
- connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, score = 0, target_identifier = NULL, pending_attacker_identifier = NULL")
- connection.execute("DELETE FROM kill_log")
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "create_player":
- display_name = form_data.get("display_name")
- new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH))
- temporary_password = secrets.token_urlsafe(16)
- hashed_password = argon2.hash(temporary_password)
- connection.execute("INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)", (new_identifier, hashed_password, display_name))
- connection.commit()
-
- elif action_type == "approve_player":
- target_identifier = form_data.get("player_identifier")
- status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()
- is_late_joiner = 1 if status_record["configuration_value"] in ['running', 'paused'] else 0
- is_alive_status = 0 if is_late_joiner else 1
- connection.execute("UPDATE player SET is_approved = 1, is_alive = ?, late_joiner = ? WHERE player_identifier = ?", (is_alive_status, is_late_joiner, target_identifier))
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "edit_name":
- target_identifier = form_data.get("player_identifier")
- new_name = form_data.get("new_display_name")
- connection.execute("UPDATE player SET display_name = ? WHERE player_identifier = ?", (new_name, target_identifier))
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "approve_name_change":
- target_identifier = form_data.get("player_identifier")
- connection.execute("UPDATE player SET display_name = pending_display_name, pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,))
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "reject_name_change":
- target_identifier = form_data.get("player_identifier")
- connection.execute("UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,))
- connection.commit()
- websocket_manager.emit("game_state_changed", to=target_identifier)
-
- elif action_type == "delete_player":
- target_identifier = form_data.get("player_identifier")
- connection.execute("DELETE FROM player WHERE player_identifier = ?", (target_identifier,))
- status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()
- if status_record["configuration_value"] == 'running':
- alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()
- if alive_count_record["alive_count"] <= 1:
- connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'")
- if alive_count_record["alive_count"] == 1:
- connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1")
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "revive_player":
- target_identifier = form_data.get("player_identifier")
- player_to_revive = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone()
- if player_to_revive and player_to_revive["is_alive"] == 0:
- alive_players = connection.execute("SELECT * FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall()
- if len(alive_players) > 0:
- random_living_player = secrets.choice(alive_players)
- random_living_identifier = random_living_player["player_identifier"]
- attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (random_living_identifier,)).fetchone()
- if attacker:
- connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target_identifier, attacker["player_identifier"]))
- connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = ? WHERE player_identifier = ?", (random_living_identifier, target_identifier))
- else:
- connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = NULL WHERE player_identifier = ?", (target_identifier,))
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "eliminate_player":
- target_identifier = form_data.get("player_identifier")
- victim = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone()
- if victim and victim["is_alive"] == 1:
- attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_identifier,)).fetchone()
- new_target = victim["target_identifier"]
- connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (target_identifier,))
-
- attacker_id = None
- if attacker:
- attacker_id = attacker["player_identifier"]
- connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target, attacker_id))
-
- timestamp = datetime.now(european_timezone).isoformat()
- connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp) VALUES (?, ?, ?, ?)", (attacker_id, target_identifier, new_target, timestamp))
-
- alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()
- if alive_count_record["alive_count"] <= 1:
- status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()
- current_status = status_record["configuration_value"] if status_record else "unknown"
- connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'")
-
- if alive_count_record["alive_count"] == 1 and current_status != 'finished':
- connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1")
-
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "edit_kill":
- kill_id = form_data.get("kill_id")
- new_killer_id = form_data.get("new_killer_identifier")
- if new_killer_id == "":
- new_killer_id = None
- new_timestamp = form_data.get("kill_timestamp")
-
- old_kill = connection.execute("SELECT * FROM kill_log WHERE kill_id = ?", (kill_id,)).fetchone()
- if old_kill:
- old_killer_id = old_kill["killer_identifier"]
- if old_killer_id != new_killer_id:
- if old_killer_id:
- connection.execute("UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?", (old_killer_id,))
- if new_killer_id:
- connection.execute("UPDATE player SET score = score + 1 WHERE player_identifier = ?", (new_killer_id,))
-
- connection.execute("UPDATE kill_log SET killer_identifier = ?, kill_timestamp = ? WHERE kill_id = ?", (new_killer_id, new_timestamp, kill_id))
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "revoke_kill":
- kill_id = form_data.get("kill_id")
- kill_record = connection.execute("SELECT * FROM kill_log WHERE kill_id = ?", (kill_id,)).fetchone()
-
- if kill_record:
- if kill_record["killer_identifier"]:
- connection.execute("UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?", (kill_record["killer_identifier"],))
-
- victim_id = kill_record["victim_identifier"]
- target_to_restore = kill_record["victim_previous_target"]
-
- player_pointing_to_target = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_to_restore,)).fetchone()
-
- connection.execute("UPDATE player SET is_alive = 1, target_identifier = ? WHERE player_identifier = ?", (target_to_restore, victim_id))
- if player_pointing_to_target:
- connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (victim_id, player_pointing_to_target["player_identifier"]))
-
- connection.execute("DELETE FROM kill_log WHERE kill_id = ?", (kill_id,))
-
- alive_count = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()["alive_count"]
- if alive_count > 1:
- status = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()["configuration_value"]
- if status == 'finished':
- connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'")
- connection.execute("UPDATE player SET score = MAX(0, score - 3) WHERE is_alive = 1 AND is_approved = 1 AND player_identifier != ?", (victim_id,))
-
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "change_target":
- player_identifier = form_data.get("player_identifier")
- new_target_identifier = form_data.get("new_target_identifier")
- if player_identifier and new_target_identifier and player_identifier != new_target_identifier:
- player_record = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (player_identifier,)).fetchone()
- other_player_record = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (new_target_identifier,)).fetchone()
- if player_record and other_player_record:
- connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target_identifier, player_identifier))
- connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (player_record["target_identifier"], other_player_record["player_identifier"]))
- connection.commit()
- has_game_state_changed = True
-
- elif action_type == "generate_reset":
- target_identifier = form_data.get("player_identifier")
- reset_token = secrets.token_urlsafe(32)
- connection.execute("UPDATE player SET reset_token = ? WHERE player_identifier = ?", (reset_token, target_identifier))
- connection.commit()
-
- elif action_type == "update_credentials":
- new_password = form_data.get("password_string")
- password_confirmation = form_data.get("password_string_confirmation")
- if new_password and new_password == password_confirmation:
- hashed_password = argon2.hash(new_password)
- connection.execute("UPDATE administrator SET password_hash = ? WHERE administrator_identifier = 1", (hashed_password,))
- connection.commit()
-
- elif action_type == "enable_two_factor":
- two_factor_input = form_data.get("two_factor_code")
- administrator_record = connection.execute("SELECT two_factor_secret FROM administrator WHERE administrator_identifier = 1").fetchone()
- time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"])
- if time_based_one_time_password_validator.verify(two_factor_input):
- connection.execute("UPDATE administrator SET two_factor_enabled = 1 WHERE administrator_identifier = 1")
- connection.commit()
-
- elif action_type == "disable_two_factor":
- connection.execute("UPDATE administrator SET two_factor_enabled = 0 WHERE administrator_identifier = 1")
- connection.commit()
-
- connection.close()
-
- if has_game_state_changed:
- websocket_manager.emit("game_state_changed")
- else:
- websocket_manager.emit("admin_state_changed", to="administrator_room")
-
-def scheduled_action_wrapper(action_type, form_data):
- with application.app_context():
- process_administrator_action(action_type, form_data)
-
-@application.route("/service_worker.js")
-def serve_service_worker():
- return send_from_directory(application.static_folder, "service_worker.js")
-
-@application.route("/health")
-@application.route("/ping")
-def handle_health_check():
- return "OK", 200
-
-@application.route("/")
-@require_player_authentication
-def handle_index():
- connection = get_database_connection()
- player = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (session["player_identifier"],)).fetchone()
- if not player:
- session.clear()
- return redirect(url_for("handle_login"))
- status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()
- game_status = status_row["configuration_value"]
- time_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'start_time'").fetchone()
- start_time = time_row["configuration_value"]
-
- target_player = None
- if player["target_identifier"]:
- target_player = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["target_identifier"],)).fetchone()
-
- pending_attacker_name = None
- if player["pending_attacker_identifier"]:
- attacker = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["pending_attacker_identifier"],)).fetchone()
- if attacker:
- pending_attacker_name = attacker["display_name"]
-
- all_players = connection.execute("SELECT * FROM player WHERE is_approved = 1 ORDER BY score DESC").fetchall()
- total_eliminations = sum(player_record["score"] for player_record in all_players)
- alive_count = sum(1 for player_record in all_players if player_record["is_alive"] == 1)
- connection.close()
-
- document_markup = get_markdown_rules()
-
- return render_template(
- "game.html",
- player=player,
- game_status=game_status,
- start_time=start_time,
- target_player=target_player,
- pending_attacker_name=pending_attacker_name,
- all_players=all_players,
- total_eliminations=total_eliminations,
- alive_count=alive_count,
- rules_markup=document_markup
- )
-
-@application.route("/register", methods=["GET", "POST"])
-def handle_registration():
- if request.method == "POST":
- display_name = request.form.get("display_name")
- password_input = request.form.get("password_string")
- password_confirmation = request.form.get("password_string_confirmation")
- if password_input and password_input == password_confirmation:
- new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH))
- hashed_password = argon2.hash(password_input)
- connection = get_database_connection()
- connection.execute("INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)", (new_identifier, hashed_password, display_name))
- connection.commit()
- connection.close()
- websocket_manager.emit("admin_state_changed", to="administrator_room")
- return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, registration_success=True, new_identifier=new_identifier)
- return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, register_mode=True)
-
-@application.route("/login", methods=["GET", "POST"])
-def handle_login():
- if request.method == "POST":
- identifier_input = request.form.get("player_identifier", "").strip().upper()
- password_input = request.form.get("password_string", "")
- connection = get_database_connection()
- player = connection.execute("SELECT password_hash FROM player WHERE player_identifier = ?", (identifier_input,)).fetchone()
- connection.close()
- if player and argon2.verify(password_input, player["password_hash"]):
- session["player_identifier"] = identifier_input
- return redirect(url_for("handle_index"))
- return render_template("login.html", identifier_length=IDENTIFIER_LENGTH)
-
-@application.route("/reset_password", methods=["GET", "POST"])
-def handle_password_reset():
- token = request.args.get("token") or request.form.get("token")
- if not token:
- abort(400)
- connection = get_database_connection()
- player = connection.execute("SELECT player_identifier FROM player WHERE reset_token = ?", (token,)).fetchone()
- if not player:
- connection.close()
- abort(403)
- if request.method == "POST":
- new_password = request.form.get("password_string")
- password_confirmation = request.form.get("password_string_confirmation")
- if new_password and new_password == password_confirmation:
- hashed_password = argon2.hash(new_password)
- connection.execute("UPDATE player SET password_hash = ?, reset_token = NULL WHERE reset_token = ?", (hashed_password, token))
- connection.commit()
- connection.close()
- return redirect(url_for("handle_login"))
- connection.close()
- return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, reset_mode=True, token=token)
-
-@application.route("/player/action", methods=["POST"])
-@require_player_authentication
-def handle_player_action():
- action_type = request.form.get("action_type")
- connection = get_database_connection()
- if action_type == "request_name_change":
- pending_name = request.form.get("pending_display_name")
- connection.execute("UPDATE player SET pending_display_name = ? WHERE player_identifier = ?", (pending_name, session["player_identifier"]))
- connection.commit()
- websocket_manager.emit("admin_state_changed", to="administrator_room")
- connection.close()
- return redirect(url_for("handle_index"))
-
-@application.route("/logout")
-def handle_logout():
- session.clear()
- return redirect(url_for("handle_login"))
-
-@application.route("/admin", methods=["GET", "POST"])
-def handle_administrator():
- connection = get_database_connection()
- administrator_record = connection.execute("SELECT * FROM administrator WHERE administrator_identifier = 1").fetchone()
-
- if not administrator_record:
- if request.method == "POST":
- password_input = request.form.get("password_string")
- password_confirmation = request.form.get("password_string_confirmation")
- if password_input and password_input == password_confirmation:
- hashed_password = argon2.hash(password_input)
- two_factor_secret = pyotp.random_base32()
- connection.execute("INSERT INTO administrator (administrator_identifier, password_hash, two_factor_secret, two_factor_enabled) VALUES (1, ?, ?, 0)", (hashed_password, two_factor_secret))
- connection.commit()
- session["administrator_authenticated"] = True
- connection.close()
- return redirect(url_for("handle_administrator"))
- connection.close()
- return render_template("administrator.html", setup_required=True)
-
- if not session.get("administrator_authenticated"):
- if request.method == "POST":
- password_input = request.form.get("password_string")
- if argon2.verify(password_input, administrator_record["password_hash"]):
- if administrator_record["two_factor_enabled"]:
- two_factor_input = request.form.get("two_factor_code")
- time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"])
- if time_based_one_time_password_validator.verify(two_factor_input):
- session["administrator_authenticated"] = True
- connection.close()
- return redirect(url_for("handle_administrator"))
- else:
- session["administrator_authenticated"] = True
- connection.close()
- return redirect(url_for("handle_administrator"))
- connection.close()
- return render_template("administrator.html", login_required=True, two_factor_enabled=bool(administrator_record["two_factor_enabled"]))
-
- status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()
- game_status = status_row["configuration_value"]
- all_players = connection.execute("SELECT * FROM player ORDER BY is_approved ASC, display_name ASC").fetchall()
- alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone()
- can_start = alive_count_record["alive_count"] >= 2
-
- kill_logs = connection.execute("""
- SELECT k.*,
- killer.display_name as killer_name,
- victim.display_name as victim_name
- FROM kill_log k
- LEFT JOIN player killer ON k.killer_identifier = killer.player_identifier
- LEFT JOIN player victim ON k.victim_identifier = victim.player_identifier
- ORDER BY k.kill_timestamp DESC
- """).fetchall()
-
- two_factor_uri = pyotp.totp.TOTP(administrator_record["two_factor_secret"]).provisioning_uri(name="Administrator", issuer_name="AssassinGame")
- qr_factory = qrcode.image.svg.SvgPathImage
- qr_image = qrcode.make(two_factor_uri, image_factory=qr_factory)
- qr_stream = io.BytesIO()
- qr_image.save(qr_stream)
- qr_code_svg = qr_stream.getvalue().decode("utf-8")
-
- connection.close()
-
- return render_template(
- "administrator.html",
- dashboard_active=True,
- game_status=game_status,
- can_start=can_start,
- all_players=all_players,
- kill_logs=kill_logs,
- two_factor_secret=administrator_record["two_factor_secret"],
- two_factor_enabled=bool(administrator_record["two_factor_enabled"]),
- qr_code_svg=qr_code_svg
- )
-
-@application.route("/admin/action", methods=["POST"])
-@require_administrator_authentication
-def handle_administrator_action():
- form_data = request.form.to_dict()
- action_type = form_data.get("action_type")
- execution_time_string = form_data.get("execution_time")
-
- if execution_time_string:
- execution_datetime = datetime.strptime(execution_time_string[:16], "%Y-%m-%dT%H:%M").replace(tzinfo=european_timezone)
- scheduler_instance.add_job(scheduled_action_wrapper, DateTrigger(run_date=execution_datetime), args=[action_type, form_data])
- else:
- process_administrator_action(action_type, form_data)
-
- return redirect(url_for("handle_administrator"))
-
-@websocket_manager.on("connect")
-def handle_websocket_connection():
- if "player_identifier" in session:
- join_room(session["player_identifier"])
- if session.get("administrator_authenticated"):
- join_room("administrator_room")
-
-@websocket_manager.on("request_elimination")
-def handle_elimination_request():
- attacker_identifier = session.get("player_identifier")
- if not attacker_identifier:
- return
- connection = get_database_connection()
- attacker = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (attacker_identifier,)).fetchone()
- if attacker and attacker["target_identifier"]:
- victim_identifier = attacker["target_identifier"]
- connection.execute("UPDATE player SET pending_attacker_identifier = ? WHERE player_identifier = ?", (attacker_identifier, victim_identifier))
- connection.commit()
- emit("elimination_verification_requested", to=victim_identifier)
- connection.close()
-
-@websocket_manager.on("confirm_elimination")
-def handle_elimination_confirmation():
- victim_identifier = session.get("player_identifier")
- if not victim_identifier:
- return
- connection = get_database_connection()
- victim = connection.execute("SELECT pending_attacker_identifier, target_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone()
- if victim and victim["pending_attacker_identifier"]:
- attacker_identifier = victim["pending_attacker_identifier"]
- new_target = victim["target_identifier"]
-
- connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,))
- connection.execute("UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?", (new_target, attacker_identifier))
-
- timestamp = datetime.now(european_timezone).isoformat()
- connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp) VALUES (?, ?, ?, ?)", (attacker_identifier, victim_identifier, new_target, timestamp))
-
- alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()
- if alive_count_record["alive_count"] <= 1:
- status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()
- current_status = status_record["configuration_value"] if status_record else "unknown"
- connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'")
-
- if alive_count_record["alive_count"] == 1 and current_status != 'finished':
- connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1")
-
- connection.commit()
- websocket_manager.emit("game_state_changed")
- connection.close()
-
-@websocket_manager.on("reject_elimination")
-def handle_elimination_rejection():
- victim_identifier = session.get("player_identifier")
- if not victim_identifier:
- return
- connection = get_database_connection()
- victim = connection.execute("SELECT pending_attacker_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone()
- if victim and victim["pending_attacker_identifier"]:
- attacker_identifier = victim["pending_attacker_identifier"]
- connection.execute("UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,))
- connection.commit()
- emit("elimination_rejected", to=attacker_identifier)
- connection.close()
-
-# --- PRODUKTIONS-SETUP (GUNICORN) ---
-with application.app_context():
- initialize_database()
-
-try:
- if getattr(scheduler_instance, 'state', 0) == 0:
- scheduler_instance.start()
-except Exception:
- pass
-
-if __name__ == "__main__":
- websocket_manager.run(application, host="0.0.0.0", port=5000)
diff --git a/gunicorn.conf.py b/gunicorn.conf.py
deleted file mode 100644
index 89be1ed..0000000
--- a/gunicorn.conf.py
+++ /dev/null
@@ -1,2 +0,0 @@
-worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker"
-workers = 1
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index a76fd41..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-Flask
-Flask-SocketIO
-APScheduler
-passlib
-argon2-cffi
-pyotp
-qrcode
-Markdown
-gunicorn
-gevent
-gevent-websocket
diff --git a/rules.md b/rules.md
deleted file mode 100644
index f7eb5ff..0000000
--- a/rules.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# *SCO*icide Squad
-_Ein Spiel am Science College Overbach_
-
-## Dein Ziel
-_Du hast immer genau eine Person zu erwischen._
-
-- Auf deinem Dashboard steht dein aktuelles Ziel. Es ist immer genau eine Person.
-
-- Gleichzeitig jagt dich jemand. Wer das ist, erfÀhrst du erst, wenn es passiert.
-
-- Erwischst du dein Ziel, ĂŒbernimmst du automatisch dessen Ziel. Die Kette bleibt geschlossen.
-
-- Ăber dein Ziel wird nicht geredet. Wer sein Ziel verrĂ€t oder gemeinsam mit anderen Ziele abgleicht, nimmt allen den SpaĂ.
-
-## Wie ein Kill zÀhlt
-_Ăbergeben eines Gegenstandes, direkt und ungesehen_
-
-- Du killst dein Ziel, indem du ihm einen beliebigen Gegenstand ĂŒbergibst.
-
-- Das Ziel muss den Gegenstand direkt in die Hand gegeben bekommen. Unvorbereitetes Abwerfen gilt nicht. Im Zweifel gilt sonst der Kill nicht. Achtet auf Fairness!
-
-- Sind andere lebende Personen unmittelbar dabei und sehen den Mord, zÀhlt der Versuch nicht.
-
-- Wehrt sich dein Ziel erfolgreich, hast du Pech gehabt. Ein weiterer Versuch ist jederzeit erlaubt.
-
-## Melden
-_Der TÀter meldet, das Opfer bestÀtigt._
-
-- Wer sein Ziel erwischt, meldet das selbst unter dem Reiter „Ziel“ in der App.
-
-- Die Meldung wird geprĂŒft: Das Ziel bestĂ€tigt, ob du es erwischt hast.
-
-- Melde zĂŒgig: Bis zur Meldung lĂ€uft die Mordkette unverĂ€ndert weiter.
-
-- Stimmt etwas nicht, korrigiert die Spielleitung die Meldung.
-
-## Punkte
-_Jeder Kill zÀhlt, gekillt werden kostet keine Punkte._
-
-- FĂŒr jeden Kill gibt es einen Punkt. Deine Punkte bleiben dir auch, wenn du gekillt wirst.
-
-- Die letzte ĂŒberlebende Person bekommt zusĂ€tzlich drei Punkte fĂŒr's Ăberleben.
-
-- Der Punktestand ist fĂŒr alle einsehbar und aktualisiert sich nach jeder Meldung.
-
-## GeschĂŒtzte Orte und Zeiten
-_Es gibt Bereiche, in denen nicht gespielt wird._
-
-- GeschĂŒtzt sind nur Zimmernachbarn in ihren jeweiligen Zimmern. Dein Zimmernachbar darf dich nicht umbringen, wenn ihr alleine im Zimmer seid.
-
-- Wer krank ist oder eine Pause braucht, sagt bescheid. Das wird respektiert.
-
-## Was nicht geht
-_Keine Gewalt, kein Zwang, kein fremdes Eigentum_
-
-- Kein körperliche Gewalt und kein Festhalten.
-
-- Achtet fremdes Eigentum und die PrivatsphÀre anderer.
-
-- Nichts beschÀdigen und niemanden in eine unangenehme Lage bringen.
-
-- Ein Nein ist ein Nein, auch mitten im Spiel.
-
-## Ablauf
-_Das Spiel lĂ€uft ĂŒber mehrere Tage bis nur noch eine Person ĂŒbrig ist._
-
-- Das Spiel startet, sobald die Spielleitung auslost.
-
-- Die Spielleitung kann jederzeit pausieren. Dann sind keine Eliminierungen gĂŒltig.
-
-- Das Spiel endet, wenn nur noch eine Person ĂŒbrig ist oder die Spielleitung es beendet.
-
-## Dein Zugang
-_FĂŒnfstellige ID und dein Passwort bringen dich immer zurĂŒck._
-
-- Deine ID steht oben. Sie Àndert sich nie.
-
-- Mit ID und Passwort kommst du auf jedem GerÀt wieder in deinen Account.
-
-- Auf diesem GerÀt bleibst du angemeldet, bist du dich abmeldest.
-
-- Passwort vergessen? Die Spielleitung kann dir einen Link zum ZurĂŒcksetzen geben.
-
-## Spielleitung
-_Bei Streit entscheidet die Spielleitung._
-
-- Die Spielleitung sieht die vollstÀndige Kette und alle Meldungen.
-
-- Die Spielleitung kann ZugÀnge und das Spiel verwalten.
-
-- Bei Unklarheiten entscheidet die Spielleitung.
\ No newline at end of file
diff --git a/static/behavior.js b/static/behavior.js
deleted file mode 100644
index 36ccec9..0000000
--- a/static/behavior.js
+++ /dev/null
@@ -1,335 +0,0 @@
-let websocket_connection;
-let pending_administrator_form = null;
-let stopwatch_interval_identifier = null;
-
-function initialize_lucide_icons() {
- if (typeof lucide !== "undefined") {
- lucide.createIcons();
- }
-}
-
-function update_game_stopwatch() {
- const stopwatch_element = document.getElementById("game-stopwatch");
- if (!stopwatch_element) {
- return;
- }
-
- const start_time_string = stopwatch_element.getAttribute("data-start-time");
- if (!start_time_string) {
- return;
- }
-
- const start_date = new Date(start_time_string);
- if (isNaN(start_date.getTime())) {
- return;
- }
-
- const current_date = new Date();
- const difference_in_milliseconds = current_date.getTime() - start_date.getTime();
-
- if (difference_in_milliseconds < 0) {
- stopwatch_element.textContent = "00:00 Minuten";
- return;
- }
-
- const total_seconds = Math.floor(difference_in_milliseconds / 1000);
- const days_passed = Math.floor(total_seconds / 86400);
- const hours_passed = Math.floor((total_seconds % 86400) / 3600);
- const minutes_passed = Math.floor((total_seconds % 3600) / 60);
- const seconds_passed = total_seconds % 60;
-
- let output_string = "";
-
- if (days_passed === 1) {
- output_string += "1 Tag und ";
- } else if (days_passed > 1) {
- output_string += days_passed + " Tage und ";
- }
-
- const padded_minutes = String(minutes_passed).padStart(2, "0");
- const padded_seconds = String(seconds_passed).padStart(2, "0");
-
- if (hours_passed === 1) {
- output_string += "1:" + padded_minutes + ":" + padded_seconds + " Stunde";
- } else if (hours_passed > 1) {
- output_string += hours_passed + ":" + padded_minutes + ":" + padded_seconds + " Stunden";
- } else {
- output_string += padded_minutes + ":" + padded_seconds + " Minuten";
- }
-
- stopwatch_element.textContent = output_string;
-}
-
-function initialize_application_behavior() {
- initialize_lucide_icons();
-
- if (stopwatch_interval_identifier) {
- clearInterval(stopwatch_interval_identifier);
- }
- stopwatch_interval_identifier = setInterval(update_game_stopwatch, 1000);
- update_game_stopwatch();
-
- const tab_buttons = document.querySelectorAll(".tab-button");
- tab_buttons.forEach(function(current_button) {
- current_button.addEventListener("click", function() {
- const target_identifier = current_button.getAttribute("data-target");
- const parent_container = current_button.closest(".application-content");
-
- parent_container.querySelectorAll(".tab-button").forEach(function(button_element) {
- button_element.classList.remove("active-tab");
- });
- parent_container.querySelectorAll(".tab-content").forEach(function(content_element) {
- content_element.classList.remove("active-content");
- });
-
- current_button.classList.add("active-tab");
- const target_panel = document.getElementById(target_identifier);
- if (target_panel) {
- target_panel.classList.add("active-content");
- }
- });
- });
-
- const player_forms = document.querySelectorAll(".asynchronous-form");
- player_forms.forEach(function(current_form) {
- current_form.addEventListener("submit", function(form_submission_event) {
- form_submission_event.preventDefault();
- execute_form_submission(current_form);
- });
- });
-
- const administrator_forms = document.querySelectorAll(".admin-asynchronous-form");
- administrator_forms.forEach(function(current_form) {
- current_form.addEventListener("submit", function(form_submission_event) {
- form_submission_event.preventDefault();
- pending_administrator_form = current_form;
- const administrator_modal = document.getElementById("admin-confirmation-modal");
- if (administrator_modal) {
- const time_input = document.getElementById("modal_execution_time");
- if (time_input) {
- time_input.value = "";
- }
- administrator_modal.classList.add("active-modal");
- }
- });
- });
-
- const confirm_administrator_button = document.getElementById("confirm-admin-action");
- if (confirm_administrator_button) {
- confirm_administrator_button.addEventListener("click", function() {
- if (pending_administrator_form) {
- const time_input = document.getElementById("modal_execution_time");
- if (time_input && time_input.value) {
- let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']");
- if (!hidden_time_input) {
- hidden_time_input = document.createElement("input");
- hidden_time_input.type = "hidden";
- hidden_time_input.name = "execution_time";
- pending_administrator_form.appendChild(hidden_time_input);
- }
- hidden_time_input.value = time_input.value;
- } else {
- let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']");
- if (hidden_time_input) {
- hidden_time_input.remove();
- }
- }
- execute_form_submission(pending_administrator_form);
- pending_administrator_form = null;
- const administrator_modal = document.getElementById("admin-confirmation-modal");
- if (administrator_modal) {
- administrator_modal.classList.remove("active-modal");
- }
- }
- });
- }
-
- const cancel_administrator_button = document.getElementById("cancel-admin-action");
- if (cancel_administrator_button) {
- cancel_administrator_button.addEventListener("click", function() {
- pending_administrator_form = null;
- const administrator_modal = document.getElementById("admin-confirmation-modal");
- if (administrator_modal) {
- administrator_modal.classList.remove("active-modal");
- }
- });
- }
-
- const trigger_elimination_button = document.getElementById("trigger-elimination");
- if (trigger_elimination_button) {
- trigger_elimination_button.addEventListener("click", function() {
- const elimination_modal = document.getElementById("elimination-confirm-modal");
- if (elimination_modal) {
- elimination_modal.classList.add("active-modal");
- }
- });
- }
-
- const proceed_elimination_button = document.getElementById("proceed-elimination");
- if (proceed_elimination_button) {
- proceed_elimination_button.addEventListener("click", function() {
- websocket_connection.emit("request_elimination");
- const elimination_modal = document.getElementById("elimination-confirm-modal");
- if (elimination_modal) {
- elimination_modal.classList.remove("active-modal");
- }
- const waiting_modal = document.getElementById("waiting-modal");
- if (waiting_modal) {
- waiting_modal.classList.add("active-modal");
- }
- });
- }
-
- const cancel_elimination_button = document.getElementById("cancel-elimination");
- if (cancel_elimination_button) {
- cancel_elimination_button.addEventListener("click", function() {
- const elimination_modal = document.getElementById("elimination-confirm-modal");
- if (elimination_modal) {
- elimination_modal.classList.remove("active-modal");
- }
- });
- }
-
- const confirm_button = document.getElementById("confirm-elimination");
- if (confirm_button) {
- confirm_button.addEventListener("click", function() {
- websocket_connection.emit("confirm_elimination");
- const modal_element = document.getElementById("verification-modal");
- if (modal_element) {
- modal_element.classList.remove("active-modal");
- }
- });
- }
-
- const reject_button = document.getElementById("reject-elimination");
- if (reject_button) {
- reject_button.addEventListener("click", function() {
- websocket_connection.emit("reject_elimination");
- const modal_element = document.getElementById("verification-modal");
- if (modal_element) {
- modal_element.classList.remove("active-modal");
- }
- });
- }
-
- const acknowledge_rejection_button = document.getElementById("acknowledge-rejection");
- if (acknowledge_rejection_button) {
- acknowledge_rejection_button.addEventListener("click", function() {
- const rejection_modal = document.getElementById("rejection-modal");
- if (rejection_modal) {
- rejection_modal.classList.remove("active-modal");
- }
- });
- }
-
- const open_name_modal_button = document.getElementById("trigger-name-change-modal");
- if (open_name_modal_button) {
- open_name_modal_button.addEventListener("click", function() {
- const name_modal = document.getElementById("name-change-modal");
- if (name_modal) {
- name_modal.classList.add("active-modal");
- }
- });
- }
-
- const close_name_modal_button = document.getElementById("cancel-name-change");
- if (close_name_modal_button) {
- close_name_modal_button.addEventListener("click", function() {
- const name_modal = document.getElementById("name-change-modal");
- if (name_modal) {
- name_modal.classList.remove("active-modal");
- }
- });
- }
-}
-
-function execute_form_submission(target_form) {
- const active_tab_element = document.querySelector(".active-tab");
- let active_target = "control-panel";
- if (active_tab_element) {
- active_target = active_tab_element.getAttribute("data-target");
- }
-
- fetch(target_form.action, {
- method: target_form.method,
- body: new FormData(target_form)
- })
- .then(function(network_response) { return network_response.text(); })
- .then(function(markup_text) {
- replace_main_content(markup_text);
- const new_tab_button = document.querySelector(".tab-button[data-target='" + active_target + "']");
- if (new_tab_button) {
- new_tab_button.click();
- }
- });
-}
-
-function replace_main_content(markup_text) {
- const document_parser = new DOMParser();
- const parsed_document = document_parser.parseFromString(markup_text, "text/html");
- const current_container = document.querySelector(".application-content");
- const new_container = parsed_document.querySelector(".application-content");
- if (current_container && new_container) {
- current_container.innerHTML = new_container.innerHTML;
- initialize_application_behavior();
- }
-}
-
-document.addEventListener("DOMContentLoaded", function() {
- if ("serviceWorker" in navigator) {
- navigator.serviceWorker.register("/service_worker.js");
- }
-
- if (typeof io !== "undefined") {
- websocket_connection = io();
-
- websocket_connection.on("game_state_changed", function() {
- if (document.querySelector(".game-section") || document.querySelector(".administrator-section")) {
- fetch(window.location.href)
- .then(function(network_response) { return network_response.text(); })
- .then(function(markup_text) {
- replace_main_content(markup_text);
- });
- }
- });
-
- websocket_connection.on("admin_state_changed", function() {
- if (document.querySelector(".administrator-section")) {
- fetch(window.location.href)
- .then(function(network_response) { return network_response.text(); })
- .then(function(markup_text) {
- replace_main_content(markup_text);
- });
- }
- });
-
- websocket_connection.on("elimination_verification_requested", function() {
- const modal_element = document.getElementById("verification-modal");
- if (modal_element) {
- modal_element.classList.add("active-modal");
- } else {
- fetch(window.location.href)
- .then(function(network_response) { return network_response.text(); })
- .then(function(markup_text) {
- replace_main_content(markup_text);
- const new_modal_element = document.getElementById("verification-modal");
- if (new_modal_element) {
- new_modal_element.classList.add("active-modal");
- }
- });
- }
- });
-
- websocket_connection.on("elimination_rejected", function() {
- const waiting_modal = document.getElementById("waiting-modal");
- if (waiting_modal) {
- waiting_modal.classList.remove("active-modal");
- }
- const rejection_modal = document.getElementById("rejection-modal");
- if (rejection_modal) {
- rejection_modal.classList.add("active-modal");
- }
- });
- }
- initialize_application_behavior();
-});
\ No newline at end of file
diff --git a/static/logo.svg b/static/logo.svg
deleted file mode 100644
index b2f2548..0000000
--- a/static/logo.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/static/manifest.json b/static/manifest.json
deleted file mode 100644
index eeff66f..0000000
--- a/static/manifest.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "SCOicide Squad",
- "short_name": "SCOicide",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#f0f2f5",
- "theme_color": "#1a1e29",
- "icons": [
- {
- "src": "/static/logo.svg",
- "sizes": "any",
- "type": "image/svg+xml"
- }
- ]
-}
\ No newline at end of file
diff --git a/static/service_worker.js b/static/service_worker.js
deleted file mode 100644
index ccef124..0000000
--- a/static/service_worker.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const cache_identifier = "scoicide-squad-cache-version-1";
-
-self.addEventListener("install", function(installation_event) {
- installation_event.waitUntil(
- caches.open(cache_identifier).then(function(cache_object) {
- return cache_object.addAll([
- "/",
- "/static/stylesheet.css",
- "/static/behavior.js",
- "/static/logo.svg",
- "/static/manifest.json"
- ]);
- })
- );
-});
-
-self.addEventListener("fetch", function(network_fetch_event) {
- network_fetch_event.respondWith(
- caches.match(network_fetch_event.request).then(function(cached_response) {
- return cached_response || fetch(network_fetch_event.request);
- })
- );
-});
\ No newline at end of file
diff --git a/static/stylesheet.css b/static/stylesheet.css
deleted file mode 100644
index e66e0df..0000000
--- a/static/stylesheet.css
+++ /dev/null
@@ -1,443 +0,0 @@
-:root {
- --lightness-primary: 0.35;
- --chroma-primary: 0.15;
- --hue-primary: 15;
-
- --lightness-background: 0.9;
- --chroma-background: 0.01;
- --hue-background: var(--hue-primary);
-
- --lightness-surface: 1;
- --chroma-surface: 0;
- --hue-surface: 0;
-
- --lightness-text: 0.15;
- --chroma-text: 0.05;
- --hue-text: 250;
-
- --lightness-success: 0.60;
- --chroma-success: 0.18;
- --hue-success: 140;
-
- --lightness-danger: 0.50;
- --chroma-danger: 0.22;
- --hue-danger: 25;
-
- --lightness-warning: 0.75;
- --chroma-warning: 0.18;
- --hue-warning: 70;
-
- --dimension-multiplier: 1;
- --spacing-small: calc(0.5rem * var(--dimension-multiplier));
- --spacing-medium: calc(1rem * var(--dimension-multiplier));
- --spacing-large: calc(1.5rem * var(--dimension-multiplier));
-
- --border-radius-small: calc(0.5rem * var(--dimension-multiplier));
- --border-radius-large: calc(1.25rem * var(--dimension-multiplier));
- --border-radius-pill: calc(999rem * var(--dimension-multiplier));
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --lightness-background: 0.2;
- --chroma-background: 0.02;
-
- --lightness-surface: 0.18;
- --chroma-surface: 0.02;
-
- --lightness-text: 0.90;
- --chroma-text: 0.02;
-
- --lightness-primary: 0.65;
- --chroma-primary: 0.20;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- * {
- transition-duration: 0s !important;
- animation-duration: 0s !important;
- }
-}
-
-body {
- background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background));
- color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text));
- font-family: system-ui, -apple-system, sans-serif;
- font-size: calc(1rem * var(--dimension-multiplier));
- line-height: 1.6;
- margin: 0;
- padding: 0;
- display: flex;
- flex-direction: column;
- min-height: 100vh;
-}
-
-.application-header {
- background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
- color: oklch(1 0 0);
- padding: var(--spacing-medium) var(--spacing-large);
- display: flex;
- justify-content: space-between;
- align-items: center;
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
- position: sticky;
- top: 0;
- z-index: 100;
-}
-
-.brand-container {
- display: flex;
- align-items: center;
- gap: var(--spacing-small);
-}
-
-.brand-logo {
- width: calc(2rem * var(--dimension-multiplier));
- height: calc(2rem * var(--dimension-multiplier));
- color: currentColor;
-}
-
-.application-title {
- margin: 0;
- font-size: calc(1.25rem * var(--dimension-multiplier));
- font-weight: 800;
- letter-spacing: -0.02em;
-}
-
-.application-content {
- flex: 1;
- padding: var(--spacing-large);
- width: 100%;
- max-width: calc(48rem * var(--dimension-multiplier));
- margin: 0 auto;
- box-sizing: border-box;
-}
-
-.content-card {
- background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface));
- padding: var(--spacing-large);
- border-radius: var(--border-radius-large);
- box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01);
- margin-bottom: var(--spacing-large);
-}
-
-.section-title {
- margin-top: 0;
- margin-bottom: var(--spacing-medium);
- display: flex;
- align-items: center;
- gap: var(--spacing-small);
-}
-
-.card-subtitle {
- margin-top: 0;
- margin-bottom: var(--spacing-small);
- opacity: 0.7;
- text-transform: uppercase;
- font-size: calc(0.875rem * var(--dimension-multiplier));
- letter-spacing: 0.05em;
-}
-
-.input-form {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-medium);
-}
-
-.inline-form {
- display: inline-block;
-}
-
-.flexible_form_container {
- display: flex;
- gap: calc(0.5rem * var(--dimension-multiplier));
- align-items: center;
- flex-wrap: wrap;
-}
-
-.form-group {
- display: flex;
- flex-direction: column;
- gap: calc(0.25rem * var(--dimension-multiplier));
-}
-
-.input-label {
- font-weight: bold;
- font-size: calc(0.875rem * var(--dimension-multiplier));
-}
-
-.text-input {
- padding: var(--spacing-medium);
- border: 2px solid oklch(0.5 0 0 / 0.2);
- border-radius: var(--border-radius-small);
- background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background));
- color: inherit;
- font-size: inherit;
- transition: border-color 0.2s;
-}
-
-.text-input:focus {
- outline: none;
- border-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
-}
-
-.small-input {
- width: calc(12rem * var(--dimension-multiplier));
- padding: var(--spacing-small);
-}
-
-.time-input {
- width: calc(13rem * var(--dimension-multiplier));
-}
-
-.action-button {
- padding: var(--spacing-medium) var(--spacing-large);
- border: none;
- border-radius: var(--border-radius-pill);
- font-size: inherit;
- font-weight: bold;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--spacing-small);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- color: oklch(1 0 0);
-}
-
-.action-button:hover, .action-button:focus {
- filter: brightness(1.1);
- transform: translateY(-1px);
-}
-
-.icon-button {
- padding: calc(0.5rem * var(--dimension-multiplier));
- border-radius: var(--border-radius-small);
-}
-
-.primary-button {
- background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
- box-shadow: 0 4px 12px oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.3);
-}
-
-.success-button {
- background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success));
-}
-
-.danger-button {
- background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));
-}
-
-.warning-button {
- background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning));
- color: oklch(0.2 0 0);
-}
-
-.secondary-button {
- background-color: transparent;
- color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text));
- border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2);
-}
-
-.large-button {
- padding: var(--spacing-large) calc(2.5rem * var(--dimension-multiplier));
- font-size: calc(1.125rem * var(--dimension-multiplier));
- width: 100%;
-}
-
-.small-button {
- padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.75rem * var(--dimension-multiplier));
- font-size: calc(0.875rem * var(--dimension-multiplier));
-}
-
-.tab-container {
- display: flex;
- background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface));
- border-radius: var(--border-radius-pill);
- padding: calc(0.375rem * var(--dimension-multiplier));
- gap: calc(0.25rem * var(--dimension-multiplier));
- box-shadow: 0 4px 12px rgba(0,0,0,0.05);
- margin-bottom: var(--spacing-large);
- overflow-x: auto;
-}
-
-.tab-button {
- flex: 1;
- border-radius: var(--border-radius-pill);
- padding: calc(0.625rem * var(--dimension-multiplier)) var(--spacing-medium);
- border: none;
- background: transparent;
- color: inherit;
- font-size: inherit;
- font-weight: bold;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: calc(0.5rem * var(--dimension-multiplier));
- white-space: nowrap;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.active-tab {
- background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
- color: oklch(1 0 0);
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
-}
-
-.tab-content {
- display: none;
-}
-
-.active-content {
- display: block;
-}
-
-.status-pill {
- display: inline-flex;
- align-items: center;
- gap: calc(0.375rem * var(--dimension-multiplier));
- padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.875rem * var(--dimension-multiplier));
- border-radius: var(--border-radius-pill);
- font-size: calc(0.875rem * var(--dimension-multiplier));
- font-weight: bold;
- color: oklch(1 0 0);
- margin-left: var(--spacing-small);
-}
-
-.warning-pill {
- background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning));
- color: oklch(0.2 0 0);
-}
-
-.success-pill {
- background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success));
-}
-
-.danger-pill {
- background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));
-}
-
-.notice-card {
- border-left: 6px solid oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
-}
-
-.danger-card {
- border-left-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));
-}
-
-.success-card {
- border-left-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success));
-}
-
-.warning-card {
- border-left-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning));
-}
-
-.target-name {
- font-size: calc(2.5rem * var(--dimension-multiplier));
- text-align: center;
- margin: var(--spacing-large) 0;
- color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
- font-weight: 800;
-}
-
-.data-table {
- width: 100%;
- border-collapse: collapse;
-}
-
-.data-table th, .data-table td {
- padding: var(--spacing-medium);
- border-bottom: 1px solid oklch(0.5 0 0 / 0.1);
- text-align: left;
-}
-
-.data-table th {
- font-weight: 800;
- text-transform: uppercase;
- font-size: calc(0.75rem * var(--dimension-multiplier));
- letter-spacing: 0.05em;
- opacity: 0.7;
-}
-
-.highlighted-row {
- background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.1);
-}
-
-.highlighted-row td {
- font-weight: bold;
-}
-
-.unregistered-row {
- opacity: 0.6;
-}
-
-.markdown-container h1, .markdown-container h2, .markdown-container h3 {
- color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
-}
-
-.markdown-container ul {
- padding-left: var(--spacing-large);
-}
-
-.modal-overlay {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0,0,0,0.6);
- backdrop-filter: blur(4px);
- justify-content: center;
- align-items: center;
- z-index: 1000;
-}
-
-.active-modal {
- display: flex;
-}
-
-.modal-dialog {
- background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface));
- padding: var(--spacing-large);
- border-radius: var(--border-radius-large);
- max-width: calc(32rem * var(--dimension-multiplier));
- width: 90%;
- box-shadow: 0 20px 40px rgba(0,0,0,0.2);
-}
-
-.modal-title {
- margin-top: 0;
- display: flex;
- align-items: center;
- gap: var(--spacing-small);
-}
-
-.modal-actions {
- display: flex;
- gap: var(--spacing-medium);
- justify-content: flex-end;
- margin-top: var(--spacing-large);
-}
-
-.qr-code-container {
- background-color: oklch(1 0 0);
- padding: var(--spacing-medium);
- border-radius: var(--border-radius-large);
- display: inline-block;
- margin-bottom: var(--spacing-medium);
- box-shadow: inset 0 0 0 2px oklch(0.5 0 0 / 0.1);
-}
-
-.qr-code-container svg {
- display: block;
- width: calc(14rem * var(--dimension-multiplier));
- height: calc(14rem * var(--dimension-multiplier));
-}
-
-.monospace-identifier {
- font-family: monospace;
-}
\ No newline at end of file
diff --git a/templates/administrator.html b/templates/administrator.html
deleted file mode 100644
index a6a9006..0000000
--- a/templates/administrator.html
+++ /dev/null
@@ -1,333 +0,0 @@
-{% extends "layout.html" %}
-{% block content %}
-
- {% if setup_required %}
-
-
Administrator Einrichtung
-
-
- {% elif login_required %}
-
-
Administrator Anmeldung
-
-
- {% elif dashboard_active %}
-
- Steuerung
- Spieler
- Kills
-
-
-
-
-
- Spielstatus:
- {% if game_status == 'pending' %}
- Ausstehend
- {% elif game_status == 'running' %}
- LĂ€uft
- {% elif game_status == 'paused' %}
- Pausiert
- {% elif game_status == 'finished' %}
- Beendet
- {% endif %}
-
-
- {% if game_status == 'pending' %}
- {% if can_start %}
-
- {% else %}
-
-
Es werden mindestens zwei freigegebene Spieler benötigt, um das Spiel zu starten.
-
- {% endif %}
- {% elif game_status == 'running' %}
-
-
- {% elif game_status == 'paused' %}
-
-
- {% elif game_status == 'finished' %}
-
- {% endif %}
-
-
-
-
Sicherheit
-
- {% if two_factor_enabled %}
-
-
Zwei-Faktor-Authentifizierung ist aktiv.
-
-
- {% else %}
-
-
Zwei-Faktor-Authentifizierung ist inaktiv. Bitte scanne den QR-Code und bestÀtige zur Aktivierung.
-
Manuelle Eingabe: {{ two_factor_secret }}
-
- {{ qr_code_svg | safe }}
-
-
-
- {% endif %}
-
-
-
-
-
-
-
-
Spieler manuell erstellen
-
-
-
-
-
-
-
- Kennung
- Name & Ănderung
- Status
- Ziel
- Aktionen
-
-
-
- {% for current_player in all_players %}
-
-
- {{ current_player.player_identifier }}
- {% if not current_player.is_approved %}
- Wartet
- {% elif current_player.late_joiner %}
- SpÀteinstieg
- {% endif %}
-
-
-
- {% if current_player.pending_display_name %}
-
-
- WĂŒnscht: {{ current_player.pending_display_name }}
-
-
-
-
- {% endif %}
-
-
- {% if not current_player.is_approved %}
-
- {% else %}
- {% if current_player.is_alive %}
- Lebend
-
- {% else %}
- Tot
-
- {% endif %}
- {% endif %}
-
-
- {% if current_player.target_identifier %}
- {{ current_player.target_identifier }}
-
- {% else %}
- -
- {% endif %}
-
-
-
- {% if current_player.reset_token %}
-
- {% endif %}
-
-
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
- Zeitpunkt
- Killer
- Opfer
- Aktionen
-
-
-
- {% for kill in kill_logs %}
-
-
-
-
-
- Keiner (Admin)
- {% for p in all_players %}
- {{ p.display_name }} ({{ p.player_identifier }})
- {% endfor %}
-
-
-
- {{ kill.victim_name or 'Unbekannt' }} ({{ kill.victim_identifier }})
-
-
-
-
-
-
-
-
- {% else %}
-
- Bisher keine Kills im System.
-
- {% endfor %}
-
-
-
-
-
-
-
-
Aktion planen
-
FĂŒhre die Aktion sofort aus oder plane sie fĂŒr die Zukunft.
-
- Zeitpunkt (Optional)
-
-
-
- BestÀtigen
- Abbrechen
-
-
-
- {% endif %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/templates/game.html b/templates/game.html
deleted file mode 100644
index 52283e1..0000000
--- a/templates/game.html
+++ /dev/null
@@ -1,187 +0,0 @@
-{% extends "layout.html" %}
-{% block content %}
-
-
-
Willkommen, {{ player.display_name }} ({{ player.player_identifier }})!
-
-
- {% if not player.is_approved %}
-
-
Konto wird geprĂŒft…
-
- {% elif game_status == 'pending' %}
-
-
Startet bald…
-
- {% elif game_status == 'finished' %}
-
-
Spiel beendet – Rangliste
-
-
-
- Platz
- Name
- Punkte
-
-
-
- {% for current_player in all_players %}
-
- {{ loop.index }}
- {{ current_player.display_name }}
- {{ current_player.score }}
-
- {% endfor %}
-
-
-
- {% elif game_status in ['running', 'paused'] %}
-
- Ziel
- Punkte
- Regeln
-
-
-
- {% if game_status == 'paused' %}
-
-
Spiel pausiert
-
Das Spiel wurde pausiert. Eliminierungen sind aktuell nicht möglich.
-
- {% endif %}
-
- {% if player.is_alive %}
-
-
Dein Ziel
- {{ target_player.display_name if target_player else 'Niemand' }}
- {% if game_status == 'running' %}
- Kill melden
- {% else %}
- Pausiert
- {% endif %}
-
- {% elif player.late_joiner %}
-
-
Warte auf Einstieg…
-
Du bist dem Spiel wÀhrend einer laufenden Runde beigetreten. Du wirst aktiv, sobald du eingewechselt wirst.
-
- {% else %}
-
-
Eliminiert
-
- {% endif %}
-
-
-
SpielĂŒbersicht
-
Ăberlebende: {{ alive_count }}
-
Deine Kills: {{ player.score }}
-
Gesamte Kills: {{ total_eliminations }}
-
Laufzeit: Wird berechnet...
-
-
-
-
Profil
- {% if player.pending_display_name %}
-
-
Der Name {{ player.pending_display_name }} wird geprĂŒft…
-
- {% else %}
-
Name Àndern
- {% endif %}
-
-
-
-
-
-
-
-
- Platz
- Name
- Punkte
-
-
-
- {% for current_player in all_players %}
-
- {{ loop.index }}
- {{ current_player.display_name }}
- {{ current_player.score }}
-
- {% endfor %}
-
-
-
-
-
-
-
- {{ rules_markup | safe }}
-
-
-
-
-
-
Kill
-
Möchtest du melden, dass du dein Ziel gekillt hast?
-
- Ja, melden
- Abbrechen
-
-
-
-
-
-
-
Bitte warten
-
Warte auf BestÀtigung vom Ziel...
-
-
-
-
-
-
Abgelehnt
-
Das Ziel hat deinen Kill abgelehnt.
-
- Verstanden
-
-
-
-
-
-
-
Wurdest du eliminiert?
-
- {% if pending_attacker_name %}
- {{ pending_attacker_name }} hat gemeldet, dass er/sie dich gekillt hat.
- {% else %}
- Dein Angreifer hat gemeldet, dass er dich gekillt hat.
- {% endif %}
-
-
- BestÀtigen
- Ablehnen
-
-
-
-
-
-
-
Name Àndern
-
Dein neuer Name muss erst geprĂŒft werden.
-
-
-
- {% endif %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/templates/layout.html b/templates/layout.html
deleted file mode 100644
index a28e45f..0000000
--- a/templates/layout.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
- SCOicide Squad
-
-
-
-
-
-
-
-
-
-
-
- {% block content %}{% endblock %}
-
-
-
\ No newline at end of file
diff --git a/templates/login.html b/templates/login.html
deleted file mode 100644
index af4cef1..0000000
--- a/templates/login.html
+++ /dev/null
@@ -1,72 +0,0 @@
-{% extends "layout.html" %}
-{% block content %}
-
- {% if reset_mode %}
-
-
Passwort zurĂŒcksetzen
-
-
- {% elif register_mode %}
-
-
Neues Konto erstellen
-
-
-
- {% elif registration_success %}
-
-
Registrierung erfolgreich
-
Deine ID fĂŒr die Anmeldung lautet:
-
{{ new_identifier }}
-
Bitte speichere oder merke sie dir!
-
-
- {% else %}
-
-
- {% endif %}
-
-{% endblock %}
\ No newline at end of file