Files
mountycrawl/test/smoke.js
T
2026-06-11 17:26:29 +02:00

225 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Tests de fumée du cœur des règles — exécuter avec : node test/smoke.js */
"use strict";
const assert = require("assert");
require("../js/potions.js");
const g = require("../js/game.js");
const p = require("../js/potions.js");
// Dés
for (let i = 0; i < 200; i++) {
const r = g.rollDice(3, 6);
assert(r.total >= 3 && r.total <= 18, "3D6 hors bornes : " + r.total);
assert.strictEqual(r.rolls.length, 3);
}
// Combat : un attaquant écrasant doit toucher la plupart du temps. Dégâts en D3,
// doublés sur coup critique (jet d'attaque ≥ 2 × jet d'esquive).
let hits = 0, crits = 0;
for (let i = 0; i < 500; i++) {
const r = g.resolveAttack({ att: 10, deg: 3, degBonus: 2 }, { esq: 1, armor: 2 });
if (r.hit) {
hits++;
if (r.critical) crits++;
assert(r.damage >= 1, "dégâts minimum 1");
const max = (3 * 3 + 2) * (r.critical ? 2 : 1) - 2;
assert(r.damage <= max, "dégâts trop élevés : " + r.damage + (r.critical ? " (critique)" : ""));
}
}
assert(hits > 450, "10D6 vs 1D6 devrait presque toujours toucher (" + hits + "/500)");
assert(crits > 0, "10D6 vs 1D6 devrait produire des coups critiques");
// autoHit : ne rate jamais (Rafale Psychique)
for (let i = 0; i < 50; i++) {
assert(g.resolveAttack({ att: 1, deg: 1 }, { esq: 10 }, { autoHit: true }).hit, "autoHit doit toucher");
}
// Maîtrise : progresse en cas de succès, plafonnée, jamais au-delà du cap
{
const talent = { pct: 89 };
for (let i = 0; i < 200; i++) g.masteryRoll(talent, 90);
assert(talent.pct <= 90 && talent.pct >= 89, "plafond de maîtrise dépassé : " + talent.pct);
const sure = { pct: 100 };
const r = g.masteryRoll(sure, 200);
assert(r.success && sure.pct > 100, "succès garanti doit faire progresser");
// Sous 50 %, même un échec rapporte +1 %
const novice = { pct: 0 }; // 0 % : échec garanti, mais on apprend quand même
const rf = g.masteryRoll(novice, 90);
assert(!rf.success && rf.gain === 1 && novice.pct === 1, "échec sous 50 % doit donner +1 %");
for (let i = 0; i < 500; i++) g.masteryRoll(novice, 90);
assert(novice.pct === 90, "la maîtrise doit finir au plafond à force d'essais : " + novice.pct);
// À 50 % ou plus, l'échec ne rapporte plus rien (vérifiable à pct=50 : jet 51-100 → échec sec)
const adept = { pct: 50 };
for (let i = 0; i < 300; i++) {
const r2 = g.masteryRoll(adept, 90);
if (!r2.success) assert(r2.gain === 0, "échec à 50 %+ ne doit rien rapporter");
adept.pct = 50; // on fige pour garder la condition du test
}
}
// Sortilège : SR borné [10, 90]
assert.strictEqual(g.resolveSpell(100, 0).sr, 90);
assert.strictEqual(g.resolveSpell(0, 100).sr, 10);
// Coûts d'amélioration officiels (Rules_3.php) : N-ième achat = N × coût de base
assert.strictEqual(g.improveCost("att", 0, "Skrim"), 12, "1er dé d'ATT Skrim = 12 PI");
assert.strictEqual(g.improveCost("att", 2, "Skrim"), 36, "3e dé d'ATT Skrim = 36 PI (exemple officiel)");
assert.strictEqual(g.improveCost("att", 0, "Kastar"), 16, "ATT non favorisée = 16 PI");
assert.strictEqual(g.improveCost("deg", 0, "Kastar"), 12, "DEG favorisé Kastar = 12 PI");
assert.strictEqual(g.improveCost("pv", 0, "Durakuir"), 12, "PV favorisés Durakuir = 12 PI");
assert.strictEqual(g.improveCost("vue", 0, "Tomawak"), 12, "Vue favorisée Tomawak = 12 PI");
assert.strictEqual(g.improveCost("reg", 0, "Darkling"), 22, "REG favorisée Darkling = 22 PI");
assert.strictEqual(g.improveCost("reg", 0, "Skrim"), 30, "REG non favorisée = 30 PI");
assert.strictEqual(g.improveCost("armor", 0, "Durakuir"), 30, "Armure = 30 PI pour tous");
assert.strictEqual(g.improveCost("esq", 1, "Tomawak"), 32, "2e dé d'ESQ = 32 PI");
// L'armure naturelle en D3 réduit bien les dégâts
{
let withArmor = 0, without = 0;
for (let i = 0; i < 400; i++) {
withArmor += g.resolveAttack({ att: 10, deg: 5 }, { esq: 1, armor: 0, armorDice: 4 }).damage;
without += g.resolveAttack({ att: 10, deg: 5 }, { esq: 1, armor: 0 }).damage;
}
assert(withArmor < without, "4D3 d'armure naturelle doit réduire les dégâts");
}
// Niveaux : 0 PI → niv 1 ; 20 PI → niv 2 ; 50 PI → niv 3 (20 + 30)
assert.strictEqual(g.levelFromTotalPI(0), 1);
assert.strictEqual(g.levelFromTotalPI(20), 2);
assert.strictEqual(g.levelFromTotalPI(49), 2);
assert.strictEqual(g.levelFromTotalPI(50), 3);
// PX de mise à mort : 10 + 2 × (niveau cible niveau troll) + niveau cible
assert.strictEqual(g.killPX(1, 5), 23, "troll niv 1 vs cible niv 5");
assert.strictEqual(g.killPX(5, 5), 15, "même niveau");
assert.strictEqual(g.killPX(60, 1), -107, "troll niv 60 vs cible niv 1 → négatif");
assert.strictEqual(g.killPX(3, 1), 7, "troll niv 3 vs cible niv 1");
// Les 5 races existent avec stats officielles, compétence et sortilège réservés
assert.strictEqual(Object.keys(g.RACES).length, 5);
for (const r of Object.values(g.RACES)) {
for (const k of ["att", "esq", "deg", "reg", "pvMax", "vue"]) {
assert(typeof r.stats[k] === "number", "stat manquante : " + k);
}
assert(r.comp && r.comp.name && r.comp.cost > 0, "compétence raciale manquante");
assert(r.sort && r.sort.name && r.sort.cost > 0, "sortilège racial manquant");
}
// Profils de base officiels (Races_*.php)
assert.deepStrictEqual(g.RACES.Skrim.stats, { att: 4, esq: 3, deg: 3, reg: 1, pvMax: 30, vue: 3 });
assert.deepStrictEqual(g.RACES.Durakuir.stats, { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 40, vue: 3 });
assert.deepStrictEqual(g.RACES.Kastar.stats, { att: 3, esq: 3, deg: 4, reg: 1, pvMax: 30, vue: 3 });
assert.deepStrictEqual(g.RACES.Tomawak.stats, { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 30, vue: 4 });
assert.deepStrictEqual(g.RACES.Darkling.stats, { att: 3, esq: 3, deg: 3, reg: 2, pvMax: 30, vue: 3 });
assert.strictEqual(g.RACES.Skrim.comp.name, "Botte Secrète");
assert.strictEqual(g.RACES.Skrim.sort.name, "Hypnotisme");
assert.strictEqual(g.RACES.Durakuir.comp.name, "Régénération Accrue");
assert.strictEqual(g.RACES.Durakuir.sort.name, "Rafale Psychique");
assert.strictEqual(g.RACES.Kastar.comp.name, "Accélération du Métabolisme");
assert.strictEqual(g.RACES.Kastar.sort.name, "Vampirisme");
assert.strictEqual(g.RACES.Tomawak.comp.name, "Camouflage");
assert.strictEqual(g.RACES.Tomawak.sort.name, "Projectile Magique");
assert.strictEqual(g.RACES.Darkling.comp.name, "Balayage");
assert.strictEqual(g.RACES.Darkling.sort.name, "Siphon des Âmes");
// Génération de caverne : connexe, bordures murées, assez de sol
for (let i = 0; i < 20; i++) {
const grid = g.generateCavern(g.MAP_W, g.MAP_H);
let floor = 0;
for (let y = 0; y < g.MAP_H; y++) {
for (let x = 0; x < g.MAP_W; x++) {
if (grid[y][x] === g.T_FLOOR) floor++;
const border = x === 0 || y === 0 || x === g.MAP_W - 1 || y === g.MAP_H - 1;
if (border) assert.strictEqual(grid[y][x], g.T_WALL, "bordure non murée");
}
}
assert(floor > 50, "caverne trop petite : " + floor + " cases de sol");
const region = g.largestRegion(grid, g.MAP_W, g.MAP_H);
assert.strictEqual(region.size, floor, "caverne non connexe");
}
// Monstres : niveaux et stats positifs à toutes les profondeurs
for (let depth = 1; depth <= 5; depth++) {
for (let i = 0; i < 50; i++) {
const m = g.makeMonster(depth, 1, 1);
assert(m.level >= 1 && m.pv >= 1 && m.att >= 1 && m.deg >= 1, "monstre invalide : " + JSON.stringify(m));
}
}
// Specs d'éditeur → entités de jeu
const gob = g.monsterFromSpec({ x: 2, y: 3, type: 0, tpl: 2 });
assert.strictEqual(gob.name, "Vieux Gobelin");
assert.strictEqual(gob.x, 2);
const boss = g.monsterFromSpec({ x: 1, y: 1, boss: true });
assert.strictEqual(boss.name, "Béhémoth");
assert(boss.pv === boss.pvMax && boss.pv > 0);
const pot = g.itemFromSpec({ x: 1, y: 1, kind: "potion" });
assert.strictEqual(pot.kind, "potion");
assert(p.POTION_IDS.length === 25, "25 potions droppables");
assert.strictEqual(p.POTION_DEFS.guerison.duration, 0);
assert.strictEqual(p.corruptionYZ(3).y, 0);
{ const { y } = p.corruptionYZ(6); assert(y >= 11 && y <= 20, "Y pour X=6 : " + y); }
{
const troll = { att: 3, esq: 3, deg: 3, reg: 1, vue: 3, armor: 0, armorDice: 0, degBonus: 0, pvMax: 30, pv: 20, potionEffects: [], blockCamoTurns: 0, tour: 1 };
const item = p.makePotionItem("bonneBouffe", 5);
p.drinkPotion(troll, item, g.rollDice, () => {});
assert.strictEqual(troll.potionEffects.length, 1);
assert.strictEqual(p.effTroll(troll).deg, 8);
p.tickPotionTurns(troll, () => {});
assert.strictEqual(troll.tour, 2);
troll.potionEffects[0].turnsLeft = 0;
p.tickPotionTurns(troll, () => {});
assert.strictEqual(troll.potionEffects.length, 0);
assert.strictEqual(p.effTroll(troll).deg, 3);
}
const sword = g.itemFromSpec({ x: 1, y: 1, kind: "weapon", idx: 2 });
assert.strictEqual(sword.kind, "gear");
assert(sword.bonus > 0);
// Validation serveur des niveaux
const srv = require("../server.js");
const goodGrid = [];
for (let y = 0; y < srv.MAP_H; y++) {
const border = y === 0 || y === srv.MAP_H - 1;
goodGrid.push(border ? "#".repeat(srv.MAP_W) : "#" + ".".repeat(srv.MAP_W - 2) + "#");
}
const goodLevel = {
name: "Test", author: "perco", grid: goodGrid,
start: { x: 1, y: 1 },
monsters: [{ x: 2, y: 2, type: 0, tpl: 1 }],
items: [{ x: 3, y: 3, kind: "potion" }],
};
assert.strictEqual(srv.validateLevel(goodLevel), null);
assert(srv.validateLevel({ ...goodLevel, name: "" }), "nom vide refusé");
assert(srv.validateLevel({ ...goodLevel, monsters: [] }), "niveau sans monstre ni porte ni sortie refusé");
// Portes : valides, et suffisantes comme objectif sans monstre
const door = { x: 4, y: 4, target: "abcdef012345" };
assert.strictEqual(srv.validateLevel({ ...goodLevel, doors: [door] }), null);
assert.strictEqual(srv.validateLevel({ ...goodLevel, monsters: [], doors: [door] }), null, "porte = objectif valable");
assert(srv.validateLevel({ ...goodLevel, doors: [{ x: 4, y: 4, target: "pas-un-id" }] }), "cible de porte invalide refusée");
assert(srv.validateLevel({ ...goodLevel, doors: [{ x: 0, y: 0, target: "abcdef012345" }] }), "porte dans un mur refusée");
const gridWithExit = goodGrid.map((r, y) => y === 5 ? r.slice(0, 5) + ">" + r.slice(6) : r);
assert.strictEqual(srv.validateLevel({ ...goodLevel, monsters: [], grid: gridWithExit }), null, "sortie = objectif valable");
assert(srv.validateLevel({ ...goodLevel, start: { x: 0, y: 0 } }), "départ dans un mur refusé");
assert(srv.validateLevel({ ...goodLevel, grid: goodGrid.slice(1) }), "grille tronquée refusée");
assert(srv.validateLevel({ ...goodLevel, monsters: [{ x: 0, y: 0, type: 0, tpl: 0 }] }), "monstre dans un mur refusé");
assert(srv.validateLevel({ ...goodLevel, items: [{ x: 3, y: 3, kind: "nawak" }] }), "objet inconnu refusé");
// Vue : le brouillard est rogné quand la portée diminue (fin de potion)
{
g.newGame("Test", "Skrim");
const t = g.state.troll;
t.x = 5; t.y = 5;
t.potionEffects = [{ name: "Test", emoji: "🔭", turnsLeft: 1, vue: 5 }];
g.refreshFov();
assert(g.inSightAt(10, 5), "vue 8 (3+5) doit voir à 5 cases");
t.potionEffects = [];
g.refreshFov();
assert(!g.inSightAt(10, 5), "vue 3 seule ne doit plus voir à 5 cases");
assert(g.inSightAt(8, 5), "vue 3 voit encore à 3 cases");
}
console.log("✅ Tous les tests de fumée passent.");