Rum: fælles BaseRoom til timer, udsendelse og speaker
Det, der ikke handler om Fup og Fakta, flytter op i BaseRoom, så et andet spil kan bruge det. Speakeren kan få sin egen lines.json. Fup og Fakta opfører sig som før. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-3
@@ -91,15 +91,16 @@ TEASES_FEST = ["Bro er cooked!", "[laughs] L, tag en tår!", "Ohio-moment! Skål
|
||||
|
||||
|
||||
class Narrator:
|
||||
def __init__(self, rng: random.Random | None = None, fest: bool = False):
|
||||
def __init__(self, rng: random.Random | None = None, fest: bool = False, lines: dict | None = None):
|
||||
self.rng = rng or random.Random()
|
||||
self.lines = LINES if lines is None else lines # et andet spil har sin egen lines.json
|
||||
self.fest = fest # fest: replikken "<begivenhed>_fest" bruges, når den findes
|
||||
self._recent: dict[str, list[int]] = {}
|
||||
|
||||
def line(self, event: str, **values) -> str | None:
|
||||
if self.fest and LINES.get(event + "_fest"):
|
||||
if self.fest and self.lines.get(event + "_fest"):
|
||||
event += "_fest"
|
||||
variants = LINES.get(event)
|
||||
variants = self.lines.get(event)
|
||||
if not variants:
|
||||
return None
|
||||
recent = self._recent.setdefault(event, [])
|
||||
|
||||
+115
-77
@@ -50,11 +50,16 @@ EXPLODE_EACH_MS = 250 # opsummeringen: tid pr. løgn, der eksploderer
|
||||
COUNT_UP_MS = 2600 # opsummeringen: pointene tæller op
|
||||
MIC_ACTIVE_FOR = 25 # mikrofonen regnes som tændt så længe efter sidste lydstump
|
||||
|
||||
_rooms: dict[str, "Room"] = {}
|
||||
_rooms: dict[str, "BaseRoom"] = {}
|
||||
|
||||
|
||||
class Room:
|
||||
def __init__(self, code: str, game: Game, host_username: str):
|
||||
class BaseRoom:
|
||||
"""Det fælles for alle spil: forbindelser, timer, udsendelse og speakerens kø.
|
||||
Spillet (`game`) skal have deadline, hold_until, tick(), phase_key() og drain_events()."""
|
||||
|
||||
mic_laughs = 0
|
||||
|
||||
def __init__(self, code: str, game, host_username: str):
|
||||
self.code = code
|
||||
self.game = game
|
||||
self.host_username = host_username
|
||||
@@ -62,31 +67,16 @@ class Room:
|
||||
self.last_activity = time.time()
|
||||
self._timer: asyncio.TimerHandle | None = None
|
||||
self._broadcast_pending = False
|
||||
self.narrator = Narrator(fest=game.fest)
|
||||
# Fest-spil har sin egen speaker til alt; ellers bruges kategoriens/standardstemmen
|
||||
self.voice = (settings.FEST_VOICE_ID or None) if game.fest else None
|
||||
self.voice: str | None = None
|
||||
self.narration: list[dict] = [] # [{id, url, ms, at, text}] til værtens storskærm
|
||||
self._narr_seq = 0
|
||||
self._voice_queue: asyncio.Queue | None = None
|
||||
self._side_timers: list[asyncio.TimerHandle] = []
|
||||
self._welcomed = False
|
||||
self._last_join_line = 0.0
|
||||
self._lobby_waiting_said = False
|
||||
# Tale-tidslinje: hvornår speakeren forventes at være færdig med den aktuelle skærms replikker
|
||||
self.speech_end = 0.0
|
||||
self._phase_key = game.phase_key()
|
||||
self._pending: dict[str, list[float]] = {} # fasenøgle -> starttider for replikker under generering
|
||||
# Hjernen og hukommelsen
|
||||
self.brain = get_brain(fest=game.fest)
|
||||
self.memory: list[str] = []
|
||||
self._tease_texts: dict[str, str] = {}
|
||||
self.fooled_pairs: dict[tuple[str, str], int] = {} # (narret, forfatter) -> antal gange i aften
|
||||
self._progress_said: list[float] = []
|
||||
self._attempts: dict[str, int] = {}
|
||||
# Rummets mikrofon (eksperimentel)
|
||||
self.mic_last = 0.0
|
||||
self.mic_laughs = 0
|
||||
self._mic_busy = False
|
||||
|
||||
def touch(self):
|
||||
self.last_activity = time.time()
|
||||
@@ -165,6 +155,112 @@ class Room:
|
||||
for h in self._side_timers:
|
||||
h.cancel()
|
||||
|
||||
def handle_events(self):
|
||||
self.game.drain_events()
|
||||
|
||||
def host_opened(self):
|
||||
pass
|
||||
|
||||
def mic_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def hear(self, audio: bytes, content_type: str) -> dict:
|
||||
return {"heard": False}
|
||||
|
||||
async def before_host_state(self, conn):
|
||||
"""Kaldes før hvert snapshot til storskærmen (fx store data, der kun sendes én gang)."""
|
||||
|
||||
async def host_action(self, conn, action: str, content: dict, now: float) -> bool:
|
||||
"""Værtshandlinger ud over start/skip/play_again/kick. False = ukendt."""
|
||||
return False
|
||||
|
||||
async def play_action(self, conn, member_id: str, action: str, content: dict, now: float):
|
||||
"""Telefonens handlinger i andre spil end Fup og Fakta (som consumers selv håndterer)."""
|
||||
await conn.send_error("bad_request", "Ukendt handling.")
|
||||
|
||||
# ── speakerens kø ────────────────────────────────────────────────────────
|
||||
def _synth(self, text: str, voice: str | None = None):
|
||||
return tts.synth(text, voice or self.voice)
|
||||
|
||||
def say(self, text: str | None, blocking: bool = True, voice: str | None = None):
|
||||
"""Læg en replik i kø. Lyden genereres med det samme, men udgives i rækkefølge.
|
||||
blocking=False: en sidebemærkning, som ikke holder skærmen tilbage (fx "det var sandheden")."""
|
||||
if not text:
|
||||
return
|
||||
if self._voice_queue is None:
|
||||
self._voice_queue = asyncio.Queue()
|
||||
asyncio.get_running_loop().create_task(self._voice_worker())
|
||||
key = self.game.phase_key()
|
||||
started = time.time() if blocking else None
|
||||
if blocking:
|
||||
self._pending.setdefault(key, []).append(started)
|
||||
task = asyncio.get_running_loop().create_task(self._synth(text, voice))
|
||||
self._voice_queue.put_nowait((text, task, key, started))
|
||||
self._update_hold()
|
||||
|
||||
async def _voice_worker(self):
|
||||
while True:
|
||||
text, task, key, started = await self._voice_queue.get()
|
||||
try:
|
||||
audio = await task
|
||||
except Exception: # noqa: BLE001
|
||||
audio = None
|
||||
pending = self._pending.get(key, [])
|
||||
if started is not None and started in pending:
|
||||
pending.remove(started)
|
||||
if not pending:
|
||||
self._pending.pop(key, None)
|
||||
# En replik til en skærm, der allerede er væk, afspilles ikke.
|
||||
if audio and key == self.game.phase_key():
|
||||
self._publish(text, audio, key, blocking=started is not None)
|
||||
self._update_hold()
|
||||
self._schedule_timer()
|
||||
|
||||
def _publish(self, text: str, audio: dict, key: str, blocking: bool = True):
|
||||
now = time.time()
|
||||
if blocking:
|
||||
start = max(now, self.speech_end)
|
||||
self.speech_end = start + audio["ms"] / 1000 + SPEECH_GAP
|
||||
self._narr_seq += 1
|
||||
self.narration.append({"id": self._narr_seq, "url": audio["url"], "ms": audio["ms"],
|
||||
"at": int(now * 1000), "text": untag(text), "key": key})
|
||||
del self.narration[:-NARRATION_KEEP]
|
||||
self.broadcast()
|
||||
|
||||
async def forward_reaction(self, reaction: dict):
|
||||
"""Emojis fra telefonerne går direkte til storskærmen (ingen fuld tilstand; de må gerne spammes)."""
|
||||
msg = {"type": "react", **reaction}
|
||||
for conn in list(self.connections):
|
||||
if getattr(conn, "is_host", False):
|
||||
try:
|
||||
await conn.send_json(msg)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
class Room(BaseRoom):
|
||||
"""Et rum med Fup og Fakta."""
|
||||
kind = "fof"
|
||||
|
||||
def __init__(self, code: str, game: Game, host_username: str):
|
||||
super().__init__(code, game, host_username)
|
||||
self.narrator = Narrator(fest=game.fest)
|
||||
# Fest-spil har sin egen speaker til alt; ellers bruges kategoriens/standardstemmen
|
||||
self.voice = (settings.FEST_VOICE_ID or None) if game.fest else None
|
||||
self._last_join_line = 0.0
|
||||
self._lobby_waiting_said = False
|
||||
# Hjernen og hukommelsen
|
||||
self.brain = get_brain(fest=game.fest)
|
||||
self.memory: list[str] = []
|
||||
self._tease_texts: dict[str, str] = {}
|
||||
self.fooled_pairs: dict[tuple[str, str], int] = {} # (narret, forfatter) -> antal gange i aften
|
||||
self._progress_said: list[float] = []
|
||||
self._attempts: dict[str, int] = {}
|
||||
# Rummets mikrofon (eksperimentel)
|
||||
self.mic_last = 0.0
|
||||
self.mic_laughs = 0
|
||||
self._mic_busy = False
|
||||
|
||||
# ── speakeren ────────────────────────────────────────────────────────────
|
||||
def handle_events(self):
|
||||
"""Tøm motorens begivenheder og lad speakeren reagere. Kaldes også efter afviste handlinger."""
|
||||
@@ -388,54 +484,6 @@ class Room:
|
||||
def _question_voice(self) -> str | None:
|
||||
return (self.game.question or {}).get("voice")
|
||||
|
||||
def _synth(self, text: str, voice: str | None = None):
|
||||
return tts.synth(text, voice or self.voice)
|
||||
|
||||
def say(self, text: str | None, blocking: bool = True, voice: str | None = None):
|
||||
"""Læg en replik i kø. Lyden genereres med det samme, men udgives i rækkefølge.
|
||||
blocking=False: en sidebemærkning, som ikke holder skærmen tilbage (fx "det var sandheden")."""
|
||||
if not text:
|
||||
return
|
||||
if self._voice_queue is None:
|
||||
self._voice_queue = asyncio.Queue()
|
||||
asyncio.get_running_loop().create_task(self._voice_worker())
|
||||
key = self.game.phase_key()
|
||||
started = time.time() if blocking else None
|
||||
if blocking:
|
||||
self._pending.setdefault(key, []).append(started)
|
||||
task = asyncio.get_running_loop().create_task(self._synth(text, voice))
|
||||
self._voice_queue.put_nowait((text, task, key, started))
|
||||
self._update_hold()
|
||||
|
||||
async def _voice_worker(self):
|
||||
while True:
|
||||
text, task, key, started = await self._voice_queue.get()
|
||||
try:
|
||||
audio = await task
|
||||
except Exception: # noqa: BLE001
|
||||
audio = None
|
||||
pending = self._pending.get(key, [])
|
||||
if started is not None and started in pending:
|
||||
pending.remove(started)
|
||||
if not pending:
|
||||
self._pending.pop(key, None)
|
||||
# En replik til en skærm, der allerede er væk, afspilles ikke.
|
||||
if audio and key == self.game.phase_key():
|
||||
self._publish(text, audio, key, blocking=started is not None)
|
||||
self._update_hold()
|
||||
self._schedule_timer()
|
||||
|
||||
def _publish(self, text: str, audio: dict, key: str, blocking: bool = True):
|
||||
now = time.time()
|
||||
if blocking:
|
||||
start = max(now, self.speech_end)
|
||||
self.speech_end = start + audio["ms"] / 1000 + SPEECH_GAP
|
||||
self._narr_seq += 1
|
||||
self.narration.append({"id": self._narr_seq, "url": audio["url"], "ms": audio["ms"],
|
||||
"at": int(now * 1000), "text": untag(text), "key": key})
|
||||
del self.narration[:-NARRATION_KEEP]
|
||||
self.broadcast()
|
||||
|
||||
def _at_deadline_minus(self, seconds: int, phase: str, event: str):
|
||||
g = self.game
|
||||
if g.deadline is None:
|
||||
@@ -594,16 +642,6 @@ class Room:
|
||||
g.start_rules_timeline(time.time(), [max(2500, a["ms"] + 600) if a else engine.RULES_STEP_MS for a in results])
|
||||
self.changed()
|
||||
|
||||
async def forward_reaction(self, reaction: dict):
|
||||
"""Emojis fra telefonerne går direkte til storskærmen (ingen fuld tilstand; de må gerne spammes)."""
|
||||
msg = {"type": "react", **reaction}
|
||||
for conn in list(self.connections):
|
||||
if getattr(conn, "is_host", False):
|
||||
try:
|
||||
await conn.send_json(msg)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
async def _brain_teases(self, names: dict):
|
||||
"""Personlige drillerier til de narrede; lyden laves med det samme, så den er klar, når stemplet lander."""
|
||||
g = self.game
|
||||
|
||||
Reference in New Issue
Block a user