873 lines
31 KiB
JavaScript
873 lines
31 KiB
JavaScript
|
|
/* quiz-connections.js — Dynamic quiz category plugin for dataset relations */
|
||
|
|
(function () {
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const { getTarotCardDisplayName } = window.TarotCardImages || {};
|
||
|
|
const quizPluginHelpers = window.QuizPluginHelpers || {};
|
||
|
|
const {
|
||
|
|
normalizeOption,
|
||
|
|
normalizeKey,
|
||
|
|
buildTemplatesFromSpec,
|
||
|
|
buildTemplatesFromVariants
|
||
|
|
} = quizPluginHelpers;
|
||
|
|
|
||
|
|
if (
|
||
|
|
typeof normalizeOption !== "function"
|
||
|
|
|| typeof normalizeKey !== "function"
|
||
|
|
|| typeof buildTemplatesFromSpec !== "function"
|
||
|
|
|| typeof buildTemplatesFromVariants !== "function"
|
||
|
|
) {
|
||
|
|
throw new Error("QuizPluginHelpers must load before quiz-connections.js");
|
||
|
|
}
|
||
|
|
|
||
|
|
const cache = {
|
||
|
|
referenceData: null,
|
||
|
|
magickDataset: null,
|
||
|
|
templates: [],
|
||
|
|
templatesByCategory: new Map()
|
||
|
|
};
|
||
|
|
|
||
|
|
function toTitleCase(value) {
|
||
|
|
const text = normalizeOption(value).toLowerCase();
|
||
|
|
if (!text) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
function labelFromId(value) {
|
||
|
|
const id = normalizeOption(value);
|
||
|
|
if (!id) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
return id
|
||
|
|
.replace(/[_-]+/g, " ")
|
||
|
|
.replace(/\s+/g, " ")
|
||
|
|
.trim()
|
||
|
|
.split(" ")
|
||
|
|
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : ""))
|
||
|
|
.join(" ");
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatHebrewLetterLabel(entry, fallbackId = "") {
|
||
|
|
if (entry?.name && entry?.char) {
|
||
|
|
return `${entry.name} (${entry.char})`;
|
||
|
|
}
|
||
|
|
if (entry?.name) {
|
||
|
|
return entry.name;
|
||
|
|
}
|
||
|
|
if (entry?.char) {
|
||
|
|
return entry.char;
|
||
|
|
}
|
||
|
|
return labelFromId(fallbackId);
|
||
|
|
}
|
||
|
|
|
||
|
|
function slugifyId(value) {
|
||
|
|
return normalizeKey(value)
|
||
|
|
.replace(/[^a-z0-9]+/g, "-")
|
||
|
|
.replace(/^-+|-+$/g, "");
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatHexagramLabel(entry) {
|
||
|
|
const number = Number(entry?.number);
|
||
|
|
const name = normalizeOption(entry?.name);
|
||
|
|
if (Number.isFinite(number) && name) {
|
||
|
|
return `Hexagram ${number}: ${name}`;
|
||
|
|
}
|
||
|
|
if (Number.isFinite(number)) {
|
||
|
|
return `Hexagram ${number}`;
|
||
|
|
}
|
||
|
|
return name || "Hexagram";
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatTrigramLabel(trigram) {
|
||
|
|
const name = normalizeOption(trigram?.name);
|
||
|
|
const element = normalizeOption(trigram?.element);
|
||
|
|
if (name && element) {
|
||
|
|
return `${name} (${element})`;
|
||
|
|
}
|
||
|
|
return name || element;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatTarotLabel(value) {
|
||
|
|
const raw = normalizeOption(value);
|
||
|
|
if (!raw) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const keyMatch = raw.match(/^key\s*(\d{1,2})\s*:\s*(.+)$/i);
|
||
|
|
if (keyMatch) {
|
||
|
|
const trumpNumber = Number(keyMatch[1]);
|
||
|
|
const fallbackName = normalizeOption(keyMatch[2]);
|
||
|
|
if (typeof getTarotCardDisplayName === "function") {
|
||
|
|
const displayName = normalizeOption(getTarotCardDisplayName(fallbackName, { trumpNumber }));
|
||
|
|
if (displayName) {
|
||
|
|
return displayName;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fallbackName;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof getTarotCardDisplayName === "function") {
|
||
|
|
const displayName = normalizeOption(getTarotCardDisplayName(raw));
|
||
|
|
if (displayName) {
|
||
|
|
return displayName;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return raw.replace(/\bPentacles\b/gi, "Disks");
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPlanetLabelById(planetId, planetsById) {
|
||
|
|
const key = normalizeKey(planetId);
|
||
|
|
const label = planetsById?.[key]?.name;
|
||
|
|
if (label) {
|
||
|
|
return normalizeOption(label);
|
||
|
|
}
|
||
|
|
if (key === "primum-mobile") {
|
||
|
|
return "Primum Mobile";
|
||
|
|
}
|
||
|
|
if (key === "olam-yesodot") {
|
||
|
|
return "Earth / Elements";
|
||
|
|
}
|
||
|
|
return normalizeOption(labelFromId(planetId));
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatPathLetter(path) {
|
||
|
|
const transliteration = normalizeOption(path?.hebrewLetter?.transliteration);
|
||
|
|
const glyph = normalizeOption(path?.hebrewLetter?.char);
|
||
|
|
if (transliteration && glyph) {
|
||
|
|
return `${transliteration} (${glyph})`;
|
||
|
|
}
|
||
|
|
return transliteration || glyph;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getSephiraName(numberValue, idValue, sephiraByNumber, sephiraById) {
|
||
|
|
const numberKey = Number(numberValue);
|
||
|
|
if (Number.isFinite(numberKey) && sephiraByNumber.has(Math.trunc(numberKey))) {
|
||
|
|
return sephiraByNumber.get(Math.trunc(numberKey));
|
||
|
|
}
|
||
|
|
|
||
|
|
const idKey = normalizeKey(idValue);
|
||
|
|
if (idKey && sephiraById.has(idKey)) {
|
||
|
|
return sephiraById.get(idKey);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Number.isFinite(numberKey)) {
|
||
|
|
return `Sephira ${Math.trunc(numberKey)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return labelFromId(idValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
function toRomanNumeral(value) {
|
||
|
|
const numeric = Number(value);
|
||
|
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||
|
|
return String(value || "");
|
||
|
|
}
|
||
|
|
|
||
|
|
const lookup = [
|
||
|
|
[1000, "M"],
|
||
|
|
[900, "CM"],
|
||
|
|
[500, "D"],
|
||
|
|
[400, "CD"],
|
||
|
|
[100, "C"],
|
||
|
|
[90, "XC"],
|
||
|
|
[50, "L"],
|
||
|
|
[40, "XL"],
|
||
|
|
[10, "X"],
|
||
|
|
[9, "IX"],
|
||
|
|
[5, "V"],
|
||
|
|
[4, "IV"],
|
||
|
|
[1, "I"]
|
||
|
|
];
|
||
|
|
|
||
|
|
let current = Math.trunc(numeric);
|
||
|
|
let result = "";
|
||
|
|
lookup.forEach(([size, symbol]) => {
|
||
|
|
while (current >= size) {
|
||
|
|
result += symbol;
|
||
|
|
current -= size;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return result || String(Math.trunc(numeric));
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDecanLabel(decan, signNameById) {
|
||
|
|
const signName = signNameById.get(normalizeKey(decan?.signId)) || labelFromId(decan?.signId);
|
||
|
|
const index = Number(decan?.index);
|
||
|
|
if (!signName || !Number.isFinite(index)) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${signName} Decan ${toRomanNumeral(index)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildEnglishGematriaCipherGroups(englishLetters, gematriaCiphers) {
|
||
|
|
const ciphers = Array.isArray(gematriaCiphers?.ciphers) ? gematriaCiphers.ciphers : [];
|
||
|
|
|
||
|
|
return ciphers
|
||
|
|
.map((cipher) => {
|
||
|
|
const cipherId = normalizeOption(cipher?.id);
|
||
|
|
const cipherName = normalizeOption(cipher?.name || labelFromId(cipherId));
|
||
|
|
const slug = slugifyId(cipherId || cipherName);
|
||
|
|
const values = Array.isArray(cipher?.values) ? cipher.values : [];
|
||
|
|
|
||
|
|
if (!slug || !cipherName || !values.length) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const categoryId = `english-gematria-${slug}`;
|
||
|
|
const category = `English Gematria: ${cipherName}`;
|
||
|
|
const rows = englishLetters
|
||
|
|
.filter((entry) => normalizeOption(entry?.letter) && Number.isFinite(Number(entry?.index)))
|
||
|
|
.map((entry) => {
|
||
|
|
const position = Math.trunc(Number(entry.index));
|
||
|
|
const value = values[position - 1];
|
||
|
|
if (!Number.isFinite(Number(value))) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: normalizeOption(entry.letter),
|
||
|
|
letter: normalizeOption(entry.letter),
|
||
|
|
value: String(Math.trunc(Number(value))),
|
||
|
|
cipherName
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
if (rows.length < 4) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
entries: rows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: categoryId,
|
||
|
|
categoryId,
|
||
|
|
category,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `In the ${entry.cipherName} cipher, ${entry.letter} has a value of`,
|
||
|
|
getAnswer: (entry) => entry.value
|
||
|
|
}
|
||
|
|
]
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildAutomatedConnectionTemplates(referenceData, magickDataset) {
|
||
|
|
if (cache.referenceData === referenceData && cache.magickDataset === magickDataset) {
|
||
|
|
return cache.templates;
|
||
|
|
}
|
||
|
|
|
||
|
|
const planetsById = referenceData?.planets && typeof referenceData.planets === "object"
|
||
|
|
? referenceData.planets
|
||
|
|
: {};
|
||
|
|
const planets = Object.values(planetsById).filter(Boolean);
|
||
|
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||
|
|
const gematriaCiphers = referenceData?.gematriaCiphers && typeof referenceData.gematriaCiphers === "object"
|
||
|
|
? referenceData.gematriaCiphers
|
||
|
|
: {};
|
||
|
|
const decansBySign = referenceData?.decansBySign && typeof referenceData.decansBySign === "object"
|
||
|
|
? referenceData.decansBySign
|
||
|
|
: {};
|
||
|
|
const signNameById = new Map(
|
||
|
|
signs
|
||
|
|
.filter((entry) => normalizeOption(entry?.id) && normalizeOption(entry?.name))
|
||
|
|
.map((entry) => [normalizeKey(entry.id), normalizeOption(entry.name)])
|
||
|
|
);
|
||
|
|
|
||
|
|
const grouped = magickDataset?.grouped || {};
|
||
|
|
const alphabets = grouped.alphabets || {};
|
||
|
|
const englishLetters = Array.isArray(alphabets?.english) ? alphabets.english : [];
|
||
|
|
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
||
|
|
const hebrewById = new Map(
|
||
|
|
hebrewLetters
|
||
|
|
.filter((entry) => normalizeOption(entry?.hebrewLetterId))
|
||
|
|
.map((entry) => [normalizeKey(entry.hebrewLetterId), entry])
|
||
|
|
);
|
||
|
|
|
||
|
|
const kabbalahTree = grouped?.kabbalah?.["kabbalah-tree"] || {};
|
||
|
|
const treePaths = Array.isArray(kabbalahTree?.paths) ? kabbalahTree.paths : [];
|
||
|
|
const treeSephiroth = Array.isArray(kabbalahTree?.sephiroth) ? kabbalahTree.sephiroth : [];
|
||
|
|
const sephiraByNumber = new Map(
|
||
|
|
treeSephiroth
|
||
|
|
.filter((entry) => Number.isFinite(Number(entry?.number)) && normalizeOption(entry?.name))
|
||
|
|
.map((entry) => [Math.trunc(Number(entry.number)), normalizeOption(entry.name)])
|
||
|
|
);
|
||
|
|
const sephiraByTreeId = new Map(
|
||
|
|
treeSephiroth
|
||
|
|
.filter((entry) => normalizeOption(entry?.sephiraId) && normalizeOption(entry?.name))
|
||
|
|
.map((entry) => [normalizeKey(entry.sephiraId), normalizeOption(entry.name)])
|
||
|
|
);
|
||
|
|
const sephirotById = grouped?.kabbalah?.sephirot && typeof grouped.kabbalah.sephirot === "object"
|
||
|
|
? grouped.kabbalah.sephirot
|
||
|
|
: {};
|
||
|
|
|
||
|
|
const cube = grouped?.kabbalah?.cube && typeof grouped.kabbalah.cube === "object"
|
||
|
|
? grouped.kabbalah.cube
|
||
|
|
: {};
|
||
|
|
const cubeWalls = Array.isArray(cube?.walls) ? cube.walls : [];
|
||
|
|
const cubeEdges = Array.isArray(cube?.edges) ? cube.edges : [];
|
||
|
|
const cubeCenter = cube?.center && typeof cube.center === "object" ? cube.center : null;
|
||
|
|
|
||
|
|
const playingCardsData = grouped?.["playing-cards-52"];
|
||
|
|
const playingCards = Array.isArray(playingCardsData)
|
||
|
|
? playingCardsData
|
||
|
|
: (Array.isArray(playingCardsData?.entries) ? playingCardsData.entries : []);
|
||
|
|
const flattenDecans = Object.values(decansBySign).flatMap((entries) => Array.isArray(entries) ? entries : []);
|
||
|
|
|
||
|
|
const iChing = referenceData?.iChing;
|
||
|
|
const trigrams = Array.isArray(iChing?.trigrams) ? iChing.trigrams : [];
|
||
|
|
const hexagrams = Array.isArray(iChing?.hexagrams) ? iChing.hexagrams : [];
|
||
|
|
const correspondences = Array.isArray(iChing?.correspondences?.tarotToTrigram)
|
||
|
|
? iChing.correspondences.tarotToTrigram
|
||
|
|
: [];
|
||
|
|
|
||
|
|
const trigramByKey = new Map(
|
||
|
|
trigrams
|
||
|
|
.map((trigram) => [normalizeKey(trigram?.name), trigram])
|
||
|
|
.filter(([key]) => Boolean(key))
|
||
|
|
);
|
||
|
|
|
||
|
|
const hexagramRows = hexagrams
|
||
|
|
.map((entry) => {
|
||
|
|
const upper = trigramByKey.get(normalizeKey(entry?.upperTrigram)) || null;
|
||
|
|
const lower = trigramByKey.get(normalizeKey(entry?.lowerTrigram)) || null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
...entry,
|
||
|
|
number: Number(entry?.number),
|
||
|
|
hexagramLabel: formatHexagramLabel(entry),
|
||
|
|
upperLabel: formatTrigramLabel(upper || { name: entry?.upperTrigram }),
|
||
|
|
lowerLabel: formatTrigramLabel(lower || { name: entry?.lowerTrigram })
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter((entry) => Number.isFinite(entry.number) && entry.hexagramLabel);
|
||
|
|
|
||
|
|
const tarotCorrespondenceRows = correspondences
|
||
|
|
.map((entry, index) => {
|
||
|
|
const trigram = trigramByKey.get(normalizeKey(entry?.trigram)) || null;
|
||
|
|
return {
|
||
|
|
id: `${index}-${normalizeKey(entry?.tarot)}-${normalizeKey(entry?.trigram)}`,
|
||
|
|
tarotLabel: formatTarotLabel(entry?.tarot),
|
||
|
|
trigramLabel: formatTrigramLabel(trigram || { name: entry?.trigram })
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter((entry) => entry.tarotLabel && entry.trigramLabel);
|
||
|
|
|
||
|
|
const englishGematriaGroups = buildEnglishGematriaCipherGroups(englishLetters, gematriaCiphers);
|
||
|
|
|
||
|
|
const hebrewNumerologyRows = hebrewLetters
|
||
|
|
.filter((entry) => normalizeOption(entry?.name) && normalizeOption(entry?.char) && Number.isFinite(Number(entry?.numerology)))
|
||
|
|
.map((entry) => ({
|
||
|
|
id: normalizeOption(entry.hebrewLetterId || entry.name),
|
||
|
|
glyphLabel: `${normalizeOption(entry.name)} (${normalizeOption(entry.char)})`,
|
||
|
|
char: normalizeOption(entry.char),
|
||
|
|
value: String(Math.trunc(Number(entry.numerology)))
|
||
|
|
}));
|
||
|
|
|
||
|
|
const englishHebrewRows = englishLetters
|
||
|
|
.map((entry) => {
|
||
|
|
const hebrew = hebrewById.get(normalizeKey(entry?.hebrewLetterId)) || null;
|
||
|
|
if (!normalizeOption(entry?.letter) || !hebrew) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
id: normalizeOption(entry.letter),
|
||
|
|
letter: normalizeOption(entry.letter),
|
||
|
|
hebrewLabel: formatHebrewLetterLabel(hebrew, entry?.hebrewLetterId),
|
||
|
|
hebrewChar: normalizeOption(hebrew?.char)
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
const kabbalahPathRows = treePaths
|
||
|
|
.map((path) => {
|
||
|
|
const pathNumber = Number(path?.pathNumber);
|
||
|
|
if (!Number.isFinite(pathNumber)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const pathNumberLabel = String(Math.trunc(pathNumber));
|
||
|
|
const fromName = getSephiraName(path?.connects?.from, path?.connectIds?.from, sephiraByNumber, sephiraByTreeId);
|
||
|
|
const toName = getSephiraName(path?.connects?.to, path?.connectIds?.to, sephiraByNumber, sephiraByTreeId);
|
||
|
|
const letterLabel = formatPathLetter(path);
|
||
|
|
const tarotLabel = formatTarotLabel(path?.tarot?.card);
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: pathNumberLabel,
|
||
|
|
pathNumberLabel,
|
||
|
|
pathPairLabel: fromName && toName ? `${fromName} ↔ ${toName}` : "",
|
||
|
|
fromName,
|
||
|
|
toName,
|
||
|
|
letterLabel,
|
||
|
|
tarotLabel
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
const sephirotRows = Object.values(sephirotById || {})
|
||
|
|
.map((sephira) => {
|
||
|
|
const sephiraName = normalizeOption(sephira?.name?.roman || sephira?.name?.en);
|
||
|
|
const planetLabel = getPlanetLabelById(sephira?.planetId, planetsById);
|
||
|
|
if (!sephiraName || !planetLabel) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
id: normalizeOption(sephira?.id || sephiraName),
|
||
|
|
sephiraName,
|
||
|
|
planetLabel
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
const decanRows = flattenDecans
|
||
|
|
.map((decan) => {
|
||
|
|
const id = normalizeOption(decan?.id);
|
||
|
|
const tarotLabel = formatTarotLabel(decan?.tarotMinorArcana);
|
||
|
|
const decanLabel = formatDecanLabel(decan, signNameById);
|
||
|
|
const rulerLabel = getPlanetLabelById(decan?.rulerPlanetId, planetsById);
|
||
|
|
if (!id || !tarotLabel) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
id,
|
||
|
|
tarotLabel,
|
||
|
|
decanLabel,
|
||
|
|
rulerLabel
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
const cubeTarotRows = [
|
||
|
|
...cubeWalls.map((wall) => {
|
||
|
|
const wallName = normalizeOption(wall?.name || labelFromId(wall?.id));
|
||
|
|
const locationLabel = wallName ? `${wallName} Wall` : "";
|
||
|
|
const tarotLabel = formatTarotLabel(wall?.associations?.tarotCard);
|
||
|
|
return tarotLabel && locationLabel
|
||
|
|
? { id: normalizeOption(wall?.id || wallName), tarotLabel, locationLabel }
|
||
|
|
: null;
|
||
|
|
}),
|
||
|
|
...cubeEdges.map((edge) => {
|
||
|
|
const edgeName = normalizeOption(edge?.name || labelFromId(edge?.id));
|
||
|
|
const locationLabel = edgeName ? `${edgeName} Edge` : "";
|
||
|
|
const hebrew = hebrewById.get(normalizeKey(edge?.hebrewLetterId)) || null;
|
||
|
|
const tarotLabel = formatTarotLabel(hebrew?.tarot?.card);
|
||
|
|
return tarotLabel && locationLabel
|
||
|
|
? { id: normalizeOption(edge?.id || edgeName), tarotLabel, locationLabel }
|
||
|
|
: null;
|
||
|
|
}),
|
||
|
|
(() => {
|
||
|
|
const tarotLabel = formatTarotLabel(cubeCenter?.associations?.tarotCard || cubeCenter?.tarotCard);
|
||
|
|
return tarotLabel ? { id: "center", tarotLabel, locationLabel: "Center" } : null;
|
||
|
|
})()
|
||
|
|
].filter(Boolean);
|
||
|
|
|
||
|
|
const cubeHebrewRows = [
|
||
|
|
...cubeWalls.map((wall) => {
|
||
|
|
const wallName = normalizeOption(wall?.name || labelFromId(wall?.id));
|
||
|
|
const locationLabel = wallName ? `${wallName} Wall` : "";
|
||
|
|
const hebrew = hebrewById.get(normalizeKey(wall?.hebrewLetterId)) || null;
|
||
|
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
||
|
|
return locationLabel && hebrewLabel
|
||
|
|
? { id: normalizeOption(wall?.id || wallName), locationLabel, hebrewLabel }
|
||
|
|
: null;
|
||
|
|
}),
|
||
|
|
...cubeEdges.map((edge) => {
|
||
|
|
const edgeName = normalizeOption(edge?.name || labelFromId(edge?.id));
|
||
|
|
const locationLabel = edgeName ? `${edgeName} Edge` : "";
|
||
|
|
const hebrew = hebrewById.get(normalizeKey(edge?.hebrewLetterId)) || null;
|
||
|
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
||
|
|
return locationLabel && hebrewLabel
|
||
|
|
? { id: normalizeOption(edge?.id || edgeName), locationLabel, hebrewLabel }
|
||
|
|
: null;
|
||
|
|
}),
|
||
|
|
(() => {
|
||
|
|
const hebrew = hebrewById.get(normalizeKey(cubeCenter?.hebrewLetterId)) || null;
|
||
|
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, cubeCenter?.hebrewLetterId);
|
||
|
|
return hebrewLabel ? { id: "center", locationLabel: "Center", hebrewLabel } : null;
|
||
|
|
})()
|
||
|
|
].filter(Boolean);
|
||
|
|
|
||
|
|
const playingCardRows = playingCards
|
||
|
|
.map((entry) => {
|
||
|
|
const cardId = normalizeOption(entry?.id);
|
||
|
|
const rankLabel = normalizeOption(entry?.rankLabel || entry?.rank);
|
||
|
|
const suitLabel = normalizeOption(entry?.suitLabel || labelFromId(entry?.suit));
|
||
|
|
const tarotLabel = formatTarotLabel(entry?.tarotCard);
|
||
|
|
const playingCardLabel = rankLabel && suitLabel ? `${rankLabel} of ${suitLabel}` : "";
|
||
|
|
return cardId && playingCardLabel && tarotLabel
|
||
|
|
? { id: cardId, playingCardLabel, tarotLabel }
|
||
|
|
: null;
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
const specGroups = [
|
||
|
|
...englishGematriaGroups,
|
||
|
|
{
|
||
|
|
entries: hebrewNumerologyRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "hebrew-number",
|
||
|
|
categoryId: "hebrew-numerology",
|
||
|
|
category: "Hebrew Gematria",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.glyphLabel} has a gematria value of`,
|
||
|
|
getAnswer: (entry) => entry.value
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: englishHebrewRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "english-hebrew",
|
||
|
|
categoryId: "english-hebrew-mapping",
|
||
|
|
category: "Alphabet Mapping",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.letter} maps to which Hebrew letter`,
|
||
|
|
getAnswer: (entry) => entry.hebrewLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "hebrew-english",
|
||
|
|
getUniquenessKey: (entry) => entry.hebrewLabel,
|
||
|
|
getKey: (entry) => entry.hebrewChar || entry.hebrewLabel,
|
||
|
|
getPrompt: (entry) => `${entry.hebrewLabel} maps to which English letter`,
|
||
|
|
getAnswer: (entry) => entry.letter
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: signs.filter((entry) => entry?.name && entry?.rulingPlanetId),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "zodiac-ruler",
|
||
|
|
categoryId: "zodiac-rulers",
|
||
|
|
category: "Zodiac Rulers",
|
||
|
|
getKey: (entry) => entry.id || entry.name,
|
||
|
|
getPrompt: (entry) => `${entry.name} is ruled by`,
|
||
|
|
getAnswer: (entry) => getPlanetLabelById(entry.rulingPlanetId, planetsById)
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: signs.filter((entry) => entry?.name && entry?.element),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "zodiac-element",
|
||
|
|
categoryId: "zodiac-elements",
|
||
|
|
category: "Zodiac Elements",
|
||
|
|
getKey: (entry) => entry.id || entry.name,
|
||
|
|
getPrompt: (entry) => `${entry.name} is`,
|
||
|
|
getAnswer: (entry) => normalizeOption(entry.element).replace(/^./, (char) => char.toUpperCase())
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: planets.filter((entry) => entry?.name && entry?.weekday),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "planet-weekday",
|
||
|
|
categoryId: "planetary-weekdays",
|
||
|
|
category: "Planetary Weekdays",
|
||
|
|
getKey: (entry) => entry.id || entry.name,
|
||
|
|
getPrompt: (entry) => `${entry.name} corresponds to`,
|
||
|
|
getAnswer: (entry) => normalizeOption(entry.weekday),
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "weekday-planet",
|
||
|
|
getUniquenessKey: (entry) => normalizeOption(entry.weekday),
|
||
|
|
getKey: (entry) => normalizeOption(entry.weekday),
|
||
|
|
getPrompt: (entry) => `${normalizeOption(entry.weekday)} corresponds to which planet`,
|
||
|
|
getAnswer: (entry) => normalizeOption(entry.name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: signs.filter((entry) => entry?.name && (entry?.tarot?.majorArcana || entry?.tarot?.card)),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "zodiac-tarot",
|
||
|
|
categoryId: "zodiac-tarot",
|
||
|
|
category: "Zodiac ↔ Tarot",
|
||
|
|
getKey: (entry) => entry.id || entry.name,
|
||
|
|
getPrompt: (entry) => `${entry.name} corresponds to`,
|
||
|
|
getAnswer: (entry) => formatTarotLabel(entry?.tarot?.majorArcana || entry?.tarot?.card),
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "tarot-zodiac",
|
||
|
|
getUniquenessKey: (entry) => formatTarotLabel(entry?.tarot?.majorArcana || entry?.tarot?.card),
|
||
|
|
getKey: (entry) => formatTarotLabel(entry?.tarot?.majorArcana || entry?.tarot?.card),
|
||
|
|
getPrompt: (entry) => `${formatTarotLabel(entry?.tarot?.majorArcana || entry?.tarot?.card)} corresponds to which zodiac sign`,
|
||
|
|
getAnswer: (entry) => normalizeOption(entry.name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: kabbalahPathRows.filter((entry) => entry.fromName && entry.toName),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "kabbalah-path-between",
|
||
|
|
categoryId: "kabbalah-path-between-sephirot",
|
||
|
|
category: "Kabbalah Paths",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `What path connects ${entry.fromName} and ${entry.toName}`,
|
||
|
|
getAnswer: (entry) => entry.pathNumberLabel
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: kabbalahPathRows.filter((entry) => entry.letterLabel),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "kabbalah-path-letter",
|
||
|
|
categoryId: "kabbalah-path-letter",
|
||
|
|
category: "Kabbalah Paths",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `Path ${entry.pathNumberLabel} carries which Hebrew letter`,
|
||
|
|
getAnswer: (entry) => entry.letterLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "kabbalah-letter-path-number",
|
||
|
|
getUniquenessKey: (entry) => entry.letterLabel,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.letterLabel} belongs to which path`,
|
||
|
|
getAnswer: (entry) => entry.pathNumberLabel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: kabbalahPathRows.filter((entry) => entry.tarotLabel),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "kabbalah-path-tarot",
|
||
|
|
categoryId: "kabbalah-path-tarot",
|
||
|
|
category: "Kabbalah ↔ Tarot",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `Path ${entry.pathNumberLabel} corresponds to which Tarot trump`,
|
||
|
|
getAnswer: (entry) => entry.tarotLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "tarot-trump-path",
|
||
|
|
getUniquenessKey: (entry) => entry.tarotLabel,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.tarotLabel} is on which path`,
|
||
|
|
getAnswer: (entry) => entry.pathNumberLabel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: sephirotRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "sephirot-planet",
|
||
|
|
categoryId: "sephirot-planets",
|
||
|
|
category: "Sephirot ↔ Planet",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.sephiraName} corresponds to which planet`,
|
||
|
|
getAnswer: (entry) => entry.planetLabel
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: decanRows.filter((entry) => entry.decanLabel),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "tarot-decan-sign",
|
||
|
|
categoryId: "tarot-decan-sign",
|
||
|
|
category: "Tarot Decans",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.tarotLabel} belongs to which decan`,
|
||
|
|
getAnswer: (entry) => entry.decanLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "decan-tarot-card",
|
||
|
|
getUniquenessKey: (entry) => entry.decanLabel,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.decanLabel} corresponds to which Tarot card`,
|
||
|
|
getAnswer: (entry) => entry.tarotLabel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: decanRows.filter((entry) => entry.rulerLabel),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "tarot-decan-ruler",
|
||
|
|
categoryId: "tarot-decan-ruler",
|
||
|
|
category: "Tarot Decans",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `The decan of ${entry.tarotLabel} is ruled by`,
|
||
|
|
getAnswer: (entry) => entry.rulerLabel
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: cubeTarotRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "tarot-cube-location",
|
||
|
|
categoryId: "tarot-cube-location",
|
||
|
|
category: "Tarot ↔ Cube",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.tarotLabel} is on which Cube location`,
|
||
|
|
getAnswer: (entry) => entry.locationLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "cube-location-tarot",
|
||
|
|
getUniquenessKey: (entry) => entry.locationLabel,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.locationLabel} corresponds to which Tarot card`,
|
||
|
|
getAnswer: (entry) => entry.tarotLabel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: cubeHebrewRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "cube-hebrew-letter",
|
||
|
|
categoryId: "cube-hebrew-letter",
|
||
|
|
category: "Cube ↔ Hebrew",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.locationLabel} corresponds to which Hebrew letter`,
|
||
|
|
getAnswer: (entry) => entry.hebrewLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "hebrew-cube-location",
|
||
|
|
getUniquenessKey: (entry) => entry.hebrewLabel,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.hebrewLabel} is on which Cube location`,
|
||
|
|
getAnswer: (entry) => entry.locationLabel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: playingCardRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "playing-card-tarot",
|
||
|
|
categoryId: "playing-card-tarot",
|
||
|
|
category: "Playing Card ↔ Tarot",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.playingCardLabel} maps to which Tarot card`,
|
||
|
|
getAnswer: (entry) => entry.tarotLabel,
|
||
|
|
inverse: {
|
||
|
|
keyPrefix: "tarot-playing-card",
|
||
|
|
getUniquenessKey: (entry) => entry.tarotLabel,
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.tarotLabel} maps to which playing card`,
|
||
|
|
getAnswer: (entry) => entry.playingCardLabel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: hexagramRows.filter((entry) => normalizeOption(entry.planetaryInfluence)),
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "iching-planet",
|
||
|
|
categoryId: "iching-planetary-influence",
|
||
|
|
category: "I Ching ↔ Planet",
|
||
|
|
getKey: (entry) => String(entry.number),
|
||
|
|
getPrompt: (entry) => `${entry.hexagramLabel} corresponds to which planetary influence`,
|
||
|
|
getAnswer: (entry) => normalizeOption(entry.planetaryInfluence)
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: hexagramRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "iching-upper-trigram",
|
||
|
|
categoryId: "iching-trigrams",
|
||
|
|
category: "I Ching Trigrams",
|
||
|
|
getKey: (entry) => `${entry.number}-upper`,
|
||
|
|
getPrompt: (entry) => `What is the upper trigram of ${entry.hexagramLabel}`,
|
||
|
|
getAnswer: (entry) => entry.upperLabel
|
||
|
|
},
|
||
|
|
{
|
||
|
|
keyPrefix: "iching-lower-trigram",
|
||
|
|
categoryId: "iching-trigrams",
|
||
|
|
category: "I Ching Trigrams",
|
||
|
|
getKey: (entry) => `${entry.number}-lower`,
|
||
|
|
getPrompt: (entry) => `What is the lower trigram of ${entry.hexagramLabel}`,
|
||
|
|
getAnswer: (entry) => entry.lowerLabel
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
entries: tarotCorrespondenceRows,
|
||
|
|
variants: [
|
||
|
|
{
|
||
|
|
keyPrefix: "iching-tarot-trigram",
|
||
|
|
categoryId: "iching-tarot-correspondence",
|
||
|
|
category: "I Ching ↔ Tarot",
|
||
|
|
getKey: (entry) => entry.id,
|
||
|
|
getPrompt: (entry) => `${entry.tarotLabel} corresponds to which I Ching trigram`,
|
||
|
|
getAnswer: (entry) => entry.trigramLabel
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const templates = specGroups.flatMap((specGroup) => {
|
||
|
|
if (Array.isArray(specGroup.variants) && specGroup.variants.length > 1) {
|
||
|
|
return buildTemplatesFromVariants(specGroup);
|
||
|
|
}
|
||
|
|
|
||
|
|
const [variant] = specGroup.variants || [];
|
||
|
|
return buildTemplatesFromSpec({
|
||
|
|
entries: specGroup.entries,
|
||
|
|
categoryId: variant?.categoryId,
|
||
|
|
category: variant?.category,
|
||
|
|
keyPrefix: variant?.keyPrefix,
|
||
|
|
getKey: variant?.getKey,
|
||
|
|
getPrompt: variant?.getPrompt,
|
||
|
|
getAnswer: variant?.getAnswer
|
||
|
|
});
|
||
|
|
});
|
||
|
|
const templatesByCategory = new Map();
|
||
|
|
|
||
|
|
templates.forEach((template) => {
|
||
|
|
if (!templatesByCategory.has(template.categoryId)) {
|
||
|
|
templatesByCategory.set(template.categoryId, []);
|
||
|
|
}
|
||
|
|
templatesByCategory.get(template.categoryId).push(template);
|
||
|
|
});
|
||
|
|
|
||
|
|
cache.referenceData = referenceData;
|
||
|
|
cache.magickDataset = magickDataset;
|
||
|
|
cache.templates = templates;
|
||
|
|
cache.templatesByCategory = templatesByCategory;
|
||
|
|
|
||
|
|
return templates;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildConnectionTemplates(referenceData, magickDataset) {
|
||
|
|
return buildAutomatedConnectionTemplates(referenceData, magickDataset).slice();
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerConnectionQuizCategories() {
|
||
|
|
const { registerQuizCategory } = window.QuizSectionUi || {};
|
||
|
|
if (typeof registerQuizCategory !== "function") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
registerQuizCategory("quiz-connections", "Connection Quizzes", buildConnectionTemplates);
|
||
|
|
}
|
||
|
|
|
||
|
|
registerConnectionQuizCategories();
|
||
|
|
|
||
|
|
window.QuizConnectionsPlugin = {
|
||
|
|
registerConnectionQuizCategories,
|
||
|
|
buildAutomatedConnectionTemplates,
|
||
|
|
buildConnectionTemplates
|
||
|
|
};
|
||
|
|
})();
|