# Simulating "Heart: The City Beneath" dice using Anydice

I like to design systems and break apart games to learn stuff, and Anydice is a great tool I have been using for a while. However I am looking at some Heart rolls and it appears Anydice is giving me garbage results, almost certainly my fault.

I am specifically looking at risky/dangerous rolls and how that impacts chance of success. Basically for those who dont know, Heart uses a pool of d10s for skill checks, and an 8, 9, or 10 is a pure success. The max theoretical pool is like 4d10 I believe, maybe 5 if you push it with some character abilities. A risky roll will remove the highest die, and a dangerous roll will remove the highest two dice.

How I have it written right now is this:

output [count {8, 9, 10} in [lowest 1 of 2d10]] named "Domain or Skill"
output [count {8, 9, 10} in [lowest 2 of 3d10]] named "Domain and Skill"
output [count {8, 9, 10} in [lowest 3 of 4d10]] named "All and Mastery"


It seems to give me bogus results though, as it shows the last output as having a lower success rate than the second output, and none of them have more than 1 success despite technically outputs 2 and 3 being able to, even if its quite unlikely. So I have something wrong here. How would I go about doing this properly?

• It might be important to specify that in Heart, unlike most dice pool systems, you don’t count how many dice hit the target number - you just look at the highest single die result. I don’t think you quite say that here, which will be important for AnyDice experts who don’t know Heart! Dec 26, 2022 at 20:10
• Also note that 6-7 is also a success, albeit “at a cost”, ie inflicting stress on the character. It might be worth clarifying if you are only interesting in a particular level of success. Dec 27, 2022 at 3:03
• I mean Anydice already has a easy function for displaying probability of getting at least X value. So I just used Ilmaris solution and looked at the at least 1 chance. Maybe its a little awkward considering other folks with more knowledge gave more accurate models but it got me the results and thats all that really matters. Also I know about success at a cost but for this specific instance I was only interested in pure successes, and the model can be easily tweaked to include success at a cost so I felt no need to elaborate on that. Dec 28, 2022 at 7:10

## Let’s not overthink this

As I understand the mechanic, on a normal roll, you want the highest dice, on a risky roll you want the second highest, and on a dangerous roll you want the third highest. Anydice lets you get these directly:

output 1@3d10 named "Normal"
output 2@3d10 named "Risky"
output 3@3d10 named "Dangerous"


Of course, we can avoid mucking around with d10s and just set up a success die:

HEART: {0:7, 1:3}

output 1@3dHEART named "Normal"
output 2@3dHEART named "Risky"
output 3@3dHEART named "Dangerous"

• Huh… I actually went and checked the rules for "Heart: The City Beneath", and it turns out that you're right — there's no need to count successful dice, since only the highest roll (or second or third highest for risky and dangerous tasks) matters. It seems both Groody and I misunderstood the mechanic based on the OP's attempted solution, and thus gave correct solutions to the wrong problem. Your answer should be the accepted one. Dec 26, 2022 at 23:37
• @IlmariKaronen notwithstanding, you can still add up the lower dice by {2..3}@3dHEART, for example. Dec 27, 2022 at 2:31
• I commented about this on the question, so I’m glad to see someone paying attention! Though I also note that this doesn’t quite model the success mechanic, as there are three levels of success: 6-7 is success at a cost (incurring some stress but still succeeding); 8-9 is complete success (no stress incurred); and 10 is critical success (dramatically succeed, and increase stress inflicted if inflicting stress). So if labelling a dice I’d put in those categories for further stats. Dec 27, 2022 at 2:59
• I think a 1 also maps to a critical failure, so there are at least five states (crit. failure, failure, success at a cost, success, crit. success), but you can encode this as HEART: {-1: 1, 0: 4, 1: 2, 2: 2, 3: 1} where -1 is a crit. failure, 0 is a failure, 1 is a success at a cost, 2 is a success, and 3 is a critical success. Of course, you could map those states to any arbitrary integers as desired Dec 27, 2022 at 17:53
• Ah yeah I had no idea. I had figured the best way to do it in anydice was to use the count function, and had no idea about this. Thanks! Still tho counting worked I just set stuff to see the chance of getting at least 1 success. Dec 28, 2022 at 7:07

Update: After actually checking how the success mechanic in "Heart: The City Beneath" works, it seems that Dale M's answer is the correct one.

My answer below, and Groody's answer, do both show how to make the OP's code work as presumably intended: counting the number of rolls of 8 or higher after removing some number of the highest rolls. But that's not how the success mechanic in Heart works.

Yeah, that won't work.

The first problem is that the built-in lowest NUMBER of DICE function returns the sum of the lowest NUMBER values, not a sequence containing those values. The second problem is that, even if it did return a sequence, trying to apply it to a dice pool like Nd10 would cause AnyDice to sum the returned sequence anyway (to turn it into a single number, and then collect all those numbers into a weighted die).

