186 lines
5.2 KiB
Python
186 lines
5.2 KiB
Python
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
|