From 6afbf3e496f95eea31311bf32d4ed36ee6fc3a8b Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Mon, 9 May 2016 22:38:01 -0400 Subject: [PATCH 01/24] Syntax update update Previously, an attempt to run `node app.js` would fail with this mysterious and wholly unhelpful error: undefined:24786 throw errorReporter.errors; ^ :73:9: Duplicate declaration, specialrnd Evidently, a declaration is duplicated if it is made across case bodies. --- src/pool.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pool.js b/src/pool.js index 569a41f..34b8768 100644 --- a/src/pool.js +++ b/src/pool.js @@ -32,6 +32,8 @@ function toPack(code) { _.choose(1, rare) ) + let specialrnd + switch (code) { case 'DGM': special = _.rand(20) @@ -57,7 +59,7 @@ function toPack(code) { case 'ISD': //http://www.mtgsalvation.com/forums/magic-fundamentals/magic-general/327956-innistrad-block-transforming-card-pack-odds?comment=4 //121 card sheet, 1 mythic, 12 rare (13), 42 uncommon (55), 66 common - let specialrnd = _.rand(121) + specialrnd = _.rand(121) if (specialrnd == 0) special = special.mythic else if (specialrnd < 13) @@ -70,7 +72,7 @@ function toPack(code) { case 'DKA': //http://www.mtgsalvation.com/forums/magic-fundamentals/magic-general/327956-innistrad-block-transforming-card-pack-odds?comment=4 //80 card sheet, 2 mythic, 6 rare (8), 24 uncommon (32), 48 common - let specialrnd = _.rand(80) + specialrnd = _.rand(80) if (specialrnd <= 1) special = special.mythic else if (specialrnd < 8) From 0f4ca3d0f27e6ba28be9f030cc527ae06122fc2e Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Mon, 9 May 2016 23:25:19 -0400 Subject: [PATCH 02/24] Add highlight and autopick text to hovered and autopicked cards When hovered over, cards now turn slightly brighter. Additionally, the user can click a card to set it to be autopicked. The card then stays highlighted, and has the text 'autopick' on its bottom border. If the user picks another card, the old card is returned to normal and the new assumes autopick behavior. --- public/src/cards.js | 19 ++++++++++++--- public/src/components/grid.js | 15 ++++++++---- public/style.css | 45 +++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/public/src/cards.js b/public/src/cards.js index f92c01a..0f2aa13 100644 --- a/public/src/cards.js +++ b/public/src/cards.js @@ -218,10 +218,23 @@ function cube() { } function clickPack(cardName) { - if (clicked !== cardName) - return clicked = cardName - let index = rawPack.findIndex(x => x.name === cardName) + let card = rawPack[index] + + if (clicked !== cardName) { + clicked = cardName + // There may be duplicate cards in a pack, but only one copy of a card is + // shown in the pick view. We must be sure to mark them all since we don't + // know which one is being displayed. + rawPack.forEach(card => card.isSelected = card.name == cardName); + App.update() + return clicked + } else if (card.hasOwnProperty('isSelected')) { + // Don't leave the is-selected overlay when moving the card to a different + // zone. + delete card['isSelected'] + } + clicked = null Zones.pack = {} App.update() diff --git a/public/src/components/grid.js b/public/src/components/grid.js index 4a47ceb..1f553bc 100644 --- a/public/src/components/grid.js +++ b/public/src/components/grid.js @@ -16,11 +16,16 @@ function zone(zoneName) { let cards = _.flat(values) let items = cards.map(card => - d.img({ - onClick: App._emit('click', zoneName, card.name), - src: card.url, - alt: card.name - })) + d.span( + { + className: card.isSelected ? 'selected-card' : 'unselected-card', + title: card.isSelected ? 'This card will be automatically picked if your time expires.' : '', + onClick: App._emit('click', zoneName, card.name), + }, + d.img({ + src: card.url, + alt: card.name, + }))) return d.div({ className: 'zone' }, d.h1({}, `${zoneName} ${cards.length}`), diff --git a/public/style.css b/public/style.css index 2eb5c41..d3b9fc4 100644 --- a/public/style.css +++ b/public/style.css @@ -39,6 +39,51 @@ time { color: white; } +/* Tint the card blue when selected. */ +.selected-card, .unselected-card { + display: inline-block; + position: relative; + margin: 0; + cursor: pointer; +} + +.selected-card:before, .unselected-card:hover:before { + content: ''; + + display: block; + position: absolute; + + top: 0; + bottom: 0; + left: 0; + right: 0; + + margin-bottom: 5px; + border-radius: 12px; + + background: rgba(200, 200, 200, 0.25); +} + +.selected-card:after { + content: 'Autopick'; + + display: inline-block; + position: absolute; + + /* Center in the middle of the bottom border on the card. */ + line-height: 25px; + bottom: 5px; + left: 0; + right: 0; + + color: #fff; + font-family: Verdana, Arial, sans-serif; + font-size: 13px; + font-weight: 700; + text-align: center; + text-shadow: 0px 0px 2px #000; +} + .error { color: red; } From d17eebe8c8eb8fae16954d087fc7af561cc5e0be Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Wed, 11 May 2016 14:12:09 -0400 Subject: [PATCH 03/24] Implement autopicking When the user selects a card for autopicking, but then their time expires, the server automatically picks the autopick card on their behalf rather than picking a card at random. This is behavior similar to that of MTGO. If the user fails to select any card at all, the old behavior applies and one is selected at random. --- public/src/cards.js | 7 ++----- public/src/components/grid.js | 6 ++++-- public/style.css | 7 +++---- src/game.js | 2 +- src/human.js | 18 ++++++++++++++---- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/public/src/cards.js b/public/src/cards.js index 0f2aa13..300fe1a 100644 --- a/public/src/cards.js +++ b/public/src/cards.js @@ -226,13 +226,10 @@ function clickPack(cardName) { // There may be duplicate cards in a pack, but only one copy of a card is // shown in the pick view. We must be sure to mark them all since we don't // know which one is being displayed. - rawPack.forEach(card => card.isSelected = card.name == cardName); + rawPack.forEach(card => card.isAutopick = card.name === cardName) App.update() + App.send('autopick', index) return clicked - } else if (card.hasOwnProperty('isSelected')) { - // Don't leave the is-selected overlay when moving the card to a different - // zone. - delete card['isSelected'] } clicked = null diff --git a/public/src/components/grid.js b/public/src/components/grid.js index 1f553bc..c4e4d6d 100644 --- a/public/src/components/grid.js +++ b/public/src/components/grid.js @@ -15,11 +15,13 @@ function zone(zoneName) { let values = _.values(zone) let cards = _.flat(values) + let isAutopickable = card => zoneName === 'pack' && card.isAutopick + let items = cards.map(card => d.span( { - className: card.isSelected ? 'selected-card' : 'unselected-card', - title: card.isSelected ? 'This card will be automatically picked if your time expires.' : '', + className: `card ${isAutopickable(card) ? 'autopick-card' : ''}`, + title: isAutopickable(card) ? 'This card will be automatically picked if your time expires.' : '', onClick: App._emit('click', zoneName, card.name), }, d.img({ diff --git a/public/style.css b/public/style.css index d3b9fc4..baecae6 100644 --- a/public/style.css +++ b/public/style.css @@ -39,15 +39,14 @@ time { color: white; } -/* Tint the card blue when selected. */ -.selected-card, .unselected-card { +.card { display: inline-block; position: relative; margin: 0; cursor: pointer; } -.selected-card:before, .unselected-card:hover:before { +.card:hover:before, .autopick-card:before { content: ''; display: block; @@ -64,7 +63,7 @@ time { background: rgba(200, 200, 200, 0.25); } -.selected-card:after { +.autopick-card:after { content: 'Autopick'; display: inline-block; diff --git a/src/game.js b/src/game.js index ef002cb..f6ff306 100644 --- a/src/game.js +++ b/src/game.js @@ -17,7 +17,7 @@ var games = {} continue for (var p of game.players) if (p.time && !--p.time) - p.pickRand() + p.pickOnTimeout() } setTimeout(playerTimer, SECOND) })() diff --git a/src/human.js b/src/human.js index 510d1f6..24c85dd 100644 --- a/src/human.js +++ b/src/human.js @@ -10,6 +10,7 @@ module.exports = class extends EventEmitter { name: sock.name, time: 0, packs: [], + autopick_index: -1, pool: [] }) this.attach(sock) @@ -19,6 +20,7 @@ module.exports = class extends EventEmitter { this.sock.ws.close() sock.mixin(this) + sock.on('autopick', this._autopick.bind(this)) sock.on('pick', this._pick.bind(this)) sock.on('hash', this._hash.bind(this)) @@ -34,6 +36,11 @@ module.exports = class extends EventEmitter { this.hash = hash(deck) this.emit('meta') } + _autopick(index) { + var [pack] = this.packs + if (pack && index < pack.length) + this.autopick_index = index + } _pick(index) { var [pack] = this.packs if (pack && index < pack.length) @@ -65,17 +72,20 @@ module.exports = class extends EventEmitter { else this.sendPack(next) + this.autopick_index = -1 this.emit('pass', pack) } - pickRand() { - var index = _.rand(this.packs[0].length) + pickOnTimeout() { + let index = this.autopick_index + if (index === -1) + index = _.rand(this.packs[0].length) this.pick(index) } kick() { this.send = ()=>{} while(this.packs.length) - this.pickRand() - this.sendPack = this.pickRand + this.pickOnTimeout() + this.sendPack = this.pickOnTimeout this.isBot = true } } From e50f0e5aa3b3c508c92949765afb28857560c388 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Fri, 13 May 2016 21:08:27 -0400 Subject: [PATCH 04/24] Implement basic connection status indicator This indicator simply displays 'Y' or 'N' if the given user is currently connected. --- public/src/components/game.js | 2 ++ src/game.js | 10 +++++++++- src/human.js | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/src/components/game.js b/public/src/components/game.js index b9615e1..24ffb92 100644 --- a/public/src/components/game.js +++ b/public/src/components/game.js @@ -62,6 +62,7 @@ export default React.createClass({ return d.table({ id: 'players' }, d.tr({}, d.th({}, '#'), + d.th({}, ''), // connection status d.th({}, 'name'), d.th({}, 'packs'), d.th({}, 'time'), @@ -85,6 +86,7 @@ function row(p, i) { return d.tr({ className }, d.td({}, i + 1), + d.td({}, p.isConnected ? 'Y' : 'N'), d.td({}, p.name), d.td({}, p.packs), d.td({}, p.time), diff --git a/src/game.js b/src/game.js index ef002cb..a25989b 100644 --- a/src/game.js +++ b/src/game.js @@ -69,6 +69,7 @@ module.exports = class Game extends Room { } join(sock) { + sock.on('exit', this.farewell.bind(this)) for (var i = 0; i < this.players.length; i++) { var p = this.players[i] if (p.id === sock.id) { @@ -111,6 +112,7 @@ module.exports = class Game extends Room { } greet(h) { + h.isConnected = true h.send('set', { isHost: h.isHost, round: this.round, @@ -119,6 +121,11 @@ module.exports = class Game extends Room { }) } + farewell(sock) { + sock.h.isConnected = false + this.meta() + } + exit(sock) { if (this.round) return @@ -137,7 +144,8 @@ module.exports = class Game extends Room { hash: p.hash, name: p.name, time: p.time, - packs: p.packs.length + packs: p.packs.length, + isConnected: p.isConnected })) for (var p of this.players) p.send('set', state) diff --git a/src/human.js b/src/human.js index 510d1f6..804ed11 100644 --- a/src/human.js +++ b/src/human.js @@ -10,7 +10,8 @@ module.exports = class extends EventEmitter { name: sock.name, time: 0, packs: [], - pool: [] + pool: [], + isConnected: false }) this.attach(sock) } From 278088dd231aa3012e5b5d1b6f7e7dd9cf799468 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Sat, 14 May 2016 15:17:49 -0400 Subject: [PATCH 05/24] Add connection status indicator icon The icon is green for currently-connected players, black for disconnected players, and gray for bots. Note that green/red for the indicator would probably be a bad choice for red-green colorblind players. Each indicator has hover text explaining its meaning. --- public/src/components/game.js | 16 +++++++++++++++- public/style.css | 20 ++++++++++++++++++++ src/bot.js | 3 ++- src/game.js | 3 ++- src/human.js | 3 ++- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/public/src/components/game.js b/public/src/components/game.js index 24ffb92..25faba8 100644 --- a/public/src/components/game.js +++ b/public/src/components/game.js @@ -84,9 +84,23 @@ function row(p, i) { : i === opp ? 'opp' : null + let connectionStatusIndicator + = p.isBot ? d.span({ + className: 'icon-bot', + title: 'This player is a bot.', + }) + : p.isConnected ? d.span({ + className: 'icon-connected', + title: 'This player is currently connected to the server.', + }) + : d.span({ + className: 'icon-disconnected', + title: 'This player is currently disconnected from the server.', + }) + return d.tr({ className }, d.td({}, i + 1), - d.td({}, p.isConnected ? 'Y' : 'N'), + d.td({}, connectionStatusIndicator), d.td({}, p.name), d.td({}, p.packs), d.td({}, p.time), diff --git a/public/style.css b/public/style.css index 2eb5c41..e9988be 100644 --- a/public/style.css +++ b/public/style.css @@ -79,6 +79,26 @@ time { background-color: rgba(255, 0, 0, .1); } +.icon-connected, .icon-disconnected, .icon-bot { + display: inline-block; + height: 10px; + width: 10px; + border-radius: 10px; + border-style: solid 1px #000; +} + +.icon-connected { + background-color: #3c3; +} + +.icon-disconnected { + background-color: #333; +} + +.icon-bot { + background-color: rgba(0, 0, 0, 0.25); +} + #img { position: fixed; bottom: 0; diff --git a/src/bot.js b/src/bot.js index 42da18f..69b05c3 100644 --- a/src/bot.js +++ b/src/bot.js @@ -4,9 +4,10 @@ module.exports = class extends EventEmitter { constructor() { Object.assign(this, { isBot: true, + isConnected: true, name: 'bot', packs: [], - time: 0 + time: 0, }) } getPack(pack) { diff --git a/src/game.js b/src/game.js index a25989b..02a18f1 100644 --- a/src/game.js +++ b/src/game.js @@ -145,7 +145,8 @@ module.exports = class Game extends Room { name: p.name, time: p.time, packs: p.packs.length, - isConnected: p.isConnected + isBot: p.isBot, + isConnected: p.isConnected, })) for (var p of this.players) p.send('set', state) diff --git a/src/human.js b/src/human.js index 804ed11..11d5954 100644 --- a/src/human.js +++ b/src/human.js @@ -6,12 +6,13 @@ var hash = require('./hash') module.exports = class extends EventEmitter { constructor(sock) { Object.assign(this, { + isBot: false, + isConnected: false, id: sock.id, name: sock.name, time: 0, packs: [], pool: [], - isConnected: false }) this.attach(sock) } From ead8996d162bebb830c214c619e591e01380fd79 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Sat, 14 May 2016 15:37:48 -0400 Subject: [PATCH 06/24] Correct Makefile dependencies to be parallelizable This is useful if you want to run `make` with the `-j` option to speed up things somewhat. (It also helps to not confuse people like me who have `make` aliased to `make -j5`, for whom the build process doesn't quite work.) --- Makefile | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 828d951..9061b02 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ +.PHONY: all install clean cards score js all: install clean cards score js node := ${CURDIR}/node_modules +all_sets := ${CURDIR}/data/AllSets.json +traceur := ${node}/.bin/traceur + +${traceur}: install install: npm install @@ -14,22 +19,22 @@ install: ln -sf ${node}/utils/utils.js public/lib clean: - rm -f data/AllSets.json + rm -f ${all_sets} -cards: data/AllSets.json +cards: ${all_sets} node src/make cards custom: node src/make custom -data/AllSets.json: - curl -so data/AllSets.json https://mtgjson.com/json/AllSets.json +${all_sets}: + curl -so ${all_sets} https://mtgjson.com/json/AllSets.json score: -node src/make score #ignore errors -js: - node_modules/.bin/traceur --out public/lib/app.js public/src/init.js +js: ${traceur} ${all_sets} + ${traceur} --out public/lib/app.js public/src/init.js run: js node run From d04e364019a7d0053ffd4c3a233c52b5a51c5c6d Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Sat, 14 May 2016 18:22:34 -0700 Subject: [PATCH 07/24] Implement automatic land suggestion Note that this commit requires a rebuild of the cards JSON files, as it now records each card's mana cost. --- public/src/app.js | 1 + public/src/cards.js | 110 ++++++++++++++++++++++++++++++ public/src/components/settings.js | 18 ++++- src/make/cards.js | 1 + 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/public/src/app.js b/public/src/app.js index 12935e6..9a8b229 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -33,6 +33,7 @@ let App = { beep: false, chat: true, cols: false, + deckSize: 40, filename: 'filename', filetype: 'txt', side: false, diff --git a/public/src/cards.js b/public/src/cards.js index f92c01a..91749d5 100644 --- a/public/src/cards.js +++ b/public/src/cards.js @@ -10,6 +10,13 @@ let Cards = { } export let BASICS = Object.keys(Cards) +let COLORS_TO_LANDS = { + 'W': 'Plains', + 'U': 'Island', + 'B': 'Swamp', + 'R': 'Mountain', + 'G': 'Forest', +} for (let name in Cards) Cards[name] = {name, @@ -140,6 +147,109 @@ let events = { delete Zones[zoneName][cardName] App.update() }, + deckSize(e) { + let n = Number(e.target.value) + if (n && n > 0) + App.state.deckSize = n + App.update() + }, + suggestLands() { + // Algorithm: count the number of mana symbols appearing in the costs of + // the cards in the pool, then assign lands roughly commensurately. + let colors = ['W', 'U', 'B', 'R', 'G'] + let colorRegex = /\{[^}]+\}/g + let manaSymbols = {} + colors.forEach(x => manaSymbols[x] = 0) + + // Count the number of mana symbols of each type. + for (let card of Object.keys(Zones['main'])) { + let quantity = Zones['main'][card] + card = Cards[card] + + if (!card.manaCost) + continue + let cardManaSymbols = card.manaCost.match(colorRegex) + + for (let color of colors) + for (let symbol of cardManaSymbols) + // Test to see if '{U}' contains 'U'. This also handles things like + // '{G/U}' triggering both 'G' and 'U'. + if (symbol.indexOf(color) !== -1) + manaSymbols[color] += quantity + } + + _resetLands() + // NB: We could set only the sideboard lands of the colors we are using to + // 5, but this reveals information to the opponent on Cockatrice (and + // possibly other clients) since it tells the opponent the sideboard size. + colors.forEach(color => Zones['side'][COLORS_TO_LANDS[color]] = 5) + + colors = colors.filter(x => manaSymbols[x] > 0) + colors.forEach(x => manaSymbols[x] = Math.max(3, manaSymbols[x])) + colors.sort((a, b) => manaSymbols[b] - manaSymbols[a]) + + // Round-robin choose the lands to go into the deck. For example, if the + // mana symbol counts are W: 2, U: 2, B: 1, cycle through the sequence + // [Plains, Island, Swamp, Plains, Island] infinitely until the deck is + // finished. + // + // This has a few nice effects: + // + // * Colors with greater mana symbol counts get more lands. + // + // * When in a typical two color deck adding 17 lands, the 9/8 split will + // be in favor of the color with slightly more mana symbols of that + // color. + // + // * Every color in the deck is represented, if it is possible to do so + // in the remaining number of cards. + // + // * Because of the minimum mana symbol count for each represented color, + // splashing cards doesn't add exactly one land of the given type + // (although the land count may still be low for that color). + // + // * The problem of deciding how to round land counts is now easy to + // solve. + let manaSymbolsToAdd = colors.map(color => manaSymbols[color]) + let colorsToAdd = [] + for (let i = 0; true; i = (i + 1) % colors.length) { + if (manaSymbolsToAdd.every(x => x === 0)) + break + if (manaSymbolsToAdd[i] === 0) + continue + colorsToAdd.push(colors[i]) + manaSymbolsToAdd[i]-- + } + + let mainDeckSize = Object.keys(Zones['main']) + .map(x => Zones['main'][x]) + .reduce((a, b) => a + b) + let landsToAdd = App.state.deckSize - mainDeckSize + + let j = 0 + for (let i = 0; i < landsToAdd; i++) { + let color = colorsToAdd[j] + let land = COLORS_TO_LANDS[color] + if (!Zones['main'].hasOwnProperty(land)) + Zones['main'][land] = 0 + Zones['main'][land]++ + + j = (j + 1) % colorsToAdd.length + } + + App.update() + }, + resetLands() { + _resetLands() + App.update() + }, +} + +function _resetLands() { + Object.keys(COLORS_TO_LANDS).forEach((key) => { + let land = COLORS_TO_LANDS[key] + Zones['main'][land] = Zones['side'][land] = 0 + }) } for (let event in events) diff --git a/public/src/components/settings.js b/public/src/components/settings.js index 4f8f1dd..7995c10 100644 --- a/public/src/components/settings.js +++ b/public/src/components/settings.js @@ -24,12 +24,28 @@ function Lands() { inputs) }) + let suggest = d.tr({}, + d.td({}, 'deck size'), + d.td({}, d.input({ + min: 0, + onChange: App._emit('deckSize'), + type: 'number', + value: App.state.deckSize, + })), + d.td({ colSpan: 2 }, d.button({ + onClick: App._emit('resetLands') + }, 'reset lands')), + d.td({colSpan: 2 }, d.button({ + onClick: App._emit('suggestLands') + }, 'suggest lands'))) + return d.table({}, d.tr({}, d.td(), symbols), main, - side) + side, + suggest) } function Sort() { diff --git a/src/make/cards.js b/src/make/cards.js index b7d9fd3..2348249 100644 --- a/src/make/cards.js +++ b/src/make/cards.js @@ -245,6 +245,7 @@ function doCard(rawCard, cards, code, set) { colors[0].toLowerCase() cards[name] = { color, name, + manaCost: rawCard.manaCost, type: rawCard.types[rawCard.types.length - 1], cmc: rawCard.cmc || 0, sets: { From a5df279d50ab5c8b28b0c3b02f26fa9fa4186818 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Sun, 15 May 2016 20:42:03 -0700 Subject: [PATCH 08/24] Require confirmation from each player before starting Each player must select their 'ready' checkbox before the game can start. --- public/src/cards.js | 3 ++ public/src/components/game.js | 58 +++++++++++++++++++++++++++-------- public/style.css | 4 +++ src/game.js | 4 +++ src/human.js | 6 ++++ 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/public/src/cards.js b/public/src/cards.js index 5d6a717..8e5e31d 100644 --- a/public/src/cards.js +++ b/public/src/cards.js @@ -89,6 +89,9 @@ let events = { _.download(data, filename + '.' + filetype) hash() }, + readyToStart(e) { + App.send('readyToStart', e.target.checked) + }, start() { let {bots, timer} = App.state let options = [bots, timer] diff --git a/public/src/components/game.js b/public/src/components/game.js index 25faba8..0966ebd 100644 --- a/public/src/components/game.js +++ b/public/src/components/game.js @@ -8,6 +8,8 @@ import Settings from './settings' import {LBox} from './checkbox' let d = React.DOM +const READY_TITLE_TEXT = 'The host may start the game once all users have clicked the "ready" checkbox.' + export default React.createClass({ componentWillMount() { App.state.players = [] @@ -51,23 +53,34 @@ export default React.createClass({ if (App.state.round || !App.state.isHost) return + let readyToStart = App.state.players.every(x => x.isReadyToStart) + let startButton + = readyToStart + ? d.button({ onClick: App._emit('start') }, 'start') + : d.button({ disabled: true, title: READY_TITLE_TEXT }, 'start') + return d.div({}, - d.div({}, - d.button({ onClick: App._emit('start') }, 'start')), + d.div({}, startButton), LBox('bots', 'bots'), LBox('timer', 'timer')) }, Players() { let rows = App.state.players.map(row) + let columns = [ + d.th({}, '#'), + d.th({}, ''), // connection status + d.th({}, 'name'), + d.th({}, 'packs'), + d.th({}, 'time'), + d.th({}, 'cock'), + d.th({}, 'mws'), + ] + + if (!App.state.round) + columns.push(d.th({ title: READY_TITLE_TEXT }, 'ready')) + return d.table({ id: 'players' }, - d.tr({}, - d.th({}, '#'), - d.th({}, ''), // connection status - d.th({}, 'name'), - d.th({}, 'packs'), - d.th({}, 'time'), - d.th({}, 'cock'), - d.th({}, 'mws')), + d.tr({}, ...columns), rows) } }) @@ -98,14 +111,35 @@ function row(p, i) { title: 'This player is currently disconnected from the server.', }) - return d.tr({ className }, + let readyCheckbox + = i === self ? d.input({ + checked: p.isReadyToStart, + onChange: App._emit('readyToStart'), + type: 'checkbox', + }) + : d.input({ + checked: p.isReadyToStart, + disabled: true, + type: 'checkbox', + }) + + let columns = [ d.td({}, i + 1), d.td({}, connectionStatusIndicator), d.td({}, p.name), d.td({}, p.packs), d.td({}, p.time), d.td({}, p.hash && p.hash.cock), - d.td({}, p.hash && p.hash.mws)) + d.td({}, p.hash && p.hash.mws), + ] + + if (!App.state.round) + columns.push(d.td({ + className: 'ready', + title: READY_TITLE_TEXT + }, readyCheckbox)) + + return d.tr({ className }, ...columns) } function decrement() { diff --git a/public/style.css b/public/style.css index f274e98..7077dbb 100644 --- a/public/style.css +++ b/public/style.css @@ -143,6 +143,10 @@ time { background-color: rgba(0, 0, 0, 0.25); } +.ready { + text-align: center; +} + #img { position: fixed; bottom: 0; diff --git a/src/game.js b/src/game.js index 28965b9..e8fe631 100644 --- a/src/game.js +++ b/src/game.js @@ -147,6 +147,7 @@ module.exports = class Game extends Room { packs: p.packs.length, isBot: p.isBot, isConnected: p.isConnected, + isReadyToStart: p.isReadyToStart, })) for (var p of this.players) p.send('set', state) @@ -218,6 +219,9 @@ module.exports = class Game extends Room { var {players} = this var p + if (!players.every(x => x.isReadyToStart)) + return + this.renew() if (/sealed/.test(this.type)) { diff --git a/src/human.js b/src/human.js index 24a5eb3..42d86c0 100644 --- a/src/human.js +++ b/src/human.js @@ -8,6 +8,7 @@ module.exports = class extends EventEmitter { Object.assign(this, { isBot: false, isConnected: false, + isReadyToStart: false, id: sock.id, name: sock.name, time: 0, @@ -22,6 +23,7 @@ module.exports = class extends EventEmitter { this.sock.ws.close() sock.mixin(this) + sock.on('readyToStart', this._readyToStart.bind(this)) sock.on('autopick', this._autopick.bind(this)) sock.on('pick', this._pick.bind(this)) sock.on('hash', this._hash.bind(this)) @@ -38,6 +40,10 @@ module.exports = class extends EventEmitter { this.hash = hash(deck) this.emit('meta') } + _readyToStart(value) { + this.isReadyToStart = value + this.emit('meta') + } _autopick(index) { var [pack] = this.packs if (pack && index < pack.length) From d34cda7be9f553702e96fd4e9df5914a319044e1 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Sun, 15 May 2016 21:31:47 -0700 Subject: [PATCH 09/24] Reactivate kick functionality The host can kick users before the game has started or during the game. If a user is kicked beforehand, they leave the game and can be replaced with a new user. If a user is kicked during the game, they make all future picks at random. --- public/src/components/game.js | 11 +++++++++++ src/game.js | 3 ++- src/human.js | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/public/src/components/game.js b/public/src/components/game.js index 0966ebd..af05b7f 100644 --- a/public/src/components/game.js +++ b/public/src/components/game.js @@ -79,6 +79,9 @@ export default React.createClass({ if (!App.state.round) columns.push(d.th({ title: READY_TITLE_TEXT }, 'ready')) + if (App.state.isHost) + columns.push(d.th({})) // kick + return d.table({ id: 'players' }, d.tr({}, ...columns), rows) @@ -139,6 +142,14 @@ function row(p, i) { title: READY_TITLE_TEXT }, readyCheckbox)) + if (App.state.isHost) + if (i !== self && !p.isBot) + columns.push(d.td({}, d.button({ + onClick: ()=> App.send('kick', i), + }, 'kick'))) + else + columns.push(d.td({})) + return d.tr({ className }, ...columns) } diff --git a/src/game.js b/src/game.js index e8fe631..a07aedb 100644 --- a/src/game.js +++ b/src/game.js @@ -99,7 +99,7 @@ module.exports = class Game extends Room { } kick(i) { - var h = this.players[i] + let h = this.players[i] if (!h || h.isBot) return @@ -109,6 +109,7 @@ module.exports = class Game extends Room { h.exit() h.err('you were kicked') + h.kick() } greet(h) { diff --git a/src/human.js b/src/human.js index 42d86c0..ed893fb 100644 --- a/src/human.js +++ b/src/human.js @@ -33,6 +33,9 @@ module.exports = class extends EventEmitter { this.send('pack', pack) this.send('pool', this.pool) } + err(message) { + this.send('error', message) + } _hash(deck) { if (!util.deck(deck, this.pool)) return From 2f39ff9fdf40f25169e3a494d135277a285c28b0 Mon Sep 17 00:00:00 2001 From: tritoch Date: Tue, 17 May 2016 15:52:39 -0500 Subject: [PATCH 10/24] Add Chaos, Random Packs, SOI --- public/src/app.js | 12 +++--- public/src/components/lobby.js | 4 +- public/src/data.js | 4 ++ src/bot.js | 25 +++++++++--- src/game.js | 24 +++++++---- src/make/cards.js | 54 ++++++++++++++++++++++++- src/pool.js | 74 +++++++++++++++++++++++++++------- src/util.js | 8 ++-- 8 files changed, 165 insertions(+), 40 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index 9a8b229..94b7861 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -16,12 +16,12 @@ let App = { seats: 8, type: 'draft', sets: [ - 'BFZ', - 'BFZ', - 'BFZ', - 'BFZ', - 'BFZ', - 'BFZ' + 'SOI', + 'SOI', + 'SOI', + 'SOI', + 'SOI', + 'SOI' ], list: '', cards: 15, diff --git a/public/src/components/lobby.js b/public/src/components/lobby.js index 1aededb..550b18b 100644 --- a/public/src/components/lobby.js +++ b/public/src/components/lobby.js @@ -60,12 +60,14 @@ function content() { ' cards ', d.select({ valueLink: App.link('packs') }, packs), ' packs') + let chaos = d.div({}) switch(App.state.type) { case 'draft' : return setsTop case 'sealed': return [setsTop, setsBot] case 'cube draft' : return [cube, cubeDraft] case 'cube sealed': return cube + case 'chaos': return chaos case 'editor': return d.a({ href: 'http://editor.draft.wtf' }, 'editor') } } @@ -74,7 +76,7 @@ function Create() { let seats = _.seq(8, 2).map(x => d.option({}, x)) - let types = ['draft', 'sealed', 'cube draft', 'cube sealed', 'editor'] + let types = ['draft', 'sealed', 'cube draft', 'cube sealed', 'chaos', 'editor'] .map(type => d.button({ disabled: type === App.state.type, diff --git a/public/src/data.js b/public/src/data.js index 7031dbc..be9f2f6 100644 --- a/public/src/data.js +++ b/public/src/data.js @@ -1,5 +1,9 @@ export default { + random: { + "Random Set": "RNG" + }, expansion: { + "Shadows Over Innistrad": "SOI", "Oath of the Gatewatch": "OGW", "Battle for Zendikar": "BFZ", "Dragons of Tarkir": "DTK", diff --git a/src/bot.js b/src/bot.js index 42da18f..33e6942 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,4 +1,5 @@ var {EventEmitter} = require('events') +var _ = require('./_') module.exports = class extends EventEmitter { constructor() { @@ -12,12 +13,26 @@ module.exports = class extends EventEmitter { getPack(pack) { var score = 99 var index = 0 + var cardcount = 0 + var scoredcards = 0 pack.forEach((card, i) => { - if (card.score < score) { - score = card.score - index = i - }}) - pack.splice(index, 1) + if (card.score) { + if (card.score < score) { + score = card.score + index = i + } + scoredcards = scoredcards + 1 + } + cardcount = i + }) + //if 50% of cards doesn't have a score, we're going to pick randomly + if (scoredcards / cardcount < .5) { + var randpick = _.rand(cardcount) + pack.splice(randpick, 1) + } + else { + pack.splice(index, 1) + } this.emit('pass', pack) } send(){} diff --git a/src/game.js b/src/game.js index f6ff306..38e75c4 100644 --- a/src/game.js +++ b/src/game.js @@ -36,12 +36,16 @@ module.exports = class Game extends Room { super() if (sets) - Object.assign(this, { sets, - title: sets.join(' / ')}) - else { - var title = type - if (type === 'cube draft') - title += ' ' + cube.packs + 'x' + cube.cards + if (type != 'chaos') { + Object.assign(this, { + sets, + title: sets.join(' / ') + }) + } + else { + var title = type + if (type === 'cube draft') + title += ' ' + cube.packs + 'x' + cube.cards Object.assign(this, { cube, title }) } @@ -229,8 +233,12 @@ module.exports = class Game extends Room { while (players.length < this.seats) players.push(new Bot) _.shuffle(players) - - this.pool = Pool(src, players.length) + + if (/chaos/.test(this.type)) + this.pool = Pool(src, players.length, true, true) + else + this.pool = Pool(src, players.length) + players.forEach((p, i) => { p.on('pass', this.pass.bind(this, p)) p.send('set', { self: i }) diff --git a/src/make/cards.js b/src/make/cards.js index 2348249..edb8daa 100644 --- a/src/make/cards.js +++ b/src/make/cards.js @@ -49,6 +49,10 @@ function before() { }) var card + for (card of raw.SOI.cards) + if (card.layout === 'double-faced') + card.rarity = 'special' + for (card of raw.ISD.cards) if (card.layout === 'double-faced') card.rarity = 'special' @@ -76,6 +80,51 @@ function before() { } function after() { + var {SOI} = Sets + SOI.special = { + "mythic": [ + "archangel avacyn", + "startled awake", + "arlinn kord" + ], + "rare": [ + "hanweir militia captain", + "elusive tormentor", + "thing in the ice", + "geier reach bandit", + "sage of ancient lore", + "westvale abbey" + ], + "uncommon": [ + "avacynian missionaries", + "pious evangel", + "town gossipmonger", + "aberrant researcher", + "daring sleuth", + "uninvited geist", + "accursed witch", + "heir of falkenrath", + "kindly stranger", + "breakneck rider", + "convicted killer", + "skin invasion", + "village messenger", + "autumnal gloom", + "duskwatch recruiter", + "hermit of the natterknolls", + "lambholt pacifist", + "harvest hand", + "neglected heirloom", + "thraben gargoyle" + ], + "common": [ + "convicted killer", + "gatstaf arsonists", + "hinterland logger", + "solitary hunter" + ] + } + SOI.size = 8 var {ISD} = Sets ISD.special = { mythic: [ @@ -245,9 +294,10 @@ function doCard(rawCard, cards, code, set) { colors[0].toLowerCase() cards[name] = { color, name, - manaCost: rawCard.manaCost, type: rawCard.types[rawCard.types.length - 1], - cmc: rawCard.cmc || 0, + cmc: rawCard.cmc || 0, + text: rawCard.text || '', + manaCost: rawCard.manaCost || '', sets: { [code]: { rarity, url: `http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=${rawCard.multiverseid}&type=card` diff --git a/src/pool.js b/src/pool.js index 34b8768..bbce9f8 100644 --- a/src/pool.js +++ b/src/pool.js @@ -25,16 +25,30 @@ function toPack(code) { var {common, uncommon, rare, mythic, special, size} = set if (mythic && !_.rand(8)) rare = mythic - + //make small sets draftable. SOI has 9 commons, none have less except early sets + if (size < 10 && code != 'SOI') + size = 10 var pack = [].concat( _.choose(size, common), _.choose(3, uncommon), _.choose(1, rare) ) - - let specialrnd - + if (code == 'SOI') + //http://markrosewater.tumblr.com/post/141794840953/if-the-as-fan-of-double-face-cards-is-1125-that + if (_.rand(8) == 0) + if (_.rand(15) < 3) + pack.push(_.choose(1, special.mythic)) + else + pack.push(_.choose(1, special.rare)) + else + pack.push(_.choose(1, common)) switch (code) { + case 'SOI': + if (_.rand(106) < 38) + special = special.uncommon + else + special = special.common + break case 'DGM': special = _.rand(20) ? special.gate @@ -73,7 +87,7 @@ function toPack(code) { //http://www.mtgsalvation.com/forums/magic-fundamentals/magic-general/327956-innistrad-block-transforming-card-pack-odds?comment=4 //80 card sheet, 2 mythic, 6 rare (8), 24 uncommon (32), 48 common specialrnd = _.rand(80) - if (specialrnd <= 1) + if (specialrnd < 2) special = special.mythic else if (specialrnd < 8) special = special.rare @@ -106,10 +120,25 @@ function toCards(pool, code) { }) } -module.exports = function (src, playerCount, isSealed) { +module.exports = function (src, playerCount, isSealed, isChaos) { if (!(src instanceof Array)) { - var isCube = true - _.shuffle(src.list) + if (!(isChaos)) { + var isCube = true + _.shuffle(src.list) + } + } + else { + for (i = 0; i < src.length; i++) { + if (src[i] == 'RNG') { + var rnglist = [] + for (var rngcode in Sets) + //TODO check this against public/src/data.js + if (rngcode != 'UNH' && rngcode != 'UGL' && rngcode != 'SOI') + rnglist.push(rngcode) + var rngindex = _.rand(rnglist.length) + src[i] = rnglist[rngindex] + } + } } if (isSealed) { var count = playerCount @@ -120,15 +149,30 @@ module.exports = function (src, playerCount, isSealed) { } var pools = [] - if (isCube || isSealed) - while (count--) - pools.push(isCube - ? toCards(src.list.splice(-size)) - : [].concat(...src.map(toPack))) - else + if (isCube || isSealed) { + if (!(isChaos)) { + while (count--) + pools.push(isCube + ? toCards(src.list.splice(-size)) + : [].concat(...src.map(toPack))) + } else { + var setlist = [] + for (var code in Sets) + if (code != 'UNH' && code != 'UGL') + setlist.push(code) + for (var i = 0; i < 3; i++) { + for (var j = 0; j < playerCount; j++) { + var setindex = _.rand(setlist.length) + var code = setlist[setindex] + setlist.splice(setindex, 1) + pools.push(toPack(code)) + } + } + } + } else { for (var code of src.reverse()) for (var i = 0; i < playerCount; i++) pools.push(toPack(code)) - + } return pools } diff --git a/src/util.js b/src/util.js index 7cfbd20..12d2c19 100644 --- a/src/util.js +++ b/src/util.js @@ -65,12 +65,14 @@ var util = module.exports = { game({seats, type, sets, cube}) { assert(typeof seats === 'number', 'typeof seats') assert(2 <= seats && seats <= 8, 'seats range') - assert(['draft', 'sealed', 'cube draft', 'cube sealed'].indexOf(type) > -1, + assert(['draft', 'sealed', 'cube draft', 'cube sealed', 'chaos'].indexOf(type) > -1, 'indexOf type') if (/cube/.test(type)) transform(cube, seats, type) - else - sets.forEach(set => assert(set in Sets, `${set} in Sets`)) + //remove this check to allow Random + //TODO add if check for random set + //else + // sets.forEach(set => assert(set in Sets, `${set} in Sets`)) } } From 605836de12439d66ef81a40747f4eb6f6c1c1360 Mon Sep 17 00:00:00 2001 From: tritoch Date: Mon, 23 May 2016 16:11:50 -0500 Subject: [PATCH 11/24] fix Chaos title / cube error --- src/game.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/game.js b/src/game.js index 38e75c4..e1d5e9c 100644 --- a/src/game.js +++ b/src/game.js @@ -35,7 +35,7 @@ module.exports = class Game extends Room { constructor({id, seats, type, sets, cube}) { super() - if (sets) + if (sets) { if (type != 'chaos') { Object.assign(this, { sets, @@ -43,9 +43,13 @@ module.exports = class Game extends Room { }) } else { - var title = type - if (type === 'cube draft') - title += ' ' + cube.packs + 'x' + cube.cards + Object.assign(this, { title: 'CHAOS!'}) + } + } + else { + var title = type + if (type === 'cube draft') + title += ' ' + cube.packs + 'x' + cube.cards Object.assign(this, { cube, title }) } From 5e3c9fdbcb0ecdd882e5bb9055c1a7f01bde8d3d Mon Sep 17 00:00:00 2001 From: tritoch Date: Mon, 23 May 2016 21:48:27 -0500 Subject: [PATCH 12/24] Sort lands separate from 0cmc nonlands --- src/make/cards.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/make/cards.js b/src/make/cards.js index edb8daa..32ce8ce 100644 --- a/src/make/cards.js +++ b/src/make/cards.js @@ -276,12 +276,15 @@ function doCard(rawCard, cards, code, set) { if (rawCard.layout === 'split') name = rawCard.names.join(' // ') + //separate landsfrom 0cmc cards by setting 0cmc to .2 + var cmcadjusted = rawCard.cmc || 0.2 + name = _.ascii(name) if (name in cards) { if (rawCard.layout === 'split') { var card = cards[name] - card.cmc += rawCard.cmc + cmcadjusted = card.cmc + rawCard.cmc if (card.color !== rawCard.color) card.color = 'multicolor' } @@ -292,10 +295,14 @@ function doCard(rawCard, cards, code, set) { var color = !colors ? 'colorless' : colors.length > 1 ? 'multicolor' : colors[0].toLowerCase() + + //set lands to .1 to sort them before nonland 0cmc + if ('Land'.indexOf(rawCard.types) > -1) + cmcadjusted = 0.1 cards[name] = { color, name, type: rawCard.types[rawCard.types.length - 1], - cmc: rawCard.cmc || 0, + cmc: cmcadjusted, text: rawCard.text || '', manaCost: rawCard.manaCost || '', sets: { From 7fbed5954faaffbbb0659d5eec88a936deeb19bb Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Sun, 15 May 2016 22:00:23 -0700 Subject: [PATCH 13/24] Merge branch 'tritoch/chaos' into live This merge includes a few fixups: * Keep the default port of 1337. * Don't include branding changes. * Minor style changes. --- public/src/app.js | 12 ++--- public/src/components/lobby.js | 9 ++-- public/src/data.js | 4 ++ src/bot.js | 25 ++++++++-- src/game.js | 17 +++++-- src/make/cards.js | 91 ++++++++++++++++++++++++++-------- src/pool.js | 74 +++++++++++++++++++++------ src/util.js | 18 ++++--- 8 files changed, 190 insertions(+), 60 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index 9a8b229..94b7861 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -16,12 +16,12 @@ let App = { seats: 8, type: 'draft', sets: [ - 'BFZ', - 'BFZ', - 'BFZ', - 'BFZ', - 'BFZ', - 'BFZ' + 'SOI', + 'SOI', + 'SOI', + 'SOI', + 'SOI', + 'SOI' ], list: '', cards: 15, diff --git a/public/src/components/lobby.js b/public/src/components/lobby.js index 1aededb..ac1cc39 100644 --- a/public/src/components/lobby.js +++ b/public/src/components/lobby.js @@ -42,6 +42,7 @@ function Sets(selectedSet, index) { function content() { let sets = App.state.sets.map(Sets) + let setsTop = d.div({}, sets.slice(0, 3)) let setsBot = d.div({}, sets.slice(3)) @@ -54,27 +55,29 @@ function content() { ] let cards = _.seq(15, 8).map(x => d.option({}, x)) - let packs = _.seq( 7, 3).map(x => d.option({}, x)) + let packs = _.seq(12, 3).map(x => d.option({}, x)) let cubeDraft = d.div({}, d.select({ valueLink: App.link('cards') }, cards), ' cards ', d.select({ valueLink: App.link('packs') }, packs), ' packs') + let chaos = d.div({}) switch(App.state.type) { case 'draft' : return setsTop case 'sealed': return [setsTop, setsBot] case 'cube draft' : return [cube, cubeDraft] case 'cube sealed': return cube + case 'chaos': return chaos case 'editor': return d.a({ href: 'http://editor.draft.wtf' }, 'editor') } } function Create() { - let seats = _.seq(8, 2).map(x => + let seats = _.seq(100, 2).map(x => d.option({}, x)) - let types = ['draft', 'sealed', 'cube draft', 'cube sealed', 'editor'] + let types = ['draft', 'sealed', 'cube draft', 'cube sealed', 'chaos', 'editor'] .map(type => d.button({ disabled: type === App.state.type, diff --git a/public/src/data.js b/public/src/data.js index 7031dbc..be9f2f6 100644 --- a/public/src/data.js +++ b/public/src/data.js @@ -1,5 +1,9 @@ export default { + random: { + "Random Set": "RNG" + }, expansion: { + "Shadows Over Innistrad": "SOI", "Oath of the Gatewatch": "OGW", "Battle for Zendikar": "BFZ", "Dragons of Tarkir": "DTK", diff --git a/src/bot.js b/src/bot.js index 69b05c3..c40ac98 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,3 +1,4 @@ +var _ = require('./_') var {EventEmitter} = require('events') module.exports = class extends EventEmitter { @@ -13,12 +14,26 @@ module.exports = class extends EventEmitter { getPack(pack) { var score = 99 var index = 0 + var cardcount = 0 + var scoredcards = 0 pack.forEach((card, i) => { - if (card.score < score) { - score = card.score - index = i - }}) - pack.splice(index, 1) + if (card.score) { + if (card.score < score) { + score = card.score + index = i + } + scoredcards = scoredcards + 1 + } + cardcount = i + }) + //if 50% of cards doesn't have a score, we're going to pick randomly + if (scoredcards / cardcount < .5) { + var randpick = _.rand(cardcount) + pack.splice(randpick, 1) + } + else { + pack.splice(index, 1) + } this.emit('pass', pack) } send(){} diff --git a/src/game.js b/src/game.js index a07aedb..8e5608f 100644 --- a/src/game.js +++ b/src/game.js @@ -36,9 +36,14 @@ module.exports = class Game extends Room { super() if (sets) - Object.assign(this, { sets, - title: sets.join(' / ')}) - else { + if (type != 'chaos') { + Object.assign(this, { + sets, + title: sets.join(' / ') + }) + } else { + title: 'CHAOS!' + } else { var title = type if (type === 'cube draft') title += ' ' + cube.packs + 'x' + cube.cards @@ -244,7 +249,11 @@ module.exports = class Game extends Room { players.push(new Bot) _.shuffle(players) - this.pool = Pool(src, players.length) + if (/chaos/.test(this.type)) + this.pool = Pool(src, players.length, true, true) + else + this.pool = Pool(src, players.length) + players.forEach((p, i) => { p.on('pass', this.pass.bind(this, p)) p.send('set', { self: i }) diff --git a/src/make/cards.js b/src/make/cards.js index 2348249..bcc48df 100644 --- a/src/make/cards.js +++ b/src/make/cards.js @@ -49,6 +49,10 @@ function before() { }) var card + for (card of raw.SOI.cards) + if (card.layout === 'double-faced') + card.rarity = 'special' + for (card of raw.ISD.cards) if (card.layout === 'double-faced') card.rarity = 'special' @@ -76,35 +80,80 @@ function before() { } function after() { + var {SOI} = Sets + SOI.special = { + "mythic": [ + "archangel avacyn", + "startled awake", + "arlinn kord" + ], + "rare": [ + "hanweir militia captain", + "elusive tormentor", + "thing in the ice", + "geier reach bandit", + "sage of ancient lore", + "westvale abbey" + ], + "uncommon": [ + "avacynian missionaries", + "pious evangel", + "town gossipmonger", + "aberrant researcher", + "daring sleuth", + "uninvited geist", + "accursed witch", + "heir of falkenrath", + "kindly stranger", + "breakneck rider", + "convicted killer", + "skin invasion", + "village messenger", + "autumnal gloom", + "duskwatch recruiter", + "hermit of the natterknolls", + "lambholt pacifist", + "harvest hand", + "neglected heirloom", + "thraben gargoyle" + ], + "common": [ + "convicted killer", + "gatstaf arsonists", + "hinterland logger", + "solitary hunter" + ] + } + SOI.size = 8 var {ISD} = Sets ISD.special = { mythic: [ - 'garruk relentless' + 'garruk relentless' ], rare: [ - 'bloodline keeper', - 'daybreak ranger', - 'instigator gang', - 'kruin outlaw', - 'ludevic\'s test subject', - 'mayor of avabruck' + 'bloodline keeper', + 'daybreak ranger', + 'instigator gang', + 'kruin outlaw', + 'ludevic\'s test subject', + 'mayor of avabruck' ], uncommon: [ - 'civilized scholar', - 'cloistered youth', - 'gatstaf shepherd', - 'hanweir watchkeep', - 'reckless waif', - 'screeching bat', - 'ulvenwald mystics' + 'civilized scholar', + 'cloistered youth', + 'gatstaf shepherd', + 'hanweir watchkeep', + 'reckless waif', + 'screeching bat', + 'ulvenwald mystics' ], common: [ - 'delver of secrets', - 'grizzled outcasts', - 'thraben sentry', - 'tormented pariah', - 'village ironsmith', - 'villagers of estwald' + 'delver of secrets', + 'grizzled outcasts', + 'thraben sentry', + 'tormented pariah', + 'village ironsmith', + 'villagers of estwald' ] } var {DKA} = Sets @@ -248,6 +297,8 @@ function doCard(rawCard, cards, code, set) { manaCost: rawCard.manaCost, type: rawCard.types[rawCard.types.length - 1], cmc: rawCard.cmc || 0, + text: rawCard.text || '', + manaCost: rawCard.manaCost || '', sets: { [code]: { rarity, url: `http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=${rawCard.multiverseid}&type=card` diff --git a/src/pool.js b/src/pool.js index 34b8768..3d35363 100644 --- a/src/pool.js +++ b/src/pool.js @@ -25,16 +25,33 @@ function toPack(code) { var {common, uncommon, rare, mythic, special, size} = set if (mythic && !_.rand(8)) rare = mythic - + //make small sets draftable. + if (size < 9 && code != 'SOI') + size = 10 var pack = [].concat( _.choose(size, common), _.choose(3, uncommon), _.choose(1, rare) ) - let specialrnd + if (code == 'SOI') + //http://markrosewater.tumblr.com/post/141794840953/if-the-as-fan-of-double-face-cards-is-1125-that + if (_.rand(8) == 0) + if (_.rand(15) < 3) + pack.push(_.choose(1, special.mythic)) + else + pack.push(_.choose(1, special.rare)) + else + pack.push(_.choose(1, common)) + let specialrnd switch (code) { + case 'SOI': + if (_.rand(106) < 38) + special = special.uncommon + else + special = special.common + break case 'DGM': special = _.rand(20) ? special.gate @@ -81,7 +98,7 @@ function toPack(code) { special = special.uncommon else special = special.common - break + break } if (special) @@ -99,17 +116,31 @@ function toCards(pool, code) { if (isCube) [code] = Object.keys(sets) card.code = mws[code] || code - var set = sets[code] delete card.sets return Object.assign(card, set) }) } -module.exports = function (src, playerCount, isSealed) { +module.exports = function (src, playerCount, isSealed, isChaos) { if (!(src instanceof Array)) { - var isCube = true - _.shuffle(src.list) + if (!(isChaos)) { + var isCube = true + _.shuffle(src.list) + } + } + else { + for (i = 0; i < src.length; i++) { + if (src[i] == 'RNG') { + var rnglist = [] + for (var rngcode in Sets) + //TODO check this against public/src/data.js + if (rngcode != 'UNH' && rngcode != 'UGL' && rngcode != 'SOI') + rnglist.push(rngcode) + var rngindex = _.rand(rnglist.length) + src[i] = rnglist[rngindex] + } + } } if (isSealed) { var count = playerCount @@ -120,15 +151,30 @@ module.exports = function (src, playerCount, isSealed) { } var pools = [] - if (isCube || isSealed) - while (count--) - pools.push(isCube - ? toCards(src.list.splice(-size)) - : [].concat(...src.map(toPack))) - else + if (isCube || isSealed) { + if (!(isChaos)) { + while (count--) + pools.push(isCube + ? toCards(src.list.splice(-size)) + : [].concat(...src.map(toPack))) + } else { + var setlist = [] + for (var code in Sets) + if (code != 'UNH' && code != 'UGL') + setlist.push(code) + for (var i = 0; i < 3; i++) { + for (var j = 0; j < playerCount; j++) { + var setindex = _.rand(setlist.length) + var code = setlist[setindex] + setlist.splice(setindex, 1) + pools.push(toPack(code)) + } + } + } + } else { for (var code of src.reverse()) for (var i = 0; i < playerCount; i++) pools.push(toPack(code)) - + } return pools } diff --git a/src/util.js b/src/util.js index 7cfbd20..7e3f7e0 100644 --- a/src/util.js +++ b/src/util.js @@ -14,17 +14,17 @@ function transform(cube, seats, type) { assert(typeof list === 'string', 'typeof list') assert(typeof cards === 'number', 'typeof cards') - assert(8 <= cards && cards <= 15, 'cards range') + assert(5 <= cards && cards <= 30, 'cards range') assert(typeof packs === 'number', 'typeof packs') - assert(3 <= packs && packs <= 7, 'packs range') + assert(3 <= packs && packs <= 12, 'packs range') list = list.split('\n').map(_.ascii) var min = type === 'cube draft' ? seats * cards * packs : seats * 90 - assert(min <= list.length && list.length <= 1e3, - `this cube needs between ${min} and 1000 cards; it has ${list.length}`) + assert(min <= list.length && list.length <= 1e5, + `this cube needs between ${min} and 100,000 cards; it has ${list.length}`) var bad = [] for (var cardName of list) @@ -64,13 +64,15 @@ var util = module.exports = { }, game({seats, type, sets, cube}) { assert(typeof seats === 'number', 'typeof seats') - assert(2 <= seats && seats <= 8, 'seats range') - assert(['draft', 'sealed', 'cube draft', 'cube sealed'].indexOf(type) > -1, + assert(2 <= seats && seats <= 100, 'seats range') + assert(['draft', 'sealed', 'cube draft', 'cube sealed', 'chaos'].indexOf(type) > -1, 'indexOf type') if (/cube/.test(type)) transform(cube, seats, type) - else - sets.forEach(set => assert(set in Sets, `${set} in Sets`)) + //remove the below check for now to allow Random sets + //TODO add if check for Random set + //else + // sets.forEach(set => assert(set in Sets, `${set} in Sets`)) } } From 6c06293444b7e4219fddeb175ea2245d8e722d2e Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Wed, 25 May 2016 21:55:56 -0700 Subject: [PATCH 14/24] Branding: change for drafts.ninja --- public/index.html | 2 +- public/src/app.js | 2 +- public/src/components/lobby.js | 9 +++------ src/sock.js | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/public/index.html b/public/index.html index 3de76f1..f922e42 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - draft + drafts.ninja diff --git a/public/src/app.js b/public/src/app.js index 94b7861..8f8fb3f 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -11,7 +11,7 @@ let App = { state: { id: null, - name: 'newfriend', + name: 'ninja', seats: 8, type: 'draft', diff --git a/public/src/components/lobby.js b/public/src/components/lobby.js index ac1cc39..ae4951b 100644 --- a/public/src/components/lobby.js +++ b/public/src/components/lobby.js @@ -11,14 +11,12 @@ export default React.createClass({ render() { return d.div({}, Chat(), - d.h1({}, 'drafts.in'), + d.h1({}, 'drafts.ninja'), d.p({ className: 'error' }, App.err), Create(), d.footer({}, d.div({}, - d.a({ className: 'icon ion-social-github', href: 'https://github.com/aeosynth/draft' }), - d.a({ className: 'icon ion-social-twitter', href: 'https://twitter.com/aeosynth' }), - d.a({ className: 'icon ion-android-mail', href: 'mailto:james.r.campos@gmail.com' })), + d.a({ className: 'icon ion-social-github', href: 'https://github.com/arxanas/draft' })), d.div({}, d.small({}, 'unaffiliated with wizards of the coast')))) } @@ -69,7 +67,6 @@ function content() { case 'cube draft' : return [cube, cubeDraft] case 'cube sealed': return cube case 'chaos': return chaos - case 'editor': return d.a({ href: 'http://editor.draft.wtf' }, 'editor') } } @@ -77,7 +74,7 @@ function Create() { let seats = _.seq(100, 2).map(x => d.option({}, x)) - let types = ['draft', 'sealed', 'cube draft', 'cube sealed', 'chaos', 'editor'] + let types = ['draft', 'sealed', 'cube draft', 'cube sealed', 'chaos'] .map(type => d.button({ disabled: type === App.state.type, diff --git a/src/sock.js b/src/sock.js index 30afa07..c473675 100644 --- a/src/sock.js +++ b/src/sock.js @@ -20,7 +20,7 @@ var mixins = { module.exports = class extends EventEmitter { constructor(ws) { this.ws = ws - var {id='', name='newfriend'} = ws.request._query + var {id='', name='ninja'} = ws.request._query this.id = id.slice(0, 25) this.name = name.slice(0, 15) From 57a1a17452640290653eb5de3fbdfa7191f4dd41 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Tue, 17 May 2016 16:07:12 -0700 Subject: [PATCH 15/24] Add logging for number of games --- src/game.js | 2 ++ src/router.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/game.js b/src/game.js index 8e5608f..d3ef03c 100644 --- a/src/game.js +++ b/src/game.js @@ -61,6 +61,7 @@ module.exports = class Game extends Room { }) this.renew() games[gameID] = this + console.log(`game ${gameID} created: ${this.title}`) } renew() { @@ -244,6 +245,7 @@ module.exports = class Game extends Room { for (p of players) p.useTimer = useTimer + console.log(`game ${this.id} started with ${this.players.length} players and ${this.seats} seats`) if (addBots) while (players.length < this.seats) players.push(new Bot) diff --git a/src/router.js b/src/router.js index bf680d2..43fe7ff 100644 --- a/src/router.js +++ b/src/router.js @@ -7,6 +7,11 @@ var rooms = { lobby: new Room } +function numGames() { + // Don't include the lobby as a game. + return Object.keys(rooms).length - 1 +} + function create(opts) { try { util.game(opts) @@ -19,6 +24,7 @@ function create(opts) { rooms[g.id] = g this.send('route', 'g/' + g.id) g.once('kill', kill) + console.log(`game ${g.id} created, there are now ${numGames()} games`) } function join(roomID) { @@ -31,6 +37,7 @@ function join(roomID) { function kill() { delete rooms[this.id] + console.log(`game ${this.id} destroyed, there are now ${numGames()} games`) } module.exports = function (ws) { From c1a0f59ec614e8c056a714436b5024a48348464d Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Tue, 17 May 2016 16:21:40 -0700 Subject: [PATCH 16/24] Log the number of connected users --- src/router.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/router.js b/src/router.js index 43fe7ff..6d91756 100644 --- a/src/router.js +++ b/src/router.js @@ -7,6 +7,10 @@ var rooms = { lobby: new Room } +// All sockets currently connected to the server, useful for broadcasting +// messages. +var socks = [] + function numGames() { // Don't include the lobby as a game. return Object.keys(rooms).length - 1 @@ -40,8 +44,22 @@ function kill() { console.log(`game ${this.id} destroyed, there are now ${numGames()} games`) } +function printNumSockets() { + console.log(`there are now ${socks.length} connected users`) +} + module.exports = function (ws) { var sock = new Sock(ws) sock.on('join', join) sock.on('create', create) + + socks.push(sock) + printNumSockets() + ws.once('close', ()=> { + let index = socks.indexOf(sock) + if (index !== -1) { + socks.splice(index, 1) + printNumSockets() + } + }) } From c4d432c5594fb0f912d9aab74f3cc09eb8dfa038 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Tue, 17 May 2016 16:29:24 -0700 Subject: [PATCH 17/24] Add timestamp to logs --- app.js | 3 ++- package.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 0fbe217..5ad9e08 100644 --- a/app.js +++ b/app.js @@ -14,4 +14,5 @@ var server = http.createServer(function(req, res) { }).listen(PORT) var eioServer = eio(server).on('connection', router) -console.log(new Date) +require('log-timestamp') +console.log('Started up') diff --git a/package.json b/package.json index 0d23e29..c530ee9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "ee": "git://github.com/aeosynth/ee", "engine.io": "^1.4.1", "engine.io-client": "^1.4.1", + "log-timestamp": "^0.1.2", "node-fetch": "^1.0.3", "send": "^0.11.1", "traceur": "0.0.65", From e2d91c2766cc3142bae1e84ffd26c06947edd5b7 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Tue, 17 May 2016 17:40:55 -0700 Subject: [PATCH 18/24] Display the number of games in progress --- public/src/app.js | 3 +++ public/src/components/lobby.js | 1 + src/router.js | 32 ++++++++++++++++++++++---------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index 8f8fb3f..2277739 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -13,6 +13,8 @@ let App = { id: null, name: 'ninja', + numGames: 0, + seats: 8, type: 'draft', sets: [ @@ -44,6 +46,7 @@ let App = { App.on('set', App.set) App.on('error', App.error) App.on('route', App.route) + App.on('numGames', numGames => App.save('numGames', numGames)) App.restore() App.connect() diff --git a/public/src/components/lobby.js b/public/src/components/lobby.js index ae4951b..82bfd28 100644 --- a/public/src/components/lobby.js +++ b/public/src/components/lobby.js @@ -12,6 +12,7 @@ export default React.createClass({ return d.div({}, Chat(), d.h1({}, 'drafts.ninja'), + d.p({}, `${App.state.numGames} ${App.state.numGames === 1 ? 'game' : 'games'} in progress`), d.p({ className: 'error' }, App.err), Create(), d.footer({}, diff --git a/src/router.js b/src/router.js index 6d91756..4fdbd2e 100644 --- a/src/router.js +++ b/src/router.js @@ -11,11 +11,30 @@ var rooms = { // messages. var socks = [] +function broadcast(...args) { + for (let sock of socks) + sock.send(...args) +} + function numGames() { // Don't include the lobby as a game. return Object.keys(rooms).length - 1 } +function connect(sock) { + socks.push(sock) + printNumSockets() + broadcast('numGames', numGames()) + + sock.ws.once('close', ()=> { + let index = socks.indexOf(sock) + if (index !== -1) { + socks.splice(index, 1) + printNumSockets() + } + }) +} + function create(opts) { try { util.game(opts) @@ -28,6 +47,7 @@ function create(opts) { rooms[g.id] = g this.send('route', 'g/' + g.id) g.once('kill', kill) + broadcast('numGames', numGames()) console.log(`game ${g.id} created, there are now ${numGames()} games`) } @@ -41,6 +61,7 @@ function join(roomID) { function kill() { delete rooms[this.id] + broadcast('numGames', numGames()) console.log(`game ${this.id} destroyed, there are now ${numGames()} games`) } @@ -52,14 +73,5 @@ module.exports = function (ws) { var sock = new Sock(ws) sock.on('join', join) sock.on('create', create) - - socks.push(sock) - printNumSockets() - ws.once('close', ()=> { - let index = socks.indexOf(sock) - if (index !== -1) { - socks.splice(index, 1) - printNumSockets() - } - }) + connect(sock) } From af72a193ef33c536ab10fdfdf3375334b5c053db Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Tue, 17 May 2016 20:58:13 -0700 Subject: [PATCH 19/24] Display the number of connected users --- public/src/app.js | 4 +-- public/src/components/lobby.js | 3 +- src/game.js | 56 ++++++++++++++++++++++++++++------ src/router.js | 39 ++--------------------- src/sock.js | 27 +++++++++++++++- 5 files changed, 78 insertions(+), 51 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index 2277739..d1fd534 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -13,7 +13,8 @@ let App = { id: null, name: 'ninja', - numGames: 0, + numPlayers: 0, + numActiveGames: 0, seats: 8, type: 'draft', @@ -46,7 +47,6 @@ let App = { App.on('set', App.set) App.on('error', App.error) App.on('route', App.route) - App.on('numGames', numGames => App.save('numGames', numGames)) App.restore() App.connect() diff --git a/public/src/components/lobby.js b/public/src/components/lobby.js index 82bfd28..be01427 100644 --- a/public/src/components/lobby.js +++ b/public/src/components/lobby.js @@ -12,7 +12,8 @@ export default React.createClass({ return d.div({}, Chat(), d.h1({}, 'drafts.ninja'), - d.p({}, `${App.state.numGames} ${App.state.numGames === 1 ? 'game' : 'games'} in progress`), + d.p({}, `${App.state.numPlayers} ${App.state.numPlayers === 1 ? 'player' : 'players'} + playing ${App.state.numActiveGames} ${App.state.numActiveGames === 1 ? 'game' : 'games'}`), d.p({ className: 'error' }, App.err), Create(), d.footer({}, diff --git a/src/game.js b/src/game.js index d3ef03c..c41aa39 100644 --- a/src/game.js +++ b/src/game.js @@ -1,14 +1,15 @@ -var _ = require('./_') -var Bot = require('./bot') -var Human = require('./human') -var Pool = require('./pool') -var Room = require('./room') +let _ = require('./_') +let Bot = require('./bot') +let Human = require('./human') +let Pool = require('./pool') +let Room = require('./room') +let Sock = require('./sock') -var SECOND = 1000 -var MINUTE = 1000 * 60 -var HOUR = 1000 * 60 * 60 +let SECOND = 1000 +let MINUTE = 1000 * 60 +let HOUR = 1000 * 60 * 60 -var games = {} +let games = {} ;(function playerTimer() { for (var id in games) { @@ -61,13 +62,43 @@ module.exports = class Game extends Room { }) this.renew() games[gameID] = this - console.log(`game ${gameID} created: ${this.title}`) + + console.log(`game ${id} created`) + Game.broadcastGameInfo() } renew() { this.expires = Date.now() + HOUR } + get isActive() { + return this.players.some(x => x.isConnected && !x.isBot) + } + + // The number of total games. This includes ones that have been long since + // abandoned but not yet garbage-collected by the `renew` mechanism. + static numGames() { + return Object.keys(games).length + } + + // The number of games which have a player still in them. + static numActiveGames() { + let count = 0 + for (let id of Object.keys(games)) { + if (games[id].isActive) + count++ + } + return count + } + + static broadcastGameInfo() { + Sock.broadcast('set', { + numGames: Game.numGames(), + numActiveGames: Game.numActiveGames(), + }) + console.log(`there are now ${Game.numGames()} games, ${Game.numActiveGames()} active`) + } + name(name, sock) { super(name, sock) sock.h.name = sock.name @@ -158,6 +189,7 @@ module.exports = class Game extends Room { })) for (var p of this.players) p.send('set', state) + Game.broadcastGameInfo() } kill(msg) { @@ -165,6 +197,9 @@ module.exports = class Game extends Room { this.players.forEach(p => p.err(msg)) delete games[this.id] + console.log(`game ${this.id} destroyed`) + Game.broadcastGameInfo() + this.emit('kill') } @@ -246,6 +281,7 @@ module.exports = class Game extends Room { p.useTimer = useTimer console.log(`game ${this.id} started with ${this.players.length} players and ${this.seats} seats`) + Game.broadcastGameInfo() if (addBots) while (players.length < this.seats) players.push(new Bot) diff --git a/src/router.js b/src/router.js index 4fdbd2e..6e4851f 100644 --- a/src/router.js +++ b/src/router.js @@ -7,34 +7,6 @@ var rooms = { lobby: new Room } -// All sockets currently connected to the server, useful for broadcasting -// messages. -var socks = [] - -function broadcast(...args) { - for (let sock of socks) - sock.send(...args) -} - -function numGames() { - // Don't include the lobby as a game. - return Object.keys(rooms).length - 1 -} - -function connect(sock) { - socks.push(sock) - printNumSockets() - broadcast('numGames', numGames()) - - sock.ws.once('close', ()=> { - let index = socks.indexOf(sock) - if (index !== -1) { - socks.splice(index, 1) - printNumSockets() - } - }) -} - function create(opts) { try { util.game(opts) @@ -47,8 +19,6 @@ function create(opts) { rooms[g.id] = g this.send('route', 'g/' + g.id) g.once('kill', kill) - broadcast('numGames', numGames()) - console.log(`game ${g.id} created, there are now ${numGames()} games`) } function join(roomID) { @@ -61,17 +31,12 @@ function join(roomID) { function kill() { delete rooms[this.id] - broadcast('numGames', numGames()) - console.log(`game ${this.id} destroyed, there are now ${numGames()} games`) -} - -function printNumSockets() { - console.log(`there are now ${socks.length} connected users`) } module.exports = function (ws) { var sock = new Sock(ws) sock.on('join', join) sock.on('create', create) - connect(sock) + + Game.broadcastGameInfo() } diff --git a/src/sock.js b/src/sock.js index c473675..a163cff 100644 --- a/src/sock.js +++ b/src/sock.js @@ -1,5 +1,13 @@ var {EventEmitter} = require('events') +// All sockets currently connected to the server. +let allSocks = [] + +function broadcastNumPlayers() { + console.log(`there are now ${allSocks.length} connected users`) + Sock.broadcast('set', { numPlayers: allSocks.length }) +} + function message(msg) { var [type, data] = JSON.parse(msg) this.emit(type, data, this) @@ -17,7 +25,7 @@ var mixins = { } } -module.exports = class extends EventEmitter { +class Sock extends EventEmitter { constructor(ws) { this.ws = ws var {id='', name='ninja'} = ws.request._query @@ -27,8 +35,20 @@ module.exports = class extends EventEmitter { for (var key in mixins) this[key] = mixins[key].bind(this) + allSocks.push(this) + broadcastNumPlayers() ws.on('message', message.bind(this)) ws.on('close', this.exit) + + // `this.exit` may be called for other reasons than the socket closing. + let sock = this + ws.on('close', ()=> { + let index = allSocks.indexOf(sock) + if (index !== -1) { + allSocks.splice(index, 1) + broadcastNumPlayers() + } + }) } mixin(h) { h.sock = this @@ -36,4 +56,9 @@ module.exports = class extends EventEmitter { for (var key in mixins) h[key] = this[key] } + static broadcast(...args) { + for (let sock of allSocks) + sock.send(...args) + } } +module.exports = Sock From ab4795bb52299ec75ccdb41926beeb3e16c1aa88 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Wed, 25 May 2016 22:25:56 -0700 Subject: [PATCH 20/24] Branding: update README with drafts.ninja features Also adjust the rest of the text for stylistic purposes. --- README.md | 53 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d56f388..c826cb0 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,49 @@ -# draft +# drafts.ninja -unaffiliated with wizards of the coast +[drafts.ninja](http://drafts.ninja) is a fork of aeosynth's `draft` project. It +supports all of the features of `draft` and more. Here are some of the +highlights: -# run + * **Pick confirmation**: In order to prevent misclicks, `draft` requires you to + click a card twice in order to select it. However, the selected card is +indistinguishable from the other cards in the pack. The UI in drafts.ninja +indicates which card is currently selected. -- [node.js](http://nodejs.org/) + * **Autopick**: If your time expires, `draft` will select a card for you at + random. This rarely turns out well. If you have preliminarily selected a +card but not confirmed it, drafts.ninja will automatically pick it for you. -- `make` + * **Connection indicators**: Are your draftmates disconnected or just slow? + drafts.ninja displays a connection indicator next to each player in the +draft, letting you know if a player is no longer with us. -- `node app.js` + * **Kick players**: If one of your players has disconnected and is holding up + the draft, you can kick them and the rest of their picks will be made +automatically for them. No more abandoning the draft halfway through! -- + * **Ready confirmation**: Each player must mark themself as ready before the + game can start. If you have unresponsive players, you can kick them before +the draft has started and get a new person. -# updating + * **Suggest lands**: After agonizing over your maindeck, you don't want to + spend a lot of time constructing your manabase. With the click of a button, +drafts.ninja will add lands to your deck using an algorithm designed to +conservatively choose your color ratio. It'll even add some basic lands to your +sideboard as well, just in case. -generally you can update with `git pull`; if that doesn't work, -rerun `make`; if that still doesn't work, please file an issue +Like `draft` before it, drafts.ninja is unaffiliated with Wizards of the Coast, +and is licensed under the MIT license. -# etc +Bugs or feature requests? Feel free to open an issue. -written in [es6], transpiled with [traceur], using [react] on the client +# Installation -for the editor component, see +drafts.ninja is a NodeJS application. Install NodeJS, then just run `make run` +in your terminal and visit [http://localhost:1337](http://localhost:1337). -[es6]: https://github.com/lukehoban/es6features -[traceur]: https://github.com/google/traceur-compiler -[react]: https://github.com/facebook/react +drafts.ninja is written in [ES6] and transpiled with [Traceur], and uses [React] +on the client-side. + + [ES6]: https://github.com/lukehoban/es6features + [Traceur]: https://github.com/google/traceur-compiler + [React]: https://github.com/facebook/react From 91272b9a485b760b4772cfc0f2f84958c3c60ca4 Mon Sep 17 00:00:00 2001 From: tritoch Date: Tue, 31 May 2016 08:34:51 -0500 Subject: [PATCH 21/24] EMA --- public/src/data.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/src/data.js b/public/src/data.js index be9f2f6..20fc214 100644 --- a/public/src/data.js +++ b/public/src/data.js @@ -75,6 +75,7 @@ export default { "Arabian Nights": "ARN" }, core: { + "Magic Origins": "ORI", "Magic 2015 Core Set": "M15", "Magic 2014 Core Set": "M14", "Magic 2013": "M13", @@ -94,7 +95,7 @@ export default { "Limited Edition Alpha": "LEA" }, other: { - "Magic Origins": "ORI", + "Eternal Masters": "EMA", "Modern Masters 2015": "MM2", "Tempest Remastered": "TPR", "Conspiracy": "CNS", From caddb87c8f33e2ef89778149dcb8d757fa38523b Mon Sep 17 00:00:00 2001 From: tritoch Date: Tue, 31 May 2016 08:35:51 -0500 Subject: [PATCH 22/24] EMA --- src/pool.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pool.js b/src/pool.js index 7d94b0f..898ab37 100644 --- a/src/pool.js +++ b/src/pool.js @@ -57,6 +57,9 @@ function toPack(code) { ? special.gate : special.shock break + case 'EMA': + special = selectRarity(set) + break case 'MMA': special = selectRarity(set) break From 848c84efbaefb35ae15353a8503bdc9d95bfee8f Mon Sep 17 00:00:00 2001 From: tritoch Date: Fri, 3 Jun 2016 08:30:58 -0500 Subject: [PATCH 23/24] Adjustable pick timer --- public/src/app.js | 2 +- public/src/components/game.js | 9 +++++++-- src/human.js | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index d1fd534..75300b7 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -31,7 +31,7 @@ let App = { packs: 3, bots: true, - timer: true, + timer: 40, beep: false, chat: true, diff --git a/public/src/components/game.js b/public/src/components/game.js index af05b7f..2403caa 100644 --- a/public/src/components/game.js +++ b/public/src/components/game.js @@ -58,11 +58,16 @@ export default React.createClass({ = readyToStart ? d.button({ onClick: App._emit('start') }, 'start') : d.button({ disabled: true, title: READY_TITLE_TEXT }, 'start') - return d.div({}, d.div({}, startButton), LBox('bots', 'bots'), - LBox('timer', 'timer')) + d.div({}, d.input({ + min: 0, + max: 60, + //onChange: App._emit('timer'), + type: 'number', + valueLink: App.link('timer'), + }), ' second timer')) }, Players() { let rows = App.state.players.map(row) diff --git a/src/human.js b/src/human.js index ed893fb..53dbddc 100644 --- a/src/human.js +++ b/src/human.js @@ -65,8 +65,8 @@ module.exports = class extends EventEmitter { if (pack.length === 1) return this.pick(0) - if (this.useTimer) - this.time = 20 + 5 * pack.length + if (this.useTimer > 0) + this.time = parseInt(this.useTimer) + parseInt(pack.length) this.send('pack', pack) } From 8ba82e2437999df59e6d123dc3f98b12d84f84e9 Mon Sep 17 00:00:00 2001 From: tritoch Date: Fri, 3 Jun 2016 08:36:07 -0500 Subject: [PATCH 24/24] remove commented --- public/src/components/game.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/src/components/game.js b/public/src/components/game.js index 2403caa..c17031e 100644 --- a/public/src/components/game.js +++ b/public/src/components/game.js @@ -64,7 +64,6 @@ export default React.createClass({ d.div({}, d.input({ min: 0, max: 60, - //onChange: App._emit('timer'), type: 'number', valueLink: App.link('timer'), }), ' second timer'))