This is really old Ruby code from 2006. I'm sorry.
The syntax higlighter screws up at the first “here document” at line 128, but at that point the code just generates HTML. I thought about rewriting the code to generate terse Markdown instead of paragraphs in HTML and/or to replace here documents with literal strings, but I think I’ll leave it be.
Definitions and Probability Functions
1#!/usr/bin/env ruby
2
3#
4# hq-prob-table.rb: generate a probability table for Issaries's HeroQuest
5# RPG, and interactively assess probabilities between
6# two Ability Ranks.
7#
8
9CRIT_ROLL = 1
10FUMB_ROLL = 20
11
12CRITICAL = 2
13SUCCESS = 1
14FAILURE = 0
15FUMBLE = -1
16
17DEGREES = [
18 :complete_victory,
19 :major_victory,
20 :minor_victory,
21 :marginal_victory,
22 :tie,
23 :marginal_defeat,
24 :minor_defeat,
25 :major_defeat,
26 :complete_defeat
27]
28
29def outcome(rank, roll)
30 if roll == CRIT_ROLL then
31 CRITICAL
32 elsif roll == FUMB_ROLL then
33 FUMBLE
34 elsif roll <= rank then
35 SUCCESS
36 else
37 FAILURE
38 end
39end
40
41def degree(mastery, a, r, rolla, rollr)
42 diff = mastery + outcome(a, rolla) - outcome(r, rollr)
43
44 case diff
45 when 3 then
46 :complete_victory
47 when 2 then
48 :major_victory
49 when 1 then
50 :minor_victory
51 when 0 then
52 if rolla < rollr then
53 return :marginal_victory
54 elsif rolla == rollr then
55 return :tie
56 else
57 return :marginal_defeat
58 end
59 when -1 then
60 :minor_defeat
61 when -2 then
62 :major_defeat
63 when -3 then
64 :complete_defeat
65 else
66 (diff > 0) ? :complete_victory : :complete_defeat
67 end
68end
69
70def counts(mastery, a, r)
71 result = Hash.new(0)
72 1.upto(20) do |rolla|
73 1.upto(20) do |rollr|
74 result[degree(mastery, a, r, rolla, rollr)] += 1
75 end
76 end
77 return result
78end
79
80def total_success(c)
81 return [
82 :complete_victory,
83 :major_victory,
84 :minor_victory,
85 :marginal_victory,].inject(0.0) { |sum, d| sum + c[d].to_f/4 }
86end
Output Routines
parse_rank
89def parse_rank(s)
90 match = /(\d+)([MmWw])?(\d+)?/.match(s)
91
92 return nil unless match
93
94 a = match[1].to_i
95 if match[3] then
96 m = match[3].to_i
97 elsif match[2] then
98 m = 1
99 else
100 m = 0
101 end
102 return a, m
103end
rank
105def rank(a, m)
106 astr = a.to_s
107 astr << 'w' if m > 0
108 astr << m.to_s if m > 1
109 return astr
110end
print_header
112def print_header
113 printf("<tr><th>higher rank</th><th>lower rank</th>" +
114 ("<th>%s</th>" * DEGREES.size) +
115 "</tr>\n",
116 *(DEGREES.collect { |d| d.to_s.tr('_',' ') }))
117end
print_probabilities
119def print_probabilities(m, a, r)
120 c = counts(m, a, r)
121
122 printf("<tr><th>%s</th><th>%d</th>" +
123 ("<td>%5.2f%%</td>" * DEGREES.size) +
124 "</tr>\n",
125 rank(a, m), r, *(DEGREES.collect { |d| c[d].to_f/4 }))
126end
print_probability_page
128def print_probability_page
129 puts <<EOT
130 <html>
131 <head>
132 <title>HeroQuest Simple Contest Probabilities of Success</title>
133 <style>td { text-align: right }</style>
134 </head>
135 <body>
136 <h1><i>HeroQuest</i> Simple Contest Probabilities of Success</h1>
137EOT
138
139 puts "<h2>Quick Probability Guide</h2>"
140 print_difference_prob_table
141
142 puts "<h2>Detailed Probabilities</h2>"
143 print_probability_table
144 puts "</body></html>"
145end
print_difference_prob_table
147def print_difference_prob_table
148 puts <<EOT
149 <p>
150 In the absence of mastery levels, there is a relationship
151 between probability of any victory and the difference between
152 Ranks:
153 <p>
154 <table border="1">
155EOT
156 print("<tr>")
157 0.upto(9) do |df|
158 printf("<th>%2d</th>", df)
159 end
160 print("</tr>\n")
161 print("<tr>")
162 0.upto(9) do |df|
163 c = counts(0, 19, 19 - df)
164 printf("<td>%5.2f%%</td>", total_success(c))
165 end
166 print("</tr>\n")
167 print("<tr>")
168 10.upto(18) do |df|
169 printf("<th>%2d</th>", df)
170 end
171 print("</tr>\n")
172 print("<tr>")
173 10.upto(18) do |df|
174 c = counts(0, 19, 19 - df)
175 printf("<td>%5.2f%%</td>", total_success(c))
176 end
177 print("</tr>\n")
178 puts "</table>"
179
180 puts <<EOT
181 <p>
182 Absent differences in mastery, the probability of ties is equal to
183 <code>(20 + lower-rank - higher-rank) x 0.25%</code>.
184 For example, between equal ranks, a tie will occur 5% of the time.
185 If the difference is 4 (e.g. 17 and 13), the chance of a tie is 4%.
186 </p>
187EOT
188end
print_probability_table
190def print_probability_table
191 puts <<EOT
192 <p>
193 How to use this table:
194 </p>
195 <ol>
196 <li>
197 If both parties have levels of Mastery, reduce each by the
198 lower of the two levels.
199 </li>
200 <li>
201 Determine which party has the higher Rank. A Victory for the
202 party with the higher rank is equivalent to a Defeat for the
203 other party.
204 </li>
205 <li>
206 Convert ranks of 20, 20w, etc. into 19, 19w, etc. Mathematically,
207 they're equivalent, due to automatic Fumble at 20.
208 </li>
209 <li>
210 Look up the probabilities on this chart. Note that "5w" is
211 "five Mastery", and "1w2" is "one Mastery two". Representing
212 the actual Mastery rune (|_|_| in ASCII art) using
213 programmatically-generated HTML is just too annoying.
214 </li>
215 </ol>
216
217 <p>
218 Note that this chart is meant only for informational purposes.
219 To actually resolve conflicts, roll as instructed in the
220 <i>HeroQuest</i> rules. (You'd need two d20 to use these
221 percentages anyway ...)
222 </p>
223
224 <table border="1">
225EOT
226
227 print_header
228
229 1.upto(61) do |t|
230
231 m = t / 20
232 a = t % 20
233 maxr = (t > 19) ? 19 : t
234
235 if a > 0 then
236 1.upto(maxr) do |r|
237 print_probabilities(m, a, r)
238 end
239 end
240 end
241
242 print_probabilities(4, 1, 19)
243 puts "</table>"
244end
Main Loop
246case ARGV.size
247when 0 then
248 print_probability_page
249when 2 then
250 a, ma = parse_rank(ARGV[0])
251 r, mr = parse_rank(ARGV[1])
252
253 c = counts(ma - mr, a, r)
254
255 printf("%16s: %5.2f%%\n", 'SUCCESS', total_success(c))
256 DEGREES.each do |d|
257 printf("%16s: %5.2f%%\n", d, c[d].to_f/4)
258 end
259else
260 puts "usage: #{$0} [ active-rank resist-rank ]"
261end