Skip to content

Commit

Permalink
enhanced ml mitigation
Browse files Browse the repository at this point in the history
  • Loading branch information
dekkerglen committed Feb 13, 2025
1 parent 87794ed commit a7aa90f
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 100 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cubecobra",
"version": "1.1.16",
"version": "1.1.17",
"description": "",
"private": true,
"main": "app.js",
Expand Down
15 changes: 15 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ const app = express();
// gzip middleware
app.use(compression());

app.use((req, res, next) => {
const originalSetHeader = res.setHeader;

res.setHeader = (name, value) => {
if (res.headersSent) {
// eslint-disable-next-line no-console
console.warn(`Headers already set at path: ${req.path}`);
}
return originalSetHeader.call(res, name, value);
};

next();
});


// do this before https redirect
app.post('/healthcheck', (req, res) => {
res.status(200).send('OK');
Expand Down
8 changes: 4 additions & 4 deletions src/client/analytics/Suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ const Suggestions: React.FC = () => {
</CardHeader>
{useImages ? (
<CardBody>
<Row>
<Row gutters={2}>
{cardsToUse.map((add, index) => (
<Col key={add.cardID} xs={12} lg={6} className="p-1">
<Col key={add.cardID} xs={6} md={4} xl={3} className="p-1">
<ImageSuggestion key={add.cardID} index={index} card={add} cube={cube} />
</Col>
))}
Expand Down Expand Up @@ -177,9 +177,9 @@ const Suggestions: React.FC = () => {
</CardHeader>
{useImages ? (
<CardBody>
<Row>
<Row gutters={2}>
{reversedCuts.map((add, index) => (
<Col key={add.cardID} xs={12} lg={6} className="p-1">
<Col key={add.cardID} xs={6} md={4} xl={3} className="p-1">
<ImageSuggestion key={add.cardID} index={index} card={add} cube={cube} />
</Col>
))}
Expand Down
4 changes: 2 additions & 2 deletions src/client/components/modals/AddToCubeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import useLocalStorage from '../../hooks/useLocalStorage';
import Alert from '../base/Alert';
import Button from '../base/Button';
import { Flexbox } from '../base/Layout';
import { Modal, ModalBody, ModalFooter,ModalHeader } from '../base/Modal';
import { Modal, ModalBody, ModalFooter, ModalHeader } from '../base/Modal';
import Select from '../base/Select';
import ImageFallback from '../ImageFallback';
import LoadingButton from '../LoadingButton';
Expand Down Expand Up @@ -75,7 +75,7 @@ const AddToCubeModal: React.FC<AddToCubeModalProps> = ({

if (!cubes || cubes.length === 0) {
return (
<Modal isOpen={isOpen} setOpen={setOpen} xs>
<Modal isOpen={isOpen} setOpen={setOpen} sm>
<ModalHeader setOpen={setOpen}>{card.name}</ModalHeader>
<ModalBody className="centered">
<Flexbox direction="col" alignItems="center" gap="2">
Expand Down
2 changes: 2 additions & 0 deletions src/jobs/update_cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,7 @@ const loadCardKingdomPrices = async (): Promise<Record<string, number>> => {

const json = await res.json();

// eslint-disable-next-line no-console
console.log(`Loaded ${json.data.length} cards from Mana Pool`);

return Object.fromEntries(json.data.map((card: any) => [card.scryfall_id, parseFloat(card.price_retail)]));
Expand All @@ -861,6 +862,7 @@ const loadManaPoolPrices = async (): Promise<Record<string, number>> => {

const json = await res.json();

// eslint-disable-next-line no-console
console.log(`Loaded ${json.data.length} cards from Mana Pool`);

return Object.fromEntries(json.data.map((card: any) => [card.scryfall_id, parseFloat(card.price_cents) / 100]));
Expand Down
179 changes: 86 additions & 93 deletions src/util/ml.js → src/util/ml.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,125 @@
import { getAllOracleIds, isOracleBasic } from './carddb';

const tf = require('@tensorflow/tfjs-node');
const fs = require('fs');
const cloudwatch = require('./cloudwatch');

const { getOracleForMl, getAllOracleIds } = require('./carddb');
const { getOracleForMl, getReasonableCardByOracle } = require('./carddb');

const indexToOracle = JSON.parse(fs.readFileSync('./model/indexToOracleMap.json'));
const oracleToIndex = Object.fromEntries(Object.entries(indexToOracle).map(([key, value]) => [value, key]));

const numOracles = Object.keys(oracleToIndex).length;

interface Model {
predict: (arg0: any) => { (): any; new (): any; dataSync: { (): any; new (): any } };
}

let encoder;
let recommendDecoder;
let deckbuilderDecoder;
let draftDecoder;
let encoder: Model;
let recommendDecoder: Model;
let deckbuilderDecoder: Model;
let draftDecoder: Model;

tf.loadGraphModel('file://./model/encoder/model.json')
.then((model) => {
.then((model: Model) => {
encoder = model;
// eslint-disable-next-line no-console
console.info('encoder loaded');
})
.catch((err) => {
.catch((err: { message: any; stack: any }) => {
cloudwatch.error(err.message, err.stack);
});

tf.loadGraphModel('file://./model/cube_decoder/model.json')
.then((model) => {
.then((model: Model) => {
recommendDecoder = model;
// eslint-disable-next-line no-console
console.info('recommend_decoder loaded');
})
.catch((err) => {
.catch((err: { message: any; stack: any }) => {
cloudwatch.error(err.message, err.stack);
});

tf.loadGraphModel('file://./model/deck_build_decoder/model.json')
.then((model) => {
.then((model: Model) => {
deckbuilderDecoder = model;
// eslint-disable-next-line no-console
console.info('deck_build_decoder loaded');
})
.catch((err) => {
.catch((err: { message: any; stack: any }) => {
cloudwatch.error(err.message, err.stack);
});

tf.loadGraphModel('file://./model/draft_decoder/model.json')
.then((model) => {
.then((model: Model) => {
draftDecoder = model;
// eslint-disable-next-line no-console
console.info('draft_decoder loaded');
})
.catch((err) => {
.catch((err: { message: any; stack: any }) => {
cloudwatch.error(err.message, err.stack);
});

// if the oracle is in the data, return the index, otherwise return the substitute
const getIndexFromOracle = (oracle) => {
if (oracleToIndex[oracle] !== undefined) {
return oracleToIndex[oracle];
}

return oracleToIndex[getOracleForMl(oracle)];
};

const softmax = (array) => {
const softmax = (array: number[]) => {
const max = Math.max(...array);
const exps = array.map((x) => Math.exp(x - max));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map((value) => value / sum);
};

const encodeIndeces = (indeces) => {
const encodeIndeces = (indeces: number[]) => {
const tensor = new Array(numOracles).fill(0);

indeces.forEach((index) => {
tensor[index] = 1;
});
indeces
.filter((index) => index !== null && index !== undefined)
.forEach((index) => {
tensor[index] = 1;
});

return tensor;
};

const encode = (oracles) => {
export const encode = (oracles: string[]) => {
if (!encoder) {
return [];
}

const vector = [encodeIndeces(oracles.map((oracle) => getIndexFromOracle(oracle)))];
const vector = [encodeIndeces(oracles.map((oracle) => oracleToIndex[oracle]))];

return tf.tidy(() => {
const tensor = tf.tensor(vector);
return encoder.predict(tensor).dataSync();
});
};

const recommend = (oracles) => {
const allOracleIds = getAllOracleIds();
const oracleIdToMlIndex = (oracleId: string) => {
const oracleIndex = oracleToIndex[oracleId];

if (oracleIndex !== undefined) {
return oracleIndex;
}

const card = getReasonableCardByOracle(oracleId);

if (card.cubeCount < 50 || card.isToken || isOracleBasic(oracleId)) {
return null;
}

const subOracle = getOracleForMl(oracleId);

return oracleToIndex[subOracle];
};

export const recommend = (oracles: string[]) => {
if (!encoder || !recommendDecoder) {
return {
adds: [],
removes: [],
};
}
const allOracles = getAllOracleIds();

const vector = [encodeIndeces(oracles.map((oracle) => getIndexFromOracle(oracle)))];
const vector = [encodeIndeces(oracles.map((oracle) => oracleIdToMlIndex(oracle)))];

const array = tf.tidy(() => {
const tensor = tf.tensor(vector);
Expand All @@ -113,19 +130,17 @@ const recommend = (oracles) => {

const res = [];

for (let i = 0; i < allOracleIds.length; i++) {
if (oracleToIndex[allOracleIds[i]] !== undefined) {
res.push({
oracle: allOracleIds[i],
rating: array[oracleToIndex[allOracleIds[i]]],
});
} else {
const substitute = getOracleForMl(allOracleIds[i]);
res.push({
oracle: allOracleIds[i],
rating: array[oracleToIndex[substitute]],
});
for (const oracle of allOracles) {
const index = oracleIdToMlIndex(oracle);

if (index === null) {
continue;
}

res.push({
oracle,
rating: array[index],
});
}

const adds = res.sort((a, b) => b.rating - a.rating).filter((card) => !oracles.includes(card.oracle));
Expand All @@ -137,14 +152,14 @@ const recommend = (oracles) => {
};
};

const build = (oracles) => {
const allOracleIds = getAllOracleIds();
export const build = (oracles: string[]) => {
if (!encoder || !deckbuilderDecoder) {
return [];
}
const allOracles = getAllOracleIds();

const array = tf.tidy(() => {
const vector = [encodeIndeces(oracles.map((oracle) => getIndexFromOracle(oracle)))];
const vector = [encodeIndeces(oracles.map((oracle) => oracleIdToMlIndex(oracle)))];
const tensor = tf.tensor(vector);

const encoded = encoder.predict(tensor);
Expand All @@ -153,76 +168,54 @@ const build = (oracles) => {

const res = [];

for (let i = 0; i < allOracleIds.length; i++) {
if (oracles.includes(allOracleIds[i])) {
if (oracleToIndex[allOracleIds[i]]) {
res.push({
oracle: allOracleIds[i],
rating: array[oracleToIndex[allOracleIds[i]]],
});
} else {
const substitute = getOracleForMl(allOracleIds[i]);
res.push({
oracle: allOracleIds[i],
rating: array[oracleToIndex[substitute]],
});
}
for (const oracle of allOracles) {
const index = oracleIdToMlIndex(oracle);

if (oracles.includes(oracle)) {
res.push({
oracle,
rating: array[index],
});
}
}

return res.sort((a, b) => b.rating - a.rating);
};

const draft = (pack, pool) => {
const allOracleIds = getAllOracleIds();
export const draft = (pack: string[], pool: string[]) => {
const array = tf.tidy(() => {
const vector = [encodeIndeces(pool.map((oracle) => getIndexFromOracle(oracle)))];
const vector = [encodeIndeces(pool.map((oracle) => oracleToIndex[oracle]))];
const tensor = tf.tensor(vector);
const encoded = encoder.predict(tensor);
return draftDecoder.predict([encoded]).dataSync();
});
const allOracles = getAllOracleIds();

const packVector = encodeIndeces(pack.map((oracle) => getIndexFromOracle(oracle)));
const packVector = encodeIndeces(pack.map((oracle) => oracleIdToMlIndex(oracle)));
const mask = packVector.map((x) => 1e9 * (1 - x));

const softmaxed = softmax(array.map((x, i) => x * packVector[i] - mask[i]));
const softmaxed = softmax(array.map((x: number, i: number) => x * packVector[i] - mask[i]));

const res = [];

for (let i = 0; i < numOracles; i++) {
const oracle = indexToOracle[i];
for (const oracle of allOracles) {
const index = oracleIdToMlIndex(oracle);

if (index === null) {
continue;
}

if (pack.includes(oracle)) {
res.push({
oracle: indexToOracle[i],
rating: softmaxed[i],
oracle,
rating: softmaxed[index],
});
}
}

for (let i = 0; i < allOracleIds.length; i++) {
if (pack.includes(allOracleIds[i])) {
if (oracleToIndex[allOracleIds[i]]) {
res.push({
oracle: allOracleIds[i],
rating: softmaxed[oracleToIndex[allOracleIds[i]]],
});
} else {
const substitute = getOracleForMl(allOracleIds[i]);
res.push({
oracle: allOracleIds[i],
rating: softmaxed[oracleToIndex[substitute]],
});
}
}
}

return res.sort((a, b) => b.rating - a.rating);
};

module.exports = {
recommend,
build,
draft,
encode,
oracleInData: (oracle) => oracleToIndex[oracle] !== undefined,
export const oracleInData = (oracle: string) => {
return oracleToIndex[oracle] !== undefined;
};

0 comments on commit a7aa90f

Please sign in to comment.