Refactor cube API, add buy/pool info
This commit is contained in:
parent
1727882fb5
commit
d0f49fce07
9 changed files with 359 additions and 246 deletions
41
cmd/buy.ts
Normal file
41
cmd/buy.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import * as ejs from "ejs";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
|
import { Article, asyncLoadJSON, CardMarketApi, MCMDB } from "../lib";
|
||||||
|
import { cubeCards, genCube } from "../lib/cube";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.error("Usage: yarn buy <uid>");
|
||||||
|
process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uid = process.argv[2];
|
||||||
|
const uidCards = `${uid}-cards.json`;
|
||||||
|
if (!existsSync("mcmCards.json")) {
|
||||||
|
console.error("Card db is missing! Run 'yarn convert-db' first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!existsSync(uidCards)) {
|
||||||
|
console.error(`Could not find ${uidCards}! Run 'yarn fetch ${uid}' first.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
let db = await asyncLoadJSON<MCMDB>("mcmCards.json");
|
||||||
|
let articles = await asyncLoadJSON<Article[]>(`${uid}-cards.json`);
|
||||||
|
|
||||||
|
const cards = cubeCards(await genCube(db, articles));
|
||||||
|
|
||||||
|
let api = new CardMarketApi();
|
||||||
|
|
||||||
|
// Can only list 100 articles per request
|
||||||
|
let offset = 0;
|
||||||
|
while (offset < cards.length) {
|
||||||
|
const xml: string = await ejs.renderFile("templates/buy.ejs", {
|
||||||
|
cards: cards.slice(offset, offset + 100)
|
||||||
|
});
|
||||||
|
await api.put("/shoppingcart", null, xml);
|
||||||
|
offset += 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
157
cmd/cube-dev.ts
157
cmd/cube-dev.ts
|
@ -1,126 +1,12 @@
|
||||||
import * as ejs from "ejs";
|
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
|
|
||||||
import {
|
import { Article, asyncLoadJSON, MCMDB } from "../lib";
|
||||||
Article,
|
import { cubeHTML, genCube } from "../lib/cube";
|
||||||
asyncLoadJSON,
|
|
||||||
CardItem,
|
|
||||||
dictMap,
|
|
||||||
filterDict,
|
|
||||||
leanArticle,
|
|
||||||
leanCard,
|
|
||||||
MCMDB,
|
|
||||||
onlyUnique,
|
|
||||||
spanBy,
|
|
||||||
} from "../lib";
|
|
||||||
|
|
||||||
const colorNames = {
|
|
||||||
CL: "Colorless",
|
|
||||||
MC: "Multicolor",
|
|
||||||
L: "Land",
|
|
||||||
W: "White",
|
|
||||||
U: "Blue",
|
|
||||||
B: "Black",
|
|
||||||
R: "Red",
|
|
||||||
G: "Green",
|
|
||||||
WU: "Azorius",
|
|
||||||
UB: "Dimir",
|
|
||||||
BR: "Rakdos",
|
|
||||||
RG: "Gruul",
|
|
||||||
WG: "Selesnya",
|
|
||||||
WB: "Orzhov",
|
|
||||||
UR: "Izzet",
|
|
||||||
BG: "Golgari",
|
|
||||||
WR: "Boros",
|
|
||||||
UG: "Simic",
|
|
||||||
WUG: "Bant",
|
|
||||||
WUB: "Esper",
|
|
||||||
UBR: "Grixis",
|
|
||||||
BRG: "Jund",
|
|
||||||
WRG: "Naya",
|
|
||||||
WBG: "Abzan",
|
|
||||||
WUR: "Jeskai",
|
|
||||||
UBG: "Sultai",
|
|
||||||
WBR: "Mardu",
|
|
||||||
URG: "Temur"
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: Record<string, (c: CardItem) => boolean> = {
|
|
||||||
W: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "W",
|
|
||||||
U: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "U",
|
|
||||||
B: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "B",
|
|
||||||
R: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "R",
|
|
||||||
G: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "G",
|
|
||||||
MC: c => c.types[0] != "Land" && c.colorIdentity.length > 1,
|
|
||||||
CL: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "CL",
|
|
||||||
L: c => c.types[0] == "Land"
|
|
||||||
};
|
|
||||||
|
|
||||||
function wubrg(a: string, b: string) {
|
|
||||||
const order = ["W", "U", "B", "R", "G"];
|
|
||||||
const indexA = order.indexOf(a);
|
|
||||||
const indexB = order.indexOf(b);
|
|
||||||
return indexA - indexB;
|
|
||||||
}
|
|
||||||
|
|
||||||
function colorid(colors: string[]): string {
|
|
||||||
if (colors.length < 1) {
|
|
||||||
return "CL";
|
|
||||||
}
|
|
||||||
return colors.sort(wubrg).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function prettyColor(color: string) {
|
|
||||||
if (color in colorNames) {
|
|
||||||
return colorNames[color];
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allTypes = [
|
|
||||||
"Creature",
|
|
||||||
"Planeswalker",
|
|
||||||
"Instant",
|
|
||||||
"Sorcery",
|
|
||||||
"Artifact",
|
|
||||||
"Enchantment"
|
|
||||||
];
|
|
||||||
function typeSort(a: string, b: string): number {
|
|
||||||
const indexA = allTypes.indexOf(a);
|
|
||||||
const indexB = allTypes.indexOf(b);
|
|
||||||
return indexA - indexB;
|
|
||||||
}
|
|
||||||
|
|
||||||
function abcSort(a: string, b: string): number {
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (b > a) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This would be properly typed but as of currently TS does not allow circular references for types
|
|
||||||
function deepSum(dict: any): number {
|
|
||||||
if (Array.isArray(dict)) {
|
|
||||||
return dict.length;
|
|
||||||
}
|
|
||||||
let total = 0;
|
|
||||||
for (let key in dict) {
|
|
||||||
if (Array.isArray(dict[key])) {
|
|
||||||
total += dict[key].length;
|
|
||||||
} else {
|
|
||||||
total += deepSum(dict[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
console.error("Usage: yarn fetch <uid>");
|
console.error("Usage: yarn cube-dev <uid>");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -137,43 +23,10 @@ async function run() {
|
||||||
let db = await asyncLoadJSON<MCMDB>("mcmCards.json");
|
let db = await asyncLoadJSON<MCMDB>("mcmCards.json");
|
||||||
let articles = await asyncLoadJSON<Article[]>(`${uid}-cards.json`);
|
let articles = await asyncLoadJSON<Article[]>(`${uid}-cards.json`);
|
||||||
|
|
||||||
let cards: CardItem[] = articles
|
const cubecards = await genCube(db, articles);
|
||||||
.filter(art => art.idProduct in db)
|
|
||||||
.map(art => {
|
|
||||||
const card = db[art.idProduct];
|
|
||||||
return {
|
|
||||||
...leanArticle(art),
|
|
||||||
...leanCard(card)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let valid = cards
|
|
||||||
.filter(
|
|
||||||
c =>
|
|
||||||
(c.language == "Italian" || c.language == "English") &&
|
|
||||||
c.price <= 0.1 &&
|
|
||||||
(c.rarity == "rare" || c.rarity == "mythic")
|
|
||||||
)
|
|
||||||
.filter(onlyUnique);
|
|
||||||
|
|
||||||
// Filter by colors and cmc (nested)
|
|
||||||
let colorcards = filterDict(valid, columns);
|
|
||||||
let cmccards = dictMap(colorcards, (cards, col) =>
|
|
||||||
dictMap(
|
|
||||||
spanBy(cards, c =>
|
|
||||||
col == "MC" || col == "L" ? colorid(c.colorIdentity) : c.types[0]
|
|
||||||
),
|
|
||||||
typed => spanBy(typed, c => c.convertedManaCost)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
const template = await ejs.renderFile("templates/cube.ejs", {
|
const template = await cubeHTML(uid, cubecards);
|
||||||
user: uid,
|
|
||||||
cards: valid,
|
|
||||||
cmccards,
|
|
||||||
columns,
|
|
||||||
utils: { wubrg, prettyColor, colorid, deepSum, typeSort, abcSort }
|
|
||||||
});
|
|
||||||
res.end(template);
|
res.end(template);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
85
cmd/cube.ts
85
cmd/cube.ts
|
@ -1,64 +1,11 @@
|
||||||
import * as ejs from "ejs";
|
|
||||||
import { existsSync, writeFile } from "fs";
|
import { existsSync, writeFile } from "fs";
|
||||||
|
|
||||||
import { Article, asyncLoadJSON, CardItem, leanArticle, leanCard, MCMDB, onlyUnique } from "../lib";
|
import { Article, asyncLoadJSON, MCMDB } from "../lib";
|
||||||
|
import { cubeHTML, genCube } from "../lib/cube";
|
||||||
const colorNames = {
|
|
||||||
CL: "Colorless",
|
|
||||||
W: "White",
|
|
||||||
U: "Blue",
|
|
||||||
B: "Black",
|
|
||||||
R: "Red",
|
|
||||||
G: "Green",
|
|
||||||
WU: "Azorius",
|
|
||||||
UB: "Dimir",
|
|
||||||
BR: "Rakdos",
|
|
||||||
RG: "Gruul",
|
|
||||||
WG: "Selesnya",
|
|
||||||
WB: "Orzhov",
|
|
||||||
UR: "Izzet",
|
|
||||||
BG: "Golgari",
|
|
||||||
WR: "Boros",
|
|
||||||
UG: "Simic",
|
|
||||||
WUG: "Bant",
|
|
||||||
WUB: "Esper",
|
|
||||||
UBR: "Grixis",
|
|
||||||
BRG: "Jund",
|
|
||||||
WRG: "Naya",
|
|
||||||
WBG: "Abzan",
|
|
||||||
WUR: "Jeskai",
|
|
||||||
UBG: "Sultai",
|
|
||||||
WBR: "Mardu",
|
|
||||||
URG: "Temur"
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: Record<string, (c: CardItem) => boolean> = {
|
|
||||||
W: c => colorid(c.colorIdentity) == "W",
|
|
||||||
U: c => colorid(c.colorIdentity) == "U",
|
|
||||||
B: c => colorid(c.colorIdentity) == "B",
|
|
||||||
R: c => colorid(c.colorIdentity) == "R",
|
|
||||||
G: c => colorid(c.colorIdentity) == "G",
|
|
||||||
MC: c => c.colorIdentity.length > 0,
|
|
||||||
CL: c => colorid(c.colorIdentity) == "CL"
|
|
||||||
};
|
|
||||||
|
|
||||||
function wubrg(a: string, b: string) {
|
|
||||||
const order = ["W", "U", "B", "R", "G"];
|
|
||||||
const indexA = order.indexOf(a);
|
|
||||||
const indexB = order.indexOf(b);
|
|
||||||
return indexA - indexB;
|
|
||||||
}
|
|
||||||
|
|
||||||
function colorid(colors: string[]): string {
|
|
||||||
if (colors.length < 1) {
|
|
||||||
return "CL";
|
|
||||||
}
|
|
||||||
return colors.sort(wubrg).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
console.error("Usage: yarn fetch <uid>");
|
console.error("Usage: yarn cube <uid>");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -75,31 +22,9 @@ async function run() {
|
||||||
let db = await asyncLoadJSON<MCMDB>("mcmCards.json");
|
let db = await asyncLoadJSON<MCMDB>("mcmCards.json");
|
||||||
let articles = await asyncLoadJSON<Article[]>(`${uid}-cards.json`);
|
let articles = await asyncLoadJSON<Article[]>(`${uid}-cards.json`);
|
||||||
|
|
||||||
let cards: CardItem[] = articles
|
const cubecards = await genCube(db, articles);
|
||||||
.filter(art => art.idProduct in db)
|
const template = await cubeHTML(uid, cubecards);
|
||||||
.map(art => {
|
|
||||||
const card = db[art.idProduct];
|
|
||||||
return {
|
|
||||||
...leanArticle(art),
|
|
||||||
...leanCard(card)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let valid = cards
|
|
||||||
.filter(
|
|
||||||
c =>
|
|
||||||
(c.language == "Italian" || c.language == "English") &&
|
|
||||||
c.price <= 0.1 &&
|
|
||||||
(c.rarity == "rare" || c.rarity == "mythic")
|
|
||||||
)
|
|
||||||
.filter(onlyUnique);
|
|
||||||
|
|
||||||
const template = await ejs.renderFile("templates/cube.ejs", {
|
|
||||||
user: uid,
|
|
||||||
cards: valid,
|
|
||||||
columns,
|
|
||||||
utils: { wubrg, colorNames, colorid }
|
|
||||||
});
|
|
||||||
let cubeFile = `${uid}-cube.html`;
|
let cubeFile = `${uid}-cube.html`;
|
||||||
writeFile(cubeFile, template, {}, err => {
|
writeFile(cubeFile, template, {}, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
54
cmd/pool.ts
Normal file
54
cmd/pool.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
|
import { Article, asyncLoadJSON, CardItem, MCMDB } from "../lib";
|
||||||
|
import { cubeCards, genCube } from "../lib/cube";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.error("Usage: yarn buy <uid>");
|
||||||
|
process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uid = process.argv[2];
|
||||||
|
const uidCards = `${uid}-cards.json`;
|
||||||
|
if (!existsSync("mcmCards.json")) {
|
||||||
|
console.error("Card db is missing! Run 'yarn convert-db' first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!existsSync(uidCards)) {
|
||||||
|
console.error(`Could not find ${uidCards}! Run 'yarn fetch ${uid}' first.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
let db = await asyncLoadJSON<MCMDB>("mcmCards.json");
|
||||||
|
let articles = await asyncLoadJSON<Article[]>(`${uid}-cards.json`);
|
||||||
|
|
||||||
|
const cards = cubeCards(await genCube(db, articles));
|
||||||
|
|
||||||
|
let types: Record<string, CardItem[]> = {};
|
||||||
|
cards.forEach(c => {
|
||||||
|
c.subtypes.forEach(t => {
|
||||||
|
if (t in types) {
|
||||||
|
types[t].push(c);
|
||||||
|
} else {
|
||||||
|
types[t] = [c];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("TRIBAL? Top 10 tribes:");
|
||||||
|
const entries = Object.entries(types)
|
||||||
|
.sort((a, b) => b[1].length - a[1].length)
|
||||||
|
.slice(0, 10)
|
||||||
|
.forEach(([typ, typcards]) => {
|
||||||
|
console.log(
|
||||||
|
` ${typ.padEnd(16, " ")}${typcards.length
|
||||||
|
.toString()
|
||||||
|
.padStart(3)} cards (${(
|
||||||
|
(typcards.length / cards.length) *
|
||||||
|
100
|
||||||
|
).toFixed(1)}%)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
52
lib/api.ts
52
lib/api.ts
|
@ -1,5 +1,5 @@
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { get } from "request";
|
import * as request from "request";
|
||||||
|
|
||||||
import { Article } from "./types";
|
import { Article } from "./types";
|
||||||
|
|
||||||
|
@ -18,28 +18,39 @@ export class CardMarketApi {
|
||||||
this.AccessSecret = process.env.ACCESS_SECRET;
|
this.AccessSecret = process.env.ACCESS_SECRET;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(path: string, params?: Record<string, any>): Promise<any> {
|
async request(
|
||||||
const oauthParameters = {
|
method: string,
|
||||||
|
path: string,
|
||||||
|
params: Record<string, any> | undefined,
|
||||||
|
options: any
|
||||||
|
): Promise<any> {
|
||||||
|
const oauth = {
|
||||||
realm: `${URI}${path}`,
|
realm: `${URI}${path}`,
|
||||||
consumer_key: this.AppToken,
|
consumer_key: this.AppToken,
|
||||||
consumer_secret: this.AppSecret,
|
consumer_secret: this.AppSecret,
|
||||||
token: this.AccessToken,
|
token: this.AccessToken,
|
||||||
token_secret: this.AccessSecret
|
token_secret: this.AccessSecret
|
||||||
};
|
};
|
||||||
|
const uri = oauth.realm + (params ? `?${stringify(params)}` : "");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
get(
|
request(
|
||||||
oauthParameters.realm + (params ? `?${stringify(params)}` : ""),
|
|
||||||
{
|
{
|
||||||
oauth: oauthParameters
|
method,
|
||||||
|
uri,
|
||||||
|
oauth,
|
||||||
|
...options
|
||||||
},
|
},
|
||||||
(error, response) => {
|
(error, response) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
reject(error);
|
||||||
} else if (response.statusCode > 299) {
|
} else if (response.statusCode > 299) {
|
||||||
throw JSON.stringify({
|
reject(
|
||||||
|
JSON.stringify({
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
statusMessage: response.statusMessage
|
statusMessage: response.statusMessage,
|
||||||
});
|
statusBody: response.body
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resolve(JSON.parse(response.body));
|
resolve(JSON.parse(response.body));
|
||||||
|
@ -51,6 +62,27 @@ export class CardMarketApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get(path: string, params?: Record<string, any>): Promise<any> {
|
||||||
|
return await this.request("GET", path, params, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(
|
||||||
|
path: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
data?: string
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.request(
|
||||||
|
"PUT",
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
data
|
||||||
|
? {
|
||||||
|
body: data
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getAllArticles(uid: string): Promise<Article[]> {
|
async getAllArticles(uid: string): Promise<Article[]> {
|
||||||
const perPage = 1000;
|
const perPage = 1000;
|
||||||
let start = 0;
|
let start = 0;
|
||||||
|
|
189
lib/cube.ts
Normal file
189
lib/cube.ts
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import * as ejs from "ejs";
|
||||||
|
|
||||||
|
import { Article, CardItem, dictMap, filterDict, leanArticle, leanCard, MCMDB, onlyUnique, spanBy } from ".";
|
||||||
|
|
||||||
|
const colorNames = {
|
||||||
|
CL: "Colorless",
|
||||||
|
MC: "Multicolor",
|
||||||
|
L: "Land",
|
||||||
|
W: "White",
|
||||||
|
U: "Blue",
|
||||||
|
B: "Black",
|
||||||
|
R: "Red",
|
||||||
|
G: "Green",
|
||||||
|
WU: "Azorius",
|
||||||
|
UB: "Dimir",
|
||||||
|
BR: "Rakdos",
|
||||||
|
RG: "Gruul",
|
||||||
|
WG: "Selesnya",
|
||||||
|
WB: "Orzhov",
|
||||||
|
UR: "Izzet",
|
||||||
|
BG: "Golgari",
|
||||||
|
WR: "Boros",
|
||||||
|
UG: "Simic",
|
||||||
|
WUG: "Bant",
|
||||||
|
WUB: "Esper",
|
||||||
|
UBR: "Grixis",
|
||||||
|
BRG: "Jund",
|
||||||
|
WRG: "Naya",
|
||||||
|
WBG: "Abzan",
|
||||||
|
WUR: "Jeskai",
|
||||||
|
UBG: "Sultai",
|
||||||
|
WBR: "Mardu",
|
||||||
|
URG: "Temur"
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: Record<string, (c: CardItem) => boolean> = {
|
||||||
|
W: c => colorid(c.colorIdentity) == "W",
|
||||||
|
U: c => colorid(c.colorIdentity) == "U",
|
||||||
|
B: c => colorid(c.colorIdentity) == "B",
|
||||||
|
R: c => colorid(c.colorIdentity) == "R",
|
||||||
|
G: c => colorid(c.colorIdentity) == "G",
|
||||||
|
MC: c => c.colorIdentity.length > 1,
|
||||||
|
CL: c => colorid(c.colorIdentity) == "CL"
|
||||||
|
};
|
||||||
|
|
||||||
|
function wubrg(a: string, b: string) {
|
||||||
|
const order = ["W", "U", "B", "R", "G"];
|
||||||
|
const indexA = order.indexOf(a);
|
||||||
|
const indexB = order.indexOf(b);
|
||||||
|
return indexA - indexB;
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorid(colors: string[]): string {
|
||||||
|
if (colors.length < 1) {
|
||||||
|
return "CL";
|
||||||
|
}
|
||||||
|
return colors.sort(wubrg).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyColor(color: string) {
|
||||||
|
if (color in colorNames) {
|
||||||
|
return colorNames[color];
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTypes = [
|
||||||
|
"Creature",
|
||||||
|
"Planeswalker",
|
||||||
|
"Instant",
|
||||||
|
"Sorcery",
|
||||||
|
"Artifact",
|
||||||
|
"Enchantment"
|
||||||
|
];
|
||||||
|
function typeSort(a: string, b: string): number {
|
||||||
|
const indexA = allTypes.indexOf(a);
|
||||||
|
const indexB = allTypes.indexOf(b);
|
||||||
|
return indexA - indexB;
|
||||||
|
}
|
||||||
|
|
||||||
|
function abcSort(a: string, b: string): number {
|
||||||
|
if (a > b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (b > a) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This would be properly typed but as of currently TS does not allow circular references for types
|
||||||
|
function deepSum(dict: any): number {
|
||||||
|
if (Array.isArray(dict)) {
|
||||||
|
return dict.length;
|
||||||
|
}
|
||||||
|
let total = 0;
|
||||||
|
for (let key in dict) {
|
||||||
|
if (Array.isArray(dict[key])) {
|
||||||
|
total += dict[key].length;
|
||||||
|
} else {
|
||||||
|
total += deepSum(dict[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cubeCards(c: CubeCards): CardItem[] {
|
||||||
|
let cards: CardItem[] = [];
|
||||||
|
dictMap(c, bytyp =>
|
||||||
|
dictMap(bytyp, bycmc =>
|
||||||
|
dictMap(bycmc, cardlist => (cards = cards.concat(cardlist)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
function byRank(a: CardItem, b: CardItem): number {
|
||||||
|
return a.edhrecRank - b.edhrecRank;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ByCMC<T> = Record<number, T>;
|
||||||
|
export type ByType<T> = Record<string, T>;
|
||||||
|
export type ByColor<T> = Record<string, T>;
|
||||||
|
export type CubeCards = ByColor<ByType<ByCMC<CardItem[]>>>;
|
||||||
|
|
||||||
|
export async function genCube(
|
||||||
|
db: MCMDB,
|
||||||
|
articles: Article[]
|
||||||
|
): Promise<CubeCards> {
|
||||||
|
const cards: CardItem[] = articles
|
||||||
|
.filter(art => art.idProduct in db)
|
||||||
|
.map(art => {
|
||||||
|
const card = db[art.idProduct];
|
||||||
|
return {
|
||||||
|
...leanArticle(art),
|
||||||
|
...leanCard(card)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const valid = cards
|
||||||
|
.filter(
|
||||||
|
c =>
|
||||||
|
(c.language == "Italian" || c.language == "English") &&
|
||||||
|
c.price < 0.2 &&
|
||||||
|
(c.rarity == "rare" || c.rarity == "mythic")
|
||||||
|
)
|
||||||
|
.filter(onlyUnique);
|
||||||
|
|
||||||
|
const categorized = filterDict(valid, columns);
|
||||||
|
|
||||||
|
// Cut cards
|
||||||
|
const filtered = dictMap(categorized, (pool, col) => {
|
||||||
|
switch (col) {
|
||||||
|
case "MC":
|
||||||
|
const colors = spanBy(pool, c => colorid(c.colorIdentity));
|
||||||
|
let cards = [];
|
||||||
|
for (const color in colors) {
|
||||||
|
cards = cards.concat(colors[color].sort(byRank).slice(0, 3));
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
case "CL":
|
||||||
|
return pool.sort(byRank).slice(0, 30);
|
||||||
|
default:
|
||||||
|
return pool.sort(byRank).slice(0, 58);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter by colors and cmc (nested)
|
||||||
|
return dictMap(filtered, (cards, col) =>
|
||||||
|
dictMap(
|
||||||
|
spanBy(cards, c =>
|
||||||
|
col == "MC" || col == "L"
|
||||||
|
? colorid(c.colorIdentity)
|
||||||
|
: c.types.slice(-1)[0]
|
||||||
|
),
|
||||||
|
typed => spanBy(typed, c => c.convertedManaCost)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cubeHTML(user, cubecards: CubeCards) {
|
||||||
|
return await ejs.renderFile("templates/cube.ejs", {
|
||||||
|
user,
|
||||||
|
cards: cubeCards(cubecards),
|
||||||
|
cubecards,
|
||||||
|
columns,
|
||||||
|
utils: { wubrg, prettyColor, colorid, deepSum, typeSort, abcSort }
|
||||||
|
});
|
||||||
|
}
|
|
@ -10,7 +10,9 @@
|
||||||
"build-db": "ts-node cmd/convert.ts",
|
"build-db": "ts-node cmd/convert.ts",
|
||||||
"find": "ts-node cmd/find.ts",
|
"find": "ts-node cmd/find.ts",
|
||||||
"cube": "ts-node cmd/cube.ts",
|
"cube": "ts-node cmd/cube.ts",
|
||||||
"cube-watch": "ts-node cmd/cube-dev.ts"
|
"cube-watch": "ts-node cmd/cube-dev.ts",
|
||||||
|
"buy": "ts-node cmd/buy.ts",
|
||||||
|
"pool-info": "ts-node cmd/pool.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ejs": "^2.6.3",
|
"@types/ejs": "^2.6.3",
|
||||||
|
|
10
templates/buy.ejs
Normal file
10
templates/buy.ejs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<request>
|
||||||
|
<action>add</action>
|
||||||
|
<% cards.forEach(card => { %>
|
||||||
|
<article>
|
||||||
|
<idArticle><%= card.idArticle %></idArticle>
|
||||||
|
<amount>1</amount>
|
||||||
|
</article>
|
||||||
|
<% }); %>
|
||||||
|
</request>
|
|
@ -2,6 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
<title>Junk rare cube</title>
|
<title>Junk rare cube</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
@ -135,6 +136,11 @@
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: #aaa;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -144,9 +150,10 @@
|
||||||
<h1>Junk rare cube (<%=cards.length%> cards)</h1>
|
<h1>Junk rare cube (<%=cards.length%> cards)</h1>
|
||||||
<h2>Using cards from <a href="https://www.cardmarket.com/en/Magic/Users/<%=user%>"><b><%= user %></b></a>
|
<h2>Using cards from <a href="https://www.cardmarket.com/en/Magic/Users/<%=user%>"><b><%= user %></b></a>
|
||||||
</h2>
|
</h2>
|
||||||
|
<h2>Est. price without shipping: € <%= cards.reduce((a, c) => a + c.price, 0).toFixed(2) %></h2>
|
||||||
</header>
|
</header>
|
||||||
<section class="columns">
|
<section class="columns">
|
||||||
<% Object.entries(cmccards).forEach(([color, colorcards]) => { %>
|
<% Object.entries(cubecards).forEach(([color, colorcards]) => { %>
|
||||||
<section class="color-section color-<%=color%>">
|
<section class="color-section color-<%=color%>">
|
||||||
<header class="color-header">
|
<header class="color-header">
|
||||||
<h3><%=utils.prettyColor(color)%> (<%=utils.deepSum(colorcards)%>)</h3>
|
<h3><%=utils.prettyColor(color)%> (<%=utils.deepSum(colorcards)%>)</h3>
|
||||||
|
@ -163,7 +170,7 @@
|
||||||
<li data-set="<%=card.set%>" data-num="<%=card.number%>">
|
<li data-set="<%=card.set%>" data-num="<%=card.number%>">
|
||||||
<a target="_blank" data-image="<%=card.scryfallImageUrl%>"
|
<a target="_blank" data-image="<%=card.scryfallImageUrl%>"
|
||||||
href="<%= card.scryfallUrl %>">
|
href="<%= card.scryfallUrl %>">
|
||||||
<%= card.name %>
|
<%= card.name %><small>€ <%=card.price.toFixed(2)%> - #<%=card.edhrecRank%></small>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
|
|
Loading…
Reference in a new issue