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

ISSUE-919: Add a decadent draft game mode #990

Merged
merged 37 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
127dac9
refactor: Extract Checkbox.jsx
keeler Apr 25, 2020
86cadbf
refactor: Extract Spaced.jsx
keeler Apr 25, 2020
4fc3972
refactor: Extract Select.jsx
keeler Apr 25, 2020
2da4519
refactor: Extract TextArea.jsx
keeler Apr 25, 2020
b3620cc
refactor: Clean and re-use toTitleCase()
keeler Apr 25, 2020
c448abb
feat: Enable debugging in VSCode
keeler Apr 25, 2020
91ff1c5
feat: Lobby UI for decadent draft
keeler Apr 25, 2020
65b8328
feat: Handle pack passing for decadent draft
keeler Apr 25, 2020
6c8fa21
fix: Exclude *.spec.js files from webpack build
keeler Apr 25, 2020
a6ac3f9
fix: Decadent draft should show Pick 1 / 1
keeler Apr 25, 2020
df88748
wip: Temporarily disable frontend tests
keeler Apr 25, 2020
5ea8df6
fix: Switch from regular to decadent draft should adjust sets
keeler Apr 26, 2020
376aae4
fix: Swap regular to decadent didn't define sets
keeler Apr 26, 2020
52dfb34
fix: Pass nothing instead of starting new round each pass
keeler Apr 26, 2020
0b364bd
fix: Re-enable frontend tests
keeler Apr 26, 2020
759e3f1
fix: Remove frontend/test from webpack copy
keeler Apr 26, 2020
26668c1
feedback: Compress set info to e.g. 40x IKO
keeler Apr 26, 2020
e3a722c
fix: Show if game option unavailable
keeler Apr 26, 2020
bdb5761
docs: Add blockquote describing the new mode
keeler Apr 26, 2020
59b681d
fix: TextArea should use link not 'list'
keeler Apr 27, 2020
ee64637
refactor: Remove now-unused useForAllSets prop
keeler Apr 27, 2020
b8ecb43
Merge branch 'master' into ISSUE-919-decadent-draft
keeler Apr 28, 2020
2e29400
feedback: Only show supported game mode selections
keeler Apr 28, 2020
5850b20
docs: Descriptions for each game mode
keeler Apr 28, 2020
8b109b7
feedback: Re-use lodash capitalize()
keeler Apr 28, 2020
61ce34a
fix: Chaos component prop type should be string
keeler Apr 30, 2020
116c755
fix: Remove unused propType on Set
keeler Apr 30, 2020
930dab5
refactor: Extract method for assigning default set
keeler Apr 30, 2020
e6fd5e5
feedback: Use setsDecadentDraft instead of setsDraft
keeler Apr 30, 2020
181700f
refactor: Extract SetReplicated component
keeler Apr 30, 2020
1895365
refactor: Pull cube list style into css class
keeler Apr 30, 2020
f9c3eb5
feedback: If decadent draft, hide bots and shuffle players options
keeler Apr 30, 2020
f9bef93
feedback: Use pick delegate
keeler May 1, 2020
7d2086c
feedback: Missed isDecadent flag on Bot class
keeler May 2, 2020
629e577
feedback: Remove unnecessary whitespace
keeler May 2, 2020
ffc7eec
fix: Don't add bots if decadent draft
keeler May 2, 2020
a182b91
fix: Attempt to fix nothing returned from render error
keeler May 5, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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!
Expand Down
77 changes: 73 additions & 4 deletions backend/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
});
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
HerveH44 marked this conversation as resolved.
Show resolved Hide resolved
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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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++;
Expand All @@ -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();
Expand Down
26 changes: 3 additions & 23 deletions backend/human.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion backend/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Player extends EventEmitter {
packSize: 15,
self: 0,
useTimer: false,
timeLength: "Slow",
timeLength: "Slow"
});
}

Expand Down
13 changes: 11 additions & 2 deletions backend/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
17 changes: 11 additions & 6 deletions frontend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ let App = {
sets: [],
setsDraft: [],
setsSealed: [],
setsDecadentDraft: [],
availableSets: {},
list: "",
cards: 15,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
},
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/Checkbox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import PropTypes from "prop-types";

import App from "../app";

const Checkbox = ({link, text, side, onChange, ...rest}) => (
<div>
{side === "right" ? text : ""}
<input
{...rest}
type="checkbox"
onChange={onChange || function (e) {
App.save(link, e.currentTarget.checked);
}}
checked={App.state[link]}/>
{side === "left" ? text : ""}
</div>
);

Checkbox.propTypes = {
link: PropTypes.string,
text: PropTypes.string,
side: PropTypes.string,
onChange: PropTypes.func
};

export default Checkbox;
5 changes: 5 additions & 0 deletions frontend/src/components/README.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions frontend/src/components/Select.jsx
Original file line number Diff line number Diff line change
@@ -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
onChange={onChange}
value={value}
{...rest}>
{opts.map((opt, index) =>
<option key={index}>{opt}</option>
)}
</select>
);

Select.propTypes = {
link: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.any,
opts: PropTypes.array
};

export default Select;
13 changes: 13 additions & 0 deletions frontend/src/components/Spaced.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

const Spaced = ({elements}) => (
elements
.map((x, index) => <span key={index}>{x}</span>)
.reduce((prev, curr) => [
prev,
<span key = {prev+curr} className = 'spacer-dot' />,
curr
])
);

export default Spaced;
18 changes: 18 additions & 0 deletions frontend/src/components/TextArea.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import PropTypes from "prop-types";

import App from "../app";

const TextArea = ({link, ...rest}) => (
<textarea
onChange={(e) => { App.save(link, e.currentTarget.value); }}
value={App.state[link]}
{...rest}
/>
);

TextArea.propTypes = {
link: PropTypes.string
};

export default TextArea;
3 changes: 3 additions & 0 deletions frontend/src/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ const events = {
options.sets = gametype === "sealed" ? setsSealed : setsDraft;
break;
}
case "decadent":
options.sets = App.state.setsDecadentDraft;
break;
case "cube":
options.cube = parseCubeOptions();
break;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/game/Cols.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from "prop-types";

import App from "../app";
import {getZoneDisplayName} from "../zones";
import {Spaced} from "../utils.jsx";
import Spaced from "../components/Spaced";
import {getCardSrc, getFallbackSrc} from "../cardimage";

class Cols extends Component {
Expand Down
Loading