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()