Undertale In Extremis
Projet sur l'organisation des ordinateurs. Simulateur de combat RPG de type Undertale en langage assembleur RISC-V.
Projet universitaire (ORG 2026.1). Un simulateur de combat RPG et un moteur Monte Carlo entièrement écrits en langage assembleur RISC-V.
La configuration
J'ai développé un moteur de combat RPG à partir de zéro en langage assembleur RISC-V brut. L'objectif était d'écrire trois machines à états IA distinctes, de les lancer dans un émulateur sans interface graphique et d'exécuter 10 000 matchs automatisés pour voir quelle stratégie s'imposait.
Le combat s'articule autour d'une économie d'action stricte. Les PM se régénèrent lentement, mais peuvent être surchargés à l'aide de compétences spécifiques.
| Compétence | Coût | Effet |
|---|---|---|
| Attaque | gratuit | Lance un 1d20. < 10 : raté, 10+ : touché, 20 : coup critique infligeant le double de dégâts. |
| Défense | gratuit | Bloque les dégâts subis. Les jets élevés déclenchent une contre-attaque. |
| Absolute Grit | 20 PM | Contourne le jet de dé pour garantir un coup critique. |
| Soul Suck | gratuit | Le lanceur subit 1 à 12 points de dégâts de recul, draine ce même montant des PM de l'ennemi et en gagne 4 fois plus pour lui-même. Le seul moyen de contourner la limite de 100 PM. |
| Final Execution | 150 PM | Une attaque nucléaire infligeant 800 % de dégâts. Échoue si la cible a plus de 50 % de PV. |
| Bouclier miroir | 30 PM | Renvoie la prochaine attaque vers l'attaquant. |
Les prétendants
J'ai écrit trois bots, chacun doté d'un cerveau complètement différent :
Flowey (Le Chaos) : RNG pur. Il n'évalue rien et se contente de lancer un nombre aléatoire pour choisir son prochain coup.
decision_random:
li a0, 6
call randomizer # choisit un nombre entre 1 et 6, c'est toute la stratégie
j decision_end
Chara (La Bousculée) : Elle optimise une seule combo : spammer Soul Suck jusqu'à atteindre 150 MP, réduire la santé de l'ennemi en dessous de 50 %, puis lancer Final Execution. Dans le code, la stratégie est également nommée « smart », mais elle ne l'est pas vraiment.
decision_smart:
# j'ai écrit cette stratégie
# elle consiste à utiliser *Soul Suck* jusqu'à pouvoir utiliser *Execute*
# mais pour cela, il faut aussi survivre et réduire la vie de l'ennemi à 50 %
# sans mourir
...
li t6, 150 # coût de l'Exécution
blt t4, t6, decision_smart_my_mana_is_low # PM < 150 ? aller farmer
bge a2, t6, decision_smart_enemy_hp_high # PV de l'ennemi > 50 ? aller l'intimider
j decision_smart_i_can_kill # sinon, exécuter
decision_smart_my_mana_is_low:
li a0, 4 # soul suck
decision_smart_enemy_hp_high:
li a0, 2 # absolute grit
decision_smart_i_can_kill:
li a0, 5 # final execution
Toby (le contre-attaquant) : Conçu spécialement pour farmer Chara. Il surveille à la fois sa propre barre de PV et celle de PM de l'ennemi. S'il a moins de 50 PV et que l'ennemi dispose de 150 PM, il lève son Bouclier miroir et laisse l'attaque de gros calibre lui revenir. En dehors de cette fenêtre, il alterne attaques et Soul Suck, et peut également lancer Final Execution lui-même lorsque les conditions sont réunies.
decision_troll_checks:
li t6, 50
ble t5, t6, decision_troll_check_enemy_mp # suis-je suffisamment bas pour m'inquiéter ?
j decision_troll_not_execute
decision_troll_check_enemy_mp:
li t6, 150
bge a3, t6, decision_troll_prepare_against_execute # l'ennemi est-il proche de 150 pm ?
j decision_troll_not_execute
decision_troll_prepare_against_execute:
li a0, 6 # bouclier miroir
ret
Résultats des tests de performance
Après avoir exécuté 10 000 parties sur Terminal à l'aide de rars.jar, le fichier jar du simulateur RARS basé sur Java pour Risc-V, les résultats sont tombés.
| Personnage | Taux de victoire |
|---|---|
| Flowey | ~52 % |
| Toby | ~29 % |
| Chara | ~17 % |
L'IA la plus stupide a remporté la grande majorité des parties.
Le taux de victoire de 17 % de Chara met en évidence le problème posé par les combos rigides et avides. Pour atteindre 150 PM, elle doit subir les dégâts de recul de Soul Suck. Souvent, Toby se contente de lever son bouclier, et le script de Chara l'oblige à continuer de vider sa propre santé jusqu'à ce qu'elle se tue littéralement avant même de pouvoir lancer son ultime.
Les 29 % de Toby proviennent du fait qu'il a réussi à appâter Chara. Contre Flowey, c’est une autre histoire : la condition du bouclier exige que l’ennemi soit proche de 150 PM, et Flowey ne s’efforce jamais délibérément d’atteindre ce niveau. La contre-attaque ne se déclenche jamais, donc Toby se contente de jouer son jeu par défaut consistant à attaquer et à utiliser Soul Suck, ce qui ne lui donne aucun avantage réel face au chaos.
Flowey a gagné 52 % du temps car il est impossible de contrer systématiquement l’absence de stratégie. Il ne subit jamais de dégâts de recul en essayant de mettre en place un coup spectaculaire ; il lance simplement des coups de grande valeur par accident. Il s'avère que si l'ensemble de votre code repose sur la prédiction du comportement de l'ennemi, vous perdez automatiquement face à un ennemi qui agit sans raison.
Est-ce vraiment surprenant ?
Probablement pas. La domination des agents aléatoires sur les agents déterministes est un résultat bien documenté dans les simulations stratégiques.
Une simulation Citadel que j’ai découverte dans un groupe d’étude a produit le même schéma : certaines stratégies battaient systématiquement le hasard, d’autres se faisaient écraser par lui, et d’autres encore battaient certains adversaires mais perdaient contre d’autres. La dynamique du jeu pierre-papier-ciseaux était présente, mais le hasard tenait toujours tête à la plupart d’entre elles.
La différence est que dans un environnement plus riche (plus de stratégies, plus de variables de décision, plus de façons pour un agent intelligent d’exploiter un agent aléatoire), les résultats ont tendance à se disperser davantage. Une stratégie déterministe bien ajustée peut se forger un avantage fiable si l'espace d'action lui en donne suffisamment pour travailler.
Ici, ce n'est probablement pas le cas. Avec seulement six actions possibles et un système de combat où un seul coup critique chanceux peut mettre fin au match quelle que soit la stratégie, l'écart entre la planification minutieuse de Chara et le martèlement aléatoire des boutons de Flowey n'est tout simplement pas si grand. Le combo de Chara nécessite plusieurs tours de préparation, et le RNG a largement l'occasion de la tuer avant qu'elle n'y parvienne. L'espace d'action est peut-être tout simplement trop restreint et trop aléatoire pour qu'une stratégie avide puisse systématiquement surpasser le chaos.
En d'autres termes : la victoire de Flowey est moins une découverte qu'une contrainte de conception. Une Chara plus intelligente aurait besoin d'un jeu plus intelligent pour le prouver.
Pourquoi ces chiffres ne sont pas entièrement fiables
Avant de tirer des conclusions de ce benchmark, il convient de reconnaître quelques biais structurels.
Les affrontements ne sont pas isolés. Les deux joueurs se voient attribuer une stratégie au hasard à chaque match, ce qui signifie que les taux de victoire sont des agrégats de toutes les paires possibles, et non des statistiques 1v1 pures. Les 52 % de Flowey incluent les matchs où Flowey a affronté un autre Flowey, et dans ces cas-là, l'un d'entre eux devait forcément gagner. Pour savoir réellement si Chara bat Toby ou si Flowey bat tout le monde de la même manière, il faudrait fixer les affrontements et tester chaque paire séparément.
L'ordre des tours n'est jamais inversé. Le joueur 1 joue toujours en premier. Dans un système où un seul coup critique peut mettre fin à un match, jouer en premier constitue un réel avantage. Les résultats n'en tiennent absolument pas compte.
Le générateur aléatoire (RNG) est un xorshift personnalisé dont la graine est dérivée de l'heure système. Il fonctionne assez bien pour un projet universitaire, mais ce n'est pas un générateur validé statistiquement. Si la distribution des résultats est inégale entre les 6 actions possibles, les résultats de Flowey en sont directement affectés, car toute sa stratégie consiste simplement à appeler cette fonction.
L'espace d'action est très restreint. Six actions possibles signifient que toute stratégie dispose d'une marge limitée pour se démarquer du chaos. Dans un système plus complexe, le combo de Chara serait peut-être plus difficile à interrompre, ou il pourrait exister des options de récupération réduisant le coût de sa mise en place. Ici, le jeu est juste assez punitif pour que ses dégâts de recul mettent souvent fin à la partie avant que la récompense n'arrive.
Chara n'a probablement pas été assez bien conçue. Sa stratégie ne prévoit aucun plan de secours défensif. Si les conditions du combo ne sont pas remplies et qu'elle subit de lourds dégâts, elle continue tout simplement à accumuler des PM. Une version plus robuste passerait en mode survie en dessous d'un certain seuil de PV, ce qui améliorerait probablement ses résultats de manière significative.
Les résultats sont intéressants d'un point de vue directionnel, mais doivent être considérés comme un instantané de cette configuration spécifique, et non comme une affirmation générale sur la stratégie par rapport au hasard.
Recherches futures
La prochaine étape évidente consiste à abandonner complètement RARS. Cette version fonctionne sur un émulateur RISC-V basé sur Java, ce qui limite fortement la vitesse de simulation. Une deuxième version ciblant du RISC-V compilé réel avec des appels système Linux au lieu des ecalls RARS devrait être nettement plus rapide, ce qui aura une grande importance lorsque le nombre de matchs commencera à se compter en millions.
Au-delà des performances, les pistes les plus intéressantes se situent du côté de la conception :
Plus de joueurs par match. Pour l’instant, c’est toujours du 1 contre 1. L’ajout d’un troisième ou d’un quatrième participant change complètement le paysage stratégique. Du coup, une contre-stratégie doit tenir compte du fait d’être attaqué de deux directions à la fois, et le hasard pur devient plus difficile à maintenir.
L’apprentissage automatique au sein de l’assembleur. L'idée est d'implémenter un algorithme d'apprentissage automatique minimal directement dans RISC-V, en utilisant des opérations matricielles pour permettre à un bot de mettre à jour ses propres poids en fonction des résultats des matchs. Pas de bibliothèques externes, pas d'environnement d'exécution de haut niveau, juste des calculs matriciels entiers dans les registres. Que cela soit pratique ou simplement un défi intéressant à relever fait partie de l'attrait du projet.
Plus de stratégies et un moteur plus profond. L'espace d'action actuel est trop restreint pour qu'une stratégie puisse véritablement prendre l'avantage sur le hasard. S'inspirer plus directement de World of Warcraft (temps de recharge, compromis entre ressources, effets de positionnement, interactions de compétences plus variées) donnerait aux stratégies bien conçues une réelle marge de manœuvre pour s'imposer face au chaos.
L'infrastructure de benchmark est déjà en place. Le goulot d'étranglement réside dans la profondeur du jeu, qui doit être suffisante pour que les résultats aient un sens.
Exécution
Pour compiler le code assembleur et exécuter le moteur localement :
make simulate