But you can write your own count VALUES in lowest NUMBER of DICE function that does what you want. Conveniently, the AnyDice function library contains "do it yourself" examples of both count VALUES in SEQUENCE and lowest NUMBER of DICE, and it's not too hard to combine the two:

\ "Do it yourself" examples from AnyDice's function library: \

function: count VALUES:s in SEQUENCE:s {
COUNT: 0
loop P over {1..#VALUES} {
COUNT: COUNT + (P@VALUES = SEQUENCE)
}
result: COUNT
}

function: lowest NUMBER:n of DICE:d {
result: {(#DICE - NUMBER + 1)..#DICE}@DICE
}

\ Combined custom function: \

function: count VALUES:s in lowest NUMBER:n of DICE:s {
COUNT: 0
loop P over {(#DICE - NUMBER + 1) .. #DICE} {
COUNT: COUNT + (P@DICE = VALUES)
}
result: COUNT
}


One practical change I had to make is that I loop over the dice and compare each of them to the values being searched for, rather than the other way around like the AnyDice example implementation of count VALUES in SEQUENCE does. Also, I had to make the DICE parameter a sequence, since I can't loop over it otherwise. This means that, when you pass in a dice pool, AnyDice will call the function with every possible (sorted) result of rolling the dice as a sequence and automatically sum the results (which is exactly what you want).

The finished program thus looks like this:

function: count VALUES:s in lowest NUMBER:n of DICE:s {
COUNT: 0
loop P over {(#DICE - NUMBER + 1) .. #DICE} {
COUNT: COUNT + (P@DICE = VALUES)
}
result: COUNT
}

output [count {8, 9, 10} in lowest 1 of 2d10] named "Domain or Skill"
output [count {8, 9, 10} in lowest 2 of 3d10] named "Domain and Skill"
output [count {8, 9, 10} in lowest 3 of 4d10] named "All and Mastery"

• Awesome thank you! Dec 26, 2022 at 2:57
• Your answer is not incorrect as it gave me the data I wanted. Anydice has a button to calculate at least X, so I just looked at the at least 1 success section. While its nice to find other ways of doing it, ultimately the method of getting the answer doesn't matter. Dec 28, 2022 at 7:17

I think your problem is that you first expand the dice into a sequence with the function, so at the time you are comparing with the numbers you count as successes, it is comparing to the totals of that result, not to the individual dice.

It is not as elegant as Ilmaris answer, but an easy way to get around this is to define a success die. You do not really care which of the success you ignore, after all, as long as you ignore a success. For example, for a d10 who has successes on an 8, 9, or 10:

SUCCESS: d10 >= 8

Then you can use that success die in your formula, to ignore one or more of those successes

output [lowest 1 of 2dSUCCESS] named "Domain or Skill"
output [lowest 2 of 3dSUCCESS] named "Domain and Skill"
output [lowest 3 of 4dSUCCESS] named "All and Mastery"


If the rules are as Guybrush says, you only consider the highest roll for normal, second highesst for risky, and third highest for dangerous. There also are four levels of success: dramatic success on a 10, complete succsess on a 8 or 9, success at cost on a 6 or 7, and failure on a 5 or below. You can model that as follows (based on the Blades in the Dark example), replace RISKY in the output with the NORMAL or DANGEROUS to see the distribution for other risk levels:

NORMAL: 1
RISKY: 2
DANGEROUS: 3

function: heart RISK:n ROLL:s {
L: RISK@ROLL
if L > 7 {
result: 2 + (L=10)
}
result: L >= 6
}

loop P over {2..4} {
output [heart DANGEROUS Pd10] named "[P]d10"
}

• FWIW, I think both answers are both elegant and inelegant in their own way (the inelegance, of course, rooted in that AnyDice only supports pool-to-sequence expansion for XdY and not lowest/highest/@ operator). Note also that expanding 2-outcome success dice is much cheaper computationally than expanding d10s, even if it doesn't make a practical difference in this case due to the small max pool size. Dec 26, 2022 at 3:53
• Ilmaris answer seems a bit more what Im going for (or at least follows the exact text of the rules a bit better) but this is an interesting way to work it for sure. Anydice is great but it has a lot of quirks and syntax Im still exploring lol. Got what Im looking for in the end tho ^^ Dec 26, 2022 at 5:43
• +1, this will run a lot faster than my answer. In cases where it works (i.e. where the number of successes for each die is a monotone function of the number rolled on the die) it's a better solution than mine. Dec 26, 2022 at 15:36
• BTW, you can also define the SUCCESS die as SUCCESS: [count {8,9,10} in d10] or as SUCCESS: d10 >= 8, which may be less prone to typos and more self-explanatory for someone reading the code. Dec 26, 2022 at 15:38
• @IlmariKaronen Thank you, that is much clearer to read, I adopted that. Dec 26, 2022 at 17:57