diff --git a/__tests__/serverjs/draftutil.test.js b/__tests__/serverjs/draftutil.test.js index 04dee7a42..1ea6fc8b6 100644 --- a/__tests__/serverjs/draftutil.test.js +++ b/__tests__/serverjs/draftutil.test.js @@ -2,8 +2,9 @@ const carddb = require('../../serverjs/cards'); const fixturesPath = 'fixtures'; const cubefixture = require('../../fixtures/examplecube'); const sinon = require('sinon'); -const methods = require('../../serverjs/draftutil'); +const methods = require('../../dist/util/draftutil'); let CardRating = require('../../models/cardrating'); +let Draft = require('../../models/draft'); import Filter from '../../src/util/Filter'; import { expectOperator } from '../helpers'; @@ -133,9 +134,10 @@ describe('getDraftFormat', () => { }); }); -describe('createDraft', () => { - let format, cards, bots, seats; +describe('populateDraft', () => { + let draft, format, cards, bots, seats; beforeAll(() => { + draft = new Draft(); format = []; cards = []; bots = []; @@ -146,7 +148,7 @@ describe('createDraft', () => { cards = []; bots = ['fakebot']; expect(() => { - methods.createDraft(format, cards, bots, seats); + methods.populateDraft(draft, format, cards, bots, seats); }).toThrow(/no cards/); }); @@ -154,7 +156,7 @@ describe('createDraft', () => { cards = ['mockcard']; bots = []; expect(() => { - methods.createDraft(format, cards, bots, seats); + methods.populateDraft(draft, format, cards, bots, seats); }).toThrow(/no bots/); }); @@ -162,13 +164,13 @@ describe('createDraft', () => { cards = ['mockcards']; bots = ['mockbot']; expect(() => { - methods.createDraft(format, cards, bots, 1); + methods.populateDraft(draft, format, cards, bots, 1); }).toThrow(/invalid seats/); expect(() => { - methods.createDraft(format, cards, bots, null); + methods.populateDraft(draft, format, cards, bots, null); }).toThrow(/invalid seats/); expect(() => { - methods.createDraft(format, cards, bots, -1); + methods.populateDraft(draft, format, cards, bots, -1); }).toThrow(/invalid seats/); }); @@ -183,7 +185,7 @@ describe('createDraft', () => { cards = exampleCube.cards; bots = ['mockbot']; format = methods.getDraftFormat({ id: -1, packs: 1, cards: 15, seats: seats }, exampleCube); - let draft = methods.createDraft(format, cards, bots, 8); + methods.populateDraft(draft, format, cards, bots, 8); expect(draft.pickNumber).toEqual(1); expect(draft.packNumber).toEqual(1); expect(draft).toHaveProperty('packs'); diff --git a/package-lock.json b/package-lock.json index 3bc449c44..702a5b14b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -241,7 +241,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.7.0.tgz", "integrity": "sha512-LSln3cexwInTMYYoFeVLKnYPPMfWNJ8PubTBs3hkh7wCu9iBaqq1OOyW+xGmEdLxT1nhsl+9SJ+h2oUDYz0l2A==", - "dev": true, "requires": { "@babel/types": "^7.7.0", "esutils": "^2.0.0" @@ -251,7 +250,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.2.tgz", "integrity": "sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA==", - "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", @@ -261,8 +259,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" } } }, @@ -788,8 +785,7 @@ "@babel/helper-plugin-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" }, "@babel/helper-regex": { "version": "7.5.5", @@ -1472,7 +1468,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -1860,7 +1855,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -1869,7 +1863,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.7.0.tgz", "integrity": "sha512-mXhBtyVB1Ujfy+0L6934jeJcSXj/VCg6whZzEcgiiZHNS0PGC7vUCsZDQCxxztkpIdF+dY1fUMcjAgEOC3ZOMQ==", - "dev": true, "requires": { "@babel/helper-builder-react-jsx": "^7.7.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -1880,7 +1873,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" @@ -1890,7 +1882,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" @@ -2053,7 +2044,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.7.0.tgz", "integrity": "sha512-IXXgSUYBPHUGhUkH+89TR6faMcBtuMW0h5OHbMuVbL3/5wK2g6a2M2BBpkLa+Kw0sAHiZ9dNVgqJMDP/O4GRBA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-transform-react-display-name": "^7.0.0", @@ -8337,8 +8327,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.13.1", @@ -8811,7 +8800,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -10427,7 +10415,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -10662,6 +10649,11 @@ "safe-buffer": "^5.1.0" } }, + "randomcolor": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.5.4.tgz", + "integrity": "sha512-nYd4nmTuuwMFzHL6W+UWR5fNERGZeVauho8mrJDUSXdNDbao4rbrUwhuLgKC/j8VCS5+34Ria8CsTDuBjrIrQA==" + }, "randomfill": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", @@ -10799,8 +10791,7 @@ "react-is": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", - "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==", - "dev": true + "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" }, "react-lifecycles-compat": { "version": "3.0.4", @@ -10854,6 +10845,17 @@ } } }, + "react-tagcloud": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-tagcloud/-/react-tagcloud-2.0.0.tgz", + "integrity": "sha512-SQLAvSDx35by4xYQtcJEYZW3294jicw7yMEmhJ70ZBpGBA/MDqvPki/hyr37YHIDr1w1lEW0aJoIO7WEjRjUdw==", + "requires": { + "@babel/preset-react": "^7.6.3", + "prop-types": "^15.6.2", + "randomcolor": "^0.5.4", + "shuffle-array": "^1.0.1" + } + }, "react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", @@ -11633,6 +11635,11 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, + "shuffle-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/shuffle-array/-/shuffle-array-1.0.1.tgz", + "integrity": "sha1-xP88/nTRb5NzBZIwGyXmV3sSiYs=" + }, "shuffle-seed": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/shuffle-seed/-/shuffle-seed-1.1.6.tgz", diff --git a/rollup.config.js b/rollup.config.js index b20c0fc2e..2cdd60726 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,5 @@ export default { - input: ['src/util/Filter.js', 'src/util/Card.js'], + input: ['src/util/Filter.js', 'src/util/Card.js', 'src/util/draftutil.js', 'src/util/Util.js'], output: { dir: 'dist/util', format: 'cjs', diff --git a/routes/cube_routes.js b/routes/cube_routes.js index b1f01ccb9..3f8871c6f 100644 --- a/routes/cube_routes.js +++ b/routes/cube_routes.js @@ -12,7 +12,7 @@ var { build_id_query, get_cube_id, } = require('../serverjs/cubefn.js'); -const draftutil = require('../serverjs/draftutil.js'); +const draftutil = require('../dist/util/draftutil.js'); const cardutil = require('../dist/util/Card.js'); const carddb = require('../serverjs/cards.js'); carddb.initializeCardDb(); @@ -1638,7 +1638,8 @@ router.post('/startdraft/:id', async (req, res) => { }); let bots = draftutil.getDraftBots(params); let format = draftutil.getDraftFormat(params, cube); - let draft = draftutil.createDraft(format, draftcards, bots, params.seats); + let draft = new Draft(); + draftutil.populateDraft(draft, format, draftcards, bots, params.seats); draft.cube = cube._id; await draft.save(); return res.redirect('/cube/draft/' + draft._id); diff --git a/serverjs/draftutil.js b/serverjs/draftutil.js deleted file mode 100644 index 469b537ed..000000000 --- a/serverjs/draftutil.js +++ /dev/null @@ -1,229 +0,0 @@ -const util = require('./util.js'); -const Draft = require('../models/draft'); -const Filter = require('../dist/util/Filter'); - -function matchingCards(cards, filter) { - if (filter === null || filter.length === 0 || filter[0] === null || filter[0] === '') { - return cards; - } - return cards.filter((card) => - Filter.filterCard( - card, - filter, - true, // in cube - ), - ); -} - -function makeFilter(filterText) { - console.log('filterText', filterText, filterText === ''); - if (!filterText || filterText === '' || filterText == '*') { - return [null]; - } - - let tokens = []; - let valid = false; - valid = Filter.tokenizeInput(filterText, tokens) && Filter.verifyTokens(tokens); - - // backwards compatibilty: treat as tag - if (!valid || !Filter.operatorsRegex.test(filterText)) { - let tagfilterText = filterText; - // if it contains spaces then wrap in quotes - if (tagfilterText.indexOf(' ') >= 0 && !tagfilterText.startsWith('"')) { - tagfilterText = '"' + filterText + '"'; - } - tagfilterText = 'tag:' + tagfilterText; // TODO: use Filter.tag instead of 'tag' - tokens = []; - valid = Filter.tokenizeInput(tagfilterText, tokens) && Filter.verifyTokens(tokens); - } - - if (!valid) { - throw new Error('Invalid card filter: ' + filterText); - } - return [Filter.parseTokens(tokens)]; -} - -/* Takes the raw data for custom format, converts to JSON and creates - a data structure: - - [pack][card in pack][token,token...] -*/ -function parseDraftFormat(packsJSON, splitter = ',') { - let format = JSON.parse(packsJSON); - for (let j = 0; j < format.length; j++) { - for (let k = 0; k < format[j].length; k++) { - format[j][k] = format[j][k].split(splitter); - for (let m = 0; m < format[j][k].length; m++) { - format[j][k][m] = makeFilter(format[j][k][m].trim()); - } - } - } - return format; -} - -function createPacks(draft, format, seats, nextCardfn) { - let messages = []; - draft.picks = []; - draft.packs = []; - for (let seat = 0; seat < seats; seat++) { - draft.picks.push([]); - draft.packs.push([]); - for (let packNum = 0; packNum < format.length; packNum++) { - draft.packs[seat].push([]); - let pack = []; - for (let cardNum = 0; cardNum < format[packNum].length; cardNum++) { - let result = nextCardfn(format[packNum][cardNum]); - if (result.messages && result.messages.length > 0) { - messages = messages.concat(result.messages); - } - if (result.card) { - pack.push(result.card); - } - } - // shuffle the cards in the pack - draft.packs[seat][packNum] = util.shuffle(pack); - } - } - return { ok: true, messages: messages }; -} - -function standardDraft(cards) { - cards = util.shuffle(cards); - - return function(cardFormat) { - return { card: cards.pop(), message: '' }; - }; -} - -function customDraft(cards, duplicates = false) { - return function(cardFilter) { - if (cards.length === 0) { - throw new Error('Unable to create draft. Not enough cards.'); - } - - // each filter is an array of parsed filter tokens, we choose one randomly - let validCards = cards; - let index = null; - let messages = []; - if (cardFilter.length > 0) { - do { - index = Math.floor(Math.random() * cardFilter.length); - validCards = matchingCards(cards, cardFilter[index]); - if (validCards.length == 0) { - // try another options and remove this filter as it is now empty - cardFilter.splice(index, 1); - messages.push('Warning: no cards matching filter: ' + cardFilter[index]); - } - } while (validCards.length == 0 && cardFilter.length > 1); - - // try to fill with any available card - if (validCards.length == 0) { - // TODO: warn user that they ran out of matching cards - messages.push('Warning: not enough cards matching any filter.'); - validCards = cards; - } - } - - if (validCards.length == 0) { - throw new Error('Unable to create draft, not enough cards matching filter.'); - } - - index = Math.floor(Math.random() * validCards.length); - - // slice out the first card with the index, or error out - let card = validCards[index]; - if (!duplicates) { - // remove from cards - index = cards.indexOf(card); - cards.splice(index, 1); - } - - return { card: card, messages: messages }; - }; -} - -var publicMethods = { - getDraftBots: function(params) { - var botcolors = Math.ceil(((params.seats - 1) * 2) / 5); - var draftbots = []; - var colors = []; - for (let i = 0; i < botcolors; i++) { - colors.push('W'); - colors.push('U'); - colors.push('B'); - colors.push('R'); - colors.push('G'); - } - colors = util.shuffle(colors); - for (let i = 0; i < params.seats - 1; i++) { - var colorcombo = [colors.pop(), colors.pop()]; - draftbots.push(colorcombo); - } - // TODO: order the bots to avoid same colors next to each other - return draftbots; - }, - - getDraftFormat: function(params, cube) { - let format; - if (params.id >= 0) { - format = parseDraftFormat(cube.draft_formats[params.id].packs); - format.custom = true; - format.multiples = cube.draft_formats[params.id].multiples; - } else { - // default format - format = []; - format.custom = false; - format.multiples = false; - for (let pack = 0; pack < params.packs; pack++) { - format[pack] = []; - for (let card = 0; card < params.cards; card++) { - format[pack].push('*'); // any card - } - } - } - return format; - }, - - // NOTE: format is an array with extra attributes, see getDraftFormat() - createDraft: function(format, cards, bots, seats) { - let draft = new Draft(); - let nextCardfn = null; - - if (cards.length === 0) { - throw new Error('Unable to create draft: no cards.'); - } - if (bots.length === 0) { - throw new Error('Unable to create draft: no bots.'); - } - if (seats < 2) { - throw new Error('Unable to create draft: invalid seats: ' + seats); - } - - if (format.custom === true) { - nextCardfn = customDraft(cards, format.multiples); - } else { - nextCardfn = standardDraft(cards); - } - - let result = createPacks(draft, format, seats, nextCardfn); - - if (result.messages.length > 0) { - // TODO: display messages to user - draft.messages = result.messages.join('\n'); - } - - if (!result.ok) { - throw new Error('Could not create draft:\n' + result.messages.join('\n')); - } - - // initial draft state - draft.initial_state = draft.packs.slice(); - draft.pickNumber = 1; - draft.packNumber = 1; - draft.bots = bots; - - return draft; - }, -}; - -module.exports = publicMethods; diff --git a/src/cube_analysis.js b/src/cube_analysis.js index 3f415bd70..ce892231a 100644 --- a/src/cube_analysis.js +++ b/src/cube_analysis.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import { Col, Container, Dropdown, DropdownMenu, DropdownToggle, DropdownItem, Nav, NavLink, Row } from 'reactstrap'; +import { getDraftFormat, calculateAsfans } from './util/draftutil'; import Filter from './util/Filter'; import Hash from './util/Hash'; @@ -43,8 +44,6 @@ class CubeAnalysis extends Component { this.updateFilter = this.updateFilter.bind(this); this.updateData = this.updateData.bind(this); this.setFilter = this.setFilter.bind(this); - this.updateAsfanCustomWithMultiples = this.updateAsfanCustomWithMultiples.bind(this); - this.updateAsfanCustomSingleton = this.updateAsfanCustomSingleton.bind(this); this.toggleFormatDropdownOpen = this.toggleFormatDropdownOpen.bind(this); this.setFormat = this.setFormat.bind(this); } @@ -58,101 +57,13 @@ class CubeAnalysis extends Component { this.setState({ nav }, this.updateData); } - updateAsfanCustomWithMultiples(format, cardsWithAsfan, pools) { - var failMessage = null; - for (var i = 0; i < format.length; i++) { - for (var j = 0; j < format[i].length; j++) { - const tagCount = format[i][j].length; - for (var tag of format[i][j]) { - const pool = pools[tag]; - if (pool && pool.length > 0) { - const poolWeight = 1 / tagCount / pool.length; - for (var cardIndex of pool) { - cardsWithAsfan[cardIndex].asfan += poolWeight; - } - } else { - failMessage = 'Unable to create draft, no card with tag "' + tag + '" found.'; - } - } - } - } - if (!failMessage) { - this.setState({ cardsWithAsfan }, this.updateFilter); - } else { - console.error(failMessage); - } - } - - updateAsfanCustomSingleton(format, cardsWithAsfan, pools) { - var failMessage = null; - for (var i = 0; i < format.length; i++) { - for (var j = 0; j < format[i].length; j++) { - const tagCount = format[i][j].length; - for (var tag of format[i][j]) { - const pool = pools[tag]; - if (pool && pool.length > 0) { - const poolCount = pool.reduce((sum, cardIndex) => sum + (1 - cardsWithAsfan[cardIndex].asfan), 0); - const poolWeight = 1 / tagCount / poolCount; - for (var cardIndex of pool) { - cardsWithAsfan[cardIndex].asfan += (1 - cardsWithAsfan[cardIndex].asfan) * poolWeight; - } - } else { - failMessage = 'Unable to create draft, no card with tag "' + tag + '" found.'; - } - } - } - } - if (!failMessage) { - this.setState({ cardsWithAsfan }, this.updateFilter); - } else { - console.error(failMessage); - } - } - async updateAsfan() { const { formatId } = this.state; const { cube } = this.props; - if (formatId == -1) { - const defaultAsfan = 15 / cube.cards.length; - const cardsWithAsfan = cube.cards.map((card) => Object.assign({}, card, { asfan: defaultAsfan })); - this.setState({ cardsWithAsfan }, this.updateFilter); - } else { - var format = JSON.parse(cube.draft_formats[formatId].packs); - for (var j = 0; j < format.length; j++) { - for (var k = 0; k < format[j].length; k++) { - format[j][k] = format[j][k].split(','); - for (var m = 0; m < format[j][k].length; m++) { - format[j][k][m] = format[j][k][m].trim().toLowerCase(); - } - } - } - var pools = {}; - const cards = cube.cards; - //sort the cards into groups by tag, then we can pull from them randomly - pools['*'] = []; - cards.forEach(function(card, index) { - pools['*'].push(index); - if (card.tags && card.tags.length > 0) { - card.tags.forEach(function(tag, tag_index) { - tag = tag.toLowerCase(); - if (tag != '*') { - if (!pools[tag]) { - pools[tag] = []; - } - if (!pools[tag].includes(index)) { - pools[tag].push(index); - } - } - }); - } - }); - var cardsWithAsfan = cards.map((card) => Object.assign({}, card, { asfan: 0 })); - if (cube.draft_formats[formatId].multiples) { - this.updateAsfanCustomWithMultiples(format, cardsWithAsfan, pools); - } else { - this.updateAsfanCustomSingleton(format, cardsWithAsfan, pools); - } - } + const cardsWithAsfan = cube.cards.map((card) => Object.assign({}, card)); + const format = getDraftFormat({ id: formatId }, cube); + calculateAsfans(format, cardsWithAsfan); + this.setState({ cardsWithAsfan }, this.updateFilter); } async updateFilter() { diff --git a/src/util/Filter.js b/src/util/Filter.js index e7de858eb..68cb70530 100644 --- a/src/util/Filter.js +++ b/src/util/Filter.js @@ -42,7 +42,7 @@ let categoryMap = new Map([ ]); const operators = ['>=', '<=', '<', '>', ':', '!=', '=']; -const operatorsRegex = new RegExp('(?:' + operators.join('|') + ')'); +export const operatorsRegex = new RegExp('(?:' + operators.join('|') + ')'); function findEndingQuotePosition(filterText, num) { if (!num) { @@ -58,7 +58,7 @@ function findEndingQuotePosition(filterText, num) { return false; } -function tokenizeInput(filterText, tokens) { +export function tokenizeInput(filterText, tokens) { filterText = filterText.trim().toLowerCase(); if (!filterText) { return true; @@ -221,7 +221,7 @@ function simplifyArg(arg, category) { return res; } -const verifyTokens = (tokens) => { +export const verifyTokens = (tokens) => { let temp = tokens; let inBounds = (num) => { return num > -1 && num < temp.length; @@ -357,7 +357,7 @@ const findClose = (tokens, pos) => { return false; }; -const parseTokens = (tokens) => { +export const parseTokens = (tokens) => { let peek = () => tokens[0]; let consume = peek; @@ -406,7 +406,7 @@ function filterCard(card, filters, inCube) { } } -function filterCards(cards, filter, inCube) { +export function filterCards(cards, filter, inCube) { return cards.filter((card) => filterCard(card, filter, inCube)); } diff --git a/src/util/draftutil.js b/src/util/draftutil.js new file mode 100644 index 000000000..12eecbb90 --- /dev/null +++ b/src/util/draftutil.js @@ -0,0 +1,281 @@ +import { filterCards, operatorsRegex, parseTokens, tokenizeInput, verifyTokens } from './Filter'; +import { arrayShuffle } from './Util'; + +function matchingCards(cards, filter, restrictAsfan = false) { + if (filter === null || filter.length === 0 || filter[0] === null || filter[0] === '') { + return cards; + } + const filtered = filterCards(cards, filter, true); + if (restrictAsfan) { + return filtered.filter((card) => card.asfan < 1); + } else { + return filtered; + } +} + +function makeFilter(filterText) { + if (!filterText || filterText === '' || filterText == '*') { + return [null]; + } + + let tokens = []; + let valid = false; + valid = tokenizeInput(filterText, tokens) && verifyTokens(tokens); + + // backwards compatibilty: treat as tag + if (!valid || !operatorsRegex.test(filterText)) { + let tagfilterText = filterText; + // if it contains spaces then wrap in quotes + if (tagfilterText.indexOf(' ') >= 0 && !tagfilterText.startsWith('"')) { + tagfilterText = '"' + filterText + '"'; + } + tagfilterText = 'tag:' + tagfilterText; // TODO: use tag instead of 'tag' + tokens = []; + valid = tokenizeInput(tagfilterText, tokens) && verifyTokens(tokens); + } + + if (!valid) { + throw new Error('Invalid card filter: ' + filterText); + } + return [parseTokens(tokens)]; +} + +/* Takes the raw data for custom format, converts to JSON and creates + a data structure: + + [pack][card in pack][token,token...] +*/ +function parseDraftFormat(packsJSON, splitter = ',') { + let format = JSON.parse(packsJSON); + for (let j = 0; j < format.length; j++) { + for (let k = 0; k < format[j].length; k++) { + format[j][k] = format[j][k].split(splitter); + for (let m = 0; m < format[j][k].length; m++) { + format[j][k][m] = makeFilter(format[j][k][m].trim()); + } + } + } + return format; +} + +function standardDraft(cards, probabilistic = false) { + if (probabilistic) { + const poolCount = cards.length; + const poolWeight = 1 / poolCount; + return (cardFormat) => { + cards.forEach((card) => (card.asfan += poolWeight)); + return { card: true, message: '' }; + }; + } else { + cards = arrayShuffle(cards); + return function(cardFormat) { + return { card: cards.pop(), message: '' }; + }; + } +} + +function customDraft(cards, duplicates = false, probabilistic = false) { + return function(cardFilter) { + if (cards.length === 0) { + throw new Error('Unable to create draft. Not enough cards.'); + } + + // each filter is an array of parsed filter tokens, we choose one randomly + let validCardGroups = []; + let index = -1; + let messages = []; + if (cardFilter.length > 0) { + do { + if (probabilistic) { + index++; + } else { + index = Math.floor(Math.random() * cardFilter.length); + } + const validCards = matchingCards(cards, cardFilter[index], probabilistic && !duplicates); + if (validCards.length == 0) { + // try another options and remove this filter as it is now empty + cardFilter.splice(index, 1); + messages.push('Warning: no cards matching filter: ' + cardFilter[index]); + } else { + validCardGroups.push(validCards); + } + } while ( + (probabilistic || validCardGroups.length == 0) && + cardFilter.length > 1 && + (!probabilistic || index + 1 < cardFilter.length) + ); + + // try to fill with any available card + if (validCardGroups.length == 0) { + // TODO: warn user that they ran out of matching cards + messages.push('Warning: not enough cards matching any filter.'); + validCardGroups = [cards]; + } + } + + if (validCardGroups.length == 0) { + throw new Error('Unable to create draft, not enough cards matching filter.'); + } + + if (probabilistic) { + validCardGroups.forEach((validCards) => { + if (duplicates) { + const poolCount = validCards.length; + const poolWeight = 1 / poolCount / validCardGroups.length; + validCards.forEach((card) => (card.asfan += poolWeight)); + } else { + const poolCount = validCards.reduce((sum, card) => sum + (1 - card.asfan), 0); + const poolWeight = 1 / poolCount / validCardGroups.length; + validCards.forEach((card) => (card.asfan += (1 - card.asfan) * poolWeight)); + } + }); + return { card: true, messages: messages }; + } else { + const validCards = validCardGroups[0]; + index = Math.floor(Math.random() * validCards.length); + + // slice out the first card with the index, or error out + let card = validCards[index]; + if (!duplicates) { + // remove from cards + index = cards.indexOf(card); + cards.splice(index, 1); + } + + return { card: card, messages: messages }; + } + }; +} + +export function getDraftBots(params) { + var botcolors = Math.ceil(((params.seats - 1) * 2) / 5); + var draftbots = []; + var colors = []; + for (let i = 0; i < botcolors; i++) { + colors.push('W'); + colors.push('U'); + colors.push('B'); + colors.push('R'); + colors.push('G'); + } + colors = arrayShuffle(colors); + for (let i = 0; i < params.seats - 1; i++) { + var colorcombo = [colors.pop(), colors.pop()]; + draftbots.push(colorcombo); + } + // TODO: order the bots to avoid same colors next to each other + return draftbots; +} + +export function getDraftFormat(params, cube) { + let format; + if (params.id >= 0) { + format = parseDraftFormat(cube.draft_formats[params.id].packs); + format.custom = true; + format.multiples = cube.draft_formats[params.id].multiples; + } else { + // default format + format = []; + format.custom = false; + format.multiples = false; + for (let pack = 0; pack < params.packs; pack++) { + format[pack] = []; + for (let card = 0; card < params.cards; card++) { + format[pack].push('*'); // any card + } + } + } + return format; +} + +function createPacks(draft, format, seats, nextCardFn) { + let messages = []; + draft.picks = []; + draft.packs = []; + for (let seat = 0; seat < seats; seat++) { + draft.picks.push([]); + draft.packs.push([]); + for (let packNum = 0; packNum < format.length; packNum++) { + draft.packs[seat].push([]); + let pack = []; + for (let cardNum = 0; cardNum < format[packNum].length; cardNum++) { + let result = nextCardFn(format[packNum][cardNum]); + if (result.messages && result.messages.length > 0) { + messages = messages.concat(result.messages); + } + if (result.card) { + pack.push(result.card); + } + } + if (!format.custom) { + // Shuffle the cards in the pack. + draft.packs[seat][packNum] = arrayShuffle(pack); + } else { + // Knowing what slots cards come from can be important. + draft.packs[seat][packNum] = pack; + } + } + } + return { ok: true, messages: messages }; +} + +// NOTE: format is an array with extra attributes, see getDraftFormat() +export function populateDraft(draft, format, cards, bots, seats) { + let nextCardFn = null; + + if (cards.length === 0) { + throw new Error('Unable to create draft: no cards.'); + } + if (bots.length === 0) { + throw new Error('Unable to create draft: no bots.'); + } + if (seats < 2) { + throw new Error('Unable to create draft: invalid seats: ' + seats); + } + + if (format.custom === true) { + nextCardFn = customDraft(cards, format.multiples); + } else { + nextCardFn = standardDraft(cards); + } + + let result = createPacks(draft, format, seats, nextCardFn); + + if (result.messages.length > 0) { + // TODO: display messages to user + draft.messages = result.messages.join('\n'); + } + + if (!result.ok) { + throw new Error('Could not create draft:\n' + result.messages.join('\n')); + } + + // initial draft state + draft.initial_state = draft.packs.slice(); + draft.pickNumber = 1; + draft.packNumber = 1; + draft.bots = bots; + + return draft; +} + +export function calculateAsfans(format, cards) { + let nextCardFn = null; + + cards.forEach((card) => (card.asfan = 0)); + + if (format.custom === true) { + nextCardFn = customDraft(cards, format.multiples, true); + } else { + nextCardFn = standardDraft(cards, null, true); + } + + return createPacks({}, format, 1, nextCardFn); +} + +export default { + calculateAsfans, + populateDraft, + getDraftBots, + getDraftFormat, +};