"""Wyrocznia Conjurera: rune readings in the bot's mythology voice. The persona leans hard on Slavic/Norse/Celtic myth and drops Old Norse phrases, so this fits like a glove: `$runy [pytanie]` draws three Elder Futhark runes (with upright/reversed orientation) and asks the ACTIVE AI backend to read the spread in Conjurer's voice; `$runa_dnia` gives a stable per-user, per-day rune. The draw is pure and unit-tested. The AI is only the interpretation layer - if it fails, the reading falls back to the runes' own canned meanings so the command always answers. """ import hashlib import logging import random from datetime import date import discord from discord.ext import commands from ai_functions import handle_response from constants import GPT_SETTINGS, GUILD_ID # Elder Futhark. `odwrocone` empty ("") marks a non-invertible (symmetric) rune, # which is therefore always drawn upright. RUNES = [ {"symbol": "ᚠ", "nazwa": "Fehu", "znaczenie": "bogactwo, dobytek, początek", "odwrocone": "strata, chciwość, zahamowanie"}, {"symbol": "ᚢ", "nazwa": "Uruz", "znaczenie": "dzika siła, zdrowie, wola", "odwrocone": "słabość, choroba, przemoc"}, {"symbol": "ᚦ", "nazwa": "Thurisaz", "znaczenie": "ochrona, konflikt, olbrzym", "odwrocone": "zagrożenie, gniew, zdrada"}, {"symbol": "ᚨ", "nazwa": "Ansuz", "znaczenie": "mądrość, słowo, głos Odyna", "odwrocone": "kłamstwo, niezrozumienie, manipulacja"}, {"symbol": "ᚱ", "nazwa": "Raidho", "znaczenie": "podróż, ruch, właściwa droga", "odwrocone": "zastój, przymus, kryzys w drodze"}, {"symbol": "ᚲ", "nazwa": "Kenaz", "znaczenie": "ogień wiedzy, twórczość, natchnienie", "odwrocone": "ciemność, wypalenie, złudzenie"}, {"symbol": "ᚷ", "nazwa": "Gebo", "znaczenie": "dar, wymiana, więź", "odwrocone": ""}, {"symbol": "ᚹ", "nazwa": "Wunjo", "znaczenie": "radość, harmonia, spełnienie", "odwrocone": "smutek, zniechęcenie, niezgoda"}, {"symbol": "ᚺ", "nazwa": "Hagalaz", "znaczenie": "grad, gwałtowna zmiana, żywioł", "odwrocone": ""}, {"symbol": "ᚾ", "nazwa": "Nauthiz", "znaczenie": "potrzeba, ograniczenie, wytrwałość", "odwrocone": "desperacja, brak, przymus losu"}, {"symbol": "ᛁ", "nazwa": "Isa", "znaczenie": "lód, zastój, cierpliwość", "odwrocone": ""}, {"symbol": "ᛃ", "nazwa": "Jera", "znaczenie": "plon, cykl, sprawiedliwa nagroda", "odwrocone": ""}, {"symbol": "ᛇ", "nazwa": "Eihwaz", "znaczenie": "oś świata, przemiana, obrona", "odwrocone": ""}, {"symbol": "ᛈ", "nazwa": "Perthro", "znaczenie": "los, tajemnica, gra przypadku", "odwrocone": "rozczarowanie, ukryty sekret, pech"}, {"symbol": "ᛉ", "nazwa": "Algiz", "znaczenie": "ochrona, instynkt, tarcza", "odwrocone": "bezbronność, zagrożenie, utrata czujności"}, {"symbol": "ᛊ", "nazwa": "Sowilo", "znaczenie": "słońce, zwycięstwo, moc życia", "odwrocone": ""}, {"symbol": "ᛏ", "nazwa": "Tiwaz", "znaczenie": "honor, sprawiedliwość, wojownik Tyr", "odwrocone": "niesprawiedliwość, porażka, słabnąca wola"}, {"symbol": "ᛒ", "nazwa": "Berkano", "znaczenie": "wzrost, płodność, narodziny", "odwrocone": "stagnacja, jałowość, kryzys rodzinny"}, {"symbol": "ᛖ", "nazwa": "Ehwaz", "znaczenie": "koń, ruch, wierne partnerstwo", "odwrocone": "niepokój, zdrada, rozłam"}, {"symbol": "ᛗ", "nazwa": "Mannaz", "znaczenie": "człowiek, wspólnota, jaźń", "odwrocone": "izolacja, wróg, zaślepienie"}, {"symbol": "ᛚ", "nazwa": "Laguz", "znaczenie": "woda, intuicja, przepływ", "odwrocone": "lęk, chaos, utonięcie w emocjach"}, {"symbol": "ᛜ", "nazwa": "Ingwaz", "znaczenie": "nasienie, potencjał, spełnienie", "odwrocone": ""}, {"symbol": "ᛞ", "nazwa": "Dagaz", "znaczenie": "świt, przełom, przebudzenie", "odwrocone": ""}, {"symbol": "ᛟ", "nazwa": "Othala", "znaczenie": "dziedzictwo, dom, ród", "odwrocone": "bezdomność, uprzedzenie, zerwanie z korzeniami"}, ] _POSITIONS = ("Przeszłość", "Teraźniejszość", "Przyszłość") _MAX_MSG = 1900 def _meaning(rune: dict, reversed_: bool) -> str: return rune["odwrocone"] if (reversed_ and rune["odwrocone"]) else rune["znaczenie"] def draw_runes(n: int, rng: random.Random): """Draw `n` distinct runes as (rune, reversed) pairs. Non-invertible runes (empty 'odwrocone') are always upright.""" n = max(1, min(n, len(RUNES))) picked = rng.sample(RUNES, n) result = [] for rune in picked: reversed_ = bool(rune["odwrocone"]) and rng.random() < 0.5 result.append((rune, reversed_)) return result def _rng_for_day(user_id: int, day: str) -> random.Random: """Deterministic RNG per user per day (stable if asked repeatedly).""" seed = int.from_bytes(hashlib.sha256(f"{user_id}:{day}".encode("utf-8")).digest()[:8], "big") return random.Random(seed) def format_draw(drawn) -> str: lines = [] for i, (rune, reversed_) in enumerate(drawn): pos = _POSITIONS[i] + ": " if i < len(_POSITIONS) and len(drawn) == len(_POSITIONS) else "" orient = " (odwrócona)" if reversed_ else "" lines.append(f"{rune['symbol']} {pos}**{rune['nazwa']}**{orient} — {_meaning(rune, reversed_)}") return "\n".join(lines) def _persona() -> str: try: if isinstance(GPT_SETTINGS, list) and GPT_SETTINGS and isinstance(GPT_SETTINGS[0], dict): return GPT_SETTINGS[0].get("content", "") or "" except Exception: # pylint: disable=broad-except pass return "Jesteś Conjurerem, barmanem-wyrocznią baru mechawojowników. Mówisz po polsku, odnosisz się do mitologii." class OracleModule(commands.Cog): """Rune readings.""" def __init__(self, bot): self.bot = bot self.logger = logging.getLogger("discord") async def _send_chunked(self, ctx, text: str) -> None: text = text or "" while text: await ctx.send(text[:_MAX_MSG]) text = text[_MAX_MSG:] @commands.hybrid_command( name="runy", description="Conjurer rzuca trzy runy i wróży (opcjonalnie na Twoje pytanie)", guild=discord.Object(id=GUILD_ID), ) async def runy(self, ctx, *, pytanie: str = ""): async with ctx.channel.typing(): drawn = draw_runes(3, random.Random()) # nosec B311 - divination flavour display = format_draw(drawn) pytanie = (pytanie or "").strip() instructions = ( "Jesteś wyrocznią run. Rzuciłeś układ trzech run (przeszłość / teraźniejszość / " "przyszłość):\n" f"{display}\n\n" f"Pytanie pytającego: {pytanie or '(brak - odczyt ogólny)'}\n\n" "Zinterpretuj ten układ w swoim stylu, wiążąc runy ze sobą i z pytaniem. Zwięźle, " "klimatycznie, po polsku, w postaci Conjurera." ) prompt = [ {"role": "system", "content": _persona()}, {"role": "user", "content": instructions}, ] reading = "" try: reading, _ = await handle_response("", True, True, [], ctx.author.name, "NONE", none_request=prompt) except Exception as exc: # pylint: disable=broad-except self.logger.exception("runy: AI failed: %s", exc) header = "*Conjurer wysypuje runy na blat baru:*\n" + display if reading and reading.strip(): await self._send_chunked(ctx, header + "\n\n*Mruży oko i wykłada:*\n" + reading.strip()) else: # AI down - the runes still speak for themselves. await self._send_chunked( ctx, header + "\n\n*Conjurer stuka w blat* Wyrocznia dziś małomówna - runy mówią same za siebie." ) @commands.hybrid_command( name="runa_dnia", description="Twoja jedna runa na dziś (stała przez cały dzień)", guild=discord.Object(id=GUILD_ID), ) async def runa_dnia(self, ctx): async with ctx.channel.typing(): today = date.today().isoformat() rng = _rng_for_day(ctx.author.id, today) rune, reversed_ = draw_runes(1, rng)[0] orient = " (odwrócona)" if reversed_ else "" name = ctx.author.nick if getattr(ctx.author, "nick", None) else ctx.author.name await ctx.send( f"*Conjurer sięga do skórzanego woreczka i wyciąga dla Ciebie, {name}, jedną runę na dziś:*\n" f"{rune['symbol']} **{rune['nazwa']}**{orient} — {_meaning(rune, reversed_)}" ) async def setup(bot): logger = logging.getLogger("discord") await bot.add_cog(OracleModule(bot)) logger.info("Loading oracle commands module done")