Fix unit tests and update some function names

This commit is contained in:
kinther 2025-08-22 15:21:06 -07:00
parent 15736e523a
commit e40f236867
8 changed files with 141 additions and 156 deletions

View file

@ -828,26 +828,25 @@ ACMD(do_score)
send_to_char(ch,
"STR %2d (%+d) DEX %2d (%+d) CON %2d (%+d)\r\n"
"INT %2d (%+d) WIS %2d (%+d) CHA %2d (%+d)\r\n",
GET_STR(ch), ability_mod(GET_STR(ch)),
GET_DEX(ch), ability_mod(GET_DEX(ch)),
GET_CON(ch), ability_mod(GET_CON(ch)),
GET_INT(ch), ability_mod(GET_INT(ch)),
GET_WIS(ch), ability_mod(GET_WIS(ch)),
GET_CHA(ch), ability_mod(GET_CHA(ch)));
GET_STR(ch), GET_ABILITY_MOD(GET_STR(ch)),
GET_DEX(ch), GET_ABILITY_MOD(GET_DEX(ch)),
GET_CON(ch), GET_ABILITY_MOD(GET_CON(ch)),
GET_INT(ch), GET_ABILITY_MOD(GET_INT(ch)),
GET_WIS(ch), GET_ABILITY_MOD(GET_WIS(ch)),
GET_CHA(ch), GET_ABILITY_MOD(GET_CHA(ch)));
/* Ascending AC breakdown */
send_to_char(ch,
"\r\n"
"Armor Class (ascending): %d\r\n"
" base: %d, armor: %d, armor magic: +%d, DEX (cap %d): %+d,\r\n"
" shield: +%d, situational: %+d, bulk score: %d\r\n",
" situational: %+d, bulk score: %d\r\n",
acb.total,
acb.base,
acb.armor_piece_sum,
acb.armor_magic_sum,
acb.dex_cap,
acb.dex_mod_applied,
acb.shield_bonus,
acb.situational,
acb.total_bulk);

View file

@ -886,6 +886,7 @@ void do_start(struct char_data *ch)
case CLASS_THIEF:
SET_SKILL(ch, SKILL_SNEAK, 5);
SET_SKILL(ch, SKILL_HIDE, 5);
SET_SKILL(ch, SKILL_TRACK, 5);
SET_SKILL(ch, SKILL_STEAL, 5);
SET_SKILL(ch, SKILL_BACKSTAB, 5);
SET_SKILL(ch, SKILL_PICK_LOCK, 5);

View file

