Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create pack analytics to be used for Bot AI. #961

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
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