diff --git a/README.md b/README.md index 62d1b705..c86a1590 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ You can also create a Docker image and run the app in a container: ## Usage -### Start server +### Start Server `npm start` @@ -98,6 +98,31 @@ This command downloads integrates all files previously downloaded from MTGJson. `npm run download_booster_rules` download and parse booster generation rules from [magic-sealed-data](https://github.com/taw/magic-sealed-data) +## Development Notes + +### VSCode + +You can debug this application by adding the following configuration to your `launch.json`: + +```json +{ + "name": "Launch via NPM", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", "start-debug" + ], + "port": 1338 +} +``` + +You should now be able to set breakpoints in `backend/` and hit them when you start the debugger. +This relies on the `--inspect-brk=1338` flag to open port 1338 for the debugger to attach to. + +Breakpoints for the frontend should be set in your browser console. + ### Contributors THANK YOU! diff --git a/backend/game.js b/backend/game.js index f901bee3..2ce9ef7d 100644 --- a/backend/game.js +++ b/backend/game.js @@ -25,6 +25,7 @@ module.exports = class Game extends Room { round: 0, bots: 0, sets: sets || [], + isDecadent: false, secret: uuid.v4(), logger: logger.child({ id: gameID }) }); @@ -36,6 +37,13 @@ module.exports = class Game extends Room { this.packsInfo = this.sets.join(" / "); this.rounds = this.sets.length; break; + case "decadent draft": + // Sets should all be the same and there can be a large number of them. + // Compress this info into e.g. "36x IKO" instead of "IKO / IKO / ...". + this.packsInfo = `${this.sets.length}x ${this.sets[0]}`; + this.rounds = this.sets.length; + this.isDecadent = true; + break; case "cube draft": this.packsInfo = `${cube.packs} packs with ${cube.cards} cards from a pool of ${cube.list.length} cards`; this.rounds = this.cube.packs; @@ -185,7 +193,63 @@ module.exports = class Game extends Room { super.join(sock); this.logger.debug(`${sock.name} joined the game`); - const h = new Human(sock); + function regularDraftPickDelegate(index) { + const pack = this.packs.shift(); + const card = pack.splice(index, 1)[0]; + + this.draftLog.pack.push( [`--> ${card.name}`].concat(pack.map(x => ` ${x.name}`)) ); + this.updateDraftStats(this.draftLog.pack[ this.draftLog.pack.length-1 ], this.pool); + + let pickcard = card.name; + if (card.foil === true) + pickcard = "*" + pickcard + "*"; + + this.pool.push(card); + this.picks.push(pickcard); + this.send("add", card); + + let [next] = this.packs; + if (!next) + this.time = 0; + else + this.sendPack(next); + + this.autopick_index = -1; + this.emit("pass", pack); + } + + function decadentDraftPickDelegate(index) { + const pack = this.packs.shift(); + const card = pack.splice(index, 1)[0]; + + this.draftLog.pack.push( [`--> ${card.name}`].concat(pack.map(x => ` ${x.name}`)) ); + this.updateDraftStats(this.draftLog.pack[ this.draftLog.pack.length-1 ], this.pool); + + let pickcard = card.name; + if (card.foil === true) + pickcard = "*" + pickcard + "*"; + + this.pool.push(card); + this.picks.push(pickcard); + this.send("add", card); + + let [next] = this.packs; + if (!next) + this.time = 0; + else + this.sendPack(next); + + // Discard the rest of the cards in the pack + // after one is chosen. + pack.length = 0; + + this.autopick_index = -1; + this.emit("pass", pack); + } + + const draftPickDelegate = this.isDecadent ? decadentDraftPickDelegate : regularDraftPickDelegate; + + const h = new Human(sock, draftPickDelegate); if (h.id === this.hostID) { h.isHost = true; sock.once("start", this.start.bind(this)); @@ -450,7 +514,8 @@ module.exports = class Game extends Room { }); break; } - case "draft": { + case "draft": + case "decadent draft": { this.pool = Pool.DraftNormal({ playersLength: this.players.length, sets: this.sets @@ -509,12 +574,16 @@ module.exports = class Game extends Room { this.startRound(); } + shouldAddBots() { + return this.addBots && !this.isDecadent; + } + start({ addBots, useTimer, timerLength, shufflePlayers }) { try { Object.assign(this, { addBots, useTimer, timerLength, shufflePlayers }); this.renew(); - if (addBots) { + if (this.shouldAddBots()) { while (this.players.length < this.seats) { this.players.push(new Bot()); this.bots++; @@ -536,7 +605,7 @@ module.exports = class Game extends Room { this.logger.info(`Game ${this.id} started.\n${this.toString()}`); Game.broadcastGameInfo(); } catch(err) { - this.logger.error(`Game ${this.id} encountered an error while starting: ${err.stack} GameState: ${this.toString()}`); + this.logger.error(`Game ${this.id} encountered an error while starting: ${err.stack} GameState: ${this.toString()}`); this.players.forEach(player => { if (!player.isBot) { player.exit(); diff --git a/backend/human.js b/backend/human.js index 784359aa..f1b90cb1 100644 --- a/backend/human.js +++ b/backend/human.js @@ -5,13 +5,14 @@ const {random} = require("lodash"); const logger = require("./logger"); module.exports = class extends Player { - constructor(sock) { + constructor(sock, pickDelegate) { super({ isBot: false, isConnected: true, name: sock.name, id: sock.id }); + this.pickDelegate = pickDelegate.bind(this); this.attach(sock); } @@ -111,28 +112,7 @@ module.exports = class extends Player { this.draftStats.push( { picked, notPicked, pool: namePool } ); } pick(index) { - const pack = this.packs.shift(); - const card = pack.splice(index, 1)[0]; - - this.draftLog.pack.push( [`--> ${card.name}`].concat(pack.map(x => ` ${x.name}`)) ); - this.updateDraftStats(this.draftLog.pack[ this.draftLog.pack.length-1 ], this.pool); - - let pickcard = card.name; - if (card.foil === true) - pickcard = "*" + pickcard + "*"; - - this.pool.push(card); - this.picks.push(pickcard); - this.send("add", card); - - let [next] = this.packs; - if (!next) - this.time = 0; - else - this.sendPack(next); - - this.autopick_index = -1; - this.emit("pass", pack); + this.pickDelegate(index); } pickOnTimeout() { let index = this.autopick_index; diff --git a/backend/player.js b/backend/player.js index d5494124..2c7a1979 100644 --- a/backend/player.js +++ b/backend/player.js @@ -35,7 +35,7 @@ class Player extends EventEmitter { packSize: 15, self: 0, useTimer: false, - timeLength: "Slow", + timeLength: "Slow" }); } diff --git a/backend/util.js b/backend/util.js index 3b3087e9..cfdaee7e 100644 --- a/backend/util.js +++ b/backend/util.js @@ -66,8 +66,17 @@ module.exports = { return true; }, game({ seats, type, sets, cube, isPrivate, modernOnly = true, chaosPacksNumber, totalChaos = true }) { - assert(["draft", "sealed", "cube draft", "cube sealed", "chaos draft", "chaos sealed"].includes(type), - "type can be draft, sealed, chaos draft, chaos sealed, cube draft or cube sealed"); + const acceptableGameTypes = [ + "draft", + "sealed", + "cube draft", + "cube sealed", + "chaos draft", + "chaos sealed", + "decadent draft" + ]; + assert(acceptableGameTypes.includes(type), + `type can be one of: ${acceptableGameTypes.join(", ")}`); assert(typeof isPrivate === "boolean", "isPrivate must be a boolean"); assert(typeof seats === "number", "seats must be a number"); assert(seats >= 1 && seats <= 100, "seats' number must be between 1 and 100"); diff --git a/frontend/src/app.js b/frontend/src/app.js index c2e7795d..5b255a6a 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -35,6 +35,7 @@ let App = { sets: [], setsDraft: [], setsSealed: [], + setsDecadentDraft: [], availableSets: {}, list: "", cards: 15, @@ -82,6 +83,9 @@ let App = { get isGameFinished() { return App.state.round === -1; }, + get isDecadentDraft() { + return /decadent draft/.test(App.state.game.type); + }, get notificationBlocked() { return ["denied", "notsupported"].includes(App.state.notificationResult); @@ -167,12 +171,13 @@ let App = { }, set(state) { Object.assign(App.state, state); - // Set default sets - if ( App.state.setsSealed.length === 0 && App.state.latestSet) { - App.state.setsSealed = times(6, constant(App.state.latestSet.code)); - } - if ( App.state.setsDraft.length === 0 && App.state.latestSet) { - App.state.setsDraft = times(3, constant(App.state.latestSet.code)); + if (App.state.latestSet) { + // Default sets to the latest set. + const defaultSetCode = App.state.latestSet.code; + const replicateDefaultSet = (desiredLength) => times(desiredLength, constant(defaultSetCode)); + App.state.setsSealed = replicateDefaultSet(6); + App.state.setsDraft = replicateDefaultSet(3); + App.state.setsDecadentDraft = replicateDefaultSet(36); } App.update(); }, diff --git a/frontend/src/components/Checkbox.jsx b/frontend/src/components/Checkbox.jsx new file mode 100644 index 00000000..25dfa104 --- /dev/null +++ b/frontend/src/components/Checkbox.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import App from "../app"; + +const Checkbox = ({link, text, side, onChange, ...rest}) => ( +
+ {side === "right" ? text : ""} + + {side === "left" ? text : ""} +
+); + +Checkbox.propTypes = { + link: PropTypes.string, + text: PropTypes.string, + side: PropTypes.string, + onChange: PropTypes.func +}; + +export default Checkbox; diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md new file mode 100644 index 00000000..ca327996 --- /dev/null +++ b/frontend/src/components/README.md @@ -0,0 +1,5 @@ +# Components + +A set of general UI components. + +Many are "connected" tools which use a "link" prop to connect to the app state. diff --git a/frontend/src/components/Select.jsx b/frontend/src/components/Select.jsx new file mode 100644 index 00000000..a98482ec --- /dev/null +++ b/frontend/src/components/Select.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import App from "../app"; + +const Select = ({ + link, + opts, + onChange = (e) => { App.save(link, e.currentTarget.value); }, + value = App.state[link], + ...rest}) => ( + +); + +Select.propTypes = { + link: PropTypes.string, + onChange: PropTypes.func, + value: PropTypes.any, + opts: PropTypes.array +}; + +export default Select; diff --git a/frontend/src/components/Spaced.jsx b/frontend/src/components/Spaced.jsx new file mode 100644 index 00000000..e729ae4e --- /dev/null +++ b/frontend/src/components/Spaced.jsx @@ -0,0 +1,13 @@ +import React from "react"; + +const Spaced = ({elements}) => ( + elements + .map((x, index) => {x}) + .reduce((prev, curr) => [ + prev, + , + curr + ]) +); + +export default Spaced; diff --git a/frontend/src/components/TextArea.jsx b/frontend/src/components/TextArea.jsx new file mode 100644 index 00000000..ed9a2444 --- /dev/null +++ b/frontend/src/components/TextArea.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import App from "../app"; + +const TextArea = ({link, ...rest}) => ( +