hq-prob-table.rb

Posted: 2006-04-14
Word Count: 990
Tags: heroquest old-stuff ruby rpg

Table of Contents

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.

Original Text

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
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
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
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
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
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

OUTPUT w/ no arguments