mirror of
https://github.com/tbamud/tbamud.git
synced 2026-03-22 04:06:33 +01:00
Fix unit tests and update some function names
This commit is contained in:
parent
15736e523a
commit
e40f236867
8 changed files with 141 additions and 156 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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] */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 55–65% */
|
||||
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 35–50% */
|
||||
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();
|
||||
|
||||
|
|
|
|||
29
src/utils.c
29
src/utils.c
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
10
src/utils.h
10
src/utils.h
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue