feat: add boilerplate

This commit is contained in:
2026-10-26 21:56:23 +01:00
commit cc6675f7c6
29 changed files with 2231 additions and 0 deletions

185
app/game.py Normal file
View File

@@ -0,0 +1,185 @@
import json
import random
from sqlalchemy.orm import Session
from app.models import Answer, GameSession, Job, Player, Round
from app.ral import option_payload
WORK_LABELS = {
"hybrid": "Hybrid",
"remote": "Full remote",
"onsite": "In-place only",
}
def playable_jobs(db: Session) -> list[Job]:
return (
db.query(Job)
.filter(Job.status.in_(["ready", "needs_salary"]))
.filter((Job.ral_exact.isnot(None)) | (Job.ral_min.isnot(None)))
.all()
)
def start_game(db: Session, session: GameSession) -> Round:
jobs = playable_jobs(db)
if not jobs:
raise ValueError("No playable jobs in the database. Add some from the admin panel.")
hard = [j for j in jobs if j.difficulty >= 2] or jobs
easy = [j for j in jobs if j.difficulty == 1] or jobs
random.shuffle(hard)
random.shuffle(easy)
selected: list[Job] = []
hi = ei = 0
for i in range(session.num_rounds):
bonus = (i + 1) % 3 == 0
pool = hard if bonus else easy
idx_source = hi if bonus else ei
job = pool[idx_source % len(pool)]
if bonus:
hi += 1
else:
ei += 1
selected.append(job)
for old in list(session.rounds):
db.delete(old)
db.flush()
first = None
for i, job in enumerate(selected, start=1):
payload = json.loads(option_payload(job))
rnd = Round(
session_id=session.id,
job_id=job.id,
round_number=i,
is_bonus=(i % 3 == 0),
options_json=json.dumps(payload["options"]),
correct_option=payload["correct"],
status="open" if i == 1 else "pending",
)
db.add(rnd)
if i == 1:
first = rnd
session.status = "playing"
session.current_round = 1
db.commit()
db.refresh(first)
return first
def current_round(db: Session, session: GameSession) -> Round | None:
return (
db.query(Round)
.filter(Round.session_id == session.id, Round.round_number == session.current_round)
.first()
)
def score_answer(rnd: Round, job: Job, choice: str, precise: int | None, work_type: str) -> int:
points = 0
mc_ok = choice == rnd.correct_option
if rnd.is_bonus:
if mc_ok:
points += 150
if precise is not None and job.ral_exact:
points += 300 if precise == job.ral_exact else -50
if work_type and work_type == job.work_type:
points += 50
else:
if mc_ok:
points += 100
if precise is not None and job.ral_exact:
points += 200 if precise == job.ral_exact else -50
return points
def submit_answer(
db: Session,
player: Player,
rnd: Round,
choice: str,
precise: int | None,
work_type: str,
) -> Answer:
existing = (
db.query(Answer)
.filter(Answer.round_id == rnd.id, Answer.player_id == player.id)
.first()
)
if existing:
return existing
if rnd.status != "open":
raise ValueError("This round is closed")
job = rnd.job
points = score_answer(rnd, job, choice, precise, work_type)
answer = Answer(
round_id=rnd.id,
player_id=player.id,
choice=choice,
precise_guess=precise,
work_type_guess=work_type or "",
points=points,
)
db.add(answer)
db.commit()
db.refresh(answer)
return answer
def maybe_close_round(db: Session, session: GameSession, rnd: Round) -> bool:
db.refresh(rnd)
active = [p for p in session.players if p.connected]
if not active:
return False
answers = db.query(Answer).filter(Answer.round_id == rnd.id).all()
answered_ids = {a.player_id for a in answers}
if not all(p.id in answered_ids for p in active):
return False
rnd.status = "revealed"
for answer in answers:
answer.player.score += answer.points
db.commit()
return True
def advance(db: Session, session: GameSession) -> Round | None:
nxt = (
db.query(Round)
.filter(Round.session_id == session.id, Round.round_number == session.current_round + 1)
.first()
)
if not nxt:
session.status = "finished"
db.commit()
return None
nxt.status = "open"
session.current_round += 1
db.commit()
db.refresh(nxt)
return nxt
def public_job(session: GameSession, job: Job, bonus: bool) -> dict:
description = job.description_clean
if bonus:
words = description.split()
description = " ".join(words[: max(40, len(words) // 2)])
if len(words) > 40:
description += " ..."
data = {
"title": job.title,
"description": description,
"has_exact": job.ral_exact is not None,
"location": job.location,
}
if not session.hide_company:
data["company"] = job.company
if not session.hide_subject:
data["subject"] = job.subject
if session.extra_guesses:
data["extra"] = job.extra_info
return data