D&D (2024) The math of the GWF Fighting Style and why its as good as a +1 (and possibly better than defense)

FrogReaver

As long as i get to be the frog
OK, fine. How about this:

I made a simulation that takes two fighters, both level 20. They are fighting an enemy. We assume the fighters have a 65% chance to hit the enemy. One fighter deals 2d6 + 6 damage to represent the fighter with GWF and the other does 2d6+5 as a defense-fighter. The enemy has a 65% chance to hit the first fighter while they have a 60% chance to hit the second fighter.

The fighters are purely going to attack 4 times each before the enemy gets a chance to attack and deal anywhere from 30 - 70 damage. This approximates the damage that each fighter could be expected to take at level 20 in a single turn, its somewhat low because of situations where the enemy decides to do less damage but AoE, the enemy attacks somewhat else, the enemy is stunned, the fighter gets healed, etc.

The fighter's hp is set to 224. The enemy's HP is randomly chosen between 1 and 169 to represent uncertainty of their health from mooks to bosses (169 is the tarrasque's health divided by 4, to represent each other player pulling their weight).

The simulation outputs the number of times the fighter dies and the average number of turns each fighter needs to defeat the enemy per combat when they don't die.

I iterated this simulation 10,000 times. The output I got was:

Number of deaths for Fighter 1 (2d6+6 damage, 65% hit probability): 5616
Number of deaths for Fighter 2 (2d6+5 damage, 60% hit probability): 5475
Average turns for Fighter 1 (2d6+6 damage, 65% hit probability): 4.76
Average turns for Fighter 2 (2d6+5 damage, 60% hit probability): 5.15

The difference between the fighter's death are 141 deaths. Out of the 10,000 enemies, the +1 AC bonus mattered for 1.41% of them. In contrast, the +1 damage reduces the average turns needed to kill by 0.39, meaning that after 3 combats, you killed an enemy quicker than you would have without GWF.

I don't know about you, but I don't think I'll make it to 10,000 enemies in a campaign, and if I do, I don't think it wise to take the extra 1% extra death insurance rather than saving 333 rounds against an enemy. If not for survival purposes, it end the combat quicker.

If you have a problem with my assumptions and think that your assumptions would be more realistic, be my guest.

C:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NUM_SIMULATIONS 10000
#define HP 224

// Function to simulate rolling two dice and adding the damage with a bonus
int roll_damage(int bonus) {
    int roll1 = rand() % 6 + 1;
    int roll2 = rand() % 6 + 1;
    return roll1 + roll2 + bonus;
}

// Function to simulate the probability of getting hit
int is_hit(double probability) {
    return (rand() % 100) < (probability * 100);
}

// Function to simulate the number of attacks before getting hit
int attacks_before_hit(double attack_probability) {
    int attacks = 0;
    while (attacks < 4 && is_hit(attack_probability)) {
        attacks++;
    }
    return attacks;
}

// Function to simulate a single fighter's attempt to reach the target damage threshold
int simulate_fighter(int target_damage, int damage_bonus, double hit_probability) {
    int current_damage = 0;
    int turns = 0;
    int hp = HP;

    while (current_damage < target_damage && hp > 0) {
        // Determine the number of attacks before the fighter is hit
        int num_attacks = attacks_before_hit(0.65);  // Both fighters attack with 65% probability

        // Apply the attacks
        for (int i = 0; i < num_attacks; i++) {
            // Roll damage
            int damage = roll_damage(damage_bonus);
            current_damage += damage;
            turns++;

            // Check if the fighter gets hit after each attack
            if (is_hit(hit_probability)) {
                int attack_damage = rand() % 41 + 30;  // Attack damage from 30 to 70
                hp -= attack_damage;
                if (hp <= 0) {
                    return -1;  // Fighter lost all HP before reaching the target
                }
            }
        }
    }

    if (hp > 0) {
        return turns;  // Return the number of turns required if the fighter survived
    } else {
        return -1;  // Return -1 if the fighter died
    }
}

int main() {
    srand(time(NULL));  // Seed the random number generator

    int deaths_fighter1 = 0;
    int deaths_fighter2 = 0;
    double total_turns_fighter1 = 0;
    double total_turns_fighter2 = 0;
    int survivals_fighter1 = 0;
    int survivals_fighter2 = 0;

    for (int i = 0; i < NUM_SIMULATIONS; i++) {
        int target_damage = rand() % 169 + 1;  // Random target damage between 1 and 169

        // Simulate Fighter 1
        int result1 = simulate_fighter(target_damage, 6, 0.65);
        if (result1 == -1) {
            deaths_fighter1++;
        } else {
            total_turns_fighter1 += result1;
            survivals_fighter1++;
        }

        // Simulate Fighter 2
        int result2 = simulate_fighter(target_damage, 5, 0.60);  // Updated hit probability to 60%
        if (result2 == -1) {
            deaths_fighter2++;
        } else {
            total_turns_fighter2 += result2;
            survivals_fighter2++;
        }
    }

    // Calculate average turns for each fighter
    double avg_turns_fighter1 = survivals_fighter1 > 0 ? total_turns_fighter1 / survivals_fighter1 : 0;
    double avg_turns_fighter2 = survivals_fighter2 > 0 ? total_turns_fighter2 / survivals_fighter2 : 0;

    // Print the results
    printf("Number of deaths for Fighter 1 (2d6+6 damage, 65%% hit probability): %d\n", deaths_fighter1);
    printf("Number of deaths for Fighter 2 (2d6+5 damage, 60%% hit probability): %d\n", deaths_fighter2);
    printf("Average turns for Fighter 1 (2d6+6 damage, 65%% hit probability): %.2f\n", avg_turns_fighter1);
    printf("Average turns for Fighter 2 (2d6+5 damage, 60%% hit probability): %.2f\n", avg_turns_fighter2);

    return 0;
}

And if my code looks kinda like spaghetti, it is. I wanted to write in python, but it would take far too long for the code to run, so its in C which I barely know how to code. I got assistance from ChatGPT, so also feel free to scrutinize the code itself, but it seems passable from my review.

And yeah, I know, whiteboard analysis, but I think the results are dramatic enough and the simulation takes enough cases into account that we can at least use it as a rough guide to our thinking.

Edit: 1.41% not .014% and 10,000 instead of 1,000
I don’t recall for sure, but don’t you need to seed the Randomizer in C with something (perhaps current time?). Nevermind, found it in main, which is where it should be :)

Doesn’t your attacks before hit function always return 4 attacks?
 
Last edited:

log in or register to remove this ad

Asisreo

Patron Badass
I don’t recall for sure, but don’t you need to seed the Randomizer in C with something (perhaps current time?). Nevermind, found it in main, which is where it should be :)

Doesn’t your attacks before hit function always return 4 attacks?
You're partially correct, good catch. Its actually having the enemy counter attack each time the fighter attacks and if the enemy succeeds, their attack interrupts the fighter's turn, which obviously isn't how normal combat works.

C:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NUM_SIMULATIONS 10000
#define HP 224

// Function to simulate rolling two dice and adding the damage with a bonus
int roll_damage(int bonus) {
    int roll1 = rand() % 6 + 1;
    int roll2 = rand() % 6 + 1;
    return roll1 + roll2 + bonus;
}

// Function to simulate the probability of getting hit
int is_hit(double probability) {
    return (rand() % 100) < (probability * 100);
}

// Function to simulate a single fighter's attempt to reach the target damage threshold
int simulate_fighter(int target_damage, int damage_bonus, double hit_probability) {
    int current_damage = 0;
    int turns = 0;
    int hp = HP;

    while (current_damage < target_damage && hp > 0) {
        // Fighter attacks 4 times before the enemy attempts to hit them
        for (int i = 0; i < 4; i++) {
            // Roll damage
            int damage = roll_damage(damage_bonus);
            current_damage += damage;
        }

        // After 4 attacks, the enemy tries to hit the fighter
        if (is_hit(hit_probability)) {
            int attack_damage = rand() % 41 + 30;  // Attack damage from 30 to 70
            hp -= attack_damage;
            if (hp <= 0) {
                return -1;  // Fighter lost all HP before reaching the target
            }
        }

        // Increment the turn count after 4 attacks and one retaliation attempt
        turns++;
    }

    if (hp > 0) {
        return turns;  // Return the number of turns required if the fighter survived
    } else {
        return -1;  // Return -1 if the fighter died
    }
}

int main() {
    srand(time(NULL));  // Seed the random number generator

    int deaths_fighter1 = 0;
    int deaths_fighter2 = 0;
    double total_turns_fighter1 = 0;
    double total_turns_fighter2 = 0;
    int survivals_fighter1 = 0;
    int survivals_fighter2 = 0;

    for (int i = 0; i < NUM_SIMULATIONS; i++) {
        int target_damage = rand() % 169 + 1;  // Random target damage between 1 and 169

        // Simulate Fighter 1
        int result1 = simulate_fighter(target_damage, 6, 0.65);  // Fighter 1 with 65% chance to be hit
        if (result1 == -1) {
            deaths_fighter1++;
        } else {
            total_turns_fighter1 += result1;
            survivals_fighter1++;
        }

        // Simulate Fighter 2
        int result2 = simulate_fighter(target_damage, 5, 0.60);  // Fighter 2 with 60% chance to be hit
        if (result2 == -1) {
            deaths_fighter2++;
        } else {
            total_turns_fighter2 += result2;
            survivals_fighter2++;
        }
    }

    // Calculate average turns for each fighter
    double avg_turns_fighter1 = survivals_fighter1 > 0 ? total_turns_fighter1 / survivals_fighter1 : 0;
    double avg_turns_fighter2 = survivals_fighter2 > 0 ? total_turns_fighter2 / survivals_fighter2 : 0;

    // Print the results
    printf("Number of deaths for Fighter 1 (2d6+6 damage, 65%% hit probability): %d\n", deaths_fighter1);
    printf("Number of deaths for Fighter 2 (2d6+5 damage, 60%% hit probability): %d\n", deaths_fighter2);
    printf("Average turns for Fighter 1 (2d6+6 damage, 65%% hit probability): %.2f\n", avg_turns_fighter1);
    printf("Average turns for Fighter 2 (2d6+5 damage, 60%% hit probability): %.2f\n", avg_turns_fighter2);

    return 0;
}

This is the updated code, the results I get are:
Code:
Number of deaths for Fighter 1 (2d6+6 damage, 65% hit probability): 23

Number of deaths for Fighter 2 (2d6+5 damage, 60% hit probability): 31
Average turns for Fighter 1 (2d6+6 damage, 65% hit probability): 2.15
Average turns for Fighter 2 (2d6+5 damage, 60% hit probability): 2.30

Actually, with these adjustments, Fighter 1 is more likely to survive and they slightly need less turns to beat a number of combat encounter.

So GWF is purely in favor of defense. Its not by much, but its still saves a respectable 1 turn out of every 6 2/3 combats.

Edit: Accidentally kept the "Code Execution Successful" line on output in the normal comment area. BEEP BOOP.

Also, here is an online IDE for C that you can use to run the code if you're on mobile or don't have a way to execute the code natively:

 
Last edited:

Stalker0

Legend
OK, fine. How about this:

I made a simulation that takes two fighters, both level 20. They are fighting an enemy. We assume the fighters have a 65% chance to hit the enemy. One fighter deals 2d6 + 6 damage to represent the fighter with GWF and the other does 2d6+5 as a defense-fighter. The enemy has a 65% chance to hit the first fighter while they have a 60% chance to hit the second fighter.

The fighters are purely going to attack 4 times each before the enemy gets a chance to attack and deal anywhere from 30 - 70 damage. This approximates the damage that each fighter could be expected to take at level 20 in a single turn, its somewhat low because of situations where the enemy decides to do less damage but AoE, the enemy attacks somewhat else, the enemy is stunned, the fighter gets healed, etc.

The fighter's hp is set to 224. The enemy's HP is randomly chosen between 1 and 169 to represent uncertainty of their health from mooks to bosses (169 is the tarrasque's health divided by 4, to represent each other player pulling their weight).

The simulation outputs the number of times the fighter dies and the average number of turns each fighter needs to defeat the enemy per combat when they don't die.

I iterated this simulation 10,000 times. The output I got was:

Number of deaths for Fighter 1 (2d6+6 damage, 65% hit probability): 5616
Number of deaths for Fighter 2 (2d6+5 damage, 60% hit probability): 5475
Average turns for Fighter 1 (2d6+6 damage, 65% hit probability): 4.76
Average turns for Fighter 2 (2d6+5 damage, 60% hit probability): 5.15

The difference between the fighter's death are 141 deaths. Out of the 10,000 enemies, the +1 AC bonus mattered for 1.41% of them. In contrast, the +1 damage reduces the average turns needed to kill by 0.39, meaning that after 3 combats, you killed an enemy quicker than you would have without GWF.

I don't know about you, but I don't think I'll make it to 10,000 enemies in a campaign, and if I do, I don't think it wise to take the extra 1% extra death insurance rather than saving 333 rounds against an enemy. If not for survival purposes, it end the combat quicker.

If you have a problem with my assumptions and think that your assumptions would be more realistic, be my guest.

C:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NUM_SIMULATIONS 10000
#define HP 224

// Function to simulate rolling two dice and adding the damage with a bonus
int roll_damage(int bonus) {
    int roll1 = rand() % 6 + 1;
    int roll2 = rand() % 6 + 1;
    return roll1 + roll2 + bonus;
}

// Function to simulate the probability of getting hit
int is_hit(double probability) {
    return (rand() % 100) < (probability * 100);
}

// Function to simulate the number of attacks before getting hit
int attacks_before_hit(double attack_probability) {
    int attacks = 0;
    while (attacks < 4 && is_hit(attack_probability)) {
        attacks++;
    }
    return attacks;
}

// Function to simulate a single fighter's attempt to reach the target damage threshold
int simulate_fighter(int target_damage, int damage_bonus, double hit_probability) {
    int current_damage = 0;
    int turns = 0;
    int hp = HP;

    while (current_damage < target_damage && hp > 0) {
        // Determine the number of attacks before the fighter is hit
        int num_attacks = attacks_before_hit(0.65);  // Both fighters attack with 65% probability

        // Apply the attacks
        for (int i = 0; i < num_attacks; i++) {
            // Roll damage
            int damage = roll_damage(damage_bonus);
            current_damage += damage;
            turns++;

            // Check if the fighter gets hit after each attack
            if (is_hit(hit_probability)) {
                int attack_damage = rand() % 41 + 30;  // Attack damage from 30 to 70
                hp -= attack_damage;
                if (hp <= 0) {
                    return -1;  // Fighter lost all HP before reaching the target
                }
            }
        }
    }

    if (hp > 0) {
        return turns;  // Return the number of turns required if the fighter survived
    } else {
        return -1;  // Return -1 if the fighter died
    }
}

int main() {
    srand(time(NULL));  // Seed the random number generator

    int deaths_fighter1 = 0;
    int deaths_fighter2 = 0;
    double total_turns_fighter1 = 0;
    double total_turns_fighter2 = 0;
    int survivals_fighter1 = 0;
    int survivals_fighter2 = 0;

    for (int i = 0; i < NUM_SIMULATIONS; i++) {
        int target_damage = rand() % 169 + 1;  // Random target damage between 1 and 169

        // Simulate Fighter 1
        int result1 = simulate_fighter(target_damage, 6, 0.65);
        if (result1 == -1) {
            deaths_fighter1++;
        } else {
            total_turns_fighter1 += result1;
            survivals_fighter1++;
        }

        // Simulate Fighter 2
        int result2 = simulate_fighter(target_damage, 5, 0.60);  // Updated hit probability to 60%
        if (result2 == -1) {
            deaths_fighter2++;
        } else {
            total_turns_fighter2 += result2;
            survivals_fighter2++;
        }
    }

    // Calculate average turns for each fighter
    double avg_turns_fighter1 = survivals_fighter1 > 0 ? total_turns_fighter1 / survivals_fighter1 : 0;
    double avg_turns_fighter2 = survivals_fighter2 > 0 ? total_turns_fighter2 / survivals_fighter2 : 0;

    // Print the results
    printf("Number of deaths for Fighter 1 (2d6+6 damage, 65%% hit probability): %d\n", deaths_fighter1);
    printf("Number of deaths for Fighter 2 (2d6+5 damage, 60%% hit probability): %d\n", deaths_fighter2);
    printf("Average turns for Fighter 1 (2d6+6 damage, 65%% hit probability): %.2f\n", avg_turns_fighter1);
    printf("Average turns for Fighter 2 (2d6+5 damage, 60%% hit probability): %.2f\n", avg_turns_fighter2);

    return 0;
}

And if my code looks kinda like spaghetti, it is. I wanted to write in python, but it would take far too long for the code to run, so its in C which I barely know how to code. I got assistance from ChatGPT, so also feel free to scrutinize the code itself, but it seems passable from my review.

And yeah, I know, whiteboard analysis, but I think the results are dramatic enough and the simulation takes enough cases into account that we can at least use it as a rough guide to our thinking.

Edit: 1.41% not .014% and 10,000 instead of 1,000
if you are doing simulation, I would model the GW ability straight up, rather than just assuming a +1 damage.

also considering the wide range of variation you are using "like the randomness of the hp you mentioned", 10000 runs isn't enough. 100,000 at minimum, honestly 1 million would probably be the best to really get the data down
 

Asisreo

Patron Badass
if you are doing simulation, I would model the GW ability straight up, rather than just assuming a +1 damage.

also considering the wide range of variation you are using "like the randomness of the hp you mentioned", 10000 runs isn't enough. 100,000 at minimum, honestly 1 million would probably be the best to really get the data down
Changing the roll value needlessly complicates the simulation and makes it take longer. We know that GWF is mathematically equivalent to a +1, so I wouldn't waste my time on it. Anyone else is free to do so.

As for running 1,000,000 simulations, the output I get is this:
Code:
Number of deaths for Fighter 1 (2d6+6 damage, 65% hit probability): 2345
Number of deaths for Fighter 2 (2d6+5 damage, 60% hit probability): 3250
Average turns for Fighter 1 (2d6+6 damage, 65% hit probability): 2.15
Average turns for Fighter 2 (2d6+5 damage, 60% hit probability): 2.29

Its practically the same data, with the same conclusions.
 

clearstream

(He, Him)
It’s very simple. (3+3+3+4+5+6)/3=4 and 4*2=8,
The maths in this thread is puzzling me. (3+3+3+4+5+6)/3 = 24/3 = 8, but why not (3+3+3+4+5+6)/6 = 4?

so the expected damage per attack on a greatsword with Great Weapon Fighting is 8. Compared to the 7 without Great Weapon Fighting. So, +1 expected damage, compared to the +2 you get from Dueling or the +[Dex mod] or +[Str mod] from Two Weapon Fighting. Obviously, Great Weapon Fighting is the worst of these options by a wide margin.
At first glance, I feel like this feat would be best on weapons that are using a d4... say a 3d4 Scythe.

(3+3+3+4)/4 = 3.25 - 2.5 = 0.75 * 3 = +2.25

Unfortunate that there is no 3d4 Scythe... the similarly impossible to find 6d2 Drows Taerg clearly OP.
 

Xeviat

Dungeon Mistress, she/her
The maths in this thread is puzzling me. (3+3+3+4+5+6)/3 = 24/3 = 8, but why not (3+3+3+4+5+6)/6 = 4?


At first glance, I feel like this feat would be best on weapons that are using a d4... say a 3d4 Scythe.

(3+3+3+4)/4 = 3.25 - 2.5 = 0.75 * 3 = +2.25

Unfortunate that there is no 3d4 Scythe... the similarly impossible to find 6d2 Drows Taerg clearly OP.
Because it's 2d6. So it's ((3+3+3+4+5+6)/6)*2, or divide by 3.
 


Ashrym

Legend
let's say that both took blindfight FS for utility as both are pathetic little humans with no darkvision.
Ohh..... I can play this game! 🙃

Let's say...... The champion fighter picked up the light cantrip via origin feat or species and doesn't need blindfighting or darkvision........ :p
The maths in this thread is puzzling me. (3+3+3+4+5+6)/3 = 24/3 = 8, but why not (3+3+3+4+5+6)/6 = 4?
There aren't 1d6 greatweapons. The average damage on something like a great axe is worse of a bonus than it is on a great sword but the damage floor is still raised.

I'm in the camp where less of a damage bonus on great weapon fighting is fine because great weapons are the best damage. We don't need to make them more best. By keeping the damage bonus from great weapon fighting style lower than other styles we're making those other styles more competitive with big weapons.

I vaguely recall the playtesting before 5th published with discussions on how to make sword'n'board more viable and a better damage bonus in the fighting style was one of the things. shrugs
 

ezo

Get off my lawn!
EDIT: Updated after Frogreaver's post.

After running my simulation the overall results are presented below. These were direct head-to-head contests.

To give the base info: Level 20, 224 hp, STR +5, attack +11, plate armor

A. Dueling: shield (AC 20), damage 1d8+7
B. Defense A: shield (AC 21), damage 1d8+5
C. Defense B: (AC 19), damage 2d6+5
D: GWF: (AC 18), damage 2d6+5, minimum d6 rolls 3
E: TWF: (AC 18, no feats!), damage d8+5, d6+5 for bonus action attack

Winner is Dueling. It is the BEST and beats all the others when a shield is used. The +2 damage bump is nice and brings it closer to GWF, and so it barely beats out GWF and Defense B. Defense A is notably weaker, and TWF ended up last. TWF is really only viable if you have something extra that really counts on landing a hit.

Really, the over all difference in win % is negiligble, with the exception of the Defense A and TWF.

Final ranks:

1. Dueling
2. GWF
3. Defense B
4. Defense A
5. TWF
 
Last edited:

FrogReaver

As long as i get to be the frog
After running my simulation the overall results are presented below. These were direct head-to-head contests. To give the base info:

Level 20, 224 hp, STR +5, attack +11, plate armor

A. Dueling: shield (AC 20), damage 1d8+7
B. Defense A: shield (AC 21), damage 1d8+5
C. Defense B: (AC 19), damage 2d6+5
D: GWF: (AC 18), damage 2d6+5, minimum d6 rolls 3
E: TWF: (AC 18, no feats!), damage d8+5, d6+5 for bonus action attack

Big shocker, but TWF trumps all. Of course, it uses the bonus action for a 5th attack every round, so hardly surprising.

Honorable mention is Dueling. Without TWF gaining that 5th attack, it is the BEST and beats all the others when a shield is used. The +2 damage bump is nice, but it barely beats out Defense B and GWF.

Really, the over all difference in win % is negiligble in most cases other than those noted above, with the exception of the clear loser: Defense A. Defense A loses by nearly a 1:2 ratio to all the other styles tested. Although the AC bump with the shield is nice, the lower damage output just can't keep.

Defense B is a better option, and is only slightly edged out by GWF.

Final ranks:

1. TWF
2. Dueling
3. GWF
4. Defense B
5. Defense A

Summary: TWF with the additional bonus action attack is clearly the best without other considerations (feats, magic items, etc.) which could favor other styles significantly. Defense A (classing sword/board with Defense style) is overwhelmingly the loser here. Between the other three, the order holds but the differences between each to the next is less than a 5% swing.

Althought we often think of AC as a big deal in 5E, the +1 bonus really just doesn't hold up compared to the other options tested, while gaining an additional attack every round via the bonus action just can't be beat. If you think your bonus action will often be used for other purposes, the Sword & Board dueler is the better bet (but barely...) over GWF or Defense B.
Something has to be wrong. Theres no way TWF with a total of 46.5 average damage before accuracy adjustment per round beats GWF with a total of 52 average damage before accuracy adjustment per round.
 

Split the Hoard


Split the Hoard
Negotiate, demand, or steal the loot you desire!

A competitive card game for 2-5 players
Remove ads

Top