Set analyser
Choose a set and get its fingerprint: the mana curve, how the elements and card types share it, a few notable cards, and facts that no API field holds. Add a second set to see the two side by side. Under the tool, how it is built.
The report appears here.
Build a set analyser
One Kairos query can give us an entire Sorcery set as structured JSON. From there, ordinary JavaScript is enough to build mana curves, element distributions, card-type breakdowns and other statistics. No key, no library; the code below is the file this page runs.
1. Fetch the set
s:001 is the same search syntax you can type into the search box: every card printed in set 001. The request goes to /cards on the query API. Each answer holds up to two hundred card records in body.data and says in body.has_more whether another page exists, so the loop asks until there is none. That is the whole API.
// ── 1. Fetch the set
export async function getSetCards(set) {
let cards = [];
for (let page = 1; ; page++) {
const query = "s:" + set;
const url =
QUERY + "/cards?q=" + encodeURIComponent(query) +
"&page_size=200&page=" + page;
const response = await fetch(url);
if (!response.ok) {
throw new Error("API returned " + response.status);
}
const body = await response.json();
cards.push(...body.data);
if (!body.has_more) {
break;
}
}
return cards;
}2. Build a mana curve
Each record has a cost: a number, or null for sites, avatars and X-cost cards. Count the cards at each cost into an object and the curve is done.
// ── 2. Build a mana curve
export function manaCurve(cards) {
const curve = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, "6+": 0 };
for (const card of cards) {
if (typeof card.cost !== "number") {
continue;
}
if (card.cost >= 6) {
curve["6+"]++;
} else {
curve[card.cost]++;
}
}
return curve;
}3. Turn counts into percentages
A tiny helper the next two steps share: an object of counts in, the same keys as whole-number percentages out.
// ── 3. Turn counts into percentages
export function percentages(counts, total) {
const shares = {};
for (const key in counts) {
shares[key] = Math.round((counts[key] / total) * 100);
}
return shares;
}4. Analyse elements
elements is a list. A card with two elements counts once for each, and a card with none carries "None", so elementless cards are counted too. The shares are percentages of the set's cards, which is why they can add up to more than a hundred.
// ── 4. Analyse elements
export function elementShares(cards) {
const counts = {
Air: 0,
Earth: 0,
Fire: 0,
Water: 0,
None: 0
};
for (const card of cards) {
for (const element of card.elements) {
counts[element]++;
}
}
return percentages(counts, cards.length);
}5. Analyse card types
type is one of Minion, Magic, Aura, Artifact, Site and Avatar. Same idea, with the counters created as the types turn up.
// ── 5. Analyse card types
export function typeShares(cards) {
const counts = {};
for (const card of cards) {
const type = card.type;
if (!counts[type]) {
counts[type] = 0;
}
counts[type]++;
}
return percentages(counts, cards.length);
}6. Find notable cards
The records are objects, so they can be compared, not just counted. Each of these walks the set once and keeps the best it has seen: the most expensive card, the three largest subtype groups from the subtypes list, and the strongest minion. power is worked out by Kairos from attack and defense, so that one needs no arithmetic here.
// ── 6. Find notable cards
export function mostExpensive(cards) {
let result = null;
for (const card of cards) {
if (typeof card.cost !== "number") {
continue;
}
if (!result || card.cost > result.cost) {
result = card;
}
}
return result;
}
// card.subtypes is a list, like ["Mortal", "Wizard"]. Count each one,
// then keep the three biggest groups.
export function topSubtypes(cards) {
const counts = {};
for (const card of cards) {
for (const subtype of card.subtypes) {
if (!counts[subtype]) {
counts[subtype] = 0;
}
counts[subtype]++;
}
}
let groups = [];
for (const subtype in counts) {
groups.push({ subtype: subtype, count: counts[subtype] });
}
groups.sort((a, b) => b.count - a.count);
return groups.slice(0, 3);
}
export function strongestMinion(cards) {
let result = null;
for (const card of cards) {
if (card.type !== "Minion") {
continue;
}
if (typeof card.power !== "number") {
continue;
}
if (!result || card.power > result.power) {
result = card;
}
}
return result;
}7. Derive some useful facts
None of these numbers is an API field. One more pass over the records and they exist.
// ── 7. Derive some useful facts
export function facts(cards) {
let costed = 0;
let costSum = 0;
let cheap = 0;
let minions = 0;
let strong = 0;
let multi = 0;
let elementless = 0;
for (const card of cards) {
if (typeof card.cost === "number") {
costed++;
costSum += card.cost;
if (card.cost <= 4) {
cheap++;
}
}
if (card.type === "Minion" && typeof card.power === "number") {
minions++;
if (card.power >= 3) {
strong++;
}
}
if (card.elements.length > 1) {
multi++;
}
if (card.elements.includes("None")) {
elementless++;
}
}
return {
averageCost: Math.round((costSum / costed) * 10) / 10,
cheapShare: Math.round((cheap / costed) * 100),
strongMinionShare: Math.round((strong / minions) * 100),
multiElement: multi,
elementlessShare: Math.round((elementless / cards.length) * 100)
};
}8. Build the report
Everything gathered into one object by one function.
// ── 8. Build the report
export async function analyseSet(set) {
const cards = await getSetCards(set);
const numbers = facts(cards);
return {
set: set,
cards: cards,
curve: manaCurve(cards),
elements: elementShares(cards),
types: typeShares(cards),
notable: {
"Largest subtype groups": topSubtypes(cards),
"Most expensive card": mostExpensive(cards),
"Highest-power Minion": strongestMinion(cards)
},
facts: [
numbers.cheapShare + "% of cards cost 4 or less.",
numbers.strongMinionShare + "% of Minions have 3+ power.",
numbers.multiElement + " cards use more than one element.",
numbers.elementlessShare + "% of the set has no element."
]
};
}Try it
const report = await analyseSet("001");
console.log(report.curve);
console.log(report.elements);
console.log(report.types);
console.log(report.notable);
console.log(report.facts);We only asked Kairos for the set once. Everything else, the mana curve, the percentages, the notable cards and the facts, came from analysing the returned records locally.
See the complete code
The file in one block: set-analyser.js. The form and the bars are their own file, analyser-page.js, and not part of the tutorial.
// Build a set analyser with the Kairos Archive query API.
// One query fetches a whole Sorcery set as structured JSON.
// Plain JavaScript does the rest.
// (The "export" words only let the page import these functions.)
export const QUERY = "https://query.kairosarchive.net";
// ── 1. Fetch the set
export async function getSetCards(set) {
let cards = [];
for (let page = 1; ; page++) {
const query = "s:" + set;
const url =
QUERY + "/cards?q=" + encodeURIComponent(query) +
"&page_size=200&page=" + page;
const response = await fetch(url);
if (!response.ok) {
throw new Error("API returned " + response.status);
}
const body = await response.json();
cards.push(...body.data);
if (!body.has_more) {
break;
}
}
return cards;
}
// ── 2. Build a mana curve
export function manaCurve(cards) {
const curve = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, "6+": 0 };
for (const card of cards) {
if (typeof card.cost !== "number") {
continue;
}
if (card.cost >= 6) {
curve["6+"]++;
} else {
curve[card.cost]++;
}
}
return curve;
}
// ── 3. Turn counts into percentages
export function percentages(counts, total) {
const shares = {};
for (const key in counts) {
shares[key] = Math.round((counts[key] / total) * 100);
}
return shares;
}
// ── 4. Analyse elements
export function elementShares(cards) {
const counts = {
Air: 0,
Earth: 0,
Fire: 0,
Water: 0,
None: 0
};
for (const card of cards) {
for (const element of card.elements) {
counts[element]++;
}
}
return percentages(counts, cards.length);
}
// ── 5. Analyse card types
export function typeShares(cards) {
const counts = {};
for (const card of cards) {
const type = card.type;
if (!counts[type]) {
counts[type] = 0;
}
counts[type]++;
}
return percentages(counts, cards.length);
}
// ── 6. Find notable cards
export function mostExpensive(cards) {
let result = null;
for (const card of cards) {
if (typeof card.cost !== "number") {
continue;
}
if (!result || card.cost > result.cost) {
result = card;
}
}
return result;
}
// card.subtypes is a list, like ["Mortal", "Wizard"]. Count each one,
// then keep the three biggest groups.
export function topSubtypes(cards) {
const counts = {};
for (const card of cards) {
for (const subtype of card.subtypes) {
if (!counts[subtype]) {
counts[subtype] = 0;
}
counts[subtype]++;
}
}
let groups = [];
for (const subtype in counts) {
groups.push({ subtype: subtype, count: counts[subtype] });
}
groups.sort((a, b) => b.count - a.count);
return groups.slice(0, 3);
}
export function strongestMinion(cards) {
let result = null;
for (const card of cards) {
if (card.type !== "Minion") {
continue;
}
if (typeof card.power !== "number") {
continue;
}
if (!result || card.power > result.power) {
result = card;
}
}
return result;
}
// ── 7. Derive some useful facts
export function facts(cards) {
let costed = 0;
let costSum = 0;
let cheap = 0;
let minions = 0;
let strong = 0;
let multi = 0;
let elementless = 0;
for (const card of cards) {
if (typeof card.cost === "number") {
costed++;
costSum += card.cost;
if (card.cost <= 4) {
cheap++;
}
}
if (card.type === "Minion" && typeof card.power === "number") {
minions++;
if (card.power >= 3) {
strong++;
}
}
if (card.elements.length > 1) {
multi++;
}
if (card.elements.includes("None")) {
elementless++;
}
}
return {
averageCost: Math.round((costSum / costed) * 10) / 10,
cheapShare: Math.round((cheap / costed) * 100),
strongMinionShare: Math.round((strong / minions) * 100),
multiElement: multi,
elementlessShare: Math.round((elementless / cards.length) * 100)
};
}
// ── 8. Build the report
export async function analyseSet(set) {
const cards = await getSetCards(set);
const numbers = facts(cards);
return {
set: set,
cards: cards,
curve: manaCurve(cards),
elements: elementShares(cards),
types: typeShares(cards),
notable: {
"Largest subtype groups": topSubtypes(cards),
"Most expensive card": mostExpensive(cards),
"Highest-power Minion": strongestMinion(cards)
},
facts: [
numbers.cheapShare + "% of cards cost 4 or less.",
numbers.strongMinionShare + "% of Minions have 3+ power.",
numbers.multiElement + " cards use more than one element.",
numbers.elementlessShare + "% of the set has no element."
]
};
}
const report = await analyseSet("001");
console.log(report.curve);
console.log(report.elements);
console.log(report.types);
console.log(report.notable);
console.log(report.facts);