</> Cómo funciona

pregunta

GET
/v1/dojo/pregunta

Función pura del español.

Cómo funciona (por dentro)

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.

Mapa de funciones

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;

El código

# -*- 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"}

Pruébalo

Llámala con el código a la vista.

Ejemplos con el SDK

curl "https://api.wordkers.com/v1/dojo/pregunta" \
  -H "X-API-Key: TU_CLAVE"

Cómo empezar con el SDK →

¿Quieres que cambie algo, o copiar la lógica para lo tuyo? Sin problema — escríbeme. Lo que ves es el algoritmo; los datos y las claves se quedan en casa.

Wordkers · Catálogo · Empezar · Tu cuenta