diff --git a/.gitignore b/.gitignore index e857631..4c35baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ node_modules .DS_Store *.swp -cards.json -sets.json -cube.json +data diff --git a/app.js b/app.js deleted file mode 100755 index 6996997..0000000 --- a/app.js +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env node - -var PORT = 1337 - , express = require('express') - , app = express() - , server = require('http').createServer(app) - , wss = require('./lib/ws')(server) - , router = require('./lib/router') - , genDeck = require('./lib/genDeck') - ; - -app - .use(express.static(__dirname + '/public')) - .use(express.bodyParser()) - ; - -app.get('/q/:qid', function(req, res) { - res.sendfile(__dirname + '/public/index.html'); -}); - -app.post('/create', function(req, res) { - var body = req.body - , id = router.create(body.sets, body.type, body.size, body.bots, body.cube) - ; - res.send({ id: id }); -}); - -app.post('/deck', function(req, res) { - var body = req.body - , deck = body.deck - , type = body.type - ; - try { - // AJAX can't initiate downloads, and forms can't json encode - deck = JSON.parse(deck) - } catch(err) { - console.log('error parsing deck', deck) - res.end() - return - } - if (deck = genDeck(deck, type)) - res.attachment('draft.' + type); - res.send(deck); -}); - -wss.on('connection', router); - -server.listen(PORT); - -console.log('http://localhost:%d', PORT); diff --git a/index.js b/index.js new file mode 100644 index 0000000..ca53b83 --- /dev/null +++ b/index.js @@ -0,0 +1,26 @@ +var PORT = 1337; + +var http = require('http'); +var express = require('express'); +var engine = require('engine.io'); +var router = require('./lib/router'); + +var app = express() +.use(express.static(__dirname + '/public')) +.use(express.bodyParser()) +.post('/create', function(req, res) { + var id = router.create(req.body); + res.send(id); +}) +.get('/q/:qid', function(req, res) { + res.sendfile(__dirname + '/public/index.html'); +}) +; + +var httpServer = http.createServer(app); + +engine.attach(httpServer).on('connection', router.connect); + +httpServer.listen(PORT, function() { + console.log('http://localhost:%d', PORT); +}); diff --git a/lib/_.js b/lib/_.js new file mode 100644 index 0000000..b325aba --- /dev/null +++ b/lib/_.js @@ -0,0 +1,26 @@ +var rand, _; +rand = function(it){ + return Math.random() * it | 0; +}; +module.exports = _ = { + next: function(arr, delta, index){ + var length; + length = arr.length; + index = (index + delta + length) % length; + return arr[index]; + }, + rand: rand, + shuffle: function(arr){ + return _.choose(arr.length, arr); + }, + choose: function(n, arr){ + var i, end, j, ref$; + i = arr.length; + end = i - n || 1; + while (i > end) { + j = rand(i--); + ref$ = [arr[j], arr[i]], arr[i] = ref$[0], arr[j] = ref$[1]; + } + return arr.slice(-n); + } +}; \ No newline at end of file diff --git a/lib/bot.js b/lib/bot.js new file mode 100644 index 0000000..0c06725 --- /dev/null +++ b/lib/bot.js @@ -0,0 +1,33 @@ +var Player, nop, Bot; +Player = require('./player'); +nop = function(){}; +Bot = (function(superclass){ + Bot.displayName = 'Bot'; + var prototype = extend$(Bot, superclass).prototype, constructor = Bot; + function Bot(){ + superclass.call(this); + } + prototype.isBot = true; + prototype.name = 'bot'; + prototype.send = nop; + prototype.sendPack = function(){ + var pack; + pack = this.packs[0]; + if (pack.length === 1) { + return this.pick(0, true); + } else { + return process.nextTick(bind$(this, 'autopick')); + } + }; + return Bot; +}(Player)); +module.exports = Bot; +function extend$(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function bind$(obj, key){ + return function(){ return obj[key].apply(obj, arguments) }; +} \ No newline at end of file diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 0000000..3aae1c0 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,22 @@ +var request, rand, couch; +request = require('request'); +rand = require('./_').rand; +couch = require('../data/couch'); +module.exports = function(data){ + var options; + options = importAll$({}, couch); + data._id = data.end + rand(1e9).toString(16).slice(-6); + options.uri += data._id; + options.json = data; + console.log(options.uri); + return request(options, function(err, res, body){ + if (err) { + throw err; + } + return console.log(body); + }); +}; +function importAll$(obj, src){ + for (var key in src) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/draft.js b/lib/draft.js new file mode 100644 index 0000000..7430619 --- /dev/null +++ b/lib/draft.js @@ -0,0 +1,139 @@ +var EventEmitter, Bot, Human, db, ref$, next, rand, shuffle, Draft; +EventEmitter = require('events').EventEmitter; +Bot = require('./bot'); +Human = require('./human'); +db = require('./db'); +ref$ = require('./_'), next = ref$.next, rand = ref$.rand, shuffle = ref$.shuffle; +Draft = (function(superclass){ + Draft.displayName = 'Draft'; + var prototype = extend$(Draft, superclass).prototype, constructor = Draft; + function Draft(opts){ + importAll$(this, opts); + importAll$(this, { + delta: -1, + id: rand(1e9).toString(16), + players: [], + round: 0, + startTime: Date.now() / 1e3 | 0 + }); + } + prototype.join = function(sock){ + var name, i$, ref$, len$, p, h; + name = sock.name; + for (i$ = 0, len$ = (ref$ = this.players).length; i$ < len$; ++i$) { + p = ref$[i$]; + if (p.name === name) { + return p.attach(sock); + } + } + if (this.players.length === this.seats) { + return p.send('error', 'draft full'); + } + h = new Human(sock); + h.isHost = sock.id === this.host; + return this.add(h); + }; + prototype.add = function(p){ + this.players.push(p); + p.on('pass', bind$(this, 'pass')); + if (p.isBot) { + return; + } + this.meta(); + if (p.isHost) { + p.once('start', bind$(this, 'start')); + return p.emit('start'); + } + }; + prototype.meta = function(){ + var players, res$, i$, ref$, len$, p, j$, ref1$, len1$, results$ = []; + res$ = []; + for (i$ = 0, len$ = (ref$ = this.players).length; i$ < len$; ++i$) { + p = ref$[i$]; + res$.push({ + name: p.name, + time: p.time, + packs: p.packs.length + }); + } + players = res$; + for (j$ = 0, len1$ = (ref1$ = this.players).length; j$ < len1$; ++j$) { + p = ref1$[j$]; + results$.push(p.send('set', { + players: players + })); + } + return results$; + }; + prototype.start = function(){ + var i, ref$, len$, p; + if (this.addBots) { + while (this.players.length < this.seats) { + this.add(new Bot); + } + } + shuffle(this.players); + for (i = 0, len$ = (ref$ = this.players).length; i < len$; ++i) { + p = ref$[i]; + p.index = i; + } + return this.startRound(); + }; + prototype.startRound = function(){ + var set, i$, ref$, len$, p, results$ = []; + if (!(set = this.sets[this.round++])) { + return this.end(); + } + this.delta *= -1; + this.activePacks = this.players.length; + for (i$ = 0, len$ = (ref$ = this.players).length; i$ < len$; ++i$) { + p = ref$[i$]; + results$.push(p.start(set)); + } + return results$; + }; + prototype.pass = function(pack, index){ + var player; + player = next(this.players, this.delta, index); + if (pack.length) { + player.receive(pack); + } else if (!--this.activePacks) { + this.startRound(); + } + return this.meta(); + }; + prototype.end = function(){ + var data, res$, i$, ref$, len$, p; + console.log('end', this.id); + data = { + sets: this.sets, + start: this.startTime + }; + data.end = Date.now() / 1e3 | 0; + res$ = []; + for (i$ = 0, len$ = (ref$ = this.players).length; i$ < len$; ++i$) { + p = ref$[i$]; + res$.push({ + isBot: p.isBot, + picks: p.picks + }); + } + data.players = res$; + return db(data); + }; + return Draft; +}(EventEmitter)); +module.exports = Draft; +function extend$(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function importAll$(obj, src){ + for (var key in src) obj[key] = src[key]; + return obj; +} +function bind$(obj, key){ + return function(){ return obj[key].apply(obj, arguments) }; +} \ No newline at end of file diff --git a/lib/genDeck.js b/lib/genDeck.js deleted file mode 100644 index e172ddf..0000000 --- a/lib/genDeck.js +++ /dev/null @@ -1,47 +0,0 @@ -var _ = require('underscore'); - -var types = { - cod: function(main, side) { - // generating strings is easier then using a proper xml builder - var mainXML = [] - , sideXML = [] - ; - _.each(main, function(num, name) { - mainXML.push(''); - }); - _.each(side, function(num, name) { - sideXML.push(''); - }); - var xml = - '' + - '' + - 'draft' + - '' + - mainXML.join('') + - '' + - '' + - sideXML.join('') + - '' + - '' - ; - return xml; - }, - dec: function(main, side) { - var deck = []; - _.each(main, function(num, name) { - deck.push(num + ' ' + name); - }); - _.each(side, function(num, name) { - deck.push('SB: ' + num + ' ' + name); - }); - return deck.join('\n'); - } -}; - -function genDeck(deck, type) { - if (!(deck && type && (type in types))) - return false - return types[type](deck.main, deck.side); -} - -module.exports = genDeck; diff --git a/lib/genHash.js b/lib/genHash.js deleted file mode 100644 index d1a3460..0000000 --- a/lib/genHash.js +++ /dev/null @@ -1,57 +0,0 @@ -var crypto = require('crypto'); - -var opts = { - cod: { - prefix: 'SB:', - cardTransform: function(card) { - return card.toLowerCase(); - }, - separator: ';', - hash: 'sha1', - digestTransform: function(digest) { - return parseInt(digest.slice(0, 10), 16).toString(32); - } - }, - dec: { - prefix: '#', - cardTransform: function(card) { - return card.toUpperCase().replace(/[^A-Z]/g, ''); - }, - separator: '', - hash: 'md5', - digestTransform: function(digest) { - return digest.slice(0, 8); - } - } -}; - -function hash(deck, options) { - var cardList = []; - for (var zone in deck) { - var prefix = zone === 'side' ? options.prefix : '' - , cards = deck[zone] - , n - , s - ; - for (var card in cards) { - n = cards[card]; - s = prefix + options.cardTransform(card); - while (n--) - cardList.push(s); - } - } - var data = cardList.sort().join(options.separator) - , hash = crypto.createHash(options.hash).update(data, 'utf8') - , digest = hash.digest('hex') - ; - return options.digestTransform(digest); -} - -function genHash(deck) { - return { - cock: hash(deck, opts.cod), - mws: hash(deck, opts.dec) - }; -} - -module.exports = genHash; diff --git a/lib/genPack.js b/lib/genPack.js index 43b0177..4b24ab4 100644 --- a/lib/genPack.js +++ b/lib/genPack.js @@ -1,71 +1,29 @@ -var _ = require('underscore') - , Cards = require('../cards/cards') - , Sets = require('../cards/sets') - , rarity = - { mythic: 1 - , rare: 2 - , uncommon: 3 - , common: 4 - } - ; - -function rand(arr, num) { - if (num) - return _.shuffle(arr).slice(0, num); - return arr[_.random(arr.length - 1)]; -} - -function getCardInfo(card) { - card = Cards[card]; - - return { - cmc: card.cmc, - color: card.color, - name: card.name, - rarity: rarity[card.rarity], - url: card.url - }; -} - -function genPack(setName) { - var pack = { id: _.random(1e8) }; - - if (typeof setName === 'object') { - pack.cards = _.map(setName.splice(-15, 15), getCardInfo); - return pack; +var Cards, Sets, ref$, choose, rand, genPack; +Cards = require('../data/cards'); +Sets = require('../data/sets'); +ref$ = require('./_'), choose = ref$.choose, rand = ref$.rand; +genPack = function(setName){ + var ref$, common, uncommon, rare, mythic, special, pack, index, i$, len$, name, results$ = []; + ref$ = Sets[setName], common = ref$.common, uncommon = ref$.uncommon, rare = ref$.rare, mythic = ref$.mythic, special = ref$.special; + mythic || (mythic = rare); + if (!rand(8)) { + rare = mythic; } - - set = Sets[setName]; - var commons = set.common - , uncommons = set.uncommon - , rares = set.rare - , mythics = set.mythic - , special = set.special - , cards = [] - ; - mythics.length || (mythics = rares); - cards.push.apply(cards, rand(commons, 10)); - cards.push.apply(cards, rand(uncommons, 3)); - if (_.random(7)) - cards.push(rand(rares)); - else - cards.push(rand(mythics)); - + pack = [].concat(choose(10, common), choose(3, uncommon), choose(1, rare)); switch (setName) { - case "Dragon's Maze": - if (_.random(21)) - cards.push(rand(special.gate)); - else - cards.push(rand(special.shock)); - break; - case 'Time Spiral': - cards.push(rand(special)); - break; + case "Dragon's Maze": + special = rand(22) + ? special.gate + : special.shock; + // fallthrough + case 'Time Spiral': + index = rand(special.length); + pack.push(special[index]); } - - pack.cards = _.map(cards, getCardInfo); - - return pack; -} - -module.exports = genPack; + for (i$ = 0, len$ = pack.length; i$ < len$; ++i$) { + name = pack[i$]; + results$.push(Cards[name]); + } + return results$; +}; +module.exports = genPack; \ No newline at end of file diff --git a/cards/getCards.js b/lib/getCards.js similarity index 100% rename from cards/getCards.js rename to lib/getCards.js diff --git a/cards/getSetNames.js b/lib/getSetNames.js similarity index 100% rename from cards/getSetNames.js rename to lib/getSetNames.js diff --git a/lib/human.js b/lib/human.js new file mode 100644 index 0000000..d9799cd --- /dev/null +++ b/lib/human.js @@ -0,0 +1,47 @@ +var Player, Human; +Player = require('./player'); +Human = (function(superclass){ + Human.displayName = 'Human'; + var prototype = extend$(Human, superclass).prototype, constructor = Human; + function Human(sock){ + superclass.call(this); + this.attach(sock); + } + prototype.isBot = false; + prototype.attach = function(sock){ + this.name = sock.name; + this.send = bind$(sock, 'send'); + this.send('set', { + main: this.main, + pack: this.packs[0] + }); + return sock.on('pick', bind$(this, 'pick')); + }; + prototype.sendPack = function(){ + var pack; + pack = this.packs[0]; + if (pack.length === 1) { + return this.pick(0, true); + } else { + return this.send('set', { + pack: pack + }); + } + }; + prototype.pick = function(index){ + var ref$; + superclass.prototype.pick.call(this, index); + return this.send('add', (ref$ = this.main)[ref$.length - 1]); + }; + return Human; +}(Player)); +module.exports = Human; +function extend$(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function bind$(obj, key){ + return function(){ return obj[key].apply(obj, arguments) }; +} \ No newline at end of file diff --git a/lib/player.js b/lib/player.js index aa281ef..002ac3e 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1,118 +1,57 @@ -var EventEmitter = require('events').EventEmitter - , _ = require('underscore') - , genHash = require('./genHash') - ; - -function Player(ws, id, name, TIME, isBot) { - this.isBot = isBot; - this.id = id; - this.name = name; - this.packs = []; - this.picks = []; - this.time = 0; - this.TIME = isBot ? 1 : TIME; - this.hasAutoPicked = false; - this.dropped = false; - - _.bindAll(this); - this.reset(ws); -} - -_.extend(Player.prototype, EventEmitter.prototype); - -Player.prototype.generateHash = function(deck) { - if (this.hash) return; - this.hash = genHash(deck); - this.emit('meta'); -}; - -Player.prototype.send = function (name, args) { - if (this.closed) return; - this.ws.json(name, args); -}; - -Player.prototype.changeName = function(name) { - if (!name) return; - this.name = name.slice(0, 15); - this.emit('meta'); -}; - -Player.prototype.receive = function(pack) { - if (this.packs.push(pack) === 1) { - this.time = this.TIME; - this.updateSelf(); - } -}; - -Player.prototype.pick = function(packId, name) { - this.hasAutoPicked = false; - this.pickCard(packId, name); -}; - -Player.prototype.autoPick = function() { - if (this.hasAutoPicked && !this.isBot) { - this.dropped = true; - this.TIME = 1; +var EventEmitter, rand, genPack, Player; +EventEmitter = require('events').EventEmitter; +rand = require('./_').rand; +genPack = require('./genPack'); +Player = (function(superclass){ + Player.displayName = 'Player'; + var prototype = extend$(Player, superclass).prototype, constructor = Player; + function Player(){ + importAll$(this, { + picks: [], + packs: [], + main: [] + }); } - else - this.hasAutoPicked = true; - - var packs = this.packs - , pack = packs[0] - , packId = pack.id - , name = _.shuffle(pack.cards)[0].name - ; - this.pickCard(packId, name); - - if (this.dropped && this.packs.length) - this.autoPick(); -}; - -Player.prototype.pickCard = function(packId, name) { - var packs = this.packs - , pack = packs[0] - , cards - , card - ; - - if ((!pack) || (pack.id !== packId)) - return; // lol networks - - cards = pack.cards; - card = _.find(cards, function(card) { return card.name === name; }); - this.picks.push(card); - cards.splice(cards.indexOf(card), 1); - pack = packs.shift() - this.time = packs.length ? this.TIME : 0; - this.updateSelf(card); - this.emit('pass', pack); -}; - -Player.prototype.updateSelf = function(card) { - if (card) - this.send('pick', card); - this.send('pack', this.packs[0]); -}; - -Player.prototype.wsClose = function() { - this.closed = true; - this.emit('close', this); -}; - -Player.prototype.end = function() { - this.ws.close(); - this.closed = true; -}; - -Player.prototype.reset = function(ws) { - this.closed = false; - this.ws = ws; - ws.on('name', this.changeName); - ws.on('close', this.wsClose); - ws.on('pick', this.pick); - ws.on('hash', this.generateHash); - this.send('picks', this.picks); - this.updateSelf(); -}; - + prototype.time = 0; + prototype.start = function(set){ + this.picks.push(this.round = []); + return this.receive(genPack(set)); + }; + prototype.receive = function(pack){ + this.packs.push(pack); + if (this.packs.length === 1) { + return this.sendPack(); + } + }; + prototype.autopick = function(){ + var index; + index = rand(this.packs[0].length); + return this.pick(index, true); + }; + prototype.pick = function(index, autopick){ + var pack, pick; + pack = this.packs.shift(); + pick = pack.splice(index, 1)[0]; + this.main.push(pick); + this.round.push({ + name: pick.name, + autopick: autopick + }); + if (this.packs.length) { + this.sendPack(); + } + return this.emit('pass', pack, this.index); + }; + return Player; +}(EventEmitter)); module.exports = Player; +function extend$(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function importAll$(obj, src){ + for (var key in src) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/q.js b/lib/q.js deleted file mode 100644 index 886908a..0000000 --- a/lib/q.js +++ /dev/null @@ -1,186 +0,0 @@ -var TIME = 90 - , EventEmitter = require('events').EventEmitter - , _ = require('underscore') - , Player = require('./player') - , genPack = require('./genPack') - , DELETE_DELAY = 1000 * 60 * 60 - ; - -try { - var cube = require('../cards/cube'); -} catch (err) { - console.log('no cubes found'); -} - -function Q(sets, type, size, bots, cubeName) { - if (type !== 'sealed') sets = sets.slice(0, 3); - if (type === 'cube') { - var shuffled = _.shuffle(cube[cubeName]); - sets = [shuffled, shuffled, shuffled]; - } - size = Number(size); - bots = Math.min(Number(bots), size - 1); - this.size = size; - this.sets = sets; - this.type = type; - this.id = _.random(1e8); - this.players = []; - this.openPacks = 0; - this.clockwise = false; - this.started = false; - this.ended = false; - - _.bindAll(this, 'decrement', 'meta', 'playerClose', 'end2'); - - function nop() {} - while (bots--) {// FIXME this is horribly hacky - this.add({ close: nop, send: nop, on: nop, json: nop }, _.random(1e8), 'bot', true); - } -} - -_.extend(Q.prototype, EventEmitter.prototype); - -Q.prototype.add = function(ws, pid, name, isBot) { - var players = this.players - , len = players.length - , self = this - , player - ; - - pid = Number(pid); - - if (player = _.find(players, function(player) - { return player.id === pid; })) { - player.reset(ws); - this.meta(); - return; - } - if (len === this.size) - return ws.error('q full'); - - player = new Player(ws, pid, name, TIME, isBot); - player.on('close', this.playerClose); - player.on('meta', this.meta); - player.on('pass', function(pack) { - self.pass(pack, this); - }); - - len = players.push(player); - if (len === this.size) - this.start(); - else - this.meta(); -}; - -Q.prototype.playerClose = function(player) { - var players = this.players; - var index = players.indexOf(player); - if (index === -1) return; - - if (!this.started) - players.splice(index, 1); - this.meta(); -}; - -Q.prototype.pass = function(pack, player) { - var players = this.players - , len = players.length - , delta = this.clockwise ? +1 : -1 - , idx = player.index + delta - ; - - idx = (idx + len) % len; // negative modulo - if (pack.cards.length) - players[idx].receive(pack); - else { - if (!--this.openPacks) - this.openPack(); - } - this.meta(); -}; - -Q.prototype.start = function() { - this.started = true; - this.players = _.shuffle(this.players) - _.each(this.players, function(player, index) { - player.index = index; - }); - if (this.type === 'sealed') { - var sets = this.sets; - this.ended = true; - _.each(this.players, function(player) { - var picks = _.chain(sets) - .map(genPack) - .pluck('cards') - .flatten() - .value() - ; - player.picks = picks; - player.send('picks', picks); - }); - this.meta(); - } else { - this.intervalID = setInterval(this.decrement, 1000); - this.openPack(); - } -}; - -Q.prototype.openPack = function() { - var set = this.sets.shift(); - if (!set) - return this.end(); - - this.openPacks = this.size; - this.clockwise = !this.clockwise; - _.each(this.players, function(player) { - player.receive(genPack(set)); - }); - this.meta(); -}; - -Q.prototype.decrement = function() { - _.each(this.players, function(player) { - if (player.packs.length && !--player.time) - player.autoPick(); - }); -}; - -Q.prototype.end = function() { - clearInterval(this.intervalID); - this.ended = true; - this.meta(); - setTimeout(this.end2, DELETE_DELAY); -}; - -Q.prototype.end2 = function() { - _.invoke(this.players, 'end'); - this.emit('end'); // remove this q from router -}; - -Q.prototype.meta = function() { - var players = this.players - , arr = [] - , ended = this.ended - , size = this.size - ; - _.each(players, function(player) { - arr.push({ - dropped: player.dropped, - hash: player.hash, - name: player.name, - packs: player.packs.length, - time: player.time - }); - }); - _.each(players, function(player, index) { - var meta = { - players: arr, - ended: ended, - index: index, - size: size - }; - player.send('meta', meta); - }); -}; - -module.exports = Q; diff --git a/lib/router.js b/lib/router.js index 340723b..cccacb2 100644 --- a/lib/router.js +++ b/lib/router.js @@ -1,31 +1,28 @@ -var Player = require('./player') - , Q = require('./q') - , routes = {} - ; - -function router(ws) { - ws.on('init', route); -} - -function route(qid, pid, name) { - var q = routes[qid]; - if (q) - q.add(this, pid, name); - else - this.error('q not found'); -} - -function end() { - delete routes[this.id]; -} - -router.create = function(sets, sealed, size, bots, cubeName) { - var q = new Q(sets, sealed, size, bots, cubeName) - , id = q.id - ; - q.on('end', end); - routes[id] = q; - return id; +var Draft, Sock, drafts, rm, router; +Draft = require('./draft'); +Sock = require('./sock'); +drafts = {}; +rm = function(){ + var key$, ref$; + return ref$ = drafts[key$ = this.id], delete drafts[key$], ref$; }; - -module.exports = router; +router = { + create: function(opts){ + var draft; + draft = new Draft(opts); + drafts[draft.id] = draft; + draft.on('end', rm); + return draft.id; + }, + connect: function(ws){ + var sock, room, draft; + sock = new Sock(ws); + room = sock.room; + if (draft = drafts[room]) { + return draft.join(sock); + } else { + return sock.send('error', 'room not found'); + } + } +}; +module.exports = router; \ No newline at end of file diff --git a/cards/scrape.js b/lib/scrape.js similarity index 100% rename from cards/scrape.js rename to lib/scrape.js diff --git a/lib/sock.js b/lib/sock.js new file mode 100644 index 0000000..9816e4d --- /dev/null +++ b/lib/sock.js @@ -0,0 +1,40 @@ +var EventEmitter, Sock; +EventEmitter = require('events').EventEmitter; +Sock = (function(superclass){ + Sock.displayName = 'Sock'; + var prototype = extend$(Sock, superclass).prototype, constructor = Sock; + function Sock(ws){ + var ref$; + this.ws = ws; + importAll$(this, { + id: (ref$ = ws.request.query).id, + name: ref$.name, + room: ref$.room + }); + ws.on('message', bind$(this, 'message')); + } + prototype.message = function(it){ + return this.emit.apply(this, JSON.parse(it)); + }; + prototype.send = function(name, args){ + return this.ws.send(JSON.stringify({ + name: name, + args: args + })); + }; + return Sock; +}(EventEmitter)); +module.exports = Sock; +function extend$(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function importAll$(obj, src){ + for (var key in src) obj[key] = src[key]; + return obj; +} +function bind$(obj, key){ + return function(){ return obj[key].apply(obj, arguments) }; +} \ No newline at end of file diff --git a/lib/ws.js b/lib/ws.js deleted file mode 100644 index a293781..0000000 --- a/lib/ws.js +++ /dev/null @@ -1,45 +0,0 @@ -var engine = require('engine.io'); - -function json(name, args) { - var obj = { - name: name, - args: args - }; - this.send(JSON.stringify(obj)); -} - -function error(msg) { - this.json('apperror', msg); - this.close(); -} - -function message(message) { - var error = null - try { - message = JSON.parse(message); - } catch (err) { - console.log(err, message); - error = err - } - if (!error) - this.emit.apply(this, message); -} - -function logError(err) { - console.log(err); -} - -function decorate(ws) { - ws.json = json; - ws.error = error; - ws.on('message', message); - ws.on('error', logError); -} - -function wss(server) { - var wss = engine.attach(server); - wss.on('connection', decorate); - return wss; -} - -module.exports = wss; diff --git a/package.json b/package.json index dfbe9f3..99bdc39 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "version": "0.0.0-17", "author": "James Campos ", "dependencies": { - "express": "~3.0.5", - "underscore": "~1.4.3", "engine.io": "~0.4.3", "request": "~2.20.0", - "cheerio": "~0.11.0" + "cheerio": "~0.11.0", + "st": "latest", + "express": "~3.2.0" }, "subdomain": "drafts.in", "domains": [ diff --git a/public/js/index.js b/public/js/index.js index 1c04e67..e490feb 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -14,7 +14,10 @@ angular ; }) .factory('ws', function() { - var ws = eio('ws://' + location.host); + var id = localStorage.pid || (localStorage.id = Math.floor(Math.random() * 1e8)); + var name = localStorage.name; + var room = location.pathname.split('/').pop(); + var ws = eio('ws://' + location.host, { query: { id: id, name: name, room: room }}); ws.on('message', function(msg) { var data = JSON.parse(msg); ws.emit(data.name, data.args); @@ -28,10 +31,9 @@ angular ; function CreateCtrl($scope, $http, $location) { - $scope.bots = 0; $scope.cube = 'mtgo holiday'; $scope.type = 'draft'; - $scope.size = 8; + $scope.seats = 8; $scope.sets = [ 'Alara Reborn', @@ -132,12 +134,23 @@ function CreateCtrl($scope, $http, $location) { $scope.set5 = 'Gatecrash'; $scope.set6 = 'Return to Ravnica'; $scope.create = function() { - var sets = [$scope.set1, $scope.set2, $scope.set3, $scope.set4, $scope.set5, $scope.set6]; - $http.post('/create', { - sets: sets, type: $scope.type, size: $scope.size, bots: $scope.bots, cube: $scope.cube - }) - .success(function(data, status) { - $location.path('/q/' + data.id); + var id = localStorage.pid || (localStorage.id = Math.floor(Math.random() * 1e8)); + var data = { + type: $scope.type, seats: $scope.seats, host: id + }; + switch(data.type) { + case 'draft': + data.sets = [$scope.set1, $scope.set2, $scope.set3]; + break; + case 'sealed': + data.sets = [$scope.set1, $scope.set2, $scope.set3, $scope.set4, $scope.set5, $scope.set6]; + break; + case 'cube': + data.cube = $scope.cube; + } + $http.post('/create', data) + .success(function(qid, status) { + $location.path('/q/' + qid); }) ; }; @@ -160,8 +173,6 @@ function QCtrl($scope, $timeout, $http, $routeParams, ws) { { land: true, cmc: 0, color: 'L', url: 'http://gatherer.wizards.com/Handlers/Image.ashx?type=card&multiverseid=73973', name: 'Swamp' } ]; - localStorage.pid || (localStorage.pid = Math.floor(Math.random() * 1e8)); - function decrement() { angular.forEach($scope.players, function(player) { if (player.time) @@ -173,30 +184,30 @@ function QCtrl($scope, $timeout, $http, $routeParams, ws) { $timeout(decrement, 1000); ws.on('open', function() { - var qid = $routeParams.qid - , pid = localStorage.pid - , name = localStorage.name - ; - ws.json('init', qid, pid, name); - }); - ws.on('apperror', function(error) { - $scope.error = error; - $scope.$apply(); + var qid = $routeParams.qid; + ws.json('join', qid); }); ws.on('error', function(error) { console.error(error); }); + ws.on('set', function(data) { + angular.extend($scope, data); + }); + ws.on('add', function(card) { + $scope.main.push(card); + }); + /* ws.on('meta', function(meta) { var players = meta.players , index = meta.index , ended = meta.ended - , size = meta.size + , seats = meta.seats , oppIndex ; - if (size === 8)// XXX magic - oppIndex = (index + (size/2)) % size; - while(size > players.length) + if (seats === 8)// XXX magic + oppIndex = (index + (seats/2)) % seats; + while(seats > players.length) players.push({}); $scope.end = ended; $scope.players = players; @@ -233,19 +244,20 @@ function QCtrl($scope, $timeout, $http, $routeParams, ws) { $scope.main = cards; $scope.$apply(); }); + */ ws.on('close', function() { $scope.end = true; $scope.$apply(); }); - $scope.pick = function(card) { - if (selected !== card) { - selected = card + $scope.pick = function(index) { + if (selected !== index) { + selected = index return } - ws.json('pick', $scope.pack.id, card.name); - $scope.pack.show = false; - selected = null + ws.json('pick', index); + $scope.pack = null; + selected = null; }; $scope.editName = function(player) { if (!player.self) return; diff --git a/public/partials/create.html b/public/partials/create.html index f8832d0..2bb4c1d 100644 --- a/public/partials/create.html +++ b/public/partials/create.html @@ -1,7 +1,7 @@ Fork me on GitHub

drafts.in

- @@ -15,17 +15,7 @@

drafts.in

- with - bots +
diff --git a/public/partials/q.html b/public/partials/q.html index 83e8b11..e82fb03 100644 --- a/public/partials/q.html +++ b/public/partials/q.html @@ -71,8 +71,8 @@

pack

-
- +
+
diff --git a/src/_.co b/src/_.co new file mode 100644 index 0000000..73e4671 --- /dev/null +++ b/src/_.co @@ -0,0 +1,18 @@ +rand = -> Math.random! * it | 0 + +module.exports = _ = + next: (arr, delta, index) -> + length = arr.length + index = (index + delta + length) % length # negative modulo + arr[index] + rand: rand + shuffle: (arr) -> + # shuffling is the special case where all items are chosen + _.choose arr.length, arr + choose: (n, arr) -> + i = arr.length + end = i - n or 1 + while i > end + j = rand i-- + arr[i, j] = arr[j, i] + arr.slice -n diff --git a/src/bot.co b/src/bot.co new file mode 100644 index 0000000..8552f7d --- /dev/null +++ b/src/bot.co @@ -0,0 +1,21 @@ +Player = require \./player + +nop = -> + +class Bot extends Player + -> + super! + + isBot: true + name: \bot + send: nop + + sendPack: -> + [pack] = @packs + + if pack.length is 1 + @pick 0 true + else + process.nextTick @~autopick + +module.exports = Bot diff --git a/src/db.co b/src/db.co new file mode 100644 index 0000000..a6b6a86 --- /dev/null +++ b/src/db.co @@ -0,0 +1,16 @@ +request = require \request +{rand} = require \./_ +couch = require \../data/couch + +module.exports = (data) -> + options = {} <<<< couch + + # sequential ids for extreme performance + data._id = data.end + rand 1e9 .toString 16 .slice -6 + options.uri += data._id + options.json = data + + console.log options.uri + request options, (err, res, body) -> + throw err if err + console.log body diff --git a/src/draft.co b/src/draft.co new file mode 100644 index 0000000..b5003f1 --- /dev/null +++ b/src/draft.co @@ -0,0 +1,90 @@ +{EventEmitter} = require \events +Bot = require \./bot +Human = require \./human +db = require \./db +{next, rand, shuffle} = require \./_ + +class Draft extends EventEmitter + (opts) -> + @ <<<< opts + @ <<<< + delta: -1 + id: rand 1e9 .toString 16 + players: [] + round: 0 + startTime: Date.now! / 1e3 | 0 + + join: (sock) -> + {name} = sock + for p of @players + if p.name is name + return p.attach sock + + if @players.length is @seats + return p.send \error 'draft full' + + h = new Human sock + h.isHost = sock.id is @host + @add h + + add: (p) -> + @players.push p + p.on \pass @~pass + + return if p.isBot + + @meta! + + if p.isHost + p.once \start @~start + p.emit \start + + meta: -> + players = for p of @players + { p.name, p.time, packs: p.packs.length } + for p of @players + p.send \set { players } + + start: -> + if @addBots + while @players.length < @seats + @add new Bot + + shuffle @players + + for p, i of @players + p.index = i + + @startRound! + + startRound: -> + unless set = @sets[@round++] + return @end! + + @delta *= -1 + @activePacks = @players.length + + for p of @players + p.start set + + pass: (pack, index) -> + player = next @players, @delta, index + + if pack.length + player.receive pack + else if !--@activePacks + @startRound! + + @meta! + + end: -> + console.log \end @id + + data = @{ sets, start: startTime } + data.end = Date.now! / 1e3 | 0 + data.players = for p of @players + p{ isBot, picks } + + db data + +module.exports = Draft diff --git a/src/genPack.co b/src/genPack.co new file mode 100644 index 0000000..086e21b --- /dev/null +++ b/src/genPack.co @@ -0,0 +1,28 @@ +Cards = require \../data/cards +Sets = require \../data/sets +{choose, rand} = require \./_ + +genPack = (setName) -> + {common, uncommon, rare, mythic, special} = Sets[setName] + mythic or= rare + unless rand 8 + rare = mythic + + pack = [].concat( + choose 10 common + choose 3 uncommon + choose 1 rare + ) + + switch setName + case "Dragon's Maze" + special = if rand 22 then <>gate else <>shock + fallthrough + case 'Time Spiral' + index = rand special.length + pack.push special[index] + + for name of pack + Cards[name] + +module.exports = genPack diff --git a/cards/getCards.co b/src/getCards.co similarity index 100% rename from cards/getCards.co rename to src/getCards.co diff --git a/cards/getSetNames.co b/src/getSetNames.co similarity index 100% rename from cards/getSetNames.co rename to src/getSetNames.co diff --git a/src/human.co b/src/human.co new file mode 100644 index 0000000..189ccfe --- /dev/null +++ b/src/human.co @@ -0,0 +1,28 @@ +Player = require \./player + +class Human extends Player + (sock) -> + super! + @attach sock + + isBot: false + + attach: (sock) -> + {@name} = sock + @send = sock~send + @send \set { @main, pack: @packs.0 } + sock.on \pick @~pick + + sendPack: -> + pack = @packs.0 + + if pack.length is 1 + @pick 0 true + else + @send \set { pack } + + pick: (index) -> + super index + @send \add @main[* - 1] + +module.exports = Human diff --git a/src/player.co b/src/player.co new file mode 100644 index 0000000..e6bc7cb --- /dev/null +++ b/src/player.co @@ -0,0 +1,39 @@ +{EventEmitter} = require \events +{rand} = require \./_ +genPack = require \./genPack + +class Player extends EventEmitter + -> + @ <<<< + picks: [] + packs: [] + main: [] + + time: 0 + + start: (set) -> + @picks.push @round = [] + @receive genPack set + + receive: (pack) -> + @packs.push pack + + if @packs.length is 1 + @sendPack! + + autopick: -> + index = rand @packs.0.length + @pick index, true + + pick: (index, autopick) -> + pack = @packs.shift! + [pick] = pack.splice index, 1 + @main.push pick + @round.push { pick.name, autopick } + + if @packs.length + @sendPack! + + @emit \pass pack, @index + +module.exports = Player diff --git a/src/router.co b/src/router.co new file mode 100644 index 0000000..bd96044 --- /dev/null +++ b/src/router.co @@ -0,0 +1,25 @@ +Draft = require \./draft +Sock = require \./sock + +drafts = {} + +rm = -> + delete drafts[@id] + + +router = + create: (opts) -> + draft = new Draft opts + drafts[draft.id] = draft + draft.on \end rm + draft.id + connect: (ws) -> + sock = new Sock ws + {room} = sock + + if draft = drafts[room] + draft.join sock + else + sock.send \error 'room not found' + +module.exports = router diff --git a/cards/scrape.co b/src/scrape.co similarity index 100% rename from cards/scrape.co rename to src/scrape.co diff --git a/src/sock.co b/src/sock.co new file mode 100644 index 0000000..3292c8c --- /dev/null +++ b/src/sock.co @@ -0,0 +1,13 @@ +{EventEmitter} = require \events + +class Sock extends EventEmitter + (@ws) -> + @ <<<< ws.request.query{ id, name, room } + ws.on \message @~message + + message: -> @emit ...JSON.parse it + + send: (name, args) -> + @ws.send JSON.stringify { name, args } + +module.exports = Sock