Undertale In Extremis
Project voor Computerorganisatie (Organização de Computadores). Undertale-achtige Assembly Risc-V RPG-gevechtssimulator.
Universiteitsproject (ORG 2026.1). Een RPG-vechtsimulator en Monte Carlo-engine volledig geschreven in RISC-V-assembly.
De Opzet
Ik bouwde een RPG-vechtengine vanaf nul in pure RISC-V-assembly. Het doel was om drie verschillende AI-toestandsmachines te schrijven, ze in een headless emulator te gooien en 10.000 geautomatiseerde wedstrijden te draaien om te zien welke strategie als beste uit de bus kwam.
Het gevecht draait om een strikte actie-economie. MP regenereert langzaam, maar kan worden overbeladen met specifieke vaardigheden.
| Vaardigheid | Kosten | Wat het doet |
|---|---|---|
| Aanval | gratis | Gooit een 1d20. < 10 mist, 10+ raakt, 20 crit voor dubbele schade. |
| Verdedigen | gratis | Blokkeert inkomende schade. Hoge worpen triggeren een tegenaanval. |
| Absolute Grit | 20 MP | Omzeilt de dobbelsteen voor een gegarandeerde kritieke treffer. |
| Zielenzuiging | gratis | De spreker krijgt 1–12 terugslagschade, zuigt datzelfde bedrag van de MP van de vijand en krijgt 4× daarvan voor zichzelf. De enige manier om de 100 MP-limiet te omzeilen. |
| Finale Executie | 150 MP | Een 800% schade-nuke. Mislukt als het doelwit boven 50% HP is. |
| Spiegelschild | 30 MP | Reflecteert de volgende inkomende aanval terug naar de aanvaller. |
De Deelnemers
Ik schreef drie bots, elk met een compleet ander brein:
Flowey (De Chaos): Pure RNG. Hij evalueert niets en gooit gewoon een willekeurig getal om zijn volgende zet te kiezen.
decision_random:
li a0, 6
call randomizer # kiest een getal tussen 1 en 6, dat is de hele strategie
j decision_end
Chara (De Blokker): Ze optimaliseert voor één combo: spam Zielenzuiging tot ze 150 MP heeft, breng de vijand onder 50% HP en laat de Finale Executie los. In de code heet de strategie ook slim, maar dat is niet helemaal waar.
decision_smart:
# eu escrevi essa estrategia
# ela se consiste em usar roubo de alma até poder usar execute
# só que pra isso também precisamos sobreviver e diminuir a vida do inimigo pra 50%
# sem morrer
...
li t6, 150 # prijs van execute
blt t4, t6, decision_smart_my_mana_is_low # mp < 150? ga farmen
bge a2, t6, decision_smart_enemy_hp_high # vijand hp > 50? ga pesten
j decision_smart_i_can_kill # anders, execute
decision_smart_my_mana_is_low:
li a0, 4 # zielenzuiging
decision_smart_enemy_hp_high:
li a0, 2 # absolute grit
decision_smart_i_can_kill:
li a0, 5 # finale executie
Toby (De Harde Tegenhanger): Specifiek gebouwd om Chara te farmen. Hij houdt zowel zijn eigen HP als de MP-balk van de vijand in de gaten. Als hij onder 50 HP is en de vijand heeft 150 MP, heft hij zijn Spiegelschild op en laat de nuke terugkomen. Buiten dat venster mengt hij aanvallen en Zielenzuiging, en kan hij ook zelf Finale Executie afvuren als de omstandigheden goed zijn.
decision_troll_checks:
li t6, 50
ble t5, t6, decision_troll_check_enemy_mp # ben ik laag genoeg om me zorgen te maken?
j decision_troll_not_execute
decision_troll_check_enemy_mp:
li t6, 150
bge a3, t6, decision_troll_prepare_against_execute # is vijand dicht bij 150 mp?
j decision_troll_not_execute
decision_troll_prepare_against_execute:
li a0, 6 # spiegelschild
ret
De Benchmarkresultaten
Na het draaien van 10.000 wedstrijden op Terminal met rars.jar, de jar van de RISC-V Java-gebaseerde simulator RARS, kwamen de resultaten binnen.
| Personage | Winratio |
|---|---|
| Flowey | ~52% |
| Toby | ~29% |
| Chara | ~17% |
De domste AI won de absolute meerderheid van de spellen.
Chara's 17% winratio onthult het probleem met rigide, hebberige combo's. Om 150 MP te bereiken, moet ze terugslagschade nemen van Zielenzuiging. Vaak houdt Toby gewoon zijn schild omhoog, en dwingt Chara's script haar om haar eigen gezondheid weg te blijven zuigen tot ze letterlijk zichzelf doodt voordat ze haar ultieme kracht kan uitspreken.
Toby's 29% komt van het succesvol lokken van Chara. Tegen Flowey is het een ander verhaal: de schildconditie vereist dat de vijand dicht bij 150 MP is, en Flowey bouwt daar nooit bewust naartoe. De tegenaanval wordt nooit getriggerd, dus Toby speelt gewoon zijn standaardspel van aanvallen en zielenzuigen, wat hem geen echt voordeel geeft ten opzichte van chaos.
Flowey won 52% van de tijd omdat het hebben van geen strategie onmogelijk consistent tegen te lezen is. Hij neemt nooit terugslagschade om een grote zet voor te bereiden; hij gooit gewoon per ongeluk waardevolle zetten eruit. Het blijkt dat als je hele codebase afhankelijk is van het voorspellen van vijandelijk gedrag, je automatisch verliest van een vijand die dingen doet zonder reden.
Is Dit Eigenlijk Verrassend?
Waarschijnlijk niet. Willekeurige agenten die deterministische domineren is een goed gedocumenteerd resultaat in strategische simulaties.
Een Citadel-simulatie die ik tegenkwam in een studiegroep produceerde hetzelfde patroon: sommige strategieën versloegen consistent willekeur, sommige werden erdoor verpletterd, en sommige versloegen bepaalde tegenstanders maar verloren van anderen. Het steen-papier-schaar-dynamiek was er, maar willekeur hield nog steeds stand tegen de meeste.
Het verschil is dat in een rijkere omgeving (meer strategieën, meer beslissingsvariabelen, meer manieren voor een slimme agent om een willekeurige uit te buiten) de resultaten de neiging hebben meer uit te spreiden. Een goed afgestemde deterministische strategie kan een betrouwbaar voordeel behalen als de actieruimte genoeg biedt om mee te werken.
Hier is dat waarschijnlijk niet het geval. Met slechts zes mogelijke acties en een gevechtssysteem waar een enkele gelukkige crit de wedstrijd kan beëindigen ongeacht strategie, is de kloof tussen Chara's zorgvuldige planning en Flowey's willekeurige knopendraaien gewoon niet zo groot. Chara's combo vereist meerdere beurten voorbereiding, en de RNG heeft ruime kans om haar te doden voordat ze er komt. De actieruimte is misschien gewoon te klein en te grillig voor een hebberige strategie om consistent chaos te overtreffen.
Met andere woorden: Flowey's overwinning is minder een bevinding en meer een ontwerpbeperking. Een slimmere Chara zou een slimmer spel nodig hebben om zich te bewijzen.
Waarom Deze Getallen Niet Volledig Betrouwbaar Zijn
Voordat we conclusies trekken uit de benchmark, zijn er een paar structurele vooroordelen die het vermelden waard zijn.
De wedstrijden zijn niet geïsoleerd. Beide spelers krijgen elke wedstrijd een willekeurig toegewezen strategie, wat betekent dat de winratio's aggregaten zijn over alle mogelijke koppelingen, geen schone 1v1-statistieken. Flowey's 52% omvat wedstrijden waarin Flowey tegen een andere Flowey vocht, en in die gevallen moest een van hen winnen. Om echt te weten of Chara Toby verslaat of dat Flowey iedereen gelijk verslaat, zou je de wedstrijd moeten fixeren en elke koppeling apart moeten draaien.
Beurtvolgorde wordt nooit geroteerd. Speler 1 gaat altijd eerst. In een systeem waar een enkele crit een wedstrijd kan beëindigen, is eerst gaan een echt voordeel. De resultaten houden hier helemaal geen rekening mee.
De RNG is een aangepaste xorshift gezaaid vanuit systeemtijd. Het werkt goed genoeg voor een universiteitsproject, maar het is geen statistisch gevalideerde generator. Als de verdeling van uitkomsten ongelijk is over de 6 mogelijke acties, worden Flowey's resultaten direct beïnvloed omdat zijn hele strategie gewoon die functie aanroepen is.
De actieruimte is erg klein. Zes mogelijke acties betekent dat elke strategie beperkte ruimte heeft om zich van chaos te onderscheiden. In een dieper systeem zou Chara's combo moeilijker te onderbreken kunnen zijn, of er zouden herstelopties kunnen zijn die de kosten van haar voorbereiding verminderen. Hier is het spel net straf genoeg dat haar terugslagschade vaak de run beëindigt voordat de beloning arriveert.
Chara was waarschijnlijk niet goed genoeg ontworpen. Haar strategie heeft geen defensieve terugval. Als de combocondities niet zijn vervuld en ze zware schade oploopt, blijft ze gewoon MP farmen. Een robuustere versie zou overschakelen naar overlevingsmodus wanneer ze onder een bepaalde HP-drempel komt, wat haar cijfers waarschijnlijk aanzienlijk zou verbeteren.
De resultaten zijn richtinggevend interessant, maar moeten worden gelezen als een momentopname van deze specifieke configuratie, niet als een algemene uitspraak over strategie versus willekeur.
Toekomstig Onderzoek
De voor de hand liggende volgende stap is om volledig van RARS af te stappen. Deze versie draait op een Java-gebaseerde RISC-V-emulator, wat een harde limiet stelt aan simulatiesnelheid. Een tweede versie gericht op echte gecompileerde RISC-V met Linux-syscalls in plaats van RARS-ecalls zou dramatisch sneller moeten zijn, wat veel uitmaakt zodra het aantal wedstrijden in de miljoenen begint te lopen.
Naast prestaties zijn de interessantere richtingen aan de ontwerpkant:
Meer spelers per wedstrijd. Op dit moment is het altijd 1v1. Het toevoegen van een derde of vierde deelnemer verandert het strategische landschap compleet. Opeens moet een tegenstrategie rekening houden met van twee kanten tegelijk worden aangevallen, en pure willekeur wordt moeilijker vol te houden.
Machine learning binnen de assembly. Het idee is om een minimaal ML-algoritme direct in RISC-V te implementeren, met matrixbewerkingen om een bot zijn eigen gewichten te laten bijwerken op basis van wedstrijdresultaten. Geen externe bibliotheken, geen high-level runtime, alleen integer matrixwiskunde in registers. Of dit praktisch is of gewoon een interessante pijn om te implementeren, maakt deel uit van de aantrekkingskracht.
Meer strategieën en een diepere engine. De huidige actieruimte is te klein voor elke strategie om betekenisvol vooruit te komen op willekeur. Meer directe inspiratie nemen uit World of Warcraft (cooldowns, resource-afwegingen, positioneringseffecten, meer gevarieerde vaardigheidsinteracties) zou goed ontworpen strategieën echte ruimte geven om zich boven chaos te bewijzen.
De benchmarkinfrastructuur is er al. De bottleneck is dat het spel diep genoeg is om de resultaten iets te laten betekenen.
Het Draaien
Om de assembly te compileren en de engine lokaal te draaien:
make simulate