@ -950,7 +950,9 @@ const char *ibt_bits[] = {
/* 5e system helpers */
/* Armor slot table for ascending AC rules */
/* Armor slot table for ascending AC rules
* Fields are AC, bulk, magic cap
*/
const struct armor_slot armor_slots[] = {
{ "head", 2, 1, 1 },
{ "body", 3, 3, 3 },
@ -958,19 +960,22 @@ const struct armor_slot armor_slots[] = {
{ "arms", 1, 1, 1 },
{ "hands", 1, 1, 1 },
{ "feet", 1, 1, 1 },
/* shield handled separately in compute_ascending_ac() */
{ "right wrist", 1, 1, 1 },
{ "left wrist", 1, 1, 1 },
};
const int NUM_ARMOR_SLOTS = sizeof(armor_slots) / sizeof(armor_slots[0]);
/* Wear-position mapping for armor_slots[] order */
const int ARMOR_WEAR_POSITIONS[] = {
WEAR_HEAD, /* "head" */
WEAR_BODY, /* "body" */
WEAR_LEGS, /* "legs" */
WEAR_ARMS, /* "arms" */
WEAR_HANDS, /* "hands" */
WEAR_FEET /* "feet" */
WEAR_HEAD, /* "head" */
WEAR_BODY, /* "body" */
WEAR_LEGS, /* "legs" */
WEAR_ARMS, /* "arms" */
WEAR_HANDS, /* "hands" */
WEAR_FEET, /* "feet" */
WEAR_WRIST_R, /* "right wrist" */
WEAR_WRIST_L /* "left wrist" */
};
/* Armor flag names for obj->value[3] */

View file

@ -75,11 +75,11 @@ static int roll_damage(struct char_data *ch, struct char_data *victim,
int ndice = GET_OBJ_VAL(wielded, 1); /* #dice */
int sdice = GET_OBJ_VAL(wielded, 2); /* sides */
dam = dice(ndice, sdice);
dam += ability_mod(GET_STR(ch)); /* STR adds to weapon damage */
dam += GET_ABILITY_MOD(GET_STR(ch)); /* STR adds to weapon damage */
} else {
/* unarmed */
dam = dice(1, 2);
dam += ability_mod(GET_STR(ch));
dam += GET_ABILITY_MOD(GET_STR(ch));
}
if (dam < 0) dam = 0;
@ -848,7 +848,7 @@ void hit(struct char_data *ch, struct char_data *victim, int type)
d20 = rand_number(1, 20);
/* Ability modifier: STR only (no ranged types yet) */
attack_mod += ability_mod(GET_STR(ch));
attack_mod += GET_ABILITY_MOD(GET_STR(ch));
/* Skill family & proficiency */
{
@ -856,7 +856,7 @@ void hit(struct char_data *ch, struct char_data *victim, int type)
const char *skillname = skill_name_for_gain(skillnum); /* maps to "unarmed", "piercing weapons", etc. */
/* proficiency from current % */
attack_mod += prof_from_skill(GET_SKILL(ch, skillnum));
attack_mod += GET_PROFICIENCY(GET_SKILL(ch, skillnum));
/* Weapon magic (cap +3) */
if (wielded && GET_OBJ_TYPE(wielded) == ITEM_WEAPON) {

View file

@ -1,4 +1,4 @@
/* tests/sim_5e.c — quick simulations for 5e-like tuning (fixed RNG + bounds) */
/* tests/sim_5e.c — quick simulations for 5e-like tuning */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -30,7 +30,7 @@ static void init_test_char(struct char_data *ch) {
memset(ch, 0, sizeof(*ch));
/* ensure skills and other saved fields exist if GET_SKILL() dereferences */
ch->player_specials = calloc(1, sizeof(struct player_special_data));
/* if your tree uses player_specials->saved, calloc above keeps it zeroed */
/* if the tree uses player_specials->saved, calloc above keeps it zeroed */
ch->in_room = 0; /* park them in room #0 (we'll make a stub room below) */
}
@ -98,36 +98,43 @@ static int duel_rounds(int atk_mod, int ndice, int sdice, int att_str_mod,
return (int) floor((double)rounds_sum / (double)trials);
}
/* build three defenders with your real slot weights and caps */
/* build three defenders with real slot weights and caps */
static void build_light(struct char_data *ch) {
init_test_char(ch);
set_ability_scores(ch, 10, 18, 10, 10, 10, 10);
equip_at(ch, WEAR_HEAD, make_armor(1,1,1,0));
equip_at(ch, WEAR_BODY, make_armor(2,1,2,0));
equip_at(ch, WEAR_HEAD, make_armor(1,1,0,0));
equip_at(ch, WEAR_BODY, make_armor(1,1,0,0));
equip_at(ch, WEAR_LEGS, make_armor(1,2,0,0));
equip_at(ch, WEAR_FEET, make_armor(1,1,0,0));
}
static void build_medium(struct char_data *ch) {
init_test_char(ch);
set_ability_scores(ch, 10, 18, 10, 10, 10, 10);
equip_at(ch, WEAR_LEGS, make_armor(2,2,2,0));
equip_at(ch, WEAR_HANDS, make_armor(1,1,0,0));
equip_at(ch, WEAR_FEET, make_armor(1,1,0,0));
equip_at(ch, WEAR_HEAD, make_armor(2,1,0,0));
equip_at(ch, WEAR_BODY, make_armor(2,2,1,0));
equip_at(ch, WEAR_LEGS, make_armor(2,2,0,0));
equip_at(ch, WEAR_HANDS, make_armor(1,1,0,0));
equip_at(ch, WEAR_FEET, make_armor(1,1,0,0));
}
static void build_heavy(struct char_data *ch, int shield_magic) {
static void build_heavy(struct char_data *ch) {
init_test_char(ch);
set_ability_scores(ch, 10, 18, 10, 10, 10, 10);
equip_at(ch, WEAR_BODY, make_armor(3,3,3,0));
equip_at(ch, WEAR_LEGS, make_armor(2,2,3,0));
struct obj_data *shield = make_armor(0,0,shield_magic,0);
equip_at(ch, WEAR_SHIELD, shield);
equip_at(ch, WEAR_HEAD, make_armor(2,1,1,0));
equip_at(ch, WEAR_BODY, make_armor(3,3,1,0));
equip_at(ch, WEAR_LEGS, make_armor(2,1,1,0));
equip_at(ch, WEAR_ARMS, make_armor(1,1,0,0));
equip_at(ch, WEAR_HANDS, make_armor(1,1,0,0));
equip_at(ch, WEAR_FEET, make_armor(1,1,0,0));
equip_at(ch, WEAR_WRIST_L, make_armor(1,1,0,0));
equip_at(ch, WEAR_WRIST_R, make_armor(1,1,0,0));
}
/* 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 = ability_mod(str_score);
mod += prof_from_skill(skill_pct);
int mod = GET_ABILITY_MOD(str_score);
mod += GET_PROFICIENCY(skill_pct);
if (weapon_magic > MAX_WEAPON_MAGIC) weapon_magic = MAX_WEAPON_MAGIC;
mod += weapon_magic;
if (mod > MAX_TOTAL_ATTACK_BONUS) mod = MAX_TOTAL_ATTACK_BONUS;
@ -152,32 +159,27 @@ int main(void) {
}
printf("\n");
/* 2) Real defenders AC using your compute_ac_breakdown */
struct char_data light, medium, heavy0, heavy3;
/* 2) Real defenders AC using compute_ac_breakdown */
struct char_data light, medium, heavy;
build_light(&light);
build_medium(&medium);
build_heavy(&heavy0, 0);
build_heavy(&heavy3, 5); /* requests +5 but shield path clamps to +3 */
build_heavy(&heavy);
struct ac_breakdown bl, bm, bh0, bh3;
struct ac_breakdown bl, bm, bh0;
compute_ac_breakdown(&light, &bl);
compute_ac_breakdown(&medium, &bm);
compute_ac_breakdown(&heavy0, &bh0);
compute_ac_breakdown(&heavy3, &bh3);
compute_ac_breakdown(&heavy, &bh0);
printf("Defender AC breakdowns:\n");
printf(" Light : total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n",
printf(" Light : total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d situ=%+d bulk=%d)\n",
bl.total, bl.base, bl.armor_piece_sum, bl.armor_magic_sum, bl.dex_cap,
bl.dex_mod_applied, bl.shield_bonus, bl.situational, bl.total_bulk);
printf(" Medium: total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n",
bl.dex_mod_applied, bl.situational, bl.total_bulk);
printf(" Medium: total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d situ=%+d bulk=%d)\n",
bm.total, bm.base, bm.armor_piece_sum, bm.armor_magic_sum, bm.dex_cap,
bm.dex_mod_applied, bm.shield_bonus, bm.situational, bm.total_bulk);
printf(" Heavy : total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n",
bm.dex_mod_applied, bm.situational, bm.total_bulk);
printf(" Heavy : total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d situ=%+d bulk=%d)\n",
bh0.total, bh0.base, bh0.armor_piece_sum, bh0.armor_magic_sum, bh0.dex_cap,
bh0.dex_mod_applied, bh0.shield_bonus, bh0.situational, bh0.total_bulk);
printf(" Heavy+(sh+3 cap): total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%+d shield=%d situ=%+d bulk=%d)\n\n",
bh3.total, bh3.base, bh3.armor_piece_sum, bh3.armor_magic_sum, bh3.dex_cap,
bh3.dex_mod_applied, bh3.shield_bonus, bh3.situational, bh3.total_bulk);
bh0.dex_mod_applied, bh0.situational, bh0.total_bulk);
/* 3) Attacker profiles vs defenders (TTK & hit%, 1d8 weapon) */
struct { int str; int skill; int wmag; const char *name; } atk[] = {
@ -188,16 +190,15 @@ int main(void) {
struct { struct char_data *def; const char *name; int hp; } def[] = {
{ &light, "Light", 40 },
{ &medium, "Medium", 50 },
{ &heavy0, "Heavy", 60 },
{ &heavy3, "Heavy+Shield+3", 60 },
{ &heavy, "Heavy", 60 },
};
printf("Matchups (trials=20000, 1d8 weapon):\n");
for (size_t i=0;i<sizeof(atk)/sizeof(atk[0]);++i) {
int str_mod = ability_mod(atk[i].str);
int str_mod = GET_ABILITY_MOD(atk[i].str);
int atk_mod = compute_attack_mod(atk[i].str, atk[i].skill, atk[i].wmag);
printf(" Attacker: %-28s => attack_mod %+d (STRmod %+d, prof %+d, wm %+d)\n",
atk[i].name, atk_mod, str_mod, prof_from_skill(atk[i].skill), MIN(atk[i].wmag, MAX_WEAPON_MAGIC));
atk[i].name, atk_mod, str_mod, GET_PROFICIENCY(atk[i].skill), MIN(atk[i].wmag, MAX_WEAPON_MAGIC));
for (size_t j=0;j<sizeof(def)/sizeof(def[0]);++j) {
int ac = compute_ascending_ac(def[j].def);
double p = hit_rate(atk_mod, ac, 20000);

View file

@ -1,13 +1,4 @@
/* tests_5e.c — unit tests for 5e-like rules
*
* Build suggestion (see Makefile note below):
* cc -g -O2 -Wall -Wextra -o tests_5e \
* tests_5e.c utils.o constants.o handler.o db.o random.o \
* (plus whatever .o your build needs for GET_EQ/GET_SKILL, etc.)
*
* If you keep it simple and link against your normal object files,
* memset(0) on char_data is enough for GET_SKILL == 0, etc.
*/
/* tests_5e.c — unit tests for 5e-like rules */
#include <math.h>
#include "conf.h"
@ -53,9 +44,8 @@ static void equip_at(struct char_data *ch, int wear_pos, struct obj_data *o) {
ch->equipment[wear_pos] = o;
}
/* Set an ability score (helpers for readability) — adjust if your tree uses different fields. */
/* Set an ability score (helpers for readability) */
static void set_ability_scores(struct char_data *ch, int str, int dex, int con, int intel, int wis, int cha) {
/* real_abils is standard in Circle; if your tree differs, tweak these lines */
ch->real_abils.str = str;
ch->real_abils.dex = dex;
ch->real_abils.con = con;
@ -81,29 +71,29 @@ static double simulate_hit_rate(int attack_mod, int target_ac, int trials) {
/* Dump a breakdown (useful when a test fails) */
static void dbg_dump_ac(const char *label, struct ac_breakdown *b) {
fprintf(stderr, "%s: total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%d shield=%d situ=%d bulk=%d)\n",
fprintf(stderr, "%s: total=%d (base=%d armor=%d magic=%d dexCap=%d dex=%d situ=%d bulk=%d)\n",
label, b->total, b->base, b->armor_piece_sum, b->armor_magic_sum,
b->dex_cap, b->dex_mod_applied, b->shield_bonus, b->situational, b->total_bulk);
b->dex_cap, b->dex_mod_applied, b->situational, b->total_bulk);
}
/* ---------- TESTS ---------- */
static void test_ability_mod(void) {
static void test_GET_ABILITY_MOD(void) {
/* Spot-check classic 5e values and a sweep */
T_EQI(ability_mod(10), 0, "ability_mod(10)");
T_EQI(ability_mod(8), -1, "ability_mod(8)");
T_EQI(ability_mod(12), 1, "ability_mod(12)");
T_EQI(ability_mod(18), 4, "ability_mod(18)");
T_EQI(ability_mod(1), -5, "ability_mod(1)");
T_EQI(GET_ABILITY_MOD(10), 0, "GET_ABILITY_MOD(10)");
T_EQI(GET_ABILITY_MOD(8), -1, "GET_ABILITY_MOD(8)");
T_EQI(GET_ABILITY_MOD(12), 1, "GET_ABILITY_MOD(12)");
T_EQI(GET_ABILITY_MOD(18), 4, "GET_ABILITY_MOD(18)");
T_EQI(GET_ABILITY_MOD(1), -5, "GET_ABILITY_MOD(1)");
/* sweep 1..30 vs floor((s-10)/2) */
for (int s = 1; s <= 30; ++s) {
int expect = (int)floor((s - 10) / 2.0);
T_EQI(ability_mod(s), expect, "ability_mod sweep");
T_EQI(GET_ABILITY_MOD(s), expect, "GET_ABILITY_MOD sweep");
}
}
static void test_prof_from_skill(void) {
/* Boundaries for your <= mapping: 0..14→0, 15..29→+1, ... 91..100→+6 */
static void test_GET_PROFICIENCY(void) {
/* Boundaries for <= mapping: 0..14→0, 15..29→+1, ... 91..100→+6 */
struct { int pct, expect; const char *lbl; } cases[] = {
{ 0, 0, "0→0"}, {14,0,"14→0"},
{15,1,"15→1"}, {29,1,"29→1"},
@ -114,7 +104,7 @@ static void test_prof_from_skill(void) {
{91,6,"91→6"}, {100,6,"100→6"}
};
for (size_t i=0;i<sizeof(cases)/sizeof(cases[0]);++i) {
T_EQI(prof_from_skill(cases[i].pct), cases[i].expect, cases[i].lbl);
T_EQI(GET_PROFICIENCY(cases[i].pct), cases[i].expect, cases[i].lbl);
}
}
@ -124,89 +114,97 @@ static void test_ac_light_medium_heavy(void) {
memset(&ch, 0, sizeof(ch));
/* LIGHT SETUP:
* Bulk target: Light (<=5).
* Use head bulk 1 (wt 1 => +1), body bulk 1 (wt 3 => +3), total 4 => Light.
* Dex 18 => +4 fully applied.
* Armor piece AC: head=1, body=2 => sum = 3.
* Magic: head+1, body+2 => per-slot ok, total 3 (at global cap).
* Bulk target: Light (<=5)
* Uses:
* Armor AC: head=1, body=1, total=2
* Bulk: head=1, body=1, total=2
* Magic: total=0
* Dex +4
* No shield.
* Expect: base 10 + piece 3 + magic 3 + Dex 4 = 20.
* Expect: base 10 + piece 2 + magic 0 + dex 4 = 16
*/
set_ability_scores(&ch, 10, 18, 10, 10, 10, 10);
equip_at(&ch, WEAR_HEAD, make_armor(1, 1, 1, 0));
equip_at(&ch, WEAR_BODY, make_armor(2, 1, 2, 0));
equip_at(&ch, WEAR_HEAD, make_armor(1,1,0,0));
equip_at(&ch, WEAR_BODY, make_armor(1,1,0,0));
equip_at(&ch, WEAR_LEGS, make_armor(1,2,0,0));
equip_at(&ch, WEAR_FEET, make_armor(1,1,0,0));
struct ac_breakdown b1; compute_ac_breakdown(&ch, &b1);
/* Sanity checks */
if (b1.total != 20) dbg_dump_ac("LIGHT", &b1);
if (b1.total != 18) dbg_dump_ac("LIGHT", &b1);
T_EQI(b1.dex_cap, 5, "Light dex cap 5");
T_EQI(b1.dex_mod_applied, 4, "Light dex +4 applied");
T_EQI(b1.armor_magic_sum, 3, "Light magic cap (global) 3");
T_EQI(b1.total, 20, "Light total AC");
T_EQI(b1.armor_magic_sum, 0, "Light magic cap (global) 3");
T_EQI(b1.total, 18, "Light total AC");
/* MEDIUM SETUP:
* Clear equipment and rebuild:
* Bulk target: Medium (6..10).
* Use legs bulk 2 (wt 2 => +4), hands bulk 1 (wt 1 => +1), feet bulk 1 (wt 1 => +1), total 6 => Medium.
* Dex 18 => +4, but cap at +2.
* Armor piece AC: legs=2, hands=1, feet=1 => sum = 4.
* Magic: legs +2 only (still under global cap).
* Expect: base 10 + piece 4 + magic 2 + Dex 2 = 18.
* Bulk target: Medium (6..10)
* Uses:
* Armor AC: legs=2, hands=1, feet=1, total=4
* Bulk: legs=2, hands=2, feet=2, total=6
* Magic: legs=1, hands=1, total=2
* Dex +4, but cap at +2
* Expect: base 10 + piece 4 + magic 2 + dex 2 = 18.
*/
memset(ch.equipment, 0, sizeof(ch.equipment));
equip_at(&ch, WEAR_LEGS, make_armor(2, 2, 2, 0)); /* wt 2 -> bulk 4 */
equip_at(&ch, WEAR_HANDS, make_armor(1, 1, 0, 0)); /* wt 1 -> bulk 1 */
equip_at(&ch, WEAR_FEET, make_armor(1, 1, 0, 0)); /* wt 1 -> bulk 1 */
equip_at(&ch, WEAR_HEAD, make_armor(2,1,0,0));
equip_at(&ch, WEAR_BODY, make_armor(2,2,1,0));
equip_at(&ch, WEAR_LEGS, make_armor(2,2,0,0));
equip_at(&ch, WEAR_HANDS, make_armor(1,1,0,0));
equip_at(&ch, WEAR_FEET, make_armor(1,1,0,0));
struct ac_breakdown b2; compute_ac_breakdown(&ch, &b2);
if (b2.total != 18) dbg_dump_ac("MEDIUM", &b2);
if (b2.total != 21) dbg_dump_ac("MEDIUM", &b2);
T_EQI(b2.dex_cap, 2, "Medium dex cap 2");
T_EQI(b2.dex_mod_applied, 2, "Medium dex +2 applied");
T_EQI(b2.total_bulk, 6, "Medium bulk score 6");
T_EQI(b2.total, 18, "Medium total AC");
T_EQI(b2.total_bulk, 7, "Medium bulk score 7");
T_EQI(b2.total, 21, "Medium total AC");
/* HEAVY SETUP:
* Bulk target: Heavy (>=11).
* Use body bulk 3 (wt 3 => +9), legs bulk 2 (wt 2 => +4), total 13 => Heavy.
* Dex 18 => +4 but cap 0.
* Armor piece AC: body=3, legs=2 => sum = 5.
* Magic: body +3 (per-slot allows up to 3), legs +3 (per-slot 1 -> runtime clamps to 1), global cap 3 -> total 3.
* Shield: base 2 + magic +5 (clamped to +3) + prof 0 => +5 total.
* Expect: base 10 + piece 5 + armorMagic 3 + Dex 0 + shield 5 = 23.
* Bulk target: Heavy (>=11)
* Uses:
* Armor AC: body=3, legs=2. total=5
* Bulk: body=3, legs=2, total=13
* Magic: body=3, legs=3, total=6 (max cap of 3, so total=3)
* Dex +4 but cap 0 due to bulk
* Shield: base 2 + magic +5 (clamped to +3) + prof 0 => +5 total
* Expect: base 10 + piece 5 + armorMagic 3 + Dex 0 + shield 5 = 23
*/
memset(ch.equipment, 0, sizeof(ch.equipment));
equip_at(&ch, WEAR_BODY, make_armor(3, 3, 3, 0)); /* bulk 3*wt3 => 9 */
equip_at(&ch, WEAR_LEGS, make_armor(2, 2, 3, 0)); /* magic will clamp via slot+global; bulk 2*wt2 => 4 */
/* Shield: test magic cap on shield and zero prof */
struct obj_data *shield = make_armor(0, 0, 5, 0);
equip_at(&ch, WEAR_SHIELD, shield);
equip_at(&ch, WEAR_HEAD, make_armor(2,1,1,0));
equip_at(&ch, WEAR_BODY, make_armor(3,3,1,0));
equip_at(&ch, WEAR_LEGS, make_armor(2,1,1,0));
equip_at(&ch, WEAR_ARMS, make_armor(1,1,0,0));
equip_at(&ch, WEAR_HANDS, make_armor(1,1,0,0));
equip_at(&ch, WEAR_FEET, make_armor(1,1,0,0));
equip_at(&ch, WEAR_WRIST_L, make_armor(1,1,0,0));
equip_at(&ch, WEAR_WRIST_R, make_armor(1,1,0,0));
struct ac_breakdown b3; compute_ac_breakdown(&ch, &b3);
if (b3.total != 23) dbg_dump_ac("HEAVY", &b3);
if (b3.total != 25) dbg_dump_ac("HEAVY", &b3);
T_EQI(b3.dex_cap, 0, "Heavy dex cap 0");
T_EQI(b3.dex_mod_applied, 0, "Heavy dex applied 0");
T_EQI(b3.total_bulk, 13, "Heavy bulk score 13");
T_EQI(b3.armor_piece_sum, 5, "Heavy piece sum 5");
T_EQI(b3.total_bulk, 10, "Heavy bulk score 10");
T_EQI(b3.armor_piece_sum, 12, "Heavy piece sum 12");
T_EQI(b3.armor_magic_sum, 3, "Heavy armor magic at global cap 3");
T_EQI(b3.shield_bonus, 5, "Shield: base 2 + magic 3 (cap) + prof 0 = 5");
T_EQI(b3.total, 23, "Heavy total AC");
T_EQI(b3.total, 25, "Heavy total AC");
}
static void test_hit_probability_sanity(void) {
/* Sanity envelope checks (Monte Carlo with seed) */
srand(42);
circle_srandom(42);
/* Even-ish fight: attack_mod = +5 vs AC 16 => expect about 5565% */
double p1 = simulate_hit_rate(/*atk*/5, /*AC*/16, 200000);
T_IN_RANGE(p1, 0.55, 0.65, "Hit rate ~60% (atk+5 vs AC16)");
T_IN_RANGE(p1, 0.45, 0.55, "Hit rate ~50% (atk+5 vs AC16)");
/* Slightly behind: atk +3 vs AC 17 => expect about 3550% */
double p2 = simulate_hit_rate(3, 17, 200000);
T_IN_RANGE(p2, 0.35, 0.50, "Hit rate ~40% (atk+3 vs AC17)");
T_IN_RANGE(p2, 0.30, 0.40, "Hit rate ~35% (atk+3 vs AC17)");
/* Way ahead: atk +8 vs AC 14 => expect ≳80% but < 95% (nat1 auto-miss) */
double p3 = simulate_hit_rate(8, 14, 200000);
T_IN_RANGE(p3, 0.80, 0.95, "Hit rate high (atk+8 vs AC14)");
T_IN_RANGE(p3, 0.75, 0.85, "Hit rate high (atk+8 vs AC14)");
/* Way behind: atk +0 vs AC 20 => expect ≲15% but > 5% (nat20 auto-hit) */
double p4 = simulate_hit_rate(0, 20, 200000);
@ -214,8 +212,8 @@ static void test_hit_probability_sanity(void) {
}
int main(void) {
test_ability_mod();
test_prof_from_skill();
test_GET_ABILITY_MOD();
test_GET_PROFICIENCY();
test_ac_light_medium_heavy();
test_hit_probability_sanity();

View file

@ -1589,7 +1589,7 @@ bool percent_success_disadv(int chance_pct) {
static int dex_cap_from_bulk(int total_bulk) {
if (total_bulk <= 5) /* Light */
return 5;
else if (total_bulk <= 10) /* Medium */
else if (total_bulk <= 9) /* Medium */
return 2;
else /* Heavy */
return 0;
@ -1631,7 +1631,7 @@ bool has_stealth_disadv(struct char_data *ch) {
}
/* Returns the 5e-style ability modifier for a given ability score. */
int ability_mod(int score) {
int GET_ABILITY_MOD(int score) {
int mod = (score - 10) / 2;
if ((score - 10) < 0 && ((score - 10) % 2 != 0))
mod -= 1; /* adjust for C truncation toward zero */
@ -1639,7 +1639,7 @@ int ability_mod(int score) {
}
/* Converts a skill percentage (0-100) into a 5e-like proficiency bonus. */
int prof_from_skill(int pct) {
int GET_PROFICIENCY(int pct) {
if (pct <= 14) return 0;
if (pct <= 29) return 1;
if (pct <= 44) return 2;
@ -1660,7 +1660,7 @@ void compute_ac_breakdown(struct char_data *ch, struct ac_breakdown *out)
memset(out, 0, sizeof(*out));
out->base = 10;
/* Armor pieces: head/body/legs/arms/hands/feet (no shield here) */
/* Armor pieces: head/body/legs/arms/hands/feet */
for (int i = 0; i < NUM_ARMOR_SLOTS; i++) {
int wear_pos = ARMOR_WEAR_POSITIONS[i];
struct obj_data *obj = GET_EQ(ch, wear_pos);
@ -1684,35 +1684,21 @@ void compute_ac_breakdown(struct char_data *ch, struct ac_breakdown *out)
/* bulk contribution */
int piece_bulk = GET_OBJ_VAL(obj, VAL_ARMOR_BULK);
if (piece_bulk < 0) piece_bulk = 0;
out->total_bulk += piece_bulk * armor_slots[i].bulk_weight;
out->total_bulk += piece_bulk;
}
/* global armor magic cap (armor only; shield handled separately) */
/* global armor magic cap (armor only) */
if (total_magic > MAX_TOTAL_ARMOR_MAGIC)
total_magic = MAX_TOTAL_ARMOR_MAGIC;
out->armor_magic_sum = total_magic;
/* Dex cap from bulk and applied dex mod */
{
int dexmod = ability_mod(GET_DEX(ch));
int dexmod = GET_ABILITY_MOD(GET_DEX(ch));
out->dex_cap = dex_cap_from_bulk(out->total_bulk); /* Light<=5:5 / <=10:2 / else:0 */
out->dex_mod_applied = (dexmod > out->dex_cap) ? out->dex_cap : dexmod;
}
/* Shield: base +2, magic (capped), +Shield Use proficiency */
{
struct obj_data *shield = GET_EQ(ch, WEAR_SHIELD);
if (shield && GET_OBJ_TYPE(shield) == ITEM_ARMOR) {
int shield_bonus = 2;
int shield_magic = GET_OBJ_VAL(shield, VAL_ARMOR_MAGIC_BONUS);
if (shield_magic < 0) shield_magic = 0;
if (shield_magic > MAX_SHIELD_MAGIC) shield_magic = MAX_SHIELD_MAGIC;
shield_bonus += shield_magic;
shield_bonus += prof_from_skill(GET_SKILL(ch, SKILL_SHIELD_USE));
out->shield_bonus = shield_bonus;
}
}
/* Situational */
out->situational = situational_ac_mods(ch);
@ -1721,7 +1707,6 @@ void compute_ac_breakdown(struct char_data *ch, struct ac_breakdown *out)
+ out->armor_piece_sum
+ out->armor_magic_sum
+ out->dex_mod_applied
+ out->shield_bonus
+ out->situational;
}

View file

@ -78,25 +78,21 @@ void remove_from_string(char *string, const char *to_remove);
/* --- Ascending AC breakdown --- */
struct ac_breakdown {
int base; /* always 10 */
int armor_piece_sum; /* sum of clamped per-piece AC (no shield) */
int armor_piece_sum; /* sum of clamped per-piece AC */
int armor_magic_sum; /* sum of clamped per-piece magic (capped globally) */
int total_bulk; /* sum of bulk * weight across armor pieces */
int dex_cap; /* cap derived from bulk (Light 5 / Med 2 / Heavy 0) */
int dex_mod_applied; /* min(DEX_mod, dex_cap) */
int shield_bonus; /* base 2 + magic (cap) + Shield Use proficiency */
int situational; /* cover, spells, etc. */
int total; /* final AC */
};
int ability_mod(int score);
int prof_from_skill(int pct);
int ability_mod(int score);
int prof_from_skill(int pct);
int GET_ABILITY_MOD(int score);
int GET_PROFICIENCY(int pct);
int compute_ascending_ac(struct char_data *ch);
int situational_ac_mods(struct char_data *ch);
int compute_armor_class_asc(struct char_data *ch);
void compute_ac_breakdown(struct char_data *ch, struct ac_breakdown *out);
int compute_ascending_ac(struct char_data *ch); /* still available */
/* Advantage/Disadvantage helpers */
int roll_d20(void);