La version pygame de NOVA. Même jeu, mêmes règles, mais sans les 32 Ko de RAM qui ont forcé la version calculatrice à tout élaguer.
Le plus simple — installe tout et démarre, en une commande :
| Windows | double-clic sur play.cmd |
| Linux / macOS | ./play.sh |
Le script trouve Python, crée un environnement isolé dans pc/.venv, installe
les dépendances et lance le jeu. Les fois suivantes, il démarre directement.
Il installe pygame-ce en priorité — le fork communautaire publie des paquets
pour les nouvelles versions de Python des mois avant pygame classique, ce qui
évite l'erreur de compilation habituelle sur un Python tout frais. Même
import pygame, et si pygame-ce n'est pas disponible il retombe sur pygame.
À la main, si tu préfères :
pip install pygame-ce numpy # ou: pip install pygame numpy
python3 nova.py| Windows | build_exe.cmd → dist\NOVA.exe |
| Linux / macOS | ./build_exe.sh → dist/NOVA |
Un seul fichier, icône incluse, aucune installation de Python requise pour y
jouer. Il n'y a pas de compilation croisée : un .exe Windows doit être
construit sur Windows.
| Option | |
|---|---|
--scale N |
Force un zoom entier (par défaut : choisi selon l'écran) |
--fullscreen |
Démarre en plein écran |
--no-crt |
Coupe les scanlines |
--no-sound |
Démarre en muet |
En jeu : Échap pause · F11 plein écran · F1 filtre CRT · M son
(3 crans) · +/- taille · la fenêtre est redimensionnable à la souris.
Le jeu s'adapte à n'importe quelle résolution, sans bandes noires. Le zoom est toujours un entier — un pixel de jeu reste un carré net — et le canvas prend ensuite autant de pixels de jeu qu'il en faut pour remplir l'écran.
| Écran | Zoom | Canvas | Arène |
|---|---|---|---|
| 1920×1080 | ×4 | 480×270 | 480×244 |
| 2560×1440 | ×5 | 512×288 | 480×244 |
| 3440×1440 (21:9) | ×5 | 688×288 | 480×244 |
| 1920×1200 (16:10) | ×4 | 480×300 | 480×244 |
| 1366×768 | ×3 | 455×256 | 455×230 |
| 3840×2160 | ×8 | 480×270 | 480×244 |
L'arène, elle, ne change pas. Sur un shmup vertical, la largeur du terrain est un réglage de difficulté : donner 40 % de place en plus à quelqu'un en ultrawide, c'est lui donner un autre jeu, plus facile. L'arène garde donc une taille fixe, centrée, et le canvas supplémentaire devient du décor — champ d'étoiles, rails, et un panneau latéral qui affiche l'équipement du vaisseau.
3440×1440. L'arène est cadrée par ses rails, les marges portent le loadout à gauche et le secteur à droite.
Sur un écran étroit (1366×768), l'arène se réduit pour tenir, et il n'y a simplement pas de marges.
Tout est synthétisé au démarrage — ondes carrées, triangles et bruit avec enveloppes, comme une NES. Aucun fichier audio dans le dépôt : 20 sons — 24 échantillons, le canon en ayant trois — construits en 0,03 seconde, et six morceaux de 56 à 80 secondes.
À écouter : docs/nova-sound-demo.wav pour les
effets, et docs/nova-music-demo.wav pour la
musique — dont le point de boucle, sans montage.
| Armes | tir simple et tir large (3+ canons), chacun en trois hauteurs, tir ennemi |
| Impacts | touche, explosion, explosion de boss |
| Vaisseau | dégât, déflecteur qui encaisse, déflecteur rechargé, bombe |
| Butin | cristal, réparation |
| Interface | navigation, validation, refus, achat, saut |
| Jingles | alerte boss, secteur terminé, fin de partie |
L'ancien lit musical, c'était huit mesures de croches : une basse triangle une fois sur quatre, et sur le reste une note au hasard dans une gamme de quatre notes. Des notes au hasard, ce n'est pas une mélodie — il n'y a rien à retenir et rien sur quoi revenir — et huit mesures font quinze secondes, donc toute l'idée repassait quatre fois par minute.
C'est une chanson maintenant : une progression d'accords, une basse marchante, et un motif développé sur quatre sections — A, B, A' une octave au-dessus, puis un C dépouillé. Le motif est tiré de la graine une seule fois, puis répondu, transposé et orné, au lieu d'être retiré à chaque mesure. Une phrase entendue quatre fois est une mélodie ; quatre phrases différentes sont du bruit bien élevé.
Et chaque section a sa propre figure d'arpège et sa propre basse : la première version en gardait une seule sur les trente-deux mesures et ne changeait que l'octave de la mélodie, ce qui fait quatre sections sur le papier et une seule à l'oreille. La section B fait tourner la progression d'un cran, les phrases courent sur quatre mesures au lieu de deux, et la dernière mesure de chaque section retire l'arpège pour qu'on entende la couture entre les sections.
Six morceaux de 56 à 80 secondes — un par secteur, plus un pour le Vide, qui ne recommence donc plus la musique du secteur 1. Chacun coûte 6,5 Mo de mémoire, alors seul celui qui joue est gardé : le reconstruire prend un cinquième de seconde, sur un écran où rien ne bouge.
Sans couture. L'ancienne boucle cliquait : mesuré, trois morceaux sur cinq
se terminaient sur une marche dans la forme d'onde 5 à 16 fois leur propre
99ᵉ centile de pas d'échantillon — un tic audible à chaque tour. Le morceau est
maintenant rendu dans un tampon plus long que la boucle, puis la queue est
repliée sur la tête : une note qui sonne encore à la fin continue au début du
tour suivant, exactement comme si le morceau ne s'était jamais arrêté. La marche
au raccord est à zéro, et test_audio.py refuse désormais tout morceau dont le
raccord dépasse son propre p99, ou qui dure moins de 45 secondes.
Le tir est automatique : il sonne six à dix fois par seconde, pendant toute la partie. C'est le seul son du jeu qui se comporte comme une boucle, et deux choses le rendent fatigant — le volume, et la répétition à l'identique.
Les deux sont traitées. Le canon est 14 dB plus discret qu'avant (0,037× l'énergie par tir), ce qui le place sous tous les autres effets sauf l'impact, et il tourne sur trois hauteurs : tenir le tir ne rejoue jamais deux fois le même échantillon.
| Effet | RMS |
|---|---|
| Alerte boss | 0,164 |
| Tir ennemi | 0,042 |
| Explosion | 0,041 |
| Tir joueur | 0,016 |
Vouloir couper le canon n'est pas vouloir le silence. M fait donc le tour :
SOUND ON → GUNS MUTED → SOUND OFF → SOUND ON
GUNS MUTED ne touche que notre propre canon. Le tir ennemi, les impacts
et l'alerte de boss restent : ce sont des informations, pas de la décoration —
les couper rendrait le jeu plus difficile. L'état choisi s'affiche à l'écran,
parce qu'on ne peut pas entendre la différence entre « moins » et « rien ».
Sans numpy, ou sur une machine sans carte son, le jeu démarre en silence plutôt que de planter.
| Joueur 1 | Joueur 2 (coop) | |
|---|---|---|
| Se déplacer | flèches ou WASD | WASD |
| Tirer | automatique | automatique |
| Bombe | Espace ou Maj |
partagée |
| Menus | flèches + Entrée |
|
| Pause | Échap, ou Start à la manette |
Échap ouvre un menu par-dessus ce qui est à l'écran : reprendre, le son, le
filtre CRT, abandonner la partie, quitter. Le combat reste visible sous le
voile, parce qu'on met en pause justement pour le regarder.
C'est un drapeau, pas un état de la machine à états. Un état aurait jeté le combat, la carte ou la boutique en dessous, et tout l'intérêt de mettre en pause au milieu d'un combat est que le combat soit encore là au retour. En revanche c'est aussi la façon la plus facile de laisser tourner une boucle de mise à jour par mégarde, alors le test compare les positions de chaque projectile avant et après cent vingt images de pause.
À la manette, Start n'est plus un second bouton de validation : il ouvre et
ferme la pause, comme partout ailleurs — sauf sur l'écran-titre, où il n'y a
rien à mettre en pause et où il valide.
Branchée avant ou pendant la partie, ça marche : stick gauche ou croix directionnelle pour se déplacer, bouton du bas ou gâchette pour la bombe, et la navigation des menus suit. Les manettes sont attribuées dans l'ordre de branchement — la première au joueur 1, la seconde au joueur 2.
Clavier et manette restent actifs en même temps. En coop, l'un peut tenir une manette pendant que l'autre joue au clavier, ce qui est la façon dont ces choses se jouent vraiment sur un canapé.
Contrairement à la calculatrice — dont le clavier matriciel sans diodes interdisait plus de deux touches — le vaisseau se déplace ici en 8 directions. Le tir reste automatique : c'est l'identité du jeu, et ça laisse les deux mains pour esquiver.
Tout ce que les 32 Ko avaient coûté :
| Calculatrice | PC | |
|---|---|---|
| Carte de secteur | un menu à 3 routes | le graphe complet, avec diagonales |
| Améliorations | 8 | 12 |
| Difficultés | une seule | 3 (Cadet / Pilote / As) |
| Événements | ✗ | 6, avec choix et conséquences |
| Ramassage de cristaux | crédité à la mort | butin à ramasser, aimantable |
| Explosions | ✗ | particules, débris, screen shake |
| Boss | un pattern | 5 boss différents, un par secteur |
| Étoiles | 2 couches, 12 étoiles | 3 couches, 140 étoiles |
Plus, en propre à cette version : sprites pixel art dessinés (au lieu de trois rectangles par vaisseau), traînées de réacteur, flash blanc à l'impact, hit-stop sur les dégâts, filtre CRT optionnel.
![]() |
![]() |
| La carte de secteur, enfin dessinée | Boss en phase 2 |
![]() |
![]() |
| Le marchand dit ce qu'il vend | Le filtre CRT, F1 |
Il grandit jusqu'au bout. Avant, c'était min(2 + secteur, 5) : tout était
débloqué dès le secteur 3, donc les secteurs 4 et 5 n'introduisaient plus rien
et le dernier tiers d'une partie n'était que le premier tiers avec des nombres
plus gros.
secteur 1 GRUNT, WEAVER secteur 4 + TANK
secteur 2 + RUSHER secteur 5 + SPINNER
secteur 3 + TURRET, LANCER le Vide + PHANTOM
Les trois derniers prennent chacun une mécanique à un boss et la boulonnent sur quelque chose de mortel.
Le rayon du LANCER était d'abord vertical, tiré vers le bas depuis un vaisseau au milieu de l'arène : il commençait nulle part et s'arrêtait au sol. Il avait l'air coupé parce qu'il était coupé — un rayon dont une extrémité pend dans le vide se lit comme un défaut d'affichage, pas comme une arme. Deux émetteurs s'amarrent aux murs opposés et le tendent entre eux, donc les deux bouts sont attachés à quelque chose. C'est tout l'intérêt : un boss t'apprend une attaque une fois, puis l'attaque revient sur un vaisseau que tu peux abattre.
| LANCER | Ils arrivent par deux, un contre chaque mur, et tendent un rayon horizontal entre eux d'un bord à l'autre de l'arène. Une seconde de télégraphe, puis le tir. Tuer l'un des deux fait tomber le rayon : la paire est la mécanique, casser le lien est la réponse. |
| SPINNER | Les anneaux de SENTINEL. Le seul ennemi du bestiaire qui ne se soucie pas d'où tu es, ce qui le rend gênant à côté de tous ceux qui te visent. |
| PHANTOM | Le Vide seulement. Dash arrière et latéral, déclenché par le fait que tu sois aligné dessous — donc se garer en dessous et tenir la gâchette, ce que la cadence de tir du Vide t'a appris à faire, est exactement ce qui échoue. |
La densité aussi : le budget passe de +10 à +13 par secteur, plafond de 104
à 150. Mais le vrai frein était ailleurs — un plafond fixe de 14 ennemis
simultanés, inchangé depuis le secteur 1. Il monte maintenant avec le secteur,
jusqu'à 20.
Le premier jet PC avait un seul boss reskinné par secteur : le combat censé être la conclusion d'un secteur était le même combat à chaque fois. Chacun a maintenant sa propre mécanique.
| SENTINEL | Éventails, puis des murs avec un seul trou, puis une spirale par-dessus. Le boss qui enseigne le vocabulaire des quatre autres. |
| HIVE MOTHER | Ouvre ses baies et lâche des escortes, arme les canons muraux, et fait pleuvoir depuis le haut. |
| LANCE | Un rayon, puis deux, puis un peigne de quatre qui balaie l'arène d'un bord à l'autre. La colonne sûre finit toujours par ne plus l'être. |
| BULWARK | Un cœur derrière deux modules destructibles qui tirent en rafales. Tant qu'un module tient, le cœur ne prend qu'un quart des dégâts — la barre passe au bleu pour le dire. Casse-les et le cœur cesse de se retenir. |
| WARDEN | Tout à la fois : canons muraux dès le début, rayons en phase 2, baies ouvertes en phase 3, modules toujours dans le chemin. |
Ils partagent un corps — dérive, points de vie, phases selon la coque restante.
Ce qui change est une seule méthode, act, dispatchée une fois par image : le
seul endroit où vit la personnalité d'un boss.
Aucun de ces motifs n'est aléatoire. Une spirale est la même spirale à chaque tour, un mur a toujours exactement un trou, un rayon est fixé là où il visait au début de la charge. C'est la différence entre difficile et injuste : le motif est fait pour être lu et traversé, pas subi. Trois choses existent uniquement pour ça — la boîte de collision du vaisseau fait 6×9 dans un sprite de 13×17, un coup à la coque offre 1,4 seconde d'invulnérabilité, et la bombe nettoie l'écran.
Le premier jet de ces boss ne s'arrêtait jamais de tirer. C'était l'erreur. Sans fenêtre pour riposter, un combat cesse d'être quelque chose qu'on lit et devient de l'arithmétique : 60 secondes d'esquive à grignoter le boss entre deux frôlements. Mesuré.
Ils respirent maintenant. Chaque boss alterne une tempête (3 à 4,4 s selon la phase) et une fenêtre (1,7 à 1,1 s) pendant laquelle tout se tait — boss, modules et canons muraux compris. Quatre coins cyan apparaissent autour du boss pour la signaler ; un cadre complet voudrait dire « blindé », alors la fenêtre a sa propre forme.
Le résultat mesuré au moment de ce changement : le combat tombe de 60 à 45 secondes, les dégâts encaissés de 7,4 à 4,5 points de coque, et la tempête peut être bien plus méchante qu'elle n'aurait osé l'être sans la pause.
Un boss retient son tir le temps de sa carte de présentation. Le joueur, lui, ne retient rien : un vaisseau garé dessous place tous ses tirs. Mesuré, avant correction — 5 à 25 % de la coque du boss partaient avant qu'il ait agi, 25 % avec un vaisseau d'origine au secteur 1, moins avec des canons maxés seulement parce que les points de vie du boss suivent l'armement apporté.
Des dégâts gratuits contre quelque chose qui ne peut pas répondre, ce n'est pas une récompense d'habileté, c'est un trou dans les règles. Le boss est donc intouchable pendant toute cette fenêtre — la constante est de 2 s et le combat l'élargit à la durée de la carte, parce qu'un bouclier qui expire pendant que le boss retient encore son tir ne fait que déplacer le trou une seconde plus loin. Les modules sont couverts aussi, sinon l'astuce se déplaçait simplement sur BULWARK et WARDEN.
Il se dessine en cercle. Le cadre veut déjà dire « modules qui blindent le cœur » et les coins cyan « en train de venter » : trois états qui changent ce que font tes tirs, trois formes.
Le boss tient le milieu de l'écran. Sans les canons muraux, les bords sont l'endroit où l'on va pour être tranquille — et un combat avec un coin sûr est un combat qu'on gagne en s'y asseyant. Ils sont destructibles, donc nettoyer un côté est un vrai choix à faire avec les secondes où l'on ne tire pas sur le boss, et ils s'arment visiblement pendant une seconde avant leur premier tir : un projectile venu du hors-champ n'est pas un motif, c'est une embuscade.
Chaque combat s'ouvre sur une carte qui nomme le boss et dit ce qu'il fait : un combat qu'on n'a jamais vu ne devrait pas commencer par une surprise qu'on ne pouvait pas lire.
Un secteur est un graphe à embranchements, et pendant un moment il n'en avait que l'apparence. Le générateur remplissait des colonnes entières : la colonne 4 était toujours un marchand, la colonne 6 toujours un atelier de réparation. Quelle que soit la route prise, on croisait les deux — et le dernier nœud avant le boss rendait la coque à neuf. On pouvait jouer un secteur n'importe comment et affronter le seigneur de guerre à pleine vie de toute façon. Rien de ce qui arrivait en chemin ne comptait.
Les nœuds spéciaux sont maintenant posés un par un, pas par colonne :
| avant | maintenant | |
|---|---|---|
| Ateliers par secteur | 1,87 | 0,54 |
| Secteurs qui en proposent un | 100 % | 54 % |
| Toutes les routes en croisent un | 100 % | 12 % |
| Atelier juste avant le boss | 100 % | 0 % |
Quand les deux sont présents, le marchand et l'atelier sont dans la même colonne, sur deux lignes différentes : on a l'un ou l'autre, et le choix a réellement été fait une colonne plus tôt, en prenant la ligne qui pouvait atteindre celui qu'on voulait. Cristaux ou coque.
La dernière colonne avant le boss est toujours une patrouille ou une patrouille d'élite. Quelle que soit la route, la dernière chose avant le boss coûte quelque chose.
En échange, un atelier vaut le détour : il rend 45 % de la barre au lieu d'un forfait de 5 points, et si la coque est déjà pleine l'équipage revend le blindage en trop.
En cherchant pourquoi on arrivait toujours au boss à pleine vie, la mesure a donné une réponse inattendue : une patrouille rapportait −0,13 point de coque. Négatif. On finissait un combat en meilleur état qu'on ne l'avait commencé.
Deux fuites, toutes deux invisibles sans compter :
- chaque ennemi tué avait 9 % de chances de lâcher un correctif de coque. À dix-huit morts par combat, cela soignait plus que le combat ne coûtait. Ramené à 3 %.
- NANOREPAIR soignait à chaque nœud, par niveau. Le Vide n'a pas de fin, donc cette réparation non plus : au niveau 3 elle surpassait tout ce que le jeu pouvait infliger. Ce n'est pas une amélioration, c'est un interrupteur. Elle soigne maintenant une fois par secteur.
Autre chose que la mesure a trouvée : la difficulté se réglait surtout par le nombre d'ennemis — or un ennemi est aussi du butin. As envoyait 35 % d'ennemis en plus, donc 35 % de cristaux en plus, donc un meilleur vaisseau, et gagnait plus souvent que Cadet. Le curseur se battait contre lui-même.
Resserrer l'écart n'a pas suffi, et il a fallu une deuxième mesure pour le voir : avec 0,85 d'ennemis, Cadet mesurait plus dur que Pilote — 12 victoires contre 13, des combats de boss plus longs (57,8 s contre 48,0) et plus de dégâts encaissés (5,4 contre 4,8), tout en tirant moins vite. Moins d'ennemis, moins de cristaux, un moins bon vaisseau, un combat plus long.
Les trois niveaux envoient maintenant exactement le même nombre d'ennemis et se séparent sur la cadence de tir — qui ne rend rien au joueur — et sur la profondeur de la barre de coque, qui est volontairement large : un boss capable de prendre un tiers de la coque en une erreur a besoin d'une coque avec laquelle on peut se permettre des erreurs. Sinon « difficile » veut seulement dire « le premier coup termine la partie », ce qui n'est pas de la difficulté, c'est de la fragilité.
pc/
nova.py point d'entrée
nova/
data.py palette, sprites (en texte), tables, événements
art.py construit les surfaces, teinte les ennemis par secteur
entities.py vaisseaux, tirs, butin — tout en pixels par seconde
combat.py la scène de combat
fx.py particules, shake, flash, étoiles, overlay CRT
audio.py synthèse chiptune : ondes, enveloppes, catalogue, musique
boss.py les cinq boss, leurs rayons et leurs modules
gamepad.py manettes : sticks, croix, boutons, branchement à chaud
sector.py génération et rendu de la carte
run.py l'état d'une partie
ui.py menus, panneaux, HUD
game.py machine à états et boucle principale
assets/nova.ico icône de l'exécutable (générée)
play.sh play.cmd installe et lance
build_exe.sh/.cmd construit l'exécutable
nova.spec recette PyInstaller
tests/ suite headless
tools/shots.py rend les captures d'écran
tools/balance.py mesure la carte, le bestiaire et les combats
tools/shots_foes.py captures des ennemis tardifs
tools/music_demo.py exporte la musique et son point de boucle
tools/make_icon.py dessine l'icône
Tout tourne sans écran (SDL_VIDEODRIVER=dummy), donc en intégration continue :
python3 tests/run_all.py # runs complets + budget d'image
python3 tools/balance.py # carte et difficulté, chiffrées
python3 tools/shots.py # régénère les capturestest_run.py joue de vraies parties à travers la vraie machine à états — mêmes
événements clavier, mêmes combats, rien n'est simulé sauf l'écran. Il valide
aussi qu'aucun écran ne peut mener à une impasse.
test_gamepad.py pilote la vraie logique de manette avec un faux périphérique :
axes, croix, boutons, transitions de menu, débranchement. Aucune manette ne peut
être branchée en CI, et un mapping testé uniquement à la main est un mapping qui
casse en silence — celui-ci a d'ailleurs trouvé un vrai conflit, le bouton Est
étant à la fois « valider » et « annuler ».
test_audio.py attrape un piège discret : un nom d'effet mal orthographié est
silencieux, pas une erreur — play("expode") ne fait rien, pour toujours,
et personne ne le remarque. Le test enregistre chaque son demandé pendant une
partie et exige que les deux ensembles coïncident dans les deux sens ; un effet
construit mais jamais déclenché est aussi une anomalie. Les sons rares
(réparation, déflecteur, refus d'achat, fin de partie) sont provoqués
explicitement, sinon le résultat dépendrait de la graine. Les noms de canon
étant des alias — play("shoot") atteint l'une des trois variantes — la
comparaison passe par audio.gun_variants, ce qui vérifie du même coup que la
rotation les atteint toutes.
Le même test exerce les trois crans de M : que GUNS MUTED fasse taire le
canon, qu'il ne fasse taire que lui, et que SOUND OFF fasse taire le reste.
Il vérifie aussi que deux tirs sur la même image ne donnent qu'un son — le
cas de la coop, et le piège des variantes : si l'anti-répétition est indexée
sur la variante choisie plutôt que sur le nom demandé, deux joueurs qui tirent
ensemble tombent sur deux variantes différentes et le canon double de volume
exactement là où il est déjà le plus présent. Les trois régressions (un cran
qui ne coupe rien, une rotation qui saute une variante, l'anti-répétition mal
indexée) ont été introduites volontairement pour vérifier que le test les voit.
Le pire défaut trouvé pendant cette passe ne faisait planter personne. La même graine, lancée trois fois de suite : gagnée, perdue au secteur 4, perdue au secteur 5.
Les motifs de boss et le comportement des ennemis tiraient leur hasard du
random du module, pas du générateur de la partie. Rien ne plantait, aucun test
ne tombait — le jeu n'était simplement pas reproductible. Ce qui veut dire que
le banc d'équilibrage rapportait du bruit d'un lancement à l'autre comme si
c'était l'effet d'un changement, et qu'une graine dans un rapport de bug ne
reproduisait rien du tout.
Tout ce qui décide de l'issue d'un combat passe maintenant par le RNG de la partie : dérive du boss, phase des spirales, trou des murs, pluie, ordre des peignes de rayons, côté des canons muraux, décalage des modules, position et cadence des ennemis, dispersion du butin. Les particules, le shake et le champ d'étoiles restent volontairement sur le générateur global : ils ne peuvent changer aucune issue.
test_determinism.py prend l'empreinte d'une patrouille et des cinq boss, joués
deux fois depuis la même graine avec la même séquence d'entrées, en perturbant
volontairement le générateur global entre les deux pour qu'un tirage qui fuit
ne puisse pas retomber juste par hasard. Sa première version passait alors que
le bug était réintroduit exprès : elle ne suivait que le boss du secteur 2, et
HIVE MOTHER n'a pas de murs. En suivant les cinq, le même bug fait tomber
SENTINEL, BULWARK et WARDEN.
Son garde-fou — une graine différente doit changer l'empreinte — a lui aussi dû être assoupli. L'ouverture de LANCE ne tire qu'un seul dé, le sens de sa dérive, donc quatre graines d'affilée peuvent légitimement donner le même combat. Exiger qu'une graine précise diffère testait la graine, pas le jeu.
La première version était un champ de répulsion : additionner un vecteur qui s'éloigne de chaque projectile proche, et suivre la somme. Ça marche contre du tir visé et échoue complètement contre tout ce dont un motif de bullet hell est fait. Un mur avec un trou le pousse le long du mur au lieu de le pousser dans le trou ; une spirale l'entoure, les vecteurs s'annulent, il s'arrête et meurt.
Mesurer la difficulté des boss avec ce pilote-là, c'était mesurer à quel point un motif est illisible pour une mauvaise heuristique — et la première chose qu'il aurait conseillée, c'est de supprimer les bonnes attaques.
Il esquive maintenant comme une personne : pour chacune des neuf directions du stick, calculer à quelle distance passerait le projectile le plus menaçant, et prendre la meilleure. Le point d'approche est calculé proprement, position relative contre vitesse relative, parce qu'un projectile proche mais qui s'éloigne n'est pas une menace et qu'un projectile lointain qui converge en est une.
Validation : sur le jeu inchangé, l'ancien pilote faisait 7/4/1 et le nouveau fait 7/6/5. Il n'est jamais moins bon, et il est beaucoup plus fort là où l'ancien perdait contre des motifs qu'il ne savait tout simplement pas lire. Ce qui veut dire au passage que l'ancien équilibrage mesurait en partie l'incompétence du robot.
Il a une information parfaite, des réflexes parfaits, et aucune mémoire : il n'apprend jamais qu'un motif se répète. Un bon humain est moins bon sur le premier point et bien meilleur sur le second, ce qui est à peu près la bonne erreur à avoir quand les chiffres servent à régler la difficulté.
Mesures actuelles :
Toutes sur 24 graines, sur une version où une graine reproduit vraiment une partie (voir plus bas — pendant un moment ce n'était pas le cas, et les chiffres publiés mesuraient du bruit) :
| Coque du boss, tir parfait | 12 à 16 s à tous les stades (3,2 s avant au secteur 5) |
| Taux de victoire (bot) | plus significatif pour l'instant — voir plus bas |
| Coque à l'entrée du boss | 75 % de la barre (89 % avant) |
| Dégâts pris par boss | 3,6 points de coque (0,6 avant) |
| Combats de boss sans une égratignure | 17 % (57 % avant) |
| Durée d'un combat de boss | 45 s (30 s avant) |
| Durée d'une partie gagnée | 13 à 16 min de combat |
| Coût d'une image (p99) | 1,76 ms sur un budget de 16,7 ms |
| Pire cas mesuré | WARDEN phase 3 en coop, 305 entités à l'écran |
| Construction des sons | 24 échantillons en 0,03 s au démarrage |
Un retour de partie réelle : « les boss, on les bute super vite ». Vérifié, et
c'était pire que ça. Les points de vie d'un boss valent base + k x dps, où k
est littéralement le nombre de secondes de tir parfait que le combat doit
durer. Il valait 2,6. Un boss du secteur 5 face à un vaisseau maxé garé dessous
mourait en 3,2 secondes — et ça empirait à mesure qu'on améliorait son
vaisseau, parce que le terme qui suit les dégâts était le petit.
Ça a tenu longtemps parce que le banc mentait. Le pilote de test tient sa ligne sur un boss 31 % du temps : il esquive au lieu de viser. Un combat de 45 secondes dans le banc était un combat de 4 secondes dans le jeu. Régler la difficulté là-dessus, c'était la régler pour quelqu'un qui ne tire pas.
k vaut maintenant 8 + 1,5 x secteur : 12 à 16 secondes de tir parfait
partout, au lieu de 9 s au premier boss et 3,2 s au dernier. Et l'enrage, qui
accélère un combat qui s'éternise, démarrait à 45 secondes parce que c'était la
durée d'un combat ; sur des combats de quarante à soixante secondes il ne
réglait plus un blocage, il exécutait le joueur à tous les coups. Repoussé à
80 s, sur une pente plus douce.
Conséquence honnête : le pilote de test ne bat plus aucun boss, donc les taux de victoire par partie ne veulent plus rien dire. Le banc affiche à la place la seule mesure qui ne dépend pas de lui — les secondes de tir parfait — et les mesures de carte, de bestiaire et de budget d'image restent valables. Un pilote capable de se battre reste à écrire.
Deux de mes propres détours sont consignés dans le code pour que personne ne les refasse. « Dégâts infligés / dps théorique » n'est pas de la précision : BULWARK et WARDEN divisent chaque coup par quatre tant que leurs modules tiennent, donc ce ratio ne peut pas dépasser 25 % sur deux des cinq boss. Et « viser pendant la fenêtre de tir » s'est mesuré à l'envers — l'alignement est tombé de 31 % à 11 % — parce que la fenêtre, c'est exactement le moment où la rafale qui vient d'être tirée traverse l'arène.
L'échelle était monotone — 75 %, 50 %, 13 % — et elle ne l'a été qu'à partir du moment où les secteurs tardifs ont eu de quoi remplir l'écran. Avant, Cadet et Pilote finissaient à égalité (13 contre 14) : ce que Cadet donne, c'est surtout de la coque en plus, et un pilote qui se fait peu toucher ne la dépense jamais. Des combats plus denses lui donnent enfin quelque chose à en faire.
La coque à l'entrée du boss est passée de 89 % à 75 %, et c'est le chiffre qui répond vraiment à « on arrive toujours au boss à pleine vie ». Rendre les ateliers rares n'y suffisait pas : encore fallait-il que le chemin coûte quelque chose.
Et boss par boss, au niveau Pilote — c'est le tableau qui a servi à régler la difficulté, parce qu'une moyenne sur cinq boss cache exactement ce qu'on veut savoir : est-ce que le premier boss rencontré est un combat ou une formalité ?
| Boss | Dégâts pris | Durée | Sans une égratignure |
|---|---|---|---|
| S1 SENTINEL | 2,2 | 31 s | 12 % |
| S2 HIVE MOTHER | 3,3 | 56 s | 33 % |
| S3 LANCE | 3,5 | 44 s | 5 % |
| S4 BULWARK | 3,9 | 47 s | 24 % |
| S5 WARDEN | 6,1 | 49 s | 7 % |
2,2 → 3,3 → 3,5 → 3,9 → 6,1 : une rampe propre, ce qu'aucune des passes précédentes n'avait obtenu.
HIVE MOTHER, le boss du secteur 2, mesurait 1,0 point de dégâts et la moitié de ses combats sans une égratignure avant cette passe — de loin le plus faible des cinq, sur le boss que la plupart des gens rencontrent en deuxième.
MIT, comme le reste du dépôt.








