The How and the Why of Indices of Game Crunch


Cover of Morgrave Miscellany

I posted my Index of Allowed Classes, Races, Cultures, and Backgrounds on Reddit. Someone asked me:

Am I missing something here? This is just a list of various races and subclasses for use in your game? Maybe something about why you chose these would be good.

Why the index? And why these?

First the question of why the index.

As a game master, I assume I have gathered more gaming material than my players. And I want to draw attention to options that I’ve found that might be interesting. I would love to see someone play a Bone Knight or Divine Herald from Morgrave Miscellany.

Second, looking at the site stats, my (now neglected) Dungeon World playbooks continues to remain quite popular. I figure, why not do something similar for 5E Dungeons and Dragons.

And why did I choose these?

To show my players what they can pick from, I needed to start somewhere. I picked through the books I had available. I intend to add to the index as things catch my eye. I had wanted to use the Sword Coast Adventure Guide, but it appears I’ve either sold it or misplaced it.

A Technical Curiosity

With that in mind, I also wanted to write some code. When I first started the index, I wanted to emulate the data driven aspect of my Burning Wheel lifepaths inspired by Warhammer Fantasy. I store those lifepaths in a YAML file and use a Hugo shortcode to render the HTML.

I outlined the following steps:

  1. Create a spreadsheet for the index
  2. Convert the spreadsheet to YAML
  3. Create a shortcode to render the YAML

In an ideal world, I would have an OAuth2 script to download the spreadsheet via an API, then process accordinly. However, I chose to manually download the CSV.

Below is my Rake (Ruby language) task for converting CSV to YAML.

Rake task to convert CSV to YAML

namespace :csv do
  desc "Given the data CSVs, convert them to YAML for dynamic rendering"
  task :convert_to_yaml do
    require 'csv'
    require 'psych'

    label_lookup_table = {
      "class_name" => "Class",
      "subclass" => "Subclass",
      "background_name" => "Background",
      "source_name" => "Source",
      "race_name" => "Race or Culture",
      "race_subtype" => "Subtype"
    }

    [
      { basename: "classes", name: "Classes and Subclasses" },
      { basename: "backgrounds", name: "Backgrounds" },
      { basename: "races-and-cultures", name: "Races and Cultures" }
    ].each do |file_data|
      basename = file_data.fetch(:basename)
      name = file_data.fetch(:name)
      rows = []
      columns = []
      data = {
        "name" => name,
        "columns" => columns,
        "rows" => rows
      }

      CSV.foreach(File.join(PROJECT_PATH, "tmp/#{basename}.csv"), headers: true) do |csv|
        if columns.empty?
          csv.headers.each do |header|
            next if header == "source_url"
            columns << { "key" => header , "label" => label_lookup_table.fetch(header, header) }
          end
        end
        row = {}
        columns.each do |column|
          key = column.fetch("key")
          row[key] = csv[key]
        end
        if csv["source_url"]
          row["source_name"] = "[#{csv["source_name"]}](#{csv["source_url"]})"
        end
        rows << row
      end

      File.open(File.join(PROJECT_PATH, "data/eberron/#{basename}.yml"), 'w+') do |f|
        f.puts Psych.dump(data)
      end
    end
  end
end

From the YAML, I use a generic data_table Hugo template to build the tables.

Hugo shortcode to process dynamic table

{{- $scope := .Get "scope" }}
{{- $container_name := .Get "container" }}
{{- $collapse := .Get "collapse" }}
{{- $tableNumber := .Page.Scratch.Get "tableNumber" }}
{{- if eq $tableNumber nil }}{{ .Page.Scratch.Set "tableNumber" 0 }}{{ end }}
{{- .Page.Scratch.Add "tableNumber" 1 }}
{{- $tableNumber = .Page.Scratch.Get "tableNumber" }}
{{- $container := index $.Site.Data $container_name }}
{{- with index $container $scope }}
  {{- if $collapse }}
  <details closed>
    <summary id="{{ anchorize $scope }}-dom-id">{{ .name }}</summary>
  {{- else }}
<h2 id="{{ anchorize $scope }}-dom-id">{{ .name }}</h2>
  {{- end }}
<div class="table-wrapper">
  <table class="data-tables stripe" aria-label="{{ .name }} Progression">
    <caption>Table {{ $tableNumber }}: {{ .name | markdownify }}</caption>
    <thead>
      <tr>
      {{- $columns := .columns }}
      {{- range $columns }}
        <th>{{ .label }}</th>
      {{- end }}
      </tr>
    </thead>
    <tbody>
      {{- range .rows }}
      {{- $data := . }}
        <tr>
          {{- range $columns }}
            <td>{{ index $data .key | markdownify }}</td>
          {{- end }}
        </tr>
      {{- end }}
    </tbody>
  </table>
</div>
{{- if $collapse }}
  </details>
{{- end }}
{{- end }}

And how to render the shortcode:

data_table container="eberron" scope="classes" collapse="true"