treta
IA adversaria online para videojuegos: aprende del jugador, contraataca, finta e improvisa — manteniéndose casi imposible de predecir.
Python puro, cero dependencias, determinista por semilla, ~O(|A|²) por turno (apta para 60 Hz).
Registro de experimento: EXP-008 (línea esraderey de microlibrerías zero-dep).
pip install treta
La idea en una frase
Los enemigos de videojuego fallan por dos extremos: los guionizados se vuelven legibles (el jugador los "resuelve") y los aleatorios se vuelven incompetentes. treta resuelve el dilema tratando la impredecibilidad como una cantidad medida y optimizada: el agente entrena un autoadversario —un predictor de su propia conducta, de la misma clase que usaría un jugador hábil— y castiga en su función de valor las acciones que ese observador anticiparía. Explota al rival exactamente hasta donde puede sin volverse legible.
Uso
from treta import Agente
ACCIONES = ["piedra", "papel", "tijera"]
VENCE = {"piedra": "tijera", "papel": "piedra", "tijera": "papel"}
# pago(a_ia, a_jugador) -> float (opcional; si falta, se estima de las recompensas)
def pago(a, j):
return 0.0 if a == j else (1.0 if VENCE[a] == j else -1.0)
ia = Agente(ACCIONES, pago=pago, semilla=42)
# bucle del juego (movimientos simultáneos o por turnos):
accion = ia.decidir() # acción del enemigo este turno
ia.observar(accion_del_jugador) # cierra el turno; aprende online
# (o ia.observar(j, recompensa=r) sin matriz de pagos)
ia.predictibilidad() # [0,1] cuán legible es la IA para un observador competente
ia.certeza_jugador() # [0,1] cuán bien modela al jugador
ia.improvisando() # True durante un gambito
ia.fintando() # True durante una finta (sembrando o cobrando un cebo)
ia.instantanea() # telemetría para HUD/depuración
Olvido explícito (0.3.0): para eventos que el juego conoce y la IA no (nueva
partida, cambio de mapa, rival sustituido), olvidar() descuenta memoria por
canales y con dosis:
ia.olvidar(predictores=0.5, # fracción de evidencia conductual a olvidar [0, 1]
pagos=0.7, # fracción de la matriz de pagos aprendida
regret=0.5, # fracción del arrepentimiento acumulado
recompensas=True, # medias de recompensa, freno y estancamiento
historial=True, # historia reciente (contextos y red)
confianza=True) # certeza del jugador, predictibilidad y presión
Las fracciones intermedias conservan las medias y descuentan su peso: el agente
no cambia de opinión, pero vuelve a ser persuadible. 1.0 borra (los pagos
vuelven al prior; el regret, al uniforme; los n-gramas se vacían). Con los
valores por defecto es un no-op exacto, y no puede llamarse con un turno abierto.
Alfabetos separados (0.3.0): si la IA y el jugador no comparten acciones (la
IA produce unidades y el jugador elige rutas; un jefe con ataques propios),
acciones_jugador separa los conjuntos — decidir() devuelve etiquetas propias,
observar() espera las del jugador y pago(a_propia, a_jugador) recibe cada una
en su dominio:
ia = Agente(["agresiva", "defensiva"],
acciones_jugador=["ruta_norte", "ruta_centro", "ruta_sur"],
semilla=42)
Omitido, ambos lados comparten acciones y el agente es bit-idéntico al de siempre.
Memoria de valores (olvido_pagos, 0.3.0): los pagos aprendidos son medias
permanentes — tras cien recompensas de +1, veinte de −1 apenas las mueven
(quedan en +0.667). El detector de deriva ya descuenta la evidencia ante un
cambio brusco; para entornos que derivan despacio (bajo su umbral),
olvido_pagos descuenta esa fracción de la evidencia de pagos con cada
observación registrada — una media exponencial de memoria ~1/f: 0.05 recuerda
20 observaciones. El defecto 0.0 no toca nada (bit-idéntico); en el turno en que la alarma del
detector dispara, su descuento calibrado sustituye al continuo (no componen). Y
ojo con las dosis diminutas (0.02): pueden rendir peor que nada, porque siguen
la deriva lo justo para callar la alarma sin sustituir su descuento.
Matriz y recompensas (modo_pago): si pasas pago= y recompensas reales en observar(), el defecto "hibrido" trata la matriz como un prior por celda (prior_matriz pseudo-observaciones) que las recompensas superan al acumularse — si la matriz jura que A es correcta pero el entorno paga B, el agente cruza a B. "matriz" restaura la semántica 0.1.x (la matriz manda; las recompensas solo mueven telemetría y fintas). "aprendido" aprende los valores solo de lo observado: la matriz no pesa como prior, pero cuando un turno no trae recompensa hace de oráculo y esa observación se registra — sin recompensas externas nunca, acabará aprendiendo la matriz celda a celda. Sin recompensas externas, híbrido y matriz son bit-idénticos.
decidir(extra=...) acepta cualquier rasgo observable hasheable (fase del nivel, distancia discretizada, arma equipada). Condiciona los modelos de contexto y, desde 0.2.0, también los valores: cada valor distinto de extra abre un bucket con su propia matriz de pagos estimada y su propio RM+, encogidos hacia los globales con prior_estado pseudo-observaciones (un estado recién visto hereda la conducta global y gana identidad con los datos). max_estados acota la tabla: los estados excedentes caen al global y 0 desactiva el mecanismo. Dos avisos: en modo_pago="matriz" la matriz es la autoridad sobre utilidades y solo el arrepentimiento se condiciona por estado — en híbrido (defecto) y aprendido los buckets condicionan también los pagos — en híbrido con jerarquía estado → global → matriz; en aprendido el escalón final es la media global aprendida, no la matriz —; y extra es un bucket discreto, no un vector de rasgos: discretiza grueso (mapa, fase, banda de economía), no pases valores casi-únicos.
Siete mecanismos, un agente
| Mecanismo | Qué aporta | Base |
|---|---|---|
Modelo del jugador: mezcla bayesiana de expertos n-grama (órdenes 0..k, olvido perezoso) + red neuronal pura (RedMicro, MLP online de entrada dispersa) |
Predice la siguiente acción del jugador; los n-gramas memorizan patrones exactos, la red generaliza | PPM/CTW (Cleary & Witten 1984; Willems 1995), mezcla bayesiana con participación fija |
| Autoadversario (la innovación de EXP-008) | El mismo aparato apuntado a la propia IA. Su acierto define predictibilidad, que entra como castigo en el valor esperado: EV'(a) = EV(a) − λ·presión·p_yo(a). La IA evita literalmente lo que un observador esperaría de ella, en proporción a cuánto la están leyendo |
Emparentado con Safe Opponent Exploitation (Ganzfried & Sandholm 2012), resuelto aquí como regularizador online barato |
| Fintas de segunda intención | El autoadversario como sistema de puntería: ante un rival ilegible que lee y contraataca, siembra un cebo, verifica por acuerdo que el rival responde a la imagen propia (su jugada prevista es la mejor respuesta a p_yo) y cobra la contra-contra mientras el acuerdo dure. Sangría de siembra acotada, salida al segundo cobro perdido, respiros anti-patrón y enfriamiento exponencial ante sondeos fallidos |
Enseñanza estratégica (Camerer, Ho & Chong 2002); la "segunda intención" de la esgrima |
| Regret Matching+ | Piso teórico-de-juegos: contra un adversario que contraataca, empuja la política mixta hacia maximin | Hart & Mas-Colell 2000; Tammelin 2014 (RM+/CFR+) |
| Improvisación por novedad | Archivo acotado de gambitos (motivos de 2–4 acciones) con descriptor conductual; ante estancamiento o legibilidad alta ejecuta el motivo más novedoso (búsqueda de novedad + calidad, QD-lite) | Lehman & Stanley 2011; Mouret & Clune 2015 |
| Detección de deriva en dos canales | Page-Hinkley conductual (log-pérdida del modelo del jugador: si el jugador cambia de estilo, olvido de predictores) y, desde 0.3.0, Page-Hinkley de pago (error entre recompensa y estimación de valor, con unidad propia y tope de pico): si el entorno cambia lo que paga sin que el rival cambie de conducta, descuenta la evidencia de valor conservando las medias y readapta en ~10 turnos donde antes tardaba >130 | Page 1954 |
| Valores por estado (0.2.0) | La misma acción puede ganar en un mapa y perder en otro: pagos estimados y política RM+ por bucket de extra, encogidos al global por peso de datos. Sin extra, el agente es bit-idéntico a 0.1.0 |
Partial pooling jerárquico (empírico-Bayes); bandit contextual de brazos discretos |
La presión modula el castigo: contra un rival estático (que no explota los patrones de la IA) el impuesto de impredecibilidad es mínimo y la explotación es agresiva; contra un rival que lee y contraataca, el castigo, la temperatura y la mezcla con RM+ suben solos.
Banco adversario (reproducible: python ejemplos/duelo.py)
Piedra-papel-tijera de suma cero (azar = pago 0, predicción por azar = 33.3 %), 4000 rondas, semilla 7. El observador externo es una mezcla de n-gramas (órdenes 0–3) que intenta predecir a la IA — el mismo modelo que usaría un jugador metódico.
| Rival | Pago medio IA (últ. mitad) | Acierto del observador sobre la IA |
|---|---|---|
| Repetidor (acción fija) | +0.868 | 88.7 % |
| Ciclador (periodo 4) | +0.712 | 78.4 % |
| Imitador (copia a la IA) | +0.774 | 79.2 % |
| Aleatorio uniforme | +0.036 | 35.0 % |
| Contra-predictor (predice a la IA con sus mismos modelos y contraataca) | +0.217 | 25.5 % (muy bajo el azar) |
| Cambiante (deriva en t=1200; últ. 400) | +0.765 | 78.0 % |
El banco incluye además un duelo con pagos dependientes del estado (la misma acción gana en el estado A y pierde en el B, con jugada rival constante): con valores por estado la IA cobra +0.89..+0.94 donde la versión global queda clavada en ~0.00 (semillas 7/11/23; tests/test_adversarios.py::test_condiciona_valores_por_estado).
Y un duelo de deriva de recompensa (el rival repite siempre la misma acción; lo que cambia es qué acción paga): el canal conductual es ciego ahí por construcción — en 0.2.0 la readaptación tardaba 134..218 turnos con el contador de derivas en cero. El canal de pago la detecta con exactamente una alarma y readapta en 8..13 turnos (semillas 7/11/23; test_detecta_deriva_de_recompensa), sin una sola falsa alarma en los duelos estacionarios, por estado (también con visitas desiguales entre estados) y de matriz-contra-entorno del banco, y a lo sumo una aislada bajo recompensa ruidosa (σ=0.3, 2000 turnos).
Desde 0.3.0 el banco añade un duelo de deriva gradual (la recompensa se desliza bajo el umbral del detector: la media permanente arrastra un sesgo que olvido_pagos=0.05 drena, recortando la pérdida post-cruce de ~0.12 a ~0.08 por turno; test_olvido_continuo_drena_la_deriva_gradual) y un duelo asimétrico (3 doctrinas propias contra 2 rutas del jugador cicladas: +0.72..+0.80 donde la mejor respuesta ciega renta +0.5; test_asimetrico_explota_rutas).
Lectura: explota con fuerza todo patrón (pago cercano a +1 donde lo hay) y queda neutral ante el azar puro. El acierto alto del observador contra rivales estáticos no es un fallo: nadie castiga ahí la legibilidad, así que la presión es ~0 y el agente elige explotación máxima. Contra el único rival que sí lee y contraataca, la IA gana con claridad y su acción cae muy por debajo del azar para el observador: el castigo por auto-predictibilidad la aleja de lo esperado y las fintas cobran, además, la imagen que el rival se forma de ella (robusto en semillas 7/11/23: pago +0.217..+0.239, acierto 23.8..25.5 %). Contra el Cambiante, Page-Hinkley detecta la deriva (1 alarma) y el pago se recupera a +0.765 en 400 rondas.
Garantías y límites (honestos)
- Garantiza: determinismo por semilla; política válida en todo paso; coste por turno O(k + |A|²) sin asignaciones grandes; con matriz de pagos exacta (
modo_pago="matriz", o híbrido sin recompensas externas), RM+ acumula arrepentimientos exactos que empujan la mezcla hacia maximin — la garantía formal de arrepentimiento sublineal aplica a RM+ jugado en solitario, no a la política mixta final (además, los turnos de gambito no actualizan el regret): el respaldo de la no-explotabilidad es el banco empírico. - No hace: espacios de acción continuos, percepción (píxeles), planificación a varios turnos ni aprendizaje entre partidas persistido (serializa tú
instantanea+ estado si lo necesitas). El "castigo por auto-predictibilidad" es un regularizador empírico, no un equilibrio exacto: el banco de arriba es su evidencia. - Alfabetos de 2 a 32 acciones por lado (propias y del jugador, compartidos o separados con
acciones_jugador). Para juegos con estado rico, discretiza lo observable y pásalo porextra: n-gramas y valores por estado lo condicionan todo salvo la red neuronal interna, que solo ve las últimas jugadas (con muchos estados casi-únicos, los buckets se fragmentan y el encogimiento los devuelve al global). - Las fintas asumen rival adversario (aprox. suma cero: su mejor respuesta minimiza tu pago). En juegos sin ese antagonismo la compuerta de margen las deja inertes.
Desarrollo
pip install -e ".[test]"
pytest # unidades + propiedades (Hypothesis) + banco adversario
Historial de cambios: CHANGELOG.md.
Licencia
MIT © esraderey
Metadata
Release files for treta 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| treta-0.3.0.tar.gz | 50.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| treta-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.7 kB
Release files / treta-0.3.0.tar.gz
| Download URL | treta-0.3.0.tar.gz |
|---|---|
| Size | 50.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
76d9cbdd11efb6ae0d14737626598a8462e061aad5180d0c71bb27603e9d8061
|
|
BLAKE2b-256 checksum How to use checksums |
c3c9e80144bc4ce7ab3f36f1c9ccd04e755ced4bb91e711bbacf1e3f04ade295
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.2
|
Release files / treta-0.3.0-py3-none-any.whl
| Download URL | treta-0.3.0-py3-none-any.whl |
|---|---|
| Size | 30.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8dd94e2f649a3f7cd5af084f1384c3d4d344e76a8ee537de9a7e0261dff97cea
|
|
BLAKE2b-256 checksum How to use checksums |
e2ec0f431cdc5423bd34e7c2c7b7aef2d528c34af2ad3c6b0c6e78cf3fbbcc46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.2
|