hq-prob-table.rb

2006-04-14

(approx. 1100 words)

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/usr/bin/env ruby

#
# hq-prob-table.rb: generate a probability table for Issaries's HeroQuest
#                   RPG, and interactively assess probabilities between
#                   two Ability Ranks.
#

CRIT_ROLL = 1
FUMB_ROLL = 20

CRITICAL = 2
SUCCESS = 1
FAILURE = 0
FUMBLE = -1

DEGREES = [
  :complete_victory, 
  :major_victory, 
  :minor_victory,
  :marginal_victory, 
  :tie, 
  :marginal_defeat, 
  :minor_defeat,
  :major_defeat, 
  :complete_defeat
]

def outcome(rank, roll)
  if roll == CRIT_ROLL then
    CRITICAL
  elsif roll == FUMB_ROLL then
    FUMBLE
  elsif roll <= rank then
    SUCCESS
  else
    FAILURE
  end
end

def degree(mastery, a, r, rolla, rollr)
  diff = mastery + outcome(a, rolla) - outcome(r, rollr)

  case diff
  when 3 then
    :complete_victory
  when 2 then
    :major_victory
  when 1 then
    :minor_victory
  when 0 then 
    if rolla < rollr then
      return :marginal_victory
    elsif rolla == rollr then
      return :tie
    else
      return :marginal_defeat
    end
  when -1 then
    :minor_defeat
  when -2 then
    :major_defeat
  when -3 then
    :complete_defeat
  else
    (diff > 0) ? :complete_victory : :complete_defeat
  end
end

def counts(mastery, a, r)
  result = Hash.new(0)
  1.upto(20) do |rolla|
    1.upto(20) do |rollr|
      result[degree(mastery, a, r, rolla, rollr)] += 1
    end
  end
  return result
end

def total_success(c)
  return [
    :complete_victory, 
    :major_victory, 
    :minor_victory,
    :marginal_victory,].inject(0.0) { |sum, d| sum + c[d].to_f/4 }
end

Output Routines

 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245

def parse_rank(s)
  match = /(\d+)([MmWw])?(\d+)?/.match(s)

  return nil unless match

  a = match[1].to_i
  if match[3] then
    m = match[3].to_i
  elsif match[2] then
    m = 1
  else
    m = 0
  end
  return a, m
end

def rank(a, m)
  astr = a.to_s
  astr << 'w' if m > 0
  astr << m.to_s if m > 1
  return astr
end

def print_header
  printf("<tr><th>higher rank</th><th>lower rank</th>" + 
	 ("<th>%s</th>" * DEGREES.size) +
	 "</tr>\n",
	 *(DEGREES.collect { |d| d.to_s.tr('_',' ') }))
end

def print_probabilities(m, a, r)
  c = counts(m, a, r)

  printf("<tr><th>%s</th><th>%d</th>" +
	 ("<td>%5.2f%%</td>" * DEGREES.size) + 
	 "</tr>\n",
	 rank(a, m), r, *(DEGREES.collect { |d| c[d].to_f/4 }))
end

def print_probability_page
  puts <<EOT
  <html>
    <head>
       <title>HeroQuest Simple Contest Probabilities of Success</title>
       <style>td { text-align: right }</style>
    </head>
  <body>
    <h1><i>HeroQuest</i> Simple Contest Probabilities of Success</h1>
EOT

  puts "<h2>Quick Probability Guide</h2>"
  print_difference_prob_table

  puts "<h2>Detailed Probabilities</h2>"
  print_probability_table
  puts "</body></html>"
end

def print_difference_prob_table
  puts <<EOT
    <p>
       In the absence of mastery levels, there is a relationship
       between probability of any victory and the difference between 
       Ranks:
    <p>
    <table border="1">
EOT
  print("<tr>")
  0.upto(9) do |df|
    printf("<th>%2d</th>", df)
  end
  print("</tr>\n")
  print("<tr>")
  0.upto(9) do |df|
    c = counts(0, 19, 19 - df)
    printf("<td>%5.2f%%</td>", total_success(c))
  end
  print("</tr>\n")
  print("<tr>")
  10.upto(18) do |df|
    printf("<th>%2d</th>", df)
  end
  print("</tr>\n")
  print("<tr>")
  10.upto(18) do |df|
    c = counts(0, 19, 19 - df)
    printf("<td>%5.2f%%</td>", total_success(c))
  end
  print("</tr>\n")
  puts "</table>"

  puts <<EOT
  <p>
    Absent differences in mastery, the probability of ties is equal to 
    <code>(20 + lower-rank - higher-rank) x 0.25%</code>.
    For example, between equal ranks, a tie will occur 5% of the time.
    If the difference is 4 (e.g. 17 and 13), the chance of a tie is 4%.
  </p>
EOT
end

def print_probability_table
  puts <<EOT
    <p>
    How to use this table:
    </p>
    <ol>
      <li>
        If both parties have levels of Mastery, reduce each by the 
        lower of the two levels.
      </li>
      <li>
        Determine which party has the higher Rank.  A Victory for the 
        party with the higher rank is equivalent to a Defeat for the 
        other party.
      </li>
      <li>
        Convert ranks of 20, 20w, etc. into 19, 19w, etc.  Mathematically,
        they're equivalent, due to automatic Fumble at 20.
      </li>
      <li>
        Look up the probabilities on this chart.  Note that "5w" is 
        "five Mastery", and "1w2" is "one Mastery two".  Representing 
        the actual Mastery rune (|_|_| in ASCII art) using 
        programmatically-generated HTML is just too annoying.
      </li>
    </ol>

    <p>
      Note that this chart is meant only for informational purposes.
      To actually resolve conflicts, roll as instructed in the
      <i>HeroQuest</i> rules.  (You'd need two d20 to use these 
      percentages anyway ...)
    </p>

    <table border="1">
EOT

  print_header

  1.upto(61) do |t|

    m = t / 20
    a = t % 20
    maxr = (t > 19) ? 19 : t

    if a > 0 then
      1.upto(maxr) do |r|
	print_probabilities(m, a, r)
      end
    end
  end

  print_probabilities(4, 1, 19)
  puts "</table>"
end

Main Loop

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
case ARGV.size
when 0 then
  print_probability_page
when 2 then
  a, ma = parse_rank(ARGV[0])
  r, mr = parse_rank(ARGV[1])

  c = counts(ma - mr, a, r)

  printf("%16s: %5.2f%%\n", 'SUCCESS', total_success(c))
  DEGREES.each do |d|
    printf("%16s: %5.2f%%\n", d, c[d].to_f/4)
  end
else
  puts "usage: #{$0} [ active-rank resist-rank ]"
end

OUTPUT w/ no arguments