</> Cómo funciona
Función pura del español.
Módulo: dojo_preguntas.py — un generador por juego (g_acentuador, g_intruso, g_junta…) y un despachador GEN que mapea el id del juego a su generador.
Reutiliza los mismos motores que sirven las demás APIs, sin duplicar nada: el silabeador (sílabas y sílaba tónica), el clasificador de género, el de número, el diccionario semántico (definición, palabras relacionadas, colocaciones, ejemplos) y el refranero (refranes al azar y sus sinónimos). La fuente de palabras es el léxico por nivel de dificultad (A–D).
Método: cada juego arma su pregunta desde datos reales. Los de opciones ponen la respuesta correcta junto a distractores tomados de otras palabras o refranes; los de cortes calculan dónde acaba cada sílaba a partir del silabeo. Si un sorteo no cuaja (palabra sin datos, refrán sin sinónimo), el despachador reintenta con otra.
Salida: {tipo, pregunta, opciones?, solucion, pista?} — lista para pintar en el cliente. El ranking del dojo es informal, así que la solución viaja en la respuesta.
flowchart TD _strip["_strip()"] _raiz["_raiz()"] _words["_words()"] _dict_words["🔒 _dict_words()"] _limpia["_limpia()"] _blank["_blank()"] _colocado["_colocado()"] g_acentuador["g_acentuador()"] g_silabeador["g_silabeador()"] g_separador["g_separador()"] g_genaro["g_genaro()"] g_numara["g_numara()"] g_definicion["g_definicion()"] g_intruso["g_intruso()"] g_cloze["g_cloze()"] g_colocaciones["g_colocaciones()"] g_junta["g_junta()"] g_refdef["g_refdef()"] g_refsin["g_refsin()"] juegos["juegos()"] generar["generar()"] _blank --> _raiz _blank --> _strip _colocado --> _raiz _colocado --> _strip _raiz --> _strip g_acentuador --> _limpia g_acentuador --> _words g_cloze --> _blank g_cloze --> _dict_words g_colocaciones --> _colocado g_colocaciones --> _dict_words g_colocaciones --> _strip g_definicion --> _dict_words g_definicion --> _strip g_genaro --> _limpia g_genaro --> _words g_intruso --> _dict_words g_numara --> _limpia g_numara --> _words g_separador --> _limpia g_separador --> _words g_silabeador --> _limpia g_silabeador --> _words generar --> juegos classDef red fill:#fde7f0,stroke:#c0397b,color:#7a1e4d; class _dict_words red;
# -*- coding: utf-8 -*-
"""Generador de preguntas del Wordkers Dojo (servidor).
Unifica la obtención de preguntas de los juegos: en vez de que cada juego se las
busque a su manera en el cliente, el servidor genera aquí `{tipo, pregunta,
opciones, solucion, …}` listo para jugar. REUTILIZA los mismos módulos que sirven
las APIs (silabeador `upln`, `genero`, `numara`, `diccionario`, `refranario`,
`ciervantes` como fuente de palabras por nivel) — sin duplicar lógica lingüística.
Cada generador `g_*(cur, nivel)` devuelve un dict de pregunta o None (si el sorteo
no cuaja; el despachador reintenta). El ranking del dojo es «anti-tontunas, no
anti-hackers», así que la solución viaja en la respuesta sin problema.
"""
import random
import re
import unicodedata
import upln
import genero
import numara as _num
import diccionario as _dicc
import refranario as _ref
import ciervantes as _cie
# ------------------------------------------------------------------ utilidades
def _strip(s):
return "".join(c for c in unicodedata.normalize("NFD", (s or "").lower())
if unicodedata.category(c) != "Mn")
def _raiz(w):
s = _strip(w)
return s[:max(4, len(s) - 2)]
def _words(cur, n, nivel):
"""Palabras del léxico por nivel de dificultad (A-D). Fuente principal."""
try:
return list(_cie.palabras_nivel(cur, nivel or "B", n) or [])
except Exception:
return []
def _dict_words(cur, n):
"""Palabras que están en el diccionario semántico (al azar)."""
try:
# […consulta al repositorio del diccionario para tomar n palabras al azar…]
return [r[0] for r in cur.fetchall()]
except Exception:
return []
def _limpia(w):
return (w or "").strip()
_STOP = set("de la el los las un una unos unas y o u con en a que del al se su sus "
"para por mas muy sin sobre entre lo le les mi tu no si es son este esta "
"ese esa como".split())
def _blank(frase, w):
"""Tapa en la frase la palabra (o su forma flexionada) con un hueco."""
raiz = _raiz(w)
ws = _strip(w)
toks = re.findall(r"\w+|[^\w\s]|\s+", frase, re.UNICODE)
for i, t in enumerate(toks):
if not re.match(r"\w+$", t, re.UNICODE):
continue
ts = _strip(t)
if ts == ws or ts.startswith(raiz) or (len(ts) >= 4 and ts[:4] == ws[:4]):
toks[i] = "_____"
return "".join(toks)
return None
def _colocado(frase, w):
"""De una colocación («agua fría»), el token de contenido que NO es la palabra."""
raiz = _raiz(w)
for t in frase.split():
ts = _strip(t)
if ts.startswith(raiz) or raiz.startswith(ts[:4]):
continue
if ts in _STOP or len(ts) < 3:
continue
return t.lower()
return None
# ------------------------------------------------------------------ generadores
_AC = {1: "Aguda", 2: "Llana", 3: "Esdrújula", 4: "Sobreesdrújula"}
def g_acentuador(cur, nivel):
for w in _words(cur, 8, nivel):
w = _limpia(w)
if len(w) < 3 or " " in w:
continue
try:
sil = upln.silabear(w)
st = upln.int_silaba_tonica(sil)
n = upln.numero_silabas(sil)
except Exception:
continue
sol = _AC.get(st)
if not sol or n < 2:
continue
sils = sil.split("=")
idx = n - st
pista = ("Su sílaba tónica es «%s»." % sils[idx]) if 0 <= idx < len(sils) else None
return {"tipo": "opciones", "pregunta": "¿Cómo es «%s»?" % w, "foco": w,
"opciones": ["Aguda", "Llana", "Esdrújula", "Sobreesdrújula"],
"solucion": sol, "pista": pista}
return None
def g_silabeador(cur, nivel):
for w in _words(cur, 8, nivel):
w = _limpia(w)
if len(w) < 3 or " " in w:
continue
try:
sil = upln.silabear(w)
n = upln.numero_silabas(sil)
except Exception:
continue
if n < 2 or n > 6:
continue
opts = sorted({n, max(1, n - 1), n + 1, n + 2})
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": "¿Cuántas sílabas tiene «%s»?" % w, "foco": w,
"opciones": [str(x) for x in opts], "solucion": str(n),
"pista": "Se separa: %s" % sil.replace("=", "-")}
return None
def g_separador(cur, nivel):
for w in _words(cur, 8, nivel):
w = _limpia(w)
if len(w) < 4 or " " in w:
continue
try:
sil = upln.silabear(w)
except Exception:
continue
parts = sil.split("=")
if len(parts) < 2:
continue
cortes, acc = [], 0
for p in parts[:-1]:
acc += len(p)
cortes.append(acc)
return {"tipo": "cortes", "pregunta": "Separa en sílabas:", "palabra": w,
"cortes": cortes, "solucion": "-".join(parts)}
return None
_GEN = {"M": "Masculino", "F": "Femenino", "C": "Común"}
def g_genaro(cur, nivel):
for w in _words(cur, 8, nivel):
w = _limpia(w)
if len(w) < 3 or " " in w:
continue
try:
g = genero.genero(w)
except Exception:
continue
sol = _GEN.get(g)
if not sol:
continue
return {"tipo": "opciones", "pregunta": "¿Qué género tiene «%s»?" % w, "foco": w,
"opciones": ["Masculino", "Femenino", "Común"], "solucion": sol}
return None
_NUM = {"singular": "Singular", "plural": "Plural", "invariable": "Invariable"}
def g_numara(cur, nivel):
for w in _words(cur, 8, nivel):
w = _limpia(w)
if len(w) < 3 or " " in w:
continue
try:
r = _num.numero(cur, w)
except Exception:
continue
nums = r.get("numero") or []
if len(nums) != 1 or nums[0] not in _NUM:
continue
return {"tipo": "opciones", "pregunta": "¿Singular o plural? «%s»" % w, "foco": w,
"opciones": ["Singular", "Plural", "Invariable"], "solucion": _NUM[nums[0]]}
return None
def g_definicion(cur, nivel):
words = _dict_words(cur, 12)
if len(words) < 4:
return None
random.shuffle(words)
for w in words:
d = _dicc.definicion(cur, w)
if not d.get("encontrado"):
continue
df = next((x.get("definicion") for x in d["definiciones"] if x.get("definicion")), None)
if not df or len(df) < 15 or _strip(w) in _strip(df):
continue
distr = [x for x in words if x != w][:3]
if len(distr) < 3:
continue
opts = distr + [w]
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": "¿Qué palabra se define así? «%s»" % df,
"opciones": opts, "solucion": w}
return None
def g_intruso(cur, nivel):
words = _dict_words(cur, 20)
random.shuffle(words)
for w in words:
r = _dicc.relacionadas(cur, w)
if not r.get("encontrado"):
continue
res = r.get("resultados") or []
rel = [x for x in (res[0]["relacionadas"] if res else []) if x and x != w]
if len(rel) < 3:
continue
random.shuffle(rel)
r3 = rel[:3]
veto = set([w] + rel)
intru = next((x for x in words if x not in veto), None)
if not intru:
continue
opts = r3 + [intru]
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": "¿Qué palabra NO tiene que ver con «%s»?" % w,
"foco": w, "opciones": opts, "solucion": intru}
return None
def g_cloze(cur, nivel):
words = _dict_words(cur, 12)
if len(words) < 4:
return None
random.shuffle(words)
for w in words:
d = _dicc.ejemplo(cur, w)
if not d.get("encontrado"):
continue
ejs = [e["ejemplo"] for e in d["ejemplos"] if e.get("ejemplo") and len(e["ejemplo"]) > 20]
if not ejs:
continue
frase = _blank(ejs[0], w)
if not frase or "_____" not in frase:
continue
distr = [x for x in words if x != w][:3]
if len(distr) < 3:
continue
opts = distr + [w]
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": frase, "opciones": opts,
"solucion": w, "cloze": True}
return None
def g_colocaciones(cur, nivel):
words = _dict_words(cur, 16)
random.shuffle(words)
chosen, pool = None, []
for w in words:
d = _dicc.colocaciones(cur, w)
res = d.get("resultados") or []
cols = res[0]["colocaciones"] if res else []
collos = list(dict.fromkeys(c for c in (_colocado(ph, w) for ph in cols) if c))
if collos and chosen is None:
chosen = (w, collos)
else:
pool += collos
if chosen and len(pool) >= 6:
break
if not chosen:
return None
w, collos = chosen
correcto = random.choice(collos)
veto, ws = set(collos), _strip(w)
distr = [c for c in dict.fromkeys(pool)
if c not in veto and _strip(c)[:4] != ws[:4]][:3]
if len(distr) < 3:
return None
opts = distr + [correcto]
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": "¿Qué palabra suele ir con «%s»?" % w,
"foco": w, "opciones": opts, "solucion": correcto}
def g_junta(cur, nivel):
lote = _ref.aleatorio(cur, 6).get("refranes", [])
base = [r for r in lote if r.get("parte1") and r.get("parte2")]
if len(base) < 4:
return None
r = random.choice(base)
distr = [x["parte2"] for x in base if x["id"] != r["id"]][:3]
if len(distr) < 3:
return None
opts = distr + [r["parte2"]]
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": "Completa el refrán: «%s…»" % r["parte1"],
"opciones": opts, "solucion": r["parte2"]}
def g_refdef(cur, nivel):
lote = _ref.aleatorio(cur, 6).get("refranes", [])
base = [r for r in lote if r.get("significado")]
if len(base) < 4:
return None
r = random.choice(base)
distr = [x["refran"] for x in base if x["id"] != r["id"]][:3]
if len(distr) < 3:
return None
opts = distr + [r["refran"]]
random.shuffle(opts)
return {"tipo": "opciones", "pregunta": "¿Qué refrán significa esto? «%s»" % r["significado"],
"opciones": opts, "solucion": r["refran"]}
def g_refsin(cur, nivel):
for _ in range(6):
lote = _ref.aleatorio(cur, 1).get("refranes", [])
if not lote:
continue
r = lote[0]
syns = _ref.sinonimos(cur, id=r["id"]).get("sinonimos") or []
if not syns:
continue
correcto = random.choice(syns)["refran"]
otros = _ref.aleatorio(cur, 6).get("refranes", [])
distr = [x["refran"] for x in otros
if x["refran"] not in (r["refran"], correcto)][:3]
if len(distr) < 3:
continue
opts = distr + [correcto]
random.shuffle(opts)
return {"tipo": "opciones",
"pregunta": "¿Qué refrán significa lo mismo que «%s»?" % r["refran"],
"opciones": opts, "solucion": correcto}
return None
# ------------------------------------------------------------------ despachador
GEN = {
"acentuador": g_acentuador,
"silabeador": g_silabeador,
"separador": g_separador,
"genaro": g_genaro,
"numara": g_numara,
"definicion": g_definicion, "dicc-definicion": g_definicion,
"intruso": g_intruso, "dicc-intruso": g_intruso,
"cloze": g_cloze, "dicc-cloze": g_cloze,
"colocaciones": g_colocaciones, "dicc-colocaciones": g_colocaciones,
"junta-refranes": g_junta,
"refranes-definicion": g_refdef,
"refranes-sinonimos": g_refsin,
}
def juegos():
return sorted(set(GEN))
def generar(cur, juego, nivel=None):
fn = GEN.get(juego)
if not fn:
return {"error": "juego desconocido", "juegos": juegos()}
for _ in range(4):
try:
q = fn(cur, nivel)
except Exception:
q = None
if q:
q["juego"] = juego
return q
return {"juego": juego, "error": "no se pudo generar una pregunta, reinténtalo"}
Llámala con el código a la vista.
curl "https://api.wordkers.com/v1/dojo/pregunta" \
-H "X-API-Key: TU_CLAVE"