From fbbc63b02b3bb3511d1f40b3213cb661175de674 Mon Sep 17 00:00:00 2001 From: kinther Date: Thu, 25 Dec 2025 08:54:28 -0800 Subject: [PATCH] Add unit tests for stealth and sleight of hand --- src/Makefile | 2 +- src/tests/sim_5e.c | 103 ++++++++++++++++++++++++++++++++++++++ src/tests/stubs_unit.c | 7 ++- src/tests/tests_5e.c | 109 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 2 deletions(-) diff --git a/src/Makefile b/src/Makefile index 8219ac8..5fc08a5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -106,4 +106,4 @@ $(SIMS_BIN): $(SIMS_OBJS) $(SIM_LINK_OBJS) | $(BINDIR) $(CC) $(CFLAGS) -o $@ $^ $(LFLAGS) $(LIBS) -lm $(SIMS_DIR)/sim_5e.o: $(SIMS_SRC) - $(CC) $(CFLAGS) -I. -c -o $@ $< \ No newline at end of file + $(CC) $(CFLAGS) -I. -c -o $@ $< diff --git a/src/tests/sim_5e.c b/src/tests/sim_5e.c index b561f4f..e8b771c 100644 --- a/src/tests/sim_5e.c +++ b/src/tests/sim_5e.c @@ -11,6 +11,9 @@ #include "utils.h" #include "handler.h" #include "constants.h" +#include "spells.h" + +extern struct player_special_data dummy_mob; /* ---------- local RNG for the sim (do NOT use MUD's rand_number here) ---------- */ static inline int randi_closed(int lo, int hi) { @@ -75,6 +78,30 @@ static double hit_rate(int attack_mod, int target_ac, int trials) { return (double)hits / (double)trials; } +/* stealth vs scan (contest): return stealth win rate */ +static double stealth_vs_scan_rate(struct char_data *sneaky, struct char_data *scanner, int trials) { + int wins = 0; + for (int i=0;i scan_total) + wins++; + } + return (double)wins / (double)trials; +} + +/* steal check vs DC (no scan vs scan): return success rate */ +static double steal_vs_dc_rate(struct char_data *thief, int dc, int trials) { + int wins = 0; + for (int i=0;i= dc) + wins++; + } + return (double)wins / (double)trials; +} + +/* Mirror compute_steal_dc logic for sims (uses real modifiers and flags). */ /* simple DPR per swing: roll dice + STR mod; 0 on miss (local RNG) */ static int swing_damage(int ndice, int sdice, int str_mod) { return dice_local(ndice, sdice) + str_mod; @@ -133,6 +160,31 @@ static void build_heavy(struct char_data *ch) { equip_at(ch, WEAR_WRIST_R, make_armor(1,1,0,0,0,0)); } +static void build_stealthy(struct char_data *ch, int dex, int skill_pct, int prof_bonus) { + init_test_char(ch); + set_ability_scores(ch, 10, dex, 10, 10, 10, 10); + ch->player.level = 1; + ch->points.prof_mod = prof_bonus - get_level_proficiency_bonus(ch); + SET_SKILL(ch, SKILL_STEALTH, skill_pct); +} + +static void build_scanner(struct char_data *ch, int wis, int skill_pct, int prof_bonus) { + init_test_char(ch); + set_ability_scores(ch, 10, 10, 10, 10, wis, 10); + ch->player.level = 1; + ch->points.prof_mod = prof_bonus - get_level_proficiency_bonus(ch); + SET_SKILL(ch, SKILL_PERCEPTION, skill_pct); +} + +static void build_thief(struct char_data *ch, int dex, int skill_pct, int prof_bonus) { + init_test_char(ch); + set_ability_scores(ch, 10, dex, 10, 10, 10, 10); + ch->player.level = 1; + ch->points.prof_mod = prof_bonus - get_level_proficiency_bonus(ch); + SET_SKILL(ch, SKILL_SLEIGHT_OF_HAND, skill_pct); +} + + /* attacker profiles: compute attack_mod = STRmod + prof(skill%) + weapon_magic */ static int compute_attack_mod(int str_score, int skill_pct, int weapon_magic) { int mod = GET_ABILITY_MOD(str_score); @@ -146,6 +198,7 @@ static int compute_attack_mod(int str_score, int skill_pct, int weapon_magic) { int main(void) { /* seed local RNG (do not rely on MUD RNG here) */ srand(42); + circle_srandom(42); /* 1) Hit-rate grid: atk_mod 0..10 vs AC 12..20 */ printf("Hit-rate grid (trials=50000):\n "); @@ -210,5 +263,55 @@ int main(void) { printf("\n"); } + /* 4) Stealth vs Scan grid (contested d20 + ability + proficiency) */ + const int profs[] = { 0, 1, 2, 3, 4, 5, 6 }; + + printf("Stealth vs Scan grid (trials=30000, Dex/Wis 14):\n"); + printf("Columns = Scan proficiency, Rows = Stealth proficiency\n "); + for (size_t i=0;iplayer_specials = calloc(1, sizeof(struct player_special_data)); + ch->in_room = 0; +} + +static void set_prof_bonus(struct char_data *ch, int prof_bonus) { + ch->player.level = 1; + ch->points.prof_mod = prof_bonus - get_level_proficiency_bonus(ch); +} + /* Make a simple armor object with given per-piece fields. */ static struct obj_data *make_armor(int piece_ac, int bulk, int magic, int stealth_disadv, int durability, int str_req) { struct obj_data *o = calloc(1, sizeof(*o)); @@ -57,6 +71,27 @@ static void set_ability_scores(struct char_data *ch, int str, int dex, int con, ch->aff_abils = ch->real_abils; /* common pattern */ } +static double simulate_skill_vs_dc(struct char_data *ch, int skillnum, int dc, int trials) { + int success = 0; + for (int i = 0; i < trials; ++i) { + int total = roll_skill_check(ch, skillnum, 0, NULL); + if (total >= dc) + success++; + } + return (double)success / (double)trials; +} + +static double simulate_stealth_vs_scan(struct char_data *sneaky, struct char_data *scanner, int trials) { + int success = 0; + for (int i = 0; i < trials; ++i) { + int stealth_total = roll_skill_check(sneaky, SKILL_STEALTH, 0, NULL); + int scan_total = roll_skill_check(scanner, SKILL_PERCEPTION, 0, NULL); + if (stealth_total > scan_total) + success++; + } + return (double)success / (double)trials; +} + /* For hit probability sanity tests: simulate pure d20 vs ascending AC with nat 1/20. */ static double simulate_hit_rate(int attack_mod, int target_ac, int trials) { int hits = 0; @@ -210,11 +245,85 @@ static void test_hit_probability_sanity(void) { T_IN_RANGE(p4, 0.05, 0.15, "Hit rate low (atk+0 vs AC20)"); } +static void test_stealth_vs_scan_proficiency(void) { + struct char_data sneaky, scanner; + init_test_char(&sneaky); + init_test_char(&scanner); + + set_ability_scores(&sneaky, 10, 14, 10, 10, 10, 10); /* DEX 14 */ + set_ability_scores(&scanner, 10, 10, 10, 10, 14, 10); /* WIS 14 */ + + SET_SKILL(&sneaky, SKILL_STEALTH, 50); + SET_SKILL(&scanner, SKILL_PERCEPTION, 50); + set_prof_bonus(&scanner, 0); /* fixed scan proficiency */ + + const int trials = 20000; + + set_prof_bonus(&sneaky, 0); + circle_srandom(12345); + double p1 = simulate_stealth_vs_scan(&sneaky, &scanner, trials); + + set_prof_bonus(&sneaky, 3); + circle_srandom(12345); + double p2 = simulate_stealth_vs_scan(&sneaky, &scanner, trials); + + set_prof_bonus(&sneaky, 6); + circle_srandom(12345); + double p3 = simulate_stealth_vs_scan(&sneaky, &scanner, trials); + + T_ASSERT(p1 < p2 && p2 < p3, + "Stealth vs scan should improve with proficiency (%.3f < %.3f < %.3f)", + p1, p2, p3); + T_ASSERT((p3 - p1) > 0.08, + "Stealth vs scan gap should be meaningful (%.3f -> %.3f)", + p1, p3); +} + +static void test_steal_vs_scan_proficiency(void) { + struct char_data thief; + init_test_char(&thief); + set_ability_scores(&thief, 10, 14, 10, 10, 10, 10); /* DEX 14 */ + SET_SKILL(&thief, SKILL_SLEIGHT_OF_HAND, 50); + + const int trials = 20000; + const int dc_no_scan = 10; /* compute_steal_dc base with neutral awake victim */ + const int dc_scan = 15; /* base + scan bonus */ + const int profs[] = { 0, 3, 6 }; + double no_scan[3], scan[3]; + + for (size_t i = 0; i < 3; ++i) { + set_prof_bonus(&thief, profs[i]); + + circle_srandom(24680); + no_scan[i] = simulate_skill_vs_dc(&thief, SKILL_SLEIGHT_OF_HAND, dc_no_scan, trials); + + circle_srandom(24680); + scan[i] = simulate_skill_vs_dc(&thief, SKILL_SLEIGHT_OF_HAND, dc_scan, trials); + } + + T_ASSERT(no_scan[0] < no_scan[1] && no_scan[1] < no_scan[2], + "Steal vs no-scan improves with proficiency (%.3f < %.3f < %.3f)", + no_scan[0], no_scan[1], no_scan[2]); + T_ASSERT(scan[0] < scan[1] && scan[1] < scan[2], + "Steal vs scan improves with proficiency (%.3f < %.3f < %.3f)", + scan[0], scan[1], scan[2]); + T_ASSERT(scan[0] < no_scan[0] && scan[1] < no_scan[1] && scan[2] < no_scan[2], + "Scan DC should reduce steal success (no-scan %.3f/%.3f/%.3f vs scan %.3f/%.3f/%.3f)", + no_scan[0], no_scan[1], no_scan[2], scan[0], scan[1], scan[2]); + + T_IN_RANGE(no_scan[0], 0.70, 0.80, "Steal no-scan +0"); + T_IN_RANGE(no_scan[2], 0.93, 0.98, "Steal no-scan +6"); + T_IN_RANGE(scan[0], 0.45, 0.55, "Steal scan +0"); + T_IN_RANGE(scan[2], 0.65, 0.75, "Steal scan +6"); +} + int main(void) { test_GET_ABILITY_MOD(); test_GET_PROFICIENCY(); test_ac_light_medium_heavy(); test_hit_probability_sanity(); + test_stealth_vs_scan_proficiency(); + test_steal_vs_scan_proficiency(); printf("Tests run: %d, failures: %d\n", tests_run, tests_failed); return tests_failed ? 1 : 0;