Skip to content
173 changes: 173 additions & 0 deletions backend/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// const fs = require('fs');
// const readline = require('readline');
// const path = require('path');

// const rankingsPath = path.join(__dirname, "../data/picks/results.txt");

const CardColors = {
"W": 0,
"U": 1,
"B": 2,
"R": 3,
"G": 4,
"C": 5
};

const CardColorNames = {
"White": 0,
"Blue": 1,
"Black": 2,
"Red": 3,
"Green": 4,
"Colorless": 5,
"Multicolor": 6
};

// /**
// * @desc ... Searches our rankings file for a card and returns what rank it is.
// * @param {Object} card ... The card object.
// * @returns {Int} ... Returns the card rank, -1 if not found.
// */
// async function pullCardRank(card) {
// const fileStream = fs.createReadStream(rankingsPath);

// const rl = readline.createInterface({
// input: fileStream,
// crlfDelay: Infinity
// });

// var rank = 0;

// console.log(card.name);

// for await (var line of rl) {
// var card_line;
// card_line = line.split(" ");
// card_line.shift();
// card_line = card_line.join(" ");

// if (card.name == card_line) {
// return rank;
// }

// rank++
// }

// return -1;
// }


// async function getCardRank(card) {
// promise = pullCardRank(card);
// result = await promise;

// return result;
// }

/**
* @desc eatColorPips ... Separates out the colored pips {4}{W} --> W for bias analysis.
* @param {String} manaCost ... String of the manacost with generic and colored costs.
* @returns {Array} ... Returns an array of the color pip bias.
*/
function eatColorPips(manaCost) {
var colorBias = [0, 0, 0, 0, 0, 0]; // The last value refers to the total number of color pips.

// Symbols to be removed from card mana costs.
const toBeRemoved = ["{", "}", "X", "/"];

for (var item in toBeRemoved) {
manaCost = manaCost.split(toBeRemoved[item]).join("");
}

for (var number = 0; number <= 9; number++) {
manaCost = manaCost.split(number).join("");
}

for (var char = 0; char < manaCost.length; char++) {
colorBias[CardColors[manaCost.charAt(char)]]++;
colorBias[colorBias.length - 1]++; // Increment the total number of color pips.
}

return colorBias;
}

/**
* @desc generatePackStats(pack) ... Examines and create an object for the statistics of a pack.
* @param {object} pack ... An object containing the name, UUID, CMC, and other relevant information about the pack.
* @returns {object} ... Returns an object containing information about the pack.
*/
function generatePackStats(packs) {
// colorBias, an array from [0, 1] reference enum CardColors, each pack is weighted by color.
var colorBias = [0, 0, 0, 0, 0, 0, 0];
var colorPipBias = [0, 0, 0, 0, 0];
var typeBias = {};
var rarityBias = {};

var cmcBias = 0;
var totalCount = packs.length;
var nonLandCount = 0;
var colorPips = 0;
// var bestPick;

for (var pack in packs) {
// packObj used to make my life easier regarding referencing.
var packObj = packs[pack];

var manaCost = packObj.manaCost;
var type = packObj.type;
var rarity = packObj.rarity;
var color = packObj.color;
var CMC = packObj.cmc;
// var rank = getCardRank(packObj);

// console.log(rank);


if (type != "Land") {
nonLandCount++;

colorBias[CardColorNames[color]]++;

if (CMC > 0) {
cmcBias += CMC;
}

if (color != "Colorless") {
var newColorBias = eatColorPips(manaCost);

for (var val = 0; val < colorPipBias.length; val++) {
colorPipBias[val] += newColorBias[val];
}

colorPips += newColorBias[newColorBias.length - 1];
}
}

typeBias[type] = (typeBias[type] || 0) + 1; // Increase the number for whatever type it is or initialize the value.
rarityBias[rarity] = (rarityBias[rarity] || 0) + 1;
}

// Adjust the weights of everything.
for (var pipVal = 0; pipVal < colorPipBias.length; pipVal++) {
colorPipBias[pipVal] /= colorPips;
}

for (var biasVal = 0; biasVal < colorBias.length; biasVal++) {
colorBias[biasVal] /= nonLandCount;
}

cmcBias /= nonLandCount;

for (var types in typeBias) {
typeBias[types] /= totalCount;
}

for (var rarities in rarityBias) {
rarityBias[rarities] /= totalCount;
}

var packStats = {"colorBias": colorBias, "colorPipBias": colorPipBias, "typeBias": typeBias, "rarityBias": rarityBias, "cmcBias": cmcBias};
return packStats;
}

module.exports = generatePackStats;
106 changes: 106 additions & 0 deletions backend/analytics.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const {describe, it} = require("mocha");
const assert = require("assert");
const cardStats = require("./analytics.js");
const boosterGenerator = require("./boosterGenerator");
const {range} = require("lodash");

// List of potentially problematic cards to test over for pack analysis.
const TestCard = {
uuid: "576e9e04-acd7-5d24-bc19-1dd765e9d1b8",
name: "Purphoros's Intervention",
names: [],
color: "Red",
colors: [ "R" ],
colorIdentity: [ "R" ],
setCode: "THB",
scryfallId: "ecc911ee-0e12-4b10-add7-9a9d63c29443",
cmc: 1,
number: "151",
type: "Sorcery",
manaCost: "{X}{R}",
rarity: "Rare",
url: "https://api.scryfall.com/cards/ecc911ee-0e12-4b10-add7-9a9d63c29443?format=image",
layout: "normal",
isDoubleFaced: false,
flippedCardURL: "",
supertypes: [],
subtypes: [],
text: "Choose one —\n" +
"• Create an X/1 red Elemental creature token with trample and haste. Sacrifice it at the beginning of the next end step.\n" +
"• Purphoros's Intervention deals twice X damage to target creature or planeswalker.",
foil: true
};

/**
* @desc withinRange ... Returns a boolean whether or not a number is within range of a set tolerance +/-.
* @param {float} val
* @param {float} tolerance
* @returns {boolean} ... Is the value within range.
*/
function withinRange(val, expected, tolerance) {
return (val <= tolerance + expected && val >= tolerance - expected);
}

/**
* @desc Compares an arr/obj and returns if they're the same.
* @param {arr/obj} val0 ... val0 to compare.
* @param {arr/obj} val1 ... val1 to compare.
* @returns {boolean} ... Are the values the same?
*/
function compareArray(val0, val1) {
for (var val in val0) {
if (val0[val] != val1[val]) {
return false;
}
}

return true;
}

var monoGreenTest = [];

for (var val = 0; val < 15; val++) {
monoGreenTest.push(TestCard);
}

describe("Acceptance tests for card analytics generation", () => {
it("Should return the known bias of a single card", () => {
var stats = cardStats(monoGreenTest);
var statsLength = 0;

for (var output in stats) {
if (stats[output] != undefined) {
statsLength++;
}
}

assert(statsLength == 5);
assert(compareArray(stats.colorBias, [0, 0, 0, 1, 0, 0, 0]));
assert(compareArray(stats.colorPipBias, [0, 0, 0, 1, 0]));
assert(stats.cmcBias == 1);
});

it("Should return statistics near 100% +/- 2", () => {
range(20).forEach(() => {
var randomBooster = boosterGenerator("MH1");
var stats = cardStats(randomBooster);

var colorBias = stats.colorBias;
var colorPipBias = stats.colorPipBias;

var colorBiasPercentage = 0;
var colorPipBiasPercentage = 0;

for (var colorBiasVal in colorBias) {
colorBiasPercentage += colorBias[colorBiasVal];
}

for (var colorPipVal in colorPipBias) {
colorPipBiasPercentage += colorPipBias[colorPipVal];
}

assert(withinRange(colorBiasPercentage, 1, .02));
assert(withinRange(colorPipBiasPercentage, 1, .02));
});
});
});
1 change: 1 addition & 0 deletions backend/bot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {sample, pull} = require("lodash");
const Player = require("./player");
const PackStats = require("./analytics");

module.exports = class extends Player {
constructor() {
Expand Down