Random deck
Pick elements and sets, say how the spellbook splits and whether Toolbox is in, and deal. Every part of the deck is one search query, and the list shows the query each part came from and how many cards it matched. Under the generator, the code that does it, section by section.
Your deck appears here.
Build a random Sorcery deck with the Kairos API
Kairos Archive exposes the same search system used by the website through a simple JSON API. You do not need an API key or a JavaScript library. In this example we search for cards, handle pagination, and use the returned records to generate a random deck. The code below is the file this page runs, in plain JavaScript.
1. Describe the deck
A few constants for the rules the deck must follow, and the shape of a choice: which elements, which sets, how many Toolbox, and how the sixty spells split by type. This is your data, not the API's.
// ── 1. Describe the deck
export const QUERY = "https://query.kairosarchive.net";
// How many copies of one card a deck may hold, by rarity.
export const COPIES = { Ordinary: 4, Exceptional: 3, Elite: 2, Unique: 1 };
export const ATLAS = 30; // sites in the atlas
export const COLLECTION = 10; // Ordinary spells for Toolbox to cast from
// The spellbook is 60 spells, split by type. A choice looks like this:
// { elements: ["Fire", "Water"], sets: ["001", "006"], toolbox: 3,
// split: { Artifact: 8, Aura: 6, Magic: 16, Minion: 27 } }2. Turn those choices into a Kairos query
One query per part of the deck, in the same syntax you can type into the search box at the top of this page. t:minion (e:fire,water or e:none) s:001,006 is:booster reads: minions, with Fire or Water or no element, printed in a booster of Alpha or Gothic. The full syntax is one page.
// ── 2. Turn those choices into a Kairos query
// The syntax is the same one the search box on kairosarchive.net takes:
// t:minion every minion (t:site, t:avatar, ...)
// e:fire,water has Fire or Water; e:none has no element at all
// s:001,006 printed in Alpha or Gothic; is:booster skips promos
// !"Toolbox" exactly that card; -!"Toolbox" leaves it out
export function makeQuery(part, choice) {
if (part === "Avatar") {
return 't:avatar -!"Duplicator" -!"Magician"';
}
if (part === "Toolbox") {
return '!"Toolbox"';
}
let query = "t:" + part.toLowerCase();
if (part === "Collection") {
query = "cat:spell rarity:ordinary";
}
if (choice.elements.length > 0) {
query += " (e:" + choice.elements.join(",").toLowerCase() + " or e:none)";
} else {
query += " e:none";
}
if (choice.sets.length > 0) {
query += " s:" + choice.sets.join(",") + " is:booster";
}
if (part === "Artifact" && choice.toolbox > 0) {
query += ' -!"Toolbox"'; // Toolbox is its own part of the deck
}
return query;
}3. Fetch matching cards
This is the whole API. fetch() calls /cards?q=…, body.data is an array of card records, and body.has_more says whether another page exists. Pages hold up to two hundred cards, so the loop asks until there are no more.
// ── 3. Fetch matching cards
// One request per page. body.data holds the card records;
// body.has_more says whether another page exists.
export async function getCards(query) {
let cards = [];
for (let page = 1; ; page++) {
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;
}4. Use the card records
From here on it is ordinary JavaScript over ordinary objects. Each record has name, type, rarity, elements and the rest of the card object. Two things this deck needs: a query like e:fire,water means Fire or Water, so a Fire and Air card slips in, and the generator should reject any card with an element the user did not pick. And a draw that respects the copy limits: four of an Ordinary, three of an Exceptional, two of an Elite, one of a Unique.
// ── 4. Use the card records
// e:fire,water also matches a card with Fire and Air, because it has
// Fire. Keep only cards whose every element was chosen. "None" is fine.
export function onlyChosenElements(cards, elements) {
return cards.filter((card) =>
card.elements.every((element) => element === "None" || elements.includes(element))
);
}
// Shuffle, then take a random number of copies of each card, never
// more than its rarity allows, until the part is full.
export function drawCards(cards, size) {
const shuffled = cards.slice().sort(() => Math.random() - 0.5);
let picked = [];
let total = 0;
for (const card of shuffled) {
if (total >= size) {
break;
}
let limit = COPIES[card.rarity] || 1;
if (limit > size - total) {
limit = size - total;
}
const copies = 1 + Math.floor(Math.random() * limit);
picked.push({ card: card, copies: copies });
total += copies;
}
return picked;
}5. Assemble the deck
The parts in order: the avatar, the spellbook by type, Toolbox, the sites, and, when Toolbox is in, a collection of ten Ordinary spells for it to cast. For each part: build the query, fetch the cards, filter them, draw, add the part to the deck. A card's copies count across the whole deck, so a name already used is skipped.
// ── 5. Assemble the deck
export async function makeDeck(choice) {
const parts = {
Avatar: 1,
Artifact: choice.split.Artifact,
Aura: choice.split.Aura,
Magic: choice.split.Magic,
Minion: choice.split.Minion,
Toolbox: choice.toolbox,
Site: ATLAS,
Collection: choice.toolbox > 0 ? COLLECTION : 0,
};
let deck = [];
let usedNames = [];
for (const part in parts) {
const size = parts[part];
if (size === 0) {
continue;
}
const query = makeQuery(part, choice);
let cards = await getCards(query);
cards = onlyChosenElements(cards, choice.elements);
cards = cards.filter((card) => !usedNames.includes(card.name)); // copies count across the deck
let picked;
if (part === "Toolbox") {
picked = [{ card: cards[0], copies: choice.toolbox }];
} else {
picked = drawCards(cards, size);
}
for (const pick of picked) {
usedNames.push(pick.card.name);
}
deck.push({ part: part, query: query, matched: cards.length, size: size, cards: picked });
}
return deck;
}Try it
Describe a deck and ask for it. What comes back is an array of parts, each with its query, how many cards matched, and the cards drawn.
const choice = {
elements: ["Fire", "Water"],
sets: ["001", "006"],
toolbox: 3,
split: { Artifact: 8, Aura: 6, Magic: 16, Minion: 27 },
};
const deck = await makeDeck(choice);
console.log(deck);Kairos handles finding and structuring the card data. Your application logic can stay ordinary, boring JavaScript.
See the complete code
The file in one block: random-deck.js. The form above and the printed list are their own file, deck-page.js, and not part of the tutorial.
// Build a random Sorcery deck with the Kairos Archive query API.
// Plain JavaScript: no key, no library. The page /fun/random-deck
// walks through this file section by section.
// ── 1. Describe the deck
export const QUERY = "https://query.kairosarchive.net";
// How many copies of one card a deck may hold, by rarity.
export const COPIES = { Ordinary: 4, Exceptional: 3, Elite: 2, Unique: 1 };
export const ATLAS = 30; // sites in the atlas
export const COLLECTION = 10; // Ordinary spells for Toolbox to cast from
// The spellbook is 60 spells, split by type. A choice looks like this:
// { elements: ["Fire", "Water"], sets: ["001", "006"], toolbox: 3,
// split: { Artifact: 8, Aura: 6, Magic: 16, Minion: 27 } }
// ── 2. Turn those choices into a Kairos query
// The syntax is the same one the search box on kairosarchive.net takes:
// t:minion every minion (t:site, t:avatar, ...)
// e:fire,water has Fire or Water; e:none has no element at all
// s:001,006 printed in Alpha or Gothic; is:booster skips promos
// !"Toolbox" exactly that card; -!"Toolbox" leaves it out
export function makeQuery(part, choice) {
if (part === "Avatar") {
return 't:avatar -!"Duplicator" -!"Magician"';
}
if (part === "Toolbox") {
return '!"Toolbox"';
}
let query = "t:" + part.toLowerCase();
if (part === "Collection") {
query = "cat:spell rarity:ordinary";
}
if (choice.elements.length > 0) {
query += " (e:" + choice.elements.join(",").toLowerCase() + " or e:none)";
} else {
query += " e:none";
}
if (choice.sets.length > 0) {
query += " s:" + choice.sets.join(",") + " is:booster";
}
if (part === "Artifact" && choice.toolbox > 0) {
query += ' -!"Toolbox"'; // Toolbox is its own part of the deck
}
return query;
}
// ── 3. Fetch matching cards
// One request per page. body.data holds the card records;
// body.has_more says whether another page exists.
export async function getCards(query) {
let cards = [];
for (let page = 1; ; page++) {
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;
}
// ── 4. Use the card records
// e:fire,water also matches a card with Fire and Air, because it has
// Fire. Keep only cards whose every element was chosen. "None" is fine.
export function onlyChosenElements(cards, elements) {
return cards.filter((card) =>
card.elements.every((element) => element === "None" || elements.includes(element))
);
}
// Shuffle, then take a random number of copies of each card, never
// more than its rarity allows, until the part is full.
export function drawCards(cards, size) {
const shuffled = cards.slice().sort(() => Math.random() - 0.5);
let picked = [];
let total = 0;
for (const card of shuffled) {
if (total >= size) {
break;
}
let limit = COPIES[card.rarity] || 1;
if (limit > size - total) {
limit = size - total;
}
const copies = 1 + Math.floor(Math.random() * limit);
picked.push({ card: card, copies: copies });
total += copies;
}
return picked;
}
// ── 5. Assemble the deck
export async function makeDeck(choice) {
const parts = {
Avatar: 1,
Artifact: choice.split.Artifact,
Aura: choice.split.Aura,
Magic: choice.split.Magic,
Minion: choice.split.Minion,
Toolbox: choice.toolbox,
Site: ATLAS,
Collection: choice.toolbox > 0 ? COLLECTION : 0,
};
let deck = [];
let usedNames = [];
for (const part in parts) {
const size = parts[part];
if (size === 0) {
continue;
}
const query = makeQuery(part, choice);
let cards = await getCards(query);
cards = onlyChosenElements(cards, choice.elements);
cards = cards.filter((card) => !usedNames.includes(card.name)); // copies count across the deck
let picked;
if (part === "Toolbox") {
picked = [{ card: cards[0], copies: choice.toolbox }];
} else {
picked = drawCards(cards, size);
}
for (const pick of picked) {
usedNames.push(pick.card.name);
}
deck.push({ part: part, query: query, matched: cards.length, size: size, cards: picked });
}
return deck;
}
const choice = {
elements: ["Fire", "Water"],
sets: ["001", "006"],
toolbox: 3,
split: { Artifact: 8, Aura: 6, Magic: 16, Minion: 27 },
};
const deck = await makeDeck(choice);
console.log(deck);