Esolang experimental inspirado en Dota 2.
DotaCode es un lenguaje de programación esotérico que investiga si los conceptos de Dota 2 — héroes, habilidades, items, buffs, debuffs, cooldowns, posición, tiempo y eventos — pueden funcionar como primitivas computacionales.
No es un clon de Dota 2. Es un experimento sobre programación reactiva, estado mutable, y sistemas de eventos discretos.
Dota 2 es un sistema complejo donde convergen:
- Estado mutable (HP, mana, posición, buffs)
- Eventos discretos (cast, hit, damage, death)
- Tiempo (cooldowns, duraciones, ticks)
- Recursos (HP, mana, gold, stacks, charges)
- Acciones condicionales (gating por stun/root/silence)
- Efectos persistentes (modifiers, auras)
La hipótesis de DotaCode es que estos conceptos forman un modelo computacional completo sin necesidad de primitivas adicionales.
| Primitiva | Descripción |
|---|---|
Entity |
Entidad con identidad, estado mutable, tags |
State |
Mapa clave→valor mutable por entity |
Event |
Ocurrencia discreta en un tick |
Effect |
Función que muta GameState |
Trigger |
Vínculo evento→condición→[effects] |
Modifier |
Estado temporal con duración, stacks, gates |
Time |
Tick monótono + cola de eventos ordenada |
STATE + EVENT + CONDITIONS → EFFECT → NEW STATE
Todo en DotaCode se construye sobre esta máquina. Las 744 acciones del catálogo se descomponen en composiciones de estas 7 primitivas.
Entity = {
id, type, state, triggers, modifiers,
position, spawn_pos, tags, alive, owner
}Las entity se crean con spawn_entity() y se destruyen con
destroy_entity(). Cada entity tiene un state mutable (HP, mana,
posición, etc.), triggers que responden a eventos, y modifiers
buff/debuff activos.
Los eventos son el motor del sistema. Cada tick, el runtime procesa la cola de eventos FIFO:
ON_CAST → ON_DAMAGE → ON_DEATH → ON_MODIFIER_EXPIRED → ...
Los triggers escuchan eventos y ejecutan effects cuando la condición se cumple.
El state de una entity es un mapa {key: value}. Ejemplo:
{"hp": 100, "hp_max": 100, "mana": 50, "damage": 25}Las operaciones de estado: set, inc, dec, clamp, get.
Los recursos son state numérico con operaciones especializadas:
spend(entity, "mana", 30) # consume si tiene
gain(entity, "hp", 20) # añade (respeta max)
set_resource(entity, "gold", 500)Cada recurso puede tener _max (tope) y _regen (regeneración/tick).
Un tick = una unidad de tiempo. Las duraciones se expresan en ticks:
apply_modifier(source, target, "STUN", 3) # 3 ticks de stun
periodic(effect, every=1) # cada tick
schedule(effect, tick_offset=5) # en 5 ticksPos = (x, y) enteros. Manhattan distance por defecto:
distance(a, b) # |ax-bx| + |ay-by|
in_range(a, b, 5) # True si distance <= 5Una habilidad es un Trigger sobre ON_CAST:
Trigger(
on="ON_CAST",
if=cooldown_ready AND mana >= cost AND not_silenced,
then=[
spend(mana, cost),
start_cooldown(ability, 5),
damage(target, 50),
]
)Un item es un contenedor de modifiers y triggers:
# Item pasivo: +regen
apply_modifier(source, hero, "ITEM_REGEN", -1, on_tick=regen_effect)
# Item activo: heal
Trigger(on="ON_ITEM_USE", if=item_ready, then=[heal(hero, 50)])Los modifiers modelan buffs y debuffs:
# Buff: +damage por 10 ticks
apply_modifier(source, hero, "DAMAGE_BUFF", 10, on_tick=damage_up)
# Debuff: stun que bloquea move y cast
apply_modifier(source, enemy, "STUN", 3,
gate={ActionType.MOVE, ActionType.CAST})El campo gate unifica stun/root/silence:
stun→ gate = {MOVE, CAST, ATTACK}root→ gate = {MOVE}silence→ gate = {CAST}
DotaCode actualmente se programa en Python usando la API del runtime. El parser de sintaxis propia es un paso futuro (el modelo se investiga antes que la sintaxis).
from src import *
def setup(gs):
hero = gs.spawn_entity("hero", {"mana": 100, "hp": 100})
enemy = gs.spawn_entity("enemy", {"hp": 80})
def on_cast(gs, ctx):
e = gs.get_entity(ctx["target"])
if e:
e.state["hp"] -= 30
return gs
t = Trigger(id=gs.new_trigger_id(), on="ON_CAST", source=hero.id, then=[on_cast])
gs.add_trigger(t)
emit("ON_CAST", source=hero.id, target=enemy.id)(gs, {})
gs = run(seed=42, setup_fn=setup)Ver examples/examples_10.py para los 10 ejemplos del megacompose.
# Motor público
py host.py corpus/01_entity.py
py host.py --list
py host.py --all # 17/17 corpus
py host.py examples/examples_10.pycorpus/ = canonical DotaCode examples. Cada archivo:
- corresponde a exactamente una primitiva, un ejemplo del megacompose o una acción del catálogo;
- muestra la forma mínima válida de usarla;
- sirve como referencia para humanos y LLMs;
- puede ejecutarse directamente con el host (
python host.py corpus/01_entity.py); - NO sustituye los tests semánticos.
$$ \boxed{ \text{1 concepto / 1 acción} \rightarrow \text{1 ejemplo canónico visible} } $$
Corpus actual: 17 canónicos verificados (7 primitivas + 10 megacompose) + 740 acciones del catálogo en corpus/actions/ — 757 archivos en total, pero solo 48 están IMPLEMENTADOS con flashcard ejercitada; 692 son CATALOG_ONLY (NOT_EXECUTABLE, NOT_IMPLEMENTED, ver corpus/actions/001_spawn_entity.py para el patrón honesto). El claim anterior 757 verificados era NOT_DEMONSTRATED para las 740.
Para conceptos complejos el ejemplo es una flashcard ejecutable con contexto:
# PRE: hero hp=50
# POST: hero hp=70 (gain respeta max)
hero = gs.spawn_entity("hero", {"hp": 50, "hp_max": 100})
gain(hero.id, "hp", 20)(gs, {})Separación limpia:
| Carpeta/archivo | Rol |
|---|---|
corpus/ |
cómo se usa |
tests/ |
cómo sabemos que funciona |
README / SPEC |
qué significa |
host.py + src/ |
cómo se ejecuta |
py -m pytest -q
py tests/test_core.py
py tests/test_minsky_dotacode.py
py examples/examples_10.py
py host.py --all # 17/17 corpus canónico (los 740 actions/ son CATALOG_ONLY, no se cuentan como verificados)
py host.py corpus/actions/026_inc_state.py # 1 de los 48 IMPLEMENTADOS
# py host.py corpus/actions/001_spawn_entity.py # CATALOG_ONLY: no ejercita, solo marca gs.globals["CATALOG_ONLY_..."]42 pruebas automatizadas, 17 corpus canónicos verificados (los 740 actions/ son catálogo: 48 IMPLEMENTADOS con POST verificado, 692 CATALOG_ONLY NOT_IMPLEMENTED) y 10 ejemplos del megacompose.
Dos programas en examples/ demuestran cómputo no trivial sin traductor
(la misma idea que en PokéCode/SpellCode/DuelCode, cada uno en su dominio):
| Ejemplo | Archivo | Qué prueba | Salida |
|---|---|---|---|
| Fibonacci 10 | examples/fibonacci.py |
Entity+State (a,b,n), Event ON_FIB, Trigger→Effect custom, Time (cola) |
0,1,1,2,3,5,8,13,21,34 |
| Fibonacci CORE 10 | examples/fibonacci_core.py |
inc_state/dec_state/emit compilados a Triggers, output_number |
0,1,1,2,3,5,8,13,21,34 (solo primitivas, motor decide) |
| FizzBuzz 1..15 | examples/fizzbuzz.py |
i%3/i%5 vía Trigger custom (honesta) |
1,2,Fizz...FizzBuzz |
| FizzBuzz CORE 1..15 | examples/fizzbuzz_core.py |
inc_state/dec_state/emit para r=i%3/r=i%5 con bucles, output_string |
1,2,Fizz... (MOD puro, en progreso) |
python examples/fibonacci.py # custom Effect
# -> fib: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
python examples/fibonacci_core.py # core primitives (DEMONSTRATED)
# -> fib_core: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # 24 estados, solo inc_state/dec_state/emit
python examples/fizzbuzz.py
# -> 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
# fibonacci.py CUSTOM_EFFECT: Trigger ON_FIB -> fib_step() hace c=a+b en Python.
# fibonacci_core.py CORE: 24 estados Minsky compilados a Trigger ON STATE_i -> [inc_state/dec_state/emit]
# fizzbuzz.py CUSTOM_EFFECT: Trigger ON_FIZZ -> step hace r3=i%3 r5=i%5 en Python (honesto, como fibonacci.py)
# Python solo ensambla; run_loop despacha y decide.Nota:
fibonacci.py=CUSTOM_EFFECT(DEMONSTRATED),fibonacci_core.py= core primitives (DEMONSTRATED, equivalente aminsky_dotacode.pyy afibonaccide Spell/Duel/Poké).
DotaCode/
├── README.md
├── MODEL_DISCOVERY.md # Fase 1: descubrimiento del modelo
├── ACTIONS.md # Catálogo de 744 acciones (29 ramas)
├── SPEC.md # Especificación formal (semántica small-step)
├── host.py # motor público todo-en-uno
├── corpus/ # 757 archivos (17 verificados + 740 catálogo)
│ ├── 01_entity.py # 7 primitivas (verificadas)
│ ├── 08_kill_counter.py# 10 megacompose (verificados)
│ └── actions/ # 740 acciones del catálogo (48 IMPLEMENTED + 692 CATALOG_ONLY)
│ ├── 001_spawn_entity.py # IMPLEMENTED (POST verificado, dummy id != hero)
│ ├── 003_exists.py # CATALOG_ONLY (NOT_IMPLEMENTED)
│ └── 026_inc_state.py # IMPLEMENTED (POST hp 100->101)
├── src/
│ ├── __init__.py
│ ├── prng.py # PRNG determinista (xorshift64)
│ ├── dtypes.py # Entity, Modifier, Event, Trigger, EventQueue
│ ├── gamestate.py # GameState (estado global del mundo)
│ ├── effects.py # 10 primitivas de Effect + composición
│ ├── triggers.py # Matching de triggers + gating
│ └── runtime.py # run_loop (ciclo de ejecución)
├── tests/
│ ├── test_core.py # 30 tests de invariantes
│ ├── test_minsky_dotacode.py # máquina de contadores (suma/multiplica)
│ └── testurings.py # auditoría TESTURINGS (no evidencia)
├── examples/
│ ├── examples_10.py # 10 ejemplos del megacompose (también en corpus/)
│ ├── fibonacci.py # Fibonacci CUSTOM_EFFECT (10 términos)
│ ├── fibonacci_core.py # Fibonacci CORE primitives (24 estados, DEMONSTRATED)
│ └── fizzbuzz.py # FizzBuzz 1..15 CUSTOM_EFFECT
minsky_dotacode.py compila una maquina de contadores a triggers de
DotaCode. Python solo ensambla; el computo lo hace run_loop despachando
eventos y aplicando efectos del runtime (inc_state, dec_state, emit).
INC(r, j) trigger ON STATE_i -> [inc_state(r), emit(STATE_j)]
JZDEC(r, j, k) trigger ON STATE_i si contador > 0
-> [dec_state(r), emit(STATE_j)]
trigger ON STATE_i si contador == 0
-> [emit(STATE_k)]
La bifurcacion la decide el motor evaluando if_cond, no un if de Python.
Resultados medidos:
suma(7,5) -> a = 12 (dos registros exactos)
multiplica(6,7) -> acc = 42
tests/test_minsky_dotacode.py incluye test_el_motor_hace_el_trabajo, que
compila el programa sin ejecutar el bucle y comprueba que no hay resultado;
solo tras run_loop aparece. Esa es la diferencia entre una reduccion y una
simulacion en Python.
Alcance: estas pruebas cubren programas concretos de maquina de contadores compilados a triggers y ejecutados con un límite finito de ticks. La universalidad de las máquinas de dos contadores es un resultado externo (Minsky, 1967); estas pruebas no demuestran que DotaCode sea Turing-completo.
tests/testurings.py (Brainfuck, Rule 110, SKI, y las dos Minsky antiguas)
no son reducciones. Auditado el 2026-08-21: calculan en Python y guardan el
resultado en gs.globals, y luego afirman sobre el valor que ellos mismos
metieron:
if instr[0] == "INC":
e.state[reg] = e.state.get(reg, 0) + 1 # dict de Python, + 1 de PythonSe conservan como especificacion ejecutable de los modelos que se quiere alcanzar, con una advertencia en la cabecera del modulo. No cuentan como evidencia. Cuando alguno se implemente sobre el runtime, sube a la seccion de arriba con su test.
- Sin parser propio — se programa vía API Python
- Sin I/O de archivos — solo stdin/stdout conceptual
- Sin multiplayer — mundo single-threaded
- Sin gráficos — solo trace estructurado
- Sin mapa de Dota — posición abstracta (x, y)
- Sin stats oficiales — valores son ejemplos, no balance
| Esolang | Modelo |
|---|---|
| PokéCode | ISA / operaciones |
| DuelCode | cartas / efectos / cadenas |
| SpellCode | hechizos / transformaciones |
| DotaCode | sistema dinámico de entidades, tiempo, recursos y eventos |
DESCARGO DE RESPONSABILIDAD / DISCLAIMER:
DotaCode es un proyecto experimental y no oficial. No está afiliado, patrocinado ni aprobado por Valve Corporation.
Dota 2 es una marca registrada de Valve Corporation. Todos los derechos de nombre, marcas, logotipos, sprites, artwork, música, código propietario y assets de Dota 2 pertenecen exclusivamente a Valve Corporation.
DotaCode no usa:
- logos oficiales de Dota 2
- sprites o artwork de Dota 2
- música de Dota 2
- código propietario de Valve
- assets oficiales de Dota 2
DotaCode es un experimento de lenguajes de programación esotéricos que se inspira en los conceptos de Dota 2 (héroes, habilidades, items, buffs, debuffs, cooldowns, posición, tiempo, eventos) como primitivas computacionales. El uso de estos conceptos es con fines educativos e investigativos.
Valve Corporation no respalda ni está afiliada a este proyecto.
Evidence before narrative. Never Guess.