juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

mkpalabras.py (8398B)


      1 #!/usr/bin/env python3
      2 """mkpalabras.py - genera palabras.c (listas del Wordle "Palabra").
      3 
      4     python3 mkpalabras.py > palabras.c
      5 
      6 Fuentes (se bajan a $XDG_CACHE_HOME/palabra la primera vez; hace falta hunspell,
      7 con `unmunch`):
      8   - Diccionarios hunspell es_ES y es_AR de LibreOffice (github LibreOffice/dictionaries).
      9   - Frecuencias de subtitulos (github hermitdave/FrequencyWords, 2018/es: es_50k y es_full).
     10   - Lista de palabras de la RAE (github JorgeDuenasLerin/diccionario-espanol-txt).
     11 VALIDAS: toda palabra de 5 letras de esas listas (y de expandir los diccionarios con
     12 unmunch) que hunspell es_ES o es_AR acepta (es_AR suma el voseo: tenés, sabés).
     13 SOLUCIONES: las palabras de 5 letras mas frecuentes (es_50k) que son validas, sacando
     14 plurales, formas verbales raras para una solucion, pronombres encliticos y la lista
     15 negra de abajo.
     16 
     17 Codificacion: letra A=0..Z=25, Ñ=26; palabra = l0<<20 | l1<<15 | l2<<10 | l3<<5 | l4
     18 (25 bits, el orden numerico es el alfabetico con la Ñ al final). En las soluciones
     19 los bits 25..27 dicen que letra lleva tilde (1..5, 0 = ninguna) y el 28 si es dieresis.
     20 """
     21 import os, re, subprocess, sys, unicodedata, urllib.request
     22 
     23 CACHE = os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")), "palabra")
     24 URLS = {
     25     "es_ES.dic": "https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_ES.dic",
     26     "es_ES.aff": "https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_ES.aff",
     27     "es_AR.dic": "https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_AR.dic",
     28     "es_AR.aff": "https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_AR.aff",
     29     "es_50k.txt": "https://raw.githubusercontent.com/hermitdave/FrequencyWords/master/content/2018/es/es_50k.txt",
     30     "es_full.txt": "https://raw.githubusercontent.com/hermitdave/FrequencyWords/master/content/2018/es/es_full.txt",
     31     "rae.txt": "https://raw.githubusercontent.com/JorgeDuenasLerin/diccionario-espanol-txt/master/data/allwords.txt",
     32 }
     33 NSOL = 2000
     34 
     35 # Fuera de las soluciones (siguen siendo intentos validos): groserias, nombres,
     36 # palabras en ingles o raras que se cuelan por los subtitulos, formas verbales feas.
     37 BLACK = set("""
     38 henry maría joder coño puta putas carajo mierda pene culos culo verga polla chupa chupe
     39 tetas teta follar folla follé follo culón nazis nazi idiota marica maricón perras zorra
     40 puto bitch sexy sexo sexis porno orgía coito semen vulva pubis fecal ojete ostia golfa
     41 cagar cagué meada meado mamón tanga braga violé violó jodió merca rache
     42 darle verte hazlo irnos verme darme darte verla verlo dame dile dime hazme hazla hazle
     43 hazte ponla ponle ponlo ponme ponte tenlo tenme vente denle denme dadle dadme
     44 sabré habré podré habrá podrá sabrá serán érase fuere diera diese fuese viese viera
     45 oyera amara hiera oiría seáis veáis amáis
     46 quizá quizás usted algún ningún según depto
     47 adama alana apolo brice bruno celia césar dante delia diego ester guido hanzo jorge
     48 judas leila lucia lucio luisa marta mateo maura mauro margo mingo mirza nerón paula
     49 paulo pedro ramón ringo romeo simón tanja lores roque silva siena milán argel congo
     50 corea libia siria japón weber falco greco grand grant green force dance table sport
     51 stand cross crack corps blues volvo voila pelis hippy hobby kanji kappa tesla curie
     52 olive rouge aries virgo hadar salma sacha rocha tamil longo mondo mongo bongo fusco
     53 cilla chita marga lauda belén suiza china india checa cague folle folló viole violo
     54 quier usara usare reirá reiré erase sansa chist chapo trena petar logar levar irisa insto
     55 amina bruce argos salia sabre haría orine orinó facha narco quepa
     56 """.split())
     57 
     58 # Pasan aunque las reglas de abajo las descartarian.
     59 WHITE = set("antes menos adiós dosis tesis tenis revés alias bahía manía élite ídolo óvulo guion canon ganas gafas".split())
     60 
     61 LETTERS = "abcdefghijklmnopqrstuvwxyzñ"
     62 OK = re.compile("^[a-zñáéíóúü]{5}$")
     63 
     64 
     65 def fetch():
     66     os.makedirs(CACHE, exist_ok=True)
     67     for name, url in URLS.items():
     68         p = os.path.join(CACHE, name)
     69         if not os.path.exists(p):
     70             print("bajando " + url, file=sys.stderr)
     71             urllib.request.urlretrieve(url, p)
     72     for d in ("es_ES", "es_AR"):
     73         p = os.path.join(CACHE, d + "_all.txt")
     74         if not os.path.exists(p):
     75             with open(p, "wb") as out:
     76                 subprocess.run(["unmunch", os.path.join(CACHE, d + ".dic"), os.path.join(CACHE, d + ".aff")],
     77                                stdout=out, stderr=subprocess.DEVNULL, check=True)
     78 
     79 
     80 def read_words(name):
     81     for line in open(os.path.join(CACHE, name), encoding="utf-8", errors="replace"):
     82         p = line.split()
     83         if p:
     84             yield p[0]
     85 
     86 
     87 def hunspell_ok(words):
     88     ok = set()
     89     for d in ("es_ES", "es_AR"):
     90         r = subprocess.run(["hunspell", "-d", os.path.join(CACHE, d), "-G"], input="\n".join(words) + "\n",
     91                            capture_output=True, text=True, check=True)
     92         ok |= set(r.stdout.split())
     93     return ok
     94 
     95 
     96 def plain(w):
     97     """sin tildes ni dieresis, conservando la ñ"""
     98     out = ""
     99     for ch in w:
    100         if ch == "ñ":
    101             out += ch
    102         else:
    103             out += unicodedata.normalize("NFD", ch)[0]
    104     return out
    105 
    106 
    107 def code(w):
    108     v = 0
    109     for ch in w:
    110         v = v * 32 + LETTERS.index(ch)
    111     return v
    112 
    113 
    114 def accent(w):
    115     for i, ch in enumerate(w):
    116         if ch in "áéíóú":
    117             return (i + 1) << 25
    118         if ch == "ü":
    119             return ((i + 1) << 25) | (1 << 28)
    120     return 0
    121 
    122 
    123 def main():
    124     fetch()
    125     allw = set(read_words("es_ES_all.txt")) | set(read_words("es_AR_all.txt"))
    126     cand = set(w for f in ("es_ES_all.txt", "es_AR_all.txt", "es_full.txt", "rae.txt")
    127                for w in read_words(f) if OK.match(w))
    128     dic = hunspell_ok(sorted(cand))
    129     allw |= dic
    130 
    131     sols = []
    132     seen = set()
    133     for line in open(os.path.join(CACHE, "es_50k.txt"), encoding="utf-8"):
    134         w, n = line.split()
    135         if w not in dic or w in BLACK:
    136             continue
    137         if w in WHITE:
    138             pass
    139         # plurales y segundas personas (cosas, estás), terceras del plural (hacen, están)
    140         elif w.endswith("s") and (w[:-1] in allw or w[:-2] in allw):
    141             continue
    142         elif w.endswith("n") and w[:-1] in allw and w[-2] in "aeáéo":
    143             continue
    144         # encliticos: darle, verte, irse
    145         elif w[-2:] in ("me", "te", "le", "lo", "la", "se") and w[:-2][-1:] in "r" and w[:-2] in allw:
    146             continue
    147         # imperativos con pronombre y tilde (ámame, léelo), de vosotros (comed), futuros (caerá)
    148         elif w[-2:] in ("lo", "la", "le", "me", "te", "se") and re.search("[áéíóú]", w):
    149             continue
    150         elif w.endswith("d") and w[:-1] + "r" in allw:
    151             continue
    152         elif w[-1] in "áé" and w[:-1] in allw and w[-2] == "r":
    153             continue
    154         elif w.endswith(("áis", "éis")):
    155             continue
    156         # condicional/imperfecto: sería, tenía (verbo + ía)
    157         elif w.endswith("ía") and (w[:-2] + "er" in allw or w[:-2] + "ir" in allw or w[:-2] in allw):
    158             continue
    159         p = plain(w)
    160         if p in seen:
    161             continue
    162         seen.add(p)
    163         sols.append(w)
    164         if len(sols) >= NSOL:
    165             break
    166 
    167     valid = sorted(set(plain(w) for w in dic), key=code)
    168     vset = set(valid)
    169     assert all(plain(w) in vset for w in sols)
    170     sols.sort(key=lambda w: code(plain(w)))
    171 
    172     print("/* palabras.c - GENERADO por mkpalabras.py, no editar a mano.")
    173     print(" * Fuentes: hunspell es_ES (LibreOffice) y FrequencyWords es_50k (hermitdave). */")
    174     print('#include "palabras.h"')
    175     print()
    176     print("const uint32_t SOLUCIONES[%d] = {" % len(sols))
    177     for i in range(0, len(sols), 6):
    178         print("    " + " ".join("0x%08x," % (code(plain(w)) | accent(w)) for w in sols[i:i + 6])
    179               + "   /* " + " ".join(sols[i:i + 6]) + " */")
    180     print("};")
    181     print("const int NSOLUCIONES = %d;" % len(sols))
    182     print()
    183     print("const uint32_t VALIDAS[%d] = {" % len(valid))
    184     for i in range(0, len(valid), 8):
    185         print("    " + " ".join("0x%07x," % code(w) for w in valid[i:i + 8]))
    186     print("};")
    187     print("const int NVALIDAS = %d;" % len(valid))
    188     print("%d soluciones, %d validas" % (len(sols), len(valid)), file=sys.stderr)
    189 
    190 
    191 if __name__ == "__main__":
    192     main()