Skip to content

Commit a1fcc39

Browse files
authored
Dr4ft 1.0.0 (#798)
Major refactoring and optimizations within the project
1 parent 2a7b397 commit a1fcc39

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+2139
-317608
lines changed

.babelrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
"@babel/plugin-proposal-function-sent",
2828
"@babel/plugin-proposal-export-namespace-from",
2929
"@babel/plugin-proposal-numeric-separator",
30-
"@babel/plugin-proposal-throw-expressions",
30+
"@babel/plugin-proposal-throw-expressions"
3131
]
3232
}

.dockerignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ data/*
22
!data/mws.json
33
!data/scores.json
44
node_modules
5-
public/lib
5+
frontend/lib

.gitignore

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
.DS_Store
33
node_modules
44
.vscode
5+
.idea
56

67
data
7-
public/src/config.js
8-
public/index.html
8+
frontend/index.html
99

1010
config.client.js
1111
config.server.js
@@ -15,4 +15,4 @@ npm-debug.log
1515
coverage/
1616
.nyc_output/
1717

18-
*.log
18+
*.log

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ matrix:
4747
install:
4848
- npm install --ignore-scripts
4949
script:
50-
- npm run setup-env
5150
- npm run lint
5251

5352
- name: Run tests

README.md

+62-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
dr<img src="https://raw.githubusercontent.com/dr4fters/dr4ft/master/public/4.png" alt="4" height="14">ft
2+
dr<img src="https://raw.githubusercontent.com/dr4fters/dr4ft/master/frontend/4.png" alt="4" height="14">ft
33
</p>
44

55
<p align='center'>
@@ -14,14 +14,34 @@
1414

1515
# dr4ft [![Chat](https://badges.gitter.im/dr4fters/dr4ft.svg)](https://gitter.im/dr4fters/dr4ft)
1616

17-
*dr4ft* is a <kbd>NodeJS</kbd> application.<br>
18-
*dr4ft* is written in [ES6] and transpiled with [Babel], and uses [React] on the client-side.
19-
20-
Found **bugs** or have **feature requests**? Feel free to [open an issue](https://github.com/dr4fters/dr4ft/issues/new)!
21-
22-
23-
24-
<br>
17+
*dr4ft* is a <kbd>NodeJS</kbd> based web-application that simulates draft and sealed format between players and/or bots.
18+
Most of MTG sets are playable thanks to MTGJson support. We follow as much as possible the rules that determine how a real booster is created.
19+
20+
The application provides the following features:
21+
22+
* Draft and sealed format
23+
* Regular, Cube and chaos game types
24+
* 1 to 100 players
25+
* 1 to 12 packs per player
26+
* All playable sets ever printed
27+
* Import your custom set and play it
28+
* In-game chat
29+
* Pick Timer
30+
* Autopick
31+
* Suggest lands
32+
* Kick players
33+
* Connection indicators
34+
* Pick confirmation
35+
* Grid and column view
36+
* Card sorting by rarity, type, color or Manacost
37+
* Bots
38+
* Notifications when a pack is available
39+
* API to create and manage a game remotely. [More docs here](https://github.com/dr4fters/dr4ft/bloc/master/doc/api.md)
40+
41+
## Technologies
42+
43+
*dr4ft* is written in [ES6] and transpiled with [Webpack] and [Babel], and uses [React] on the client-side.
44+
The application uses [SocketIO] and the Websocket technology between client and server.
2545

2646
# Project History
2747

@@ -61,9 +81,39 @@ You can also create a Docker image and run the app in a container:
6181
`docker run -dp 1337:1337 dr4ft-app`<br>
6282
4) Visit [http://localhost:1337](http://localhost:1337)
6383

84+
## Usage
6485

86+
### Start server
6587

66-
<br>
88+
`npm start`
89+
90+
This command start the server
91+
92+
`npm run download_allsets`
93+
This command downloads all sets from MTGJson and integrates them.
94+
95+
`npm run update_database`
96+
This command downloads integrates all files previously downloaded from MTGJson.
97+
98+
`npm run download_booster_rules`
99+
download and parse booster generation rules from [magic-sealed-data](https://github.com/taw/magic-sealed-data)
100+
101+
### Contributors
102+
103+
THANK YOU!
104+
105+
### Contribute!
106+
107+
Be a part of this project! You can run the test using the following.
108+
109+
1. Install dependencies from package.json by running `npm install`
110+
2. Run the test via `npm test`
111+
3. Make some fun new modules!
112+
113+
Found **bugs** or have **feature requests**? Feel free to [open an issue](https://github.com/dr4fters/dr4ft/issues/new)!
114+
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
115+
116+
Please make sure to update tests as appropriate.
67117

68118
<p align='center'>
69119
<sub><i>The project is unaffiliated with Wizards of the Coast, and is licensed under the MIT license.</i></sub>
@@ -75,3 +125,5 @@ You can also create a Docker image and run the app in a container:
75125
[ES6]: https://github.com/lukehoban/es6features
76126
[Babel]: https://github.com/babel/babel
77127
[React]: https://github.com/facebook/react
128+
[Webpack]: https://webpack.js.org/
129+
[SocketIO]: https://socket.io

app.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ const helmet = require("helmet");
66
const fileUpload = require("express-fileupload");
77
const cors = require("cors");
88
const bodyParser = require("body-parser");
9-
const logger = require("./src/logger");
10-
const router = require("./src/router");
11-
const apiRouter = require("./src/api/");
12-
const allSets = require("./src/make/allsets");
13-
const config = require("./config.server");
9+
const logger = require("./backend/logger");
10+
const router = require("./backend/router");
11+
const apiRouter = require("./backend/api/");
12+
const allSets = require("./scripts/download_allsets");
13+
const {app: config, version} = require("./config");
1414
const app = express();
1515

1616

@@ -38,5 +38,5 @@ const io = eio(server);
3838
io.on("connection", router);
3939

4040
server.listen(config.PORT);
41-
logger.info(`Started up on port ${config.PORT} with version ${config.VERSION}`);
41+
logger.info(`Started up on port ${config.PORT} with version ${version}`);
4242

src/api/cubes.js backend/api/cubes.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ const logger = require("../logger");
66
cubesRouter
77
.get("/", (req, res) => {
88

9-
fs.readdir("src/cubes", function(err, fileNames) {
9+
fs.readdir("backend/cubes", function(err, fileNames) {
1010
if (err) {
1111
logger.error(err);
1212
res.status(500).end();
1313
} else {
1414
let cubes = {};
1515
fileNames.forEach(name => {
1616
if (name.includes(".txt")) {
17-
const cube = fs.readFileSync(`src/cubes/${name}`);
17+
const cube = fs.readFileSync(`backend/cubes/${name}`);
1818
const key = name.slice(0, name.length-4);
1919
cubes[key] = cube.toString();
2020
}

src/api/games.js backend/api/games.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,20 @@ gamesRouter
7272
"bots": req.game.bots
7373
});
7474
})
75-
75+
/**
76+
* sends an object according to the endpoint api/games/:gameId/status.
77+
* It shows if the game started, the current pack and players' infos.
78+
*/
7679
.get("/:gameId/status", checkGameId, (req, res) => {
7780
res.send(req.game.getStatus());
7881
})
7982

83+
/**
84+
* can accept a `seat`(from 0 to X) or an `id` (playerId) to get informations,
85+
* according to the endoint api/games/:gameId/decks.
86+
* If no `seat` and `id` are requested,
87+
* then it returns an array of the decks of all players.
88+
*/
8089
// secret=[string]&seat=[int]&id[string]
8190
.get("/:gameId/deck", checkGameId, checkGameSecret, (req, res) => {
8291
res.send(req.game.getDecks(req.query));
File renamed without changes.

src/api/sets.js backend/api/sets.js

+14-19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
const fs = require("fs");
22
const express = require("express");
33
const setsRouter = express.Router();
4-
const { getSets, getCards, writeSets, writeCards, getPlayableSets, getLatestReleasedSet } = require("../data");
5-
const doSet = require("../make/doSet");
6-
const Sock = require("../sock");
4+
const { getSets, saveSetAndCards } = require("../data");
5+
const doSet = require("../import/doSet");
76
const logger = require("../logger");
8-
const parser = require("../make/xml/parser");
7+
const parser = require("../import/xml/parser");
98

109
if (!fs.existsSync("data/custom")) {
1110
fs.mkdirSync("data/custom");
@@ -41,35 +40,31 @@ setsRouter
4140
});
4241

4342
function integrateJson(json) {
44-
const newCards = getCards();
4543
const sets = getSets();
4644

47-
// Avoid overwriting existing sets
4845
if ((json.code in sets)) {
4946
// Unless it's a custom set. In this case, we allow overriding
50-
if (sets[json.code].type != CUSTOM_TYPE) {
51-
throw new Error(`Set existing already. Not saving agin set with code "${json.code}" to database`);
47+
if (sets[json.code].type !== CUSTOM_TYPE) {
48+
throw new Error(`Set existing already. Not saving again set with code "${json.code}" to database`);
5249
} else {
5350
logger.info(`Custom set ${json.code} already existing. Overriding with new file...`);
5451
}
5552
}
5653

57-
const [parsedSet, parsedCards] = doSet(json, {}, newCards);
58-
parsedSet.type = CUSTOM_TYPE; //Force set as custom
59-
54+
//TODO: that should be done done by a service -> parse and save (and write file)
55+
json.type = CUSTOM_TYPE; //Force set as custom
56+
const [set, cards] = doSet(json);
57+
saveSetAndCards({ set, cards });
6058
logger.info(`adding new set with code "${json.code}" to database`);
61-
sets[json.code] = parsedSet;
62-
writeSets(sets);
63-
Sock.broadcast("set", { availableSets: getPlayableSets(), latestSet: getLatestReleasedSet() });
64-
65-
writeCards(parsedCards);
6659

60+
//TODO: That should be done by something else. Move out of controller
6761
//Moving custom set to custom directory
68-
fs.writeFile(`data/custom/${json.code}.json`, JSON.stringify(json), (err) => {
62+
fs.writeFile(`data/custom/${json.code}.json`, JSON.stringify(json, undefined, 4), (err) => {
6963
if (err) {
70-
throw new Error(err);
64+
logger.error(`Could not save file ${json.code}.json. ${err}`);
65+
} else {
66+
logger.info(`Saved custom set as file ${json.code}.json`);
7167
}
72-
logger.info(`Saved custom set as file ${json.code}.json`);
7368
});
7469
}
7570

backend/boosterGenerator.js

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
const {getCardByUuid, getSet} = require("./data");
2+
const logger = require("./logger");
3+
const boosterRules = require("../data/boosterRules.json");
4+
const weighted = require("weighted");
5+
const {sample, sampleSize, random, concat} = require("lodash");
6+
7+
const makeBoosterFromRules = (setCode) => {
8+
const set = getSet(setCode);
9+
if (!set) {
10+
throw new Error(`${setCode} does not exist`);
11+
}
12+
13+
const setRules = boosterRules[setCode];
14+
if (!setRules) {
15+
return getDefaultBooster(set);
16+
}
17+
18+
try {
19+
const { boosters, totalWeight, sheets } = setRules;
20+
const boosterSheets = weighted(
21+
boosters.map(({sheets}) => sheets),
22+
boosters.map(({weight}) => weight),
23+
{total: totalWeight});
24+
return Object.entries(boosterSheets)
25+
.flatMap(chooseCards(sheets));
26+
} catch (error) {
27+
logger.error(`could not produce a booster of ${setCode}. Falling back to default booster. ${error.stack}`);
28+
return getDefaultBooster(set);
29+
}
30+
};
31+
32+
const getDefaultBooster = (set) => {
33+
let { Basic, Common, Uncommon, Rare, Mythic, size } = set;
34+
35+
if (Mythic && !random(7))
36+
Rare = Mythic;
37+
38+
if (!Rare) {
39+
Rare = Uncommon; //In some sets rare didn't exist. So we replace them with uncommons
40+
}
41+
42+
//make small sets draftable.
43+
if (size < 10)
44+
size = 10;
45+
46+
const cardNames = concat(
47+
sampleSize(Common, size),
48+
sampleSize(Uncommon, 3),
49+
sampleSize(Rare, 1)
50+
);
51+
52+
if (Basic) {
53+
cardNames.push(sample(Basic));
54+
}
55+
56+
return cardNames.map(getCardByUuid);
57+
};
58+
59+
const chooseCards = sheets => ([sheetCode, numberOfCardsToPick]) => {
60+
const sheet = sheets[sheetCode];
61+
62+
const randomCards = sheet.balance_colors
63+
? getRandomCardsWithColorBalance(sheet, numberOfCardsToPick)
64+
: getRandomCards(sheet, numberOfCardsToPick);
65+
66+
return randomCards.map(toCard(sheetCode));
67+
};
68+
69+
function getRandomCardsWithColorBalance({cardsByColor, cards}, numberOfCardsToPick) {
70+
const ret = new Set();
71+
72+
// Pick one card of each color
73+
["G", "U", "W", "B", "R"].forEach((color) => {
74+
ret.add(sample(cardsByColor[color]));
75+
});
76+
77+
const n = Object.keys(cards).length;
78+
const nums = {
79+
"W": cardsByColor["W"].length * numberOfCardsToPick - n,
80+
"B": cardsByColor["B"].length * numberOfCardsToPick - n,
81+
"U": cardsByColor["U"].length * numberOfCardsToPick - n,
82+
"R": cardsByColor["R"].length * numberOfCardsToPick - n,
83+
"G": cardsByColor["G"].length * numberOfCardsToPick - n,
84+
"c": (cardsByColor["c"] || []).length * numberOfCardsToPick,
85+
};
86+
const total = (numberOfCardsToPick - 5) * n;
87+
while (ret.size < numberOfCardsToPick) {
88+
const randomColor = weighted.select(nums, { total });
89+
ret.add(sample(cardsByColor[randomColor]));
90+
}
91+
return [...ret];
92+
}
93+
94+
function getRandomCards({cards, totalWeight: total}, numberOfCardsToPick) {
95+
const ret = new Set();
96+
97+
// Fast way to avoid duplicate
98+
while (ret.size < numberOfCardsToPick) {
99+
ret.add(weighted.select(cards, { total }));
100+
}
101+
102+
return [...ret];
103+
}
104+
105+
const toCard = (sheetCode) => (uuid) => ({
106+
...getCardByUuid(uuid),
107+
foil: /foil/.test(sheetCode)
108+
});
109+
110+
module.exports = makeBoosterFromRules;

backend/boosterGenerator.spec.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const {describe, it} = require("mocha");
2+
const assert = require("assert");
3+
const boosterGenerator = require("./boosterGenerator");
4+
const {range} = require("lodash");
5+
6+
describe("Acceptance tests for boosterGenerator function", () => {
7+
it("should create a MH1 booster", () => {
8+
const got = boosterGenerator("MH1");
9+
assert(got.length > 10);
10+
got.forEach(card => assert(card.name != undefined));
11+
});
12+
it("should create a RNA booster", () => {
13+
const got = boosterGenerator("RNA");
14+
got.forEach(card => assert(card.name != undefined));
15+
});
16+
it("should create tons of EMN booster", () => {
17+
range(1000).forEach(() => {
18+
const got = boosterGenerator("EMN");
19+
assert(got.length > 10);
20+
got.forEach(card => assert(card.name != undefined));
21+
});
22+
});
23+
});

0 commit comments

Comments
 (0)