Skip to content

Commit

Permalink
Dr4ft 1.0.0 (#798)
Browse files Browse the repository at this point in the history
Major refactoring and optimizations within the project
  • Loading branch information
HerveH44 committed Mar 7, 2020
1 parent 2a7b397 commit a1fcc39
Show file tree
Hide file tree
Showing 107 changed files with 2,139 additions and 317,608 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-throw-expressions"
]
}
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ data/*
!data/mws.json
!data/scores.json
node_modules
public/lib
frontend/lib
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
.DS_Store
node_modules
.vscode
.idea

data
public/src/config.js
public/index.html
frontend/index.html

config.client.js
config.server.js
Expand All @@ -15,4 +15,4 @@ npm-debug.log
coverage/
.nyc_output/

*.log
*.log
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ matrix:
install:
- npm install --ignore-scripts
script:
- npm run setup-env
- npm run lint

- name: Run tests
Expand Down
72 changes: 62 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
dr<img src="https://raw.githubusercontent.com/dr4fters/dr4ft/master/public/4.png" alt="4" height="14">ft
dr<img src="https://raw.githubusercontent.com/dr4fters/dr4ft/master/frontend/4.png" alt="4" height="14">ft
</p>

<p align='center'>
Expand All @@ -14,14 +14,34 @@

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

*dr4ft* is a <kbd>NodeJS</kbd> application.<br>
*dr4ft* is written in [ES6] and transpiled with [Babel], and uses [React] on the client-side.

Found **bugs** or have **feature requests**? Feel free to [open an issue](https://github.com/dr4fters/dr4ft/issues/new)!



<br>
*dr4ft* is a <kbd>NodeJS</kbd> based web-application that simulates draft and sealed format between players and/or bots.
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.

The application provides the following features:

* Draft and sealed format
* Regular, Cube and chaos game types
* 1 to 100 players
* 1 to 12 packs per player
* All playable sets ever printed
* Import your custom set and play it
* In-game chat
* Pick Timer
* Autopick
* Suggest lands
* Kick players
* Connection indicators
* Pick confirmation
* Grid and column view
* Card sorting by rarity, type, color or Manacost
* Bots
* Notifications when a pack is available
* API to create and manage a game remotely. [More docs here](https://github.com/dr4fters/dr4ft/bloc/master/doc/api.md)

## Technologies

*dr4ft* is written in [ES6] and transpiled with [Webpack] and [Babel], and uses [React] on the client-side.
The application uses [SocketIO] and the Websocket technology between client and server.

# Project History

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

## Usage

### Start server

<br>
`npm start`

This command start the server

`npm run download_allsets`
This command downloads all sets from MTGJson and integrates them.

`npm run update_database`
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)

### Contributors

THANK YOU!

### Contribute!

Be a part of this project! You can run the test using the following.

1. Install dependencies from package.json by running `npm install`
2. Run the test via `npm test`
3. Make some fun new modules!

Found **bugs** or have **feature requests**? Feel free to [open an issue](https://github.com/dr4fters/dr4ft/issues/new)!
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

<p align='center'>
<sub><i>The project is unaffiliated with Wizards of the Coast, and is licensed under the MIT license.</i></sub>
Expand All @@ -75,3 +125,5 @@ You can also create a Docker image and run the app in a container:
[ES6]: https://github.com/lukehoban/es6features
[Babel]: https://github.com/babel/babel
[React]: https://github.com/facebook/react
[Webpack]: https://webpack.js.org/
[SocketIO]: https://socket.io
12 changes: 6 additions & 6 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ const helmet = require("helmet");
const fileUpload = require("express-fileupload");
const cors = require("cors");
const bodyParser = require("body-parser");
const logger = require("./src/logger");
const router = require("./src/router");
const apiRouter = require("./src/api/");
const allSets = require("./src/make/allsets");
const config = require("./config.server");
const logger = require("./backend/logger");
const router = require("./backend/router");
const apiRouter = require("./backend/api/");
const allSets = require("./scripts/download_allsets");
const {app: config, version} = require("./config");
const app = express();


Expand Down Expand Up @@ -38,5 +38,5 @@ const io = eio(server);
io.on("connection", router);

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

4 changes: 2 additions & 2 deletions src/api/cubes.js → backend/api/cubes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ const logger = require("../logger");
cubesRouter
.get("/", (req, res) => {

fs.readdir("src/cubes", function(err, fileNames) {
fs.readdir("backend/cubes", function(err, fileNames) {
if (err) {
logger.error(err);
res.status(500).end();
} else {
let cubes = {};
fileNames.forEach(name => {
if (name.includes(".txt")) {
const cube = fs.readFileSync(`src/cubes/${name}`);
const cube = fs.readFileSync(`backend/cubes/${name}`);
const key = name.slice(0, name.length-4);
cubes[key] = cube.toString();
}
Expand Down
11 changes: 10 additions & 1 deletion src/api/games.js → backend/api/games.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,20 @@ gamesRouter
"bots": req.game.bots
});
})

/**
* sends an object according to the endpoint api/games/:gameId/status.
* It shows if the game started, the current pack and players' infos.
*/
.get("/:gameId/status", checkGameId, (req, res) => {
res.send(req.game.getStatus());
})

/**
* can accept a `seat`(from 0 to X) or an `id` (playerId) to get informations,
* according to the endoint api/games/:gameId/decks.
* If no `seat` and `id` are requested,
* then it returns an array of the decks of all players.
*/
// secret=[string]&seat=[int]&id[string]
.get("/:gameId/deck", checkGameId, checkGameSecret, (req, res) => {
res.send(req.game.getDecks(req.query));
Expand Down
File renamed without changes.
33 changes: 14 additions & 19 deletions src/api/sets.js → backend/api/sets.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
const fs = require("fs");
const express = require("express");
const setsRouter = express.Router();
const { getSets, getCards, writeSets, writeCards, getPlayableSets, getLatestReleasedSet } = require("../data");
const doSet = require("../make/doSet");
const Sock = require("../sock");
const { getSets, saveSetAndCards } = require("../data");
const doSet = require("../import/doSet");
const logger = require("../logger");
const parser = require("../make/xml/parser");
const parser = require("../import/xml/parser");

if (!fs.existsSync("data/custom")) {
fs.mkdirSync("data/custom");
Expand Down Expand Up @@ -41,35 +40,31 @@ setsRouter
});

function integrateJson(json) {
const newCards = getCards();
const sets = getSets();

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

const [parsedSet, parsedCards] = doSet(json, {}, newCards);
parsedSet.type = CUSTOM_TYPE; //Force set as custom

//TODO: that should be done done by a service -> parse and save (and write file)
json.type = CUSTOM_TYPE; //Force set as custom
const [set, cards] = doSet(json);
saveSetAndCards({ set, cards });
logger.info(`adding new set with code "${json.code}" to database`);
sets[json.code] = parsedSet;
writeSets(sets);
Sock.broadcast("set", { availableSets: getPlayableSets(), latestSet: getLatestReleasedSet() });

writeCards(parsedCards);

//TODO: That should be done by something else. Move out of controller
//Moving custom set to custom directory
fs.writeFile(`data/custom/${json.code}.json`, JSON.stringify(json), (err) => {
fs.writeFile(`data/custom/${json.code}.json`, JSON.stringify(json, undefined, 4), (err) => {
if (err) {
throw new Error(err);
logger.error(`Could not save file ${json.code}.json. ${err}`);
} else {
logger.info(`Saved custom set as file ${json.code}.json`);
}
logger.info(`Saved custom set as file ${json.code}.json`);
});
}

Expand Down
110 changes: 110 additions & 0 deletions backend/boosterGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const {getCardByUuid, getSet} = require("./data");
const logger = require("./logger");
const boosterRules = require("../data/boosterRules.json");
const weighted = require("weighted");
const {sample, sampleSize, random, concat} = require("lodash");

const makeBoosterFromRules = (setCode) => {
const set = getSet(setCode);
if (!set) {
throw new Error(`${setCode} does not exist`);
}

const setRules = boosterRules[setCode];
if (!setRules) {
return getDefaultBooster(set);
}

try {
const { boosters, totalWeight, sheets } = setRules;
const boosterSheets = weighted(
boosters.map(({sheets}) => sheets),
boosters.map(({weight}) => weight),
{total: totalWeight});
return Object.entries(boosterSheets)
.flatMap(chooseCards(sheets));
} catch (error) {
logger.error(`could not produce a booster of ${setCode}. Falling back to default booster. ${error.stack}`);
return getDefaultBooster(set);
}
};

const getDefaultBooster = (set) => {
let { Basic, Common, Uncommon, Rare, Mythic, size } = set;

if (Mythic && !random(7))
Rare = Mythic;

if (!Rare) {
Rare = Uncommon; //In some sets rare didn't exist. So we replace them with uncommons
}

//make small sets draftable.
if (size < 10)
size = 10;

const cardNames = concat(
sampleSize(Common, size),
sampleSize(Uncommon, 3),
sampleSize(Rare, 1)
);

if (Basic) {
cardNames.push(sample(Basic));
}

return cardNames.map(getCardByUuid);
};

const chooseCards = sheets => ([sheetCode, numberOfCardsToPick]) => {
const sheet = sheets[sheetCode];

const randomCards = sheet.balance_colors
? getRandomCardsWithColorBalance(sheet, numberOfCardsToPick)
: getRandomCards(sheet, numberOfCardsToPick);

return randomCards.map(toCard(sheetCode));
};

function getRandomCardsWithColorBalance({cardsByColor, cards}, numberOfCardsToPick) {
const ret = new Set();

// Pick one card of each color
["G", "U", "W", "B", "R"].forEach((color) => {
ret.add(sample(cardsByColor[color]));
});

const n = Object.keys(cards).length;
const nums = {
"W": cardsByColor["W"].length * numberOfCardsToPick - n,
"B": cardsByColor["B"].length * numberOfCardsToPick - n,
"U": cardsByColor["U"].length * numberOfCardsToPick - n,
"R": cardsByColor["R"].length * numberOfCardsToPick - n,
"G": cardsByColor["G"].length * numberOfCardsToPick - n,
"c": (cardsByColor["c"] || []).length * numberOfCardsToPick,
};
const total = (numberOfCardsToPick - 5) * n;
while (ret.size < numberOfCardsToPick) {
const randomColor = weighted.select(nums, { total });
ret.add(sample(cardsByColor[randomColor]));
}
return [...ret];
}

function getRandomCards({cards, totalWeight: total}, numberOfCardsToPick) {
const ret = new Set();

// Fast way to avoid duplicate
while (ret.size < numberOfCardsToPick) {
ret.add(weighted.select(cards, { total }));
}

return [...ret];
}

const toCard = (sheetCode) => (uuid) => ({
...getCardByUuid(uuid),
foil: /foil/.test(sheetCode)
});

module.exports = makeBoosterFromRules;
23 changes: 23 additions & 0 deletions backend/boosterGenerator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const {describe, it} = require("mocha");
const assert = require("assert");
const boosterGenerator = require("./boosterGenerator");
const {range} = require("lodash");

describe("Acceptance tests for boosterGenerator function", () => {
it("should create a MH1 booster", () => {
const got = boosterGenerator("MH1");
assert(got.length > 10);
got.forEach(card => assert(card.name != undefined));
});
it("should create a RNA booster", () => {
const got = boosterGenerator("RNA");
got.forEach(card => assert(card.name != undefined));
});
it("should create tons of EMN booster", () => {
range(1000).forEach(() => {
const got = boosterGenerator("EMN");
assert(got.length > 10);
got.forEach(card => assert(card.name != undefined));
});
});
});
Loading

0 comments on commit a1fcc39

Please sign in to comment.