mirror of
https://github.com/tbamud/tbamud.git
synced 2026-03-21 11:46:33 +01:00
Add unit tests for stealth and sleight of hand
This commit is contained in:
parent
448cb1f7a9
commit
fbbc63b02b
4 changed files with 219 additions and 2 deletions
|
|
@ -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 $@ $<
|
||||
$(CC) $(CFLAGS) -I. -c -o $@ $<
|
||||
|
|
|
|||
|
|
@ -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<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)
|
||||
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<trials;++i) {
|
||||
int total = roll_skill_check(thief, SKILL_SLEIGHT_OF_HAND, 0, NULL);
|
||||
if (total >= 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;i<sizeof(profs)/sizeof(profs[0]);++i) printf("Scan%+2d ", profs[i]);
|
||||
printf("\n");
|
||||
for (size_t si=0; si<sizeof(profs)/sizeof(profs[0]); ++si) {
|
||||
printf("Stealth%+2d ", profs[si]);
|
||||
for (size_t vi=0; vi<sizeof(profs)/sizeof(profs[0]); ++vi) {
|
||||
struct char_data sneaky, scanner;
|
||||
build_stealthy(&sneaky, 14, 50, profs[si]);
|
||||
build_scanner(&scanner, 14, 50, profs[vi]);
|
||||
double p = stealth_vs_scan_rate(&sneaky, &scanner, 30000);
|
||||
printf(" %5.1f", p*100.0);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
/* 5) Steal vs fixed DC grid */
|
||||
const int row_label_width = 22;
|
||||
const int col_width = 6;
|
||||
const int dc_list[] = { 5, 10, 14, 16, 18, 20 };
|
||||
const size_t uniq_count = sizeof(dc_list) / sizeof(dc_list[0]);
|
||||
|
||||
printf("Steal vs DC grid (trials=30000, Dex 14):\n");
|
||||
printf("Columns = DC, Rows = Sleight of Hand proficiency\n");
|
||||
printf("%-*s", row_label_width, "");
|
||||
for (size_t i=0;i<uniq_count;++i) {
|
||||
char col_label[8];
|
||||
snprintf(col_label, sizeof(col_label), "DC%d", dc_list[i]);
|
||||
printf(" %*s", col_width, col_label);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
for (size_t ti=0; ti<sizeof(profs)/sizeof(profs[0]); ++ti) {
|
||||
char row_label[32];
|
||||
snprintf(row_label, sizeof(row_label), "Sleight of Hand%+d", profs[ti]);
|
||||
printf("%-*s", row_label_width, row_label);
|
||||
for (size_t di=0; di<uniq_count; ++di) {
|
||||
struct char_data thief;
|
||||
build_thief(&thief, 14, 50, profs[ti]);
|
||||
double p = steal_vs_dc_rate(&thief, dc_list[di], 30000);
|
||||
printf(" %*.*f", col_width, 1, p*100.0);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/* --- Globals expected by utils.c --- */
|
||||
FILE *logfile = NULL; /* used by mudlog/basic_mud_vlog */
|
||||
struct descriptor_data *descriptor_list = NULL;
|
||||
struct char_data dummy_mob; /* used as a safe send_to_char target */
|
||||
struct player_special_data dummy_mob; /* dummy specials area for mobs */
|
||||
|
||||
/* --- Minimal world so in_room lookups are safe in tests/sims --- */
|
||||
struct room_data stub_room; /* zeroed room */
|
||||
|
|
@ -19,6 +19,11 @@ struct weather_data weather_info;
|
|||
const char *pc_class_types[] = { "class", NULL };
|
||||
|
||||
/* --- Functions utils.c references that we don't need in tests --- */
|
||||
bool has_save_proficiency(int class_num, int ability) {
|
||||
(void)class_num; (void)ability;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void send_to_char(struct char_data *ch, const char *messg, ...) {
|
||||
/* no-op for tests */
|
||||
(void)ch; (void)messg;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
#include "structs.h"
|
||||
#include "utils.h"
|
||||
#include "constants.h"
|
||||
#include "spells.h"
|
||||
|
||||
extern struct player_special_data dummy_mob;
|
||||
|
||||
/* ---------- Tiny test framework ---------- */
|
||||
static int tests_run = 0, tests_failed = 0;
|
||||
|
|
@ -27,6 +30,17 @@ static int tests_run = 0, tests_failed = 0;
|
|||
|
||||
/* ---------- Helpers for test setup ---------- */
|
||||
|
||||
static void init_test_char(struct char_data *ch) {
|
||||
memset(ch, 0, sizeof(*ch));
|
||||
ch->player_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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue