Implementing proc chance

hi all,

guess I’m back to CS again, always with the aim to release a complicated, exhausting, and brain-damaging gamebook with the style of RPG.

now here is my question: how exactly proc chance works?

let’s say my character has a skill named “Counter” and it has 10% chance to proccing (trigger) right after an opponent successfully hit me.

how to differ 10%, 20%, 50%, 75% proc chance in computer’s math or at least, in human logic. all I know is 10% means in 10 rounds of battle, there should be 1 round where it triggered, 20% means 2 in 10 rounds, and so on.

Reason: I’m taking Life Of A Mercenary as reference for my first gamebook, and one of its drawback from my perspective is the lack of depth and variation in its combat, so that’s what I’m doing, improving combat scenes in text-based games.

any help will be appreciated, thank you :slightly_smiling_face:

1 Like

The short answer is that CS probably isn’t the best medium for games that regularly rely on luck/chance. that said, one way to do it is to generate a random number and check it against whatever your chance to proc is. so if you wanted to do a saving throw or whatever (i’m not a d&d guy):

*temp dieroll 0
*rand dieroll 1 100
*comment because we want 10% of outcomes, we check to see if the result is in the 91-100 range
*if (dieroll > 90)
    You hit the dragon with the fire sword. it says "Ow!"
*if (dieroll < 91)
    You swing your sword, but it glances off the dragon's scales.

It sounds like you’re also thinking about it in a sort of pseudo-random way that “feels” more fair, and if you wanted to implement it that way I’d say the best way to think about it is in “stacks”. Every time you Attack or whatever, increment one value and then once it hits 10 do the proc effect and reset that value to 0

2 Likes

One issue I’ve found with randomisation is that you have to preroll it. If you call the random number on the same screen you rolled it, going in and out of the stat screen will reroll the number.

7 Likes

Also if you are going to use *rand, you would have to tune it a bit to achieve the needed state.

Here is the example I tested a while ago: I used *rand 1 3 and cycled it through the rounds of 6/10/100/1000 rolls. According to the results, the chances of each 1/2/3 to appear get closer to 33,3333% only with 100 and 1000 rolls. With the lower number of rolls 1 had the highest chance, followed by 2. And 3 had the lowest chance. I tested it because in the game itself (where *rand 1 3 goes only through one roll) I noticed that I barely get the third option, and the test kind of proved it.

Test table

2 Likes

You could use the True Hit method that the Fire Emblem franchise has been using since its sixth game.

From the Fire Emblem Wiki about its RNG:

Since I had some free time, I made some code and tested it in CSIDE to compare these two methods. You can find the results here. The cells highlighted in light green shows which encounter has successfully triggered the skill.


Setting variables, proc rates and attack scene

Variables

Setting proc rate and attack scene

Fire Emblem Method

Things We Need

  • 1. Two randomly generated numbers
  • 2. The average of those aforementioned numbers
  • Code for Determining RNG

    Code For Combat

    One-Roll Method

    Things We Need

    1. One randomly rolled number

    Code for Rolling

    Code for Attack


    I should mention that this is assuming you’re not implementing a system that will display your Hit chance through other various calculations, like in the Fire Emblem games.

    I think that the code should still be similar, but instead of comparing actualhit with pursuit_proc_rate, you would have to compare actualhit with displayedhit.

    You can read more about Fire Emblem’s RNG here.

    5 Likes

    ^THIS
    Make sure any random rolls happen before you need it, otherwise it plays havock with your game by rerolling every time someone moves to the stat screen and back.

    If you just want a 10% chance each round, use
    *rand diceroll 1 10
    ^But roll this before you get to the code below

    *if (diceroll > 1)
    Your attack fails
    *if (diceroll = 1)
    Your attack succeeds.

    If you want to specify exactly how many rounds it’ll take, you can do that as well with a random roll.
    If you had again *rand diceroll (1-10)
    You could either specify how much health you’ve lost.
    ie
    *if (diceroll = 9)
    The battle is long and bloody. You strike the finishing blow and then collapse yourself.
    *set health 2

    *if (diceroll =1)
    You parry your opponent’s strike and their sword flies from their hand. You’ve won.
    *comment no health impact

    etc
    Or you can have a counter.
    *if (diceroll = 3)
    *set counter 3
    (etc)

    *label battle
    *page_break
    You strike.

    *if (counter > 1)
    Your attack fails.
    *set counter -1
    *set health -5
    *goto battle

    *if (counter =1)
    Your attack succeeds.
    *goto afterbattle

    Obviously you can get more complicated if you want. Like have a diceroll for succeed or fail the attack and then a second roll for what sort of succeed or fail state you had. (ie big/medium/slight damage.) It depends on what you want to do with it.

    Edit: Just a piece of advice- be careful with how you use RNG’s. People will hate you if they’re 150k words into a 200k story and in good health, and they get killed because the RNG didn’t smile on them and they have to start from the beginning. Sometimes it’s worth building fail safes into these things so there’s only so much a character can get hammered by poor chance before a battle wraps up.

    3 Likes

    Oh, and if you want to “cheat” in the player’s favour by making it more likely that they hit for each successive miss, you can do this:

    *label attack_calc
    *rand bonus 1 10
    *rand attack 1 10
    *set bonus (bonus + cheat)
    *set attack (attack + bonus)
    *return
    ______________________
    
    *if (attack < 10)
      You miss!
      
      *set cheat +1
      *gosub attack_calc
      *goto next
    *else
      You hit!
      
      *set cheat 0
      *gosub attack_calc
      *goto next
    

    What’s happening here is that it’s pre-rolling your next attack after the last one, and if it missed, it increases the likelihood of success by secretly adding +1 to the roll. Two misses and you get a +2 to the roll, and so on. Eventually, you’ll hit, and then the cheat resets.

    4 Likes

    For percentiles, one simple method that works well with CS’s stat bars is a d100, roll-low-to-hit system. (Like the one in Call of Cthulhu.)

    Every time there’s RNG involved, you generate a random number between 1 and 100. If that number is equal to or less than the target number, you succeed. If it’s above, you fail.

    In this case, the target number would be your proc chance. For a 10% proc chance, check if the random number is <= 10. For a 75% chance, check if the the random number is <= 75.

    E.g.,

    *temp die_roll 0
    *rand die_roll 1 100
    *if (die_roll <= 75)
        *goto success
    *else
        *goto failure
    
    Longer Example
    *title Chancey Mc Chance Chance
    
    *create attack 80
    *create counter 10
    *create die_roll 0
    
    You are a knight with these stats:
    
    *stat_chart
        percent attack Attack Power
        percent counter Counter Skill
    
    *page_break You're exploring a forest.
    
    You come across a dragon!
    
    *choice
        #Attack!
            *rand die_roll 1 100
            *if (die_roll <= attack)
                You hit it!
                *goto dragon_counterattack
            *else
                It dodges!
                *goto dragon_counterattack
        #Run away!
            As you flee, the dragon gives chase.
            *goto dragon_counterattack
    
    *label dragon_counterattack
    The dragon rears up and swipes at you with its claws!
    
    *rand die_roll 1 100
    *if (die_roll <= counter)
        Luckily, you're able to counter its strike, reducing the damage it inflicts.
    
    What's your next move?
    *ending
    

    As previously mentioned, you generally want to use *rand on the page before you need it, as otherwise if the player goes into the stats screen and returns the RNG will re-roll.

    Because of this, I personally find it useful to pre-roll a chapter’s/encounter’s worth of values in advance and store them in an array. I like it because

    • It sidesteps the problem of having *rand commands re-roll if the player goes into the stats screen.
    • Save scumming is less effective.
    • It makes testing easier, as I can force successes/failures/bypass the randomizer by filling the array directly.
    • I can implement cheat modes / different difficulties without adding a lot of code - just tweak the subroutine that rolls the random numbers. No hunting down individual *rand commands!
    Example
    *title Chancey Mc Chance Chance
    
    *create attack 80
    *create counter 10
    
    *create die_roll_1 0
    *create die_roll_2 0
    *create die_roll_3 0
    *create die_roll_4 0
    *create die_roll_5 0
    *create total_die_rolls 5
    
    *create roll_index 0
    
    *comment ------------------------------------------------------------
    
    *gosub roll_dice
    *set roll_index 1
    
    You are a knight with these stats:
    
    *stat_chart
        percent attack Attack Power
        percent counter Counter Skill
    
    *page_break You're exploring a forest.
    
    You come across a dragon!
    
    *choice
        #Attack!
            *if (die_roll[roll_index] <= attack)
                You hit it!
                *set roll_index + 1
                *goto dragon_counterattack
            *else
                It dodges!
                *set roll_index + 1
                *goto dragon_counterattack
        #Run away!
            As you flee, the dragon gives chase.
            *goto dragon_counterattack
    
    *label dragon_counterattack
    The dragon rears up and swipes at you with its claws!
    
    *if (die_roll[roll_index] <= counter)
        Luckily, you're able to counter its strike, reducing the damage it inflicts.
    
    *set roll_index + 1
    
    What's your next move?
    *ending
    
    *comment ------------------------------------------------------------
    
    *label roll_dice
    *temp i 1
    *label roll_dice_loop
    *if (i <= total_die_rolls)
        *rand die_roll[i] 1 100
        *set i + 1
        *goto roll_dice_loop
    *return
    

    While it is possible *rand is biased to be less fair the fewer times you use it, I would suggest it’s more likely you’re simply observing the law of large numbers in action.

    3 Likes

    As @Lan says, this is just how probability works. There’s no invisible hand guiding results to “feel” right – the more times you test a probability, the closer you’ll get to an average, but that’s not because the numbers are intrinsically drawn to an average, that’s just how we measure stuff. You don’t roll a three-sided die once and expect it to land precisely one third on each side, do you? How would that even work?

    2 Likes

    Yup. That’s why when you need to use statistics to accurately represent something, there will usually be a minimum number of samples you need to take in order to get something that is likely to be representative. The more samples run (properly) the more accurate you’re likely to be.

    This is how false understanding of probablity in gambling gets people. In effect, each roll of the dice is independent from the one before. (The dice doesn’t keep track of what numbers it has shown on previous rolls.) Although if you run enough trials eventually the numbers should even out just by chance, it may take a lot of rounds to do so, and do so potentially in a weird way that isn’t even across the whole test. Just because you’ve been rolling 3’s for the last 10 rounds, if the dice is unweighted, it doesn’t mean you’re any more or less likely to roll another 3 on the next go.

    4 Likes

    That’s true. But in case if the required logic should work this way:

    it would be more convenient to not use *rand alone, but to add some tweaking as a safety measure. To prevent the situation when 10 rounds of battle passed, but the 10% chance variable wasn’t triggered (which could happen with rolling dice only once per round). Like “true hit method” described above or additional variable you suggested.

    wow, the community is so helpful, I got so many things to read and test now

    thank you very much, I’ll let you know when my battle scene is ready for testing
    :pray:

    read my other reply!

    I did. I even mentioned it in the last sentence. :wink:

    1 Like

    Right you are!