The How and the Why of Indices of Game Crunch

I posted my 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, continues to . I figure, why not do something similar for Dungeons and Dragons: Fifth Edition (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 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 . I store those lifepaths in a Yet Another Markup Language (YAML 📖) file and use a Hugo shortcode to render the Hypertext Markup Language (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 Application Programming Interface (API 📖), then process accordinly. However, I chose to manually download the Comma Separated Value (CSV 📖) file.

Below is my Rake (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>
    <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 scope="col">{{ .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"