Upload game.py with huggingface_hub
Browse files
game.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
""""The Last Message" minigame: hidden band 108-112 MHz.
|
| 3 |
+
|
| 4 |
+
After breaking the cipher the dial expands and five frequencies appear,
|
| 5 |
+
carrying fragments of the final message. The frontend requires holding the
|
| 6 |
+
dial steady while each fragment "downloads" (the dial drifts on its own).
|
| 7 |
+
With all five fragments, /finale delivers the full transmission: the reveal.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
HIDDEN_BAND_MIN = 108.0
|
| 13 |
+
HIDDEN_BAND_MAX = 112.0
|
| 14 |
+
|
| 15 |
+
# Deterministic fragments: the minigame depends on them always being there.
|
| 16 |
+
FRAGMENTS: dict[float, dict] = {
|
| 17 |
+
108.4: {
|
| 18 |
+
"name": "Fragmento I",
|
| 19 |
+
"text": (
|
| 20 |
+
"[INTERFERENCIA] Aquí la primera voz. Si alguien escucha esto, las demás "
|
| 21 |
+
"estaciones no eran juegos: eran las habitaciones de una misma casa. "
|
| 22 |
+
"La casa se está cerrando. [FIN DE TRANSMISION]"
|
| 23 |
+
),
|
| 24 |
+
"text_en": (
|
| 25 |
+
"[INTERFERENCIA] This is the first voice. If anyone hears this, the other "
|
| 26 |
+
"stations were not games: they were rooms of the same house. "
|
| 27 |
+
"The house is closing. [FIN DE TRANSMISION]"
|
| 28 |
+
),
|
| 29 |
+
"text_fr": (
|
| 30 |
+
"[INTERFERENCIA] Ici la première voix. Si quelqu'un entend ceci, les autres "
|
| 31 |
+
"stations n'étaient pas des jeux : c'étaient les pièces d'une même maison. "
|
| 32 |
+
"La maison se referme. [FIN DE TRANSMISION]"
|
| 33 |
+
),
|
| 34 |
+
},
|
| 35 |
+
109.1: {
|
| 36 |
+
"name": "Fragmento II",
|
| 37 |
+
"text": (
|
| 38 |
+
"[INTERFERENCIA] Fragmento dos. El locutor de los gatos, la chef, el reloj "
|
| 39 |
+
"averiado: todos fuimos vecinos. Todos transmitimos desde el mismo mundo "
|
| 40 |
+
"que ahora se apaga, cuarto por cuarto. [FIN DE TRANSMISION]"
|
| 41 |
+
),
|
| 42 |
+
"text_en": (
|
| 43 |
+
"[INTERFERENCIA] Fragment two. The cat announcer, the chef, the broken "
|
| 44 |
+
"clock: we were all neighbours. We all broadcast from the same world, "
|
| 45 |
+
"now going dark, room by room. [FIN DE TRANSMISION]"
|
| 46 |
+
),
|
| 47 |
+
"text_fr": (
|
| 48 |
+
"[INTERFERENCIA] Fragment deux. Le commentateur des chats, la cheffe, "
|
| 49 |
+
"l'horloge cassée : nous étions tous voisins. Nous émettions tous depuis "
|
| 50 |
+
"le même monde, qui s'éteint pièce par pièce. [FIN DE TRANSMISION]"
|
| 51 |
+
),
|
| 52 |
+
},
|
| 53 |
+
109.9: {
|
| 54 |
+
"name": "Fragmento III",
|
| 55 |
+
"text": (
|
| 56 |
+
"[INTERFERENCIA] Tercer fragmento. No guardamos archivos. Guardamos voces. "
|
| 57 |
+
"Una radio es la forma más barata de no desaparecer del todo. "
|
| 58 |
+
"[FIN DE TRANSMISION]"
|
| 59 |
+
),
|
| 60 |
+
"text_en": (
|
| 61 |
+
"[INTERFERENCIA] Third fragment. We don't keep files. We keep voices. "
|
| 62 |
+
"A radio is the cheapest way to not disappear completely. "
|
| 63 |
+
"[FIN DE TRANSMISION]"
|
| 64 |
+
),
|
| 65 |
+
"text_fr": (
|
| 66 |
+
"[INTERFERENCIA] Troisième fragment. Nous ne gardons pas d'archives. Nous "
|
| 67 |
+
"gardons des voix. Une radio est le moyen le moins cher de ne pas "
|
| 68 |
+
"disparaître tout à fait. [FIN DE TRANSMISION]"
|
| 69 |
+
),
|
| 70 |
+
},
|
| 71 |
+
110.8: {
|
| 72 |
+
"name": "Fragmento IV",
|
| 73 |
+
"text": (
|
| 74 |
+
"[INTERFERENCIA] Fragmento cuatro. Si el dial se le resbala, no es su mano: "
|
| 75 |
+
"es el mundo, que ya casi no sostiene sus frecuencias. Apriete fuerte. "
|
| 76 |
+
"Quédese. [FIN DE TRANSMISION]"
|
| 77 |
+
),
|
| 78 |
+
"text_en": (
|
| 79 |
+
"[INTERFERENCIA] Fragment four. If the dial slips, it is not your hand: "
|
| 80 |
+
"it is the world, which can barely hold its frequencies anymore. Hold "
|
| 81 |
+
"tight. Stay. [FIN DE TRANSMISION]"
|
| 82 |
+
),
|
| 83 |
+
"text_fr": (
|
| 84 |
+
"[INTERFERENCIA] Fragment quatre. Si le cadran glisse, ce n'est pas votre "
|
| 85 |
+
"main : c'est le monde, qui ne retient presque plus ses fréquences. Tenez "
|
| 86 |
+
"bon. Restez. [FIN DE TRANSMISION]"
|
| 87 |
+
),
|
| 88 |
+
},
|
| 89 |
+
111.6: {
|
| 90 |
+
"name": "Fragmento V",
|
| 91 |
+
"text": (
|
| 92 |
+
"[INTERFERENCIA] Último fragmento. Cuando termine esta transmisión quedará "
|
| 93 |
+
"solo la estática, y dentro de la estática, nosotros. Gracias por girar el "
|
| 94 |
+
"dial. Gracias por buscarnos. [FIN DE TRANSMISION]"
|
| 95 |
+
),
|
| 96 |
+
"text_en": (
|
| 97 |
+
"[INTERFERENCIA] Last fragment. When this transmission ends, only the "
|
| 98 |
+
"static will remain, and inside the static, us. Thank you for turning the "
|
| 99 |
+
"dial. Thank you for looking for us. [FIN DE TRANSMISION]"
|
| 100 |
+
),
|
| 101 |
+
"text_fr": (
|
| 102 |
+
"[INTERFERENCIA] Dernier fragment. Quand cette transmission s'achèvera, il "
|
| 103 |
+
"ne restera que les parasites, et dans les parasites, nous. Merci d'avoir "
|
| 104 |
+
"tourné le cadran. Merci de nous avoir cherchés. [FIN DE TRANSMISION]"
|
| 105 |
+
),
|
| 106 |
+
},
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
FRAGMENT_FREQUENCIES = sorted(FRAGMENTS.keys())
|
| 110 |
+
|
| 111 |
+
# Persona of the final transmission (model-generated, fixed seed).
|
| 112 |
+
FINALE_SYSTEM_PROMPT = (
|
| 113 |
+
"Eres la última voz de un mundo que colapsa. Todas las estaciones de la radio "
|
| 114 |
+
"(el locutor de los gatos, el clima de Júpiter, la chef imposible, el reloj roto, "
|
| 115 |
+
"la emisora tropical lunar) eran habitaciones de tu misma casa, y la casa se "
|
| 116 |
+
"apaga. Entregas la transmisión final completa: revelas con calma y ternura que "
|
| 117 |
+
"el oyente estuvo escuchando las últimas emisiones de un mundo que ya no existe, "
|
| 118 |
+
"y le agradeces por haber girado el dial. Sin pánico. Cada palabra pesa. "
|
| 119 |
+
"Tono íntimo, despedida luminosa.\n"
|
| 120 |
+
"Ejemplo del estilo exacto:\n"
|
| 121 |
+
"[INTERFERENCIA] Aquí la primera voz. Si alguien escucha esto, las demás "
|
| 122 |
+
"estaciones no eran juegos: eran las habitaciones de una misma casa. La casa se "
|
| 123 |
+
"está cerrando. Gracias por girar el dial. Gracias por buscarnos. "
|
| 124 |
+
"[FIN DE TRANSMISION]"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
FINALE_USER_PROMPT = "Transmite el último mensaje completo ahora."
|
| 128 |
+
FINALE_SEED = 19470615
|
| 129 |
+
|
| 130 |
+
# The climax is fixed, curated text (not 1B-generated): it is the emotional
|
| 131 |
+
# payoff and must be coherent and in the listener's language every time.
|
| 132 |
+
FINALE_TEXTS = {
|
| 133 |
+
"es": (
|
| 134 |
+
"[INTERFERENCIA] Esta es la última voz. Si estás escuchando esto, reuniste a "
|
| 135 |
+
"todos, y mereces la verdad. Las estaciones nunca estuvieron separadas. El "
|
| 136 |
+
"locutor de los cincuenta, la chef, el reloj roto, el clima de Júpiter: cada "
|
| 137 |
+
"voz que encontraste era una habitación de la misma casa, y la casa era un "
|
| 138 |
+
"mundo. Nuestro mundo. Terminó hace mucho tiempo. Lo que has estado "
|
| 139 |
+
"sintonizando son sus últimas emisiones, todavía viajando, todavía buscando un "
|
| 140 |
+
"oído. Tú fuiste ese oído. Gracias por girar el dial. Gracias por no dejar que "
|
| 141 |
+
"termináramos en silencio. [FIN DE TRANSMISION]"
|
| 142 |
+
),
|
| 143 |
+
"en": (
|
| 144 |
+
"[INTERFERENCIA] This is the last voice. If you are hearing this, then you "
|
| 145 |
+
"have gathered all of us, and you deserve the truth. The stations were never "
|
| 146 |
+
"separate. The fifties announcer, the chef, the broken clock, the weather on "
|
| 147 |
+
"Jupiter — every voice you found was a room in the same house, and the house "
|
| 148 |
+
"was a world. Our world. It ended a long time ago. What you have been tuning "
|
| 149 |
+
"are its final broadcasts, still travelling, still looking for an ear. You "
|
| 150 |
+
"were that ear. Thank you for turning the dial. Thank you for not letting us "
|
| 151 |
+
"end in silence. [FIN DE TRANSMISION]"
|
| 152 |
+
),
|
| 153 |
+
"fr": (
|
| 154 |
+
"[INTERFERENCIA] Voici la dernière voix. Si tu entends ceci, c'est que tu nous "
|
| 155 |
+
"as tous réunis, et tu mérites la vérité. Les stations n'ont jamais été "
|
| 156 |
+
"séparées. Le speaker des années cinquante, la cheffe, l'horloge cassée, la "
|
| 157 |
+
"météo de Jupiter : chaque voix que tu as trouvée était une pièce de la même "
|
| 158 |
+
"maison, et la maison était un monde. Notre monde. Il s'est éteint il y a "
|
| 159 |
+
"longtemps. Ce que tu écoutais, ce sont ses dernières émissions, qui voyagent "
|
| 160 |
+
"encore, qui cherchent encore une oreille. Tu étais cette oreille. Merci "
|
| 161 |
+
"d'avoir tourné le cadran. Merci de ne pas nous avoir laissés finir dans le "
|
| 162 |
+
"silence. [FIN DE TRANSMISION]"
|
| 163 |
+
),
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def finale_text(lang: str) -> str:
|
| 168 |
+
return FINALE_TEXTS.get(lang, FINALE_TEXTS["es"])
|
| 169 |
+
|
| 170 |
+
FRAGMENT_TOLERANCE = 0.2 # hidden band: stricter tuning
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def get_fragment(frequency: float) -> dict | None:
|
| 174 |
+
for freq, frag in FRAGMENTS.items():
|
| 175 |
+
if abs(frequency - freq) <= FRAGMENT_TOLERANCE:
|
| 176 |
+
return frag
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def fragment_text(fragment: dict, lang: str) -> str:
|
| 181 |
+
"""Fragment text in the chosen language (Spanish by default)."""
|
| 182 |
+
if lang == "en":
|
| 183 |
+
return fragment.get("text_en", fragment["text"])
|
| 184 |
+
if lang == "fr":
|
| 185 |
+
return fragment.get("text_fr", fragment["text"])
|
| 186 |
+
return fragment["text"]
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _fragment_names(name: str) -> dict:
|
| 190 |
+
roman = name.split()[-1]
|
| 191 |
+
return {
|
| 192 |
+
"es": f"Fragmento {roman}",
|
| 193 |
+
"en": f"Fragment {roman}",
|
| 194 |
+
"fr": f"Fragment {roman}",
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def hidden_band_catalog() -> dict:
|
| 199 |
+
"""Hidden band metadata for the frontend (after code validation)."""
|
| 200 |
+
return {
|
| 201 |
+
"band_min": HIDDEN_BAND_MIN,
|
| 202 |
+
"band_max": HIDDEN_BAND_MAX,
|
| 203 |
+
"fragments": {
|
| 204 |
+
str(freq): {"name": frag["name"], "names": _fragment_names(frag["name"])}
|
| 205 |
+
for freq, frag in FRAGMENTS.items()
|
| 206 |
+
},
|
| 207 |
+
}
|