1 /**
  2  * @name Dominion Set Generator
  3  *
  4  * @fileOverview Generates sets for the card game Dominion.
  5  *
  6  * Copyright 2012, Wei-Hwa Huang
  7  * Licensed for use by Funsockets, Inc.
  8  * Ask Wei-Hwa for other licensing information.  If you can't figure out how to
  9  * ask Wei-Hwa, you're not allowed to use this code.
 10  *
 11  * Unimplemented but coming soon features: 
 12  * <ul>
 13  *  <li> Consider previous card sets
 14  *  <li> Add data for Guilds
 15  *  <li> Allow user to ban specific cards
 16  *  <li> Canonicalize card names?
 17  * </ul>
 18  *
 19  * Warning: requires dominionSetGeneratorData.js to be loaded !
 20  *
 21  * Last update: 2012-12-21
 22  *
 23  * @version 1.2.1
 24  * @author Wei-Hwa Huang
 25  */
 26 
 27 /**
 28  * Creates a unit of DominionSetGeneratorAttributeData.
 29  * A DominionSetGeneratorAttributeData is sort of like a 'parameter' for the
 30  * set picker.  It has four numeric values, and a 'Notes' section.
 31  *
 32  * @class
 33  * @constructor
 34  * @param {Number} min The 'minimum' threshhold: If this is undefined,
 35  *   we create an attribute with the default values of (0,0,1,1,'Unused').
 36  * @param {Number} max The 'maximum' threshhold.
 37  * @param {Number} minW Weight for the 'minimum' threshhold.
 38  * @param {Number} maxW Weight for the 'maximum' threshhold.
 39  * @param {String} notes The 'notes' section.
 40  */
 41 function DominionSetGeneratorAttributeData(min, max, minW, maxW, notes) {
 42   if (min === undefined) {
 43     this.min = 0;
 44     this.max = 0;
 45     this.minW = 1;
 46     this.maxW = 1;
 47     this.notes = 'Unused';
 48   } else {
 49     this.min = min;
 50     this.max = max;
 51     this.minW = minW;
 52     this.maxW = maxW;
 53     this.notes = notes;
 54   }
 55 }
 56 
 57 /**
 58  * Creates a DominionSetGenerator.  The same SetGenerator can be used to
 59  * generate multiple sets.
 60  *
 61  * @constructor
 62  */
 63 function DominionSetGenerator() {
 64 
 65   this.maxSetSize = 10;
 66   this.weight = new Object();     // weight of each card; changes a lot
 67   this.commentary = new Object(); // explanation of the weights
 68   this.owned = new Object();      // cards owned. Object, keys are cardnames.
 69   this.cardset = new Object();    // the current set of cards so far.
 70 
 71   this.tMin = new Object();
 72   this.tMax = new Object();
 73   this.tMinW = new Object();
 74   this.tMaxW = new Object();
 75   this.tNotes = new Object();
 76   this.isDefault = new Object();  // for each attribute (key), true if 
 77                                   // we should use the default value,
 78                                   // false if we should use the user-supplied
 79                                   // (t) value.
 80   this.sgd = DominionSetGeneratorData;  // convenience assignment
 81   this.balanceFactor = 1.0;       // how much to weight the overall effect
 82                                   // of default values.
 83 
 84   this.useShelters = false;       // default values
 85   this.platinumColony = false;
 86   this.useSheltersDonline = false;       // default values
 87   this.platinumColonyDonline = false;
 88   this.youngWitchBane = "Cellar";
 89 
 90 
 91   for (attr in this.sgd.Min) {
 92     this.isDefault[attr] = true;
 93   }
 94 
 95 
 96   this.logData = new Array();
 97 }
 98 
 99 /**
100  * Should we use shelters in this game?
101  * Note that this only changes when the set is regenerated.
102  * The official rules don't specify what to do when there are
103  * more than 10 cards in the set (such as for veto mode), so
104  * we calculate it based on all the cards in the set (but not
105  * the Bane for the Young Witch).  Note that this means it is
106  * theoretically possible for there to be shelters but no
107  * Dark Ages cards in a veto game.
108  *
109  * @return {Boolean} Whether to use Shelters.
110  */
111 DominionSetGenerator.prototype.getUseShelters = function() {
112   return this.useShelters;
113 };
114 
115 /**
116  * Should we have Platinum/Colony in this game?
117  * Note that this only changes when the set is regenerated.
118  * The official rules don't specify what to do when there are
119  * more than 10 cards in the set (such as for veto mode), so
120  * we calculate it based on all the cards in the set (but not
121  * the Bane for the Young Witch).  Note that this means it is
122  * theoretically possible for there to be Platinum/Colony but no
123  * Prosperity cards in a veto game.
124  *
125  * @return {Boolean} Whether to use Platinum/Colony.
126  */
127 DominionSetGenerator.prototype.getPlatinumColony = function() {
128   return this.platinumColony;
129 };
130 
131 /**
132  * Should we use shelters in this game? 
133  * Note that this only changes when the set is regenerated.
134  * This uses "Donline rules", where the choice of whether to
135  * use is based on the proportion of total cards owned.
136  *
137  * @return {Boolean} Whether to use Shelters.
138  */
139 DominionSetGenerator.prototype.getUseSheltersDonline = function() {
140   return this.useSheltersDonline;
141 };
142 
143 /**
144  * Should we have Platinum/Colony in this game?
145  * Note that this only changes when the set is regenerated.
146  * This uses "Donline rules", where the choice of whether to
147  * use is based on the proportion of total cards owned.
148  *
149  * @return {Boolean} Whether to use Platinum/Colony.
150  */
151 DominionSetGenerator.prototype.getPlatinumColonyDonline = function() {
152   return this.platinumColonyDonline;
153 };
154 
155 /**
156  * What is the Bane for Young Witch?
157  * Note that this only changes when the set is regenerated.
158  * Also, sets that don't have a Young Witch still have a Bane 
159  * (in case it's needed for Black Market or some other weird card).
160  *
161  * @return {Boolean} Whether to use Shelters.
162  */
163 DominionSetGenerator.prototype.getYoungWitchBane = function() {
164   return this.youngWitchBane;
165 };
166 
167 /**
168  * What is the Bane for Young Witch? (Goko format)
169  * Note that this only changes when the set is regenerated.
170  * Also, sets that don't have a Young Witch still have a Bane 
171  * (in case it's needed for Black Market or some other weird card).
172  *
173  * @return {Boolean} Whether to use Shelters.
174  */
175 DominionSetGenerator.prototype.getYoungWitchBaneGoko = function() {
176   var card = this.youngWitchBane.toLowerCase()
177               .replace(/(?:^|\s|\-)\S/g, function(a) { return a.toUpperCase(); })
178               .replace(/\(.*\)/g, '')
179               .replace(/\-\>.*/g, '')
180               .replace(/[^a-zA-Z]/g, '');
181   return(card.charAt(0).toLowerCase() + card.slice(1));
182 };
183 
184 /**
185  * Sets the effect strength of the "balanced" generator.
186  * The default value is 1.0; set this to 0.0 to get a 
187  * generator that weights everything uniformly.  Set this
188  * to a larger number to really exaggerate the selection
189  * towards balanced sets, and set this to a negative number
190  * to skew away from balanced sets. 
191  *
192  * @param {Number} value The balance factor.
193  */
194 DominionSetGenerator.prototype.setBalanceFactor = function(value) {
195   this.balanceFactor = value;
196 };
197 
198 /**
199  * Gets the effect strength of the "balanced" generator.
200  *
201  * @return {Number} The balance factor.
202  */
203 DominionSetGenerator.prototype.getBalanceFactor = function() {
204   return this.balanceFactor;
205 };
206 
207 /**
208 /**
209  * Sets the size of the set to be generated.
210  *
211  * @private
212  * @param {Number} value The size of the set to be generated.
213  */
214 DominionSetGenerator.prototype.setMaxSetSize_ = function(value) {
215   this.maxSetSize = value;
216 };
217 
218 /**
219  * Gets the size of the set to be generated.
220  *
221  * @return {Number} The size of the set to be generated.
222  */
223 DominionSetGenerator.prototype.getMaxSetSize = function() {
224   return this.maxSetSize;
225 };
226 
227 /**
228  * Gets the set that is currently being generated.
229  * Note that
230  * generateSet() returns the set anyway, so you might never need
231  * to call this unless you need to see the same set again.
232  *
233  * @return {Object} The set of cards, in the form of an Object
234  * where the keys are Strings with the names of cards.
235  */
236 DominionSetGenerator.prototype.getCardset = function() {
237   return (this.cardset);
238 };
239 
240 /**
241  * Gets the set that is currently being generated, in Goko camelCase form.
242  * Note that
243  * generateSet() returns the set anyway, so you might never need
244  * to call this unless you need to see the same set again.
245  *
246  * @return {Object} The set of cards, in the form of an Array
247  * where the contents are Strings with the names of cards.
248  */
249 DominionSetGenerator.prototype.getGokoCardset = function() {
250   var answer = new Array();
251   for (var card in this.cardset) {
252     var gcard = card.toLowerCase()
253               .replace(/(?:^|\s|\-)\S/g, function(a) { return a.toUpperCase(); })
254               .replace(/\(.*\)/g, '')
255               .replace(/\-\>.*/g, '')
256               .replace(/[^a-zA-Z]/g, '');
257 
258     answer.push(gcard.charAt(0).toLowerCase() + gcard.slice(1));
259   }
260   return answer;
261 };
262 
263 /**
264  * Clears the internal log.
265  */
266 DominionSetGenerator.prototype.clearLog = function() {
267   this.logData = new Array();
268 };
269 
270 /**
271  * Gets the internal log.
272  * @return {String} The internal log as one long string,
273  * with internal line-breaks.
274  */
275 DominionSetGenerator.prototype.getLog = function() {
276   return this.logData.join('\n');
277 };
278 
279 /**
280  * Adds a line to the internal log.
281  *
282  * @private
283  * @param {String} text The log line to add.
284  */
285 DominionSetGenerator.prototype.log_ = function(text) {
286   this.logData.push(text);
287 };
288 
289 /**
290  * Dumps the internal log to the Javascript Console (as multiple lines).
291  */
292 DominionSetGenerator.prototype.dumpLogToConsole = function() {
293 
294   // Protection against the console not existing.
295   if (!window.console) {
296     (function() {
297       var names = ['log', 'debug', 'info', 'warn', 'error',
298           'assert', 'dir', 'dirxml', 'group', 'groupEnd', 'time',
299           'timeEnd', 'count', 'trace', 'profile', 'profileEnd'],
300           i, l = names.length;
301 
302       window.console = {};
303 
304       for (i = 0; i < l; i++) {
305         window.console[names[i]] = function() {};
306       }
307     }());
308   }
309 
310   for (var i = 0; i < this.logData.length; ++i) {
311     window.console.log(this.logData[i]);
312   }
313 };
314 
315 /**
316  * Logs a description of weights.
317  *
318  * @private
319  * @param {Number} len Integer showing number of cards (on each end) to log.
320  */
321 DominionSetGenerator.prototype.logWeights_ = function(len) {
322   var threshhold = 0;
323   var sorted = Object.keys(this.sgd.cardNames);
324   var w = this.weight;
325   sorted.sort(function(a, b) { return (w[b] - w[a]); });
326 
327   this.log_(' ... Top ' + len + ' weights:');
328   for (var lcv = 0; lcv < len; ++lcv) {
329     var card = sorted[lcv];
330     this.log_(' ... ... ' + card + ' (' + this.weight[card] +
331              ') [' + this.commentary[card] + ']');
332     threshhold = this.weight[card];
333   }
334   var index = len;
335   var ties = 0;
336   while (this.weight[sorted[index]] == threshhold && index < sorted.length) {
337     ties++;
338     index++;
339   }
340   if (ties > 0) {
341     this.log_(' ... ... and ' + ties + ' other' + ((ties == 1) ? '' : 's'));
342   }
343 
344   this.log_(' ... Bottom ' + len + ' weights:');
345 
346   index = sorted.length - 1;
347   while (this.weight[sorted[index]] == 0 && index >= 0) {
348     index--;
349   }
350   var lcv;
351   for (lcv = index; lcv >= index - len && lcv >= 0; --lcv) {
352     var card = sorted[lcv];
353     this.log_(' ... ... ' + card + ' (' + this.weight[card] +
354               ') [' + this.commentary[card] + ']');
355     threshhold = this.weight[card];
356   }
357   ties = 0;
358   threshhold = this.weight[sorted[lcv]];
359   while (this.weight[sorted[lcv]] == threshhold && lcv >= 0) {
360     ties++;
361     lcv--;
362   }
363   if (ties > 0) {
364     this.log_(' ... ... and ' + ties + ' other' + ((ties == 1) ? '' : 's'));
365   }
366 };
367 
368 /**
369  * Exponentiation convenience function.
370  *
371  * @private
372  * @param {Number} base Base.
373  * @param {Number} exp Exponent.
374  * @return {Number} The power!
375  */
376 DominionSetGenerator.prototype.power_ = function(base, exp) {
377   return Math.exp(exp * Math.log(base));
378 };
379 
380 
381 /**
382  * Finds a random card (with the appropriate weights).
383  * If the weights are weird enough that there's a problem, returns 'ERROR'.
384  *
385  * @private
386  * @return {String} The name of the random card.
387  */
388 DominionSetGenerator.prototype.randCard_ = function() {
389   // returns a randomly-chosen card based on the current set of weights.
390   var totalWeight = 0;
391   for (var card in this.sgd.cardNames) {
392     totalWeight += this.weight[card];
393   }
394   var randValue = totalWeight * Math.random();
395   for (var card in this.sgd.cardNames) {
396     if (randValue < this.weight[card]) {
397       return card;
398     } else {
399       randValue -= this.weight[card];
400     }
401   }
402   return 'ERROR';
403 };
404 
405 /**
406  * For the supplied attribute, add up the values of that attribute for all
407  * cards in the current set, and return that.
408  *
409  * @private
410  * @param {String} attribute The attribute to look at.
411  * @return {Number} The sum of the values of the attribute in the current set.
412  */
413 DominionSetGenerator.prototype.getCardsetValue_ = function(attribute) {
414   var answer = 0;
415   for (var card in this.cardset) {
416     try {
417       if (this.sgd.cardData[card][attribute] != undefined)
418         answer += this.sgd.cardData[card][attribute];
419     } catch (err) {
420       throw this.logData + "\nCan't get attribute " + attribute + " of " + card;
421     }
422   }
423   return answer;
424 };
425 
426 /**
427  * Calculate the correct multiplier for the given card, attribute pair.
428  *
429  * @private
430  * @param {String} card The card to calculate for.
431  * @param {String} attribute The card to calculate for.
432  * @param {Boolean} firstCard Is this the first card to be generated?
433  * @return {Number} The calculated multiplier.
434  */
435 DominionSetGenerator.prototype.calcBalanceMultiplier_ = function(card, attribute, firstCard) {
436   var value = this.sgd.cardData[card][attribute];
437   if (value == undefined || value == 0) return 1.0;
438 
439   // for the first card, ignore any non-user-defined weights.
440   if (firstCard && this.isDefault[attribute]) return 1.0;  
441 
442   var answer = 1.0;
443   var attData = this.getAttributeData(attribute);
444   var cardsetValue = this.getCardsetValue_(attribute);
445   if (cardsetValue < attData.min) {
446     var multiplier = attData.minW;
447     answer *= multiplier;
448   }
449   if (cardsetValue + value > attData.max) {
450     var multiplier = this.power_(attData.maxW, cardsetValue);
451     answer *= multiplier;
452   }
453   return answer;
454 }
455 
456 
457 /**
458  * Calculate the correct multiplier for the given card.
459  * For each card, we only keep the most extreme multipliers using
460  * the following algorithm:
461  * (1) Sort all the multipliers.
462  * (2) Look at the two multipliers at the ends.  If one is
463  *   > 1.0 and one is < 1.0, keep both.
464  * (3) After all that, keep the three most extreme ones.
465  *
466  * @private
467  * @param {String} card The card to calculate for.
468  * @param {Boolean} firstCard Is this the first card to be generated?
469  * @return {Number} The calculated multiplier.
470  */
471 DominionSetGenerator.prototype.calcEffectiveMultiplier_ = function(card, firstCard) {
472   // returns the appropriate multiplier for the card.
473   var attList = new Array();
474   var multipliers = new Object();
475   for (var attribute in this.sgd.cardAttributes) {
476     var multiplier = this.calcBalanceMultiplier_(card, attribute, firstCard);
477     if (multiplier != 1.0) {
478       attList.push(attribute);
479       multipliers[attribute] = multiplier;
480     }
481   }
482   attList.sort(function(a, b) {
483     return (multipliers[b] - multipliers[a]);
484   });
485   
486   var att;
487   var weight = 1.0;
488   while (attList.length > 0 && multipliers[attList[0]] > 1.0
489                       && multipliers[attList[attList.length-1]] < 1.0) {
490     att = attList.shift();
491     weight *= multipliers[att];
492     this.commentary[card] += 'x' + multipliers[att] + 
493                              ' (' + att + '); ';
494     att = attList.pop();
495     weight *= multipliers[att];
496     this.commentary[card] += 'x' + multipliers[att] + 
497                              ' (' + att + '); ';
498   }
499 
500   var extra = 0;
501   while (attList.length > 0 && extra < 3) {
502     if (multipliers[attList[0]] > 1.0) {
503       att = attList.shift();
504     } else {
505       att = attList.pop();
506     }
507     ++extra;
508     weight *= multipliers[att];
509     this.commentary[card] += 'x' + multipliers[att] + 
510                              ' (' + att + '); ';
511   }
512   
513   this.commentary[card] += 'ignoring ' + attList.length + ' attribs :';
514 
515   while (attList.length > 0) {
516     att = attList.shift();
517     this.commentary[card] += 'x' + multipliers[att] + 
518                              ' (' + att + '); ';
519   }
520 
521   if (isNaN(weight)) {
522     this.log_('Error: NaN in ' + card + ' ' + this.commentary[card]);
523   }
524   
525   if (weight > 1e+200) {
526     return 1e+200;
527   } else {
528     return weight;
529   }
530 };
531 
532 /**
533  * Calculate the correct weight for the given card.
534  * Right now it's the same as the balance multiplier,
535  * with chosen cards and unowned cards filtered out.
536  * This will get more complex in a later version.
537  *
538  * @private
539  * @param {String} card The card to calculate for.
540  * @param {Boolean} firstCard Is this the first card to be generated?
541  * @return {Number} The calculated weight.
542  */
543 DominionSetGenerator.prototype.calcWeight_ = function(card, firstCard) {
544   if (card in this.cardset) {
545     return 0;  // already have the card
546   }
547   if (!(card in this.owned)) {
548     return 0;  // don't own the card
549   }
550   return this.calcEffectiveMultiplier_(card, firstCard);
551 };
552 
553 /**
554  * Update the weights for all the cards.
555  *
556  * @private
557  * @param {Boolean} firstCard Is this the first card to be generated?
558  */
559 DominionSetGenerator.prototype.updateWeights_ = function(firstCard) {
560   for (var card in this.sgd.cardNames) {
561     this.commentary[card] = '';
562     this.weight[card] = this.calcWeight_(card, firstCard);
563   }
564 };
565 
566 /// externally visible functions
567 
568 /**
569  * Get the current data for the given attribute.
570  *
571  * @param {String} attr Which attribute to get.
572  * @return {DominionSetGeneratorAttributeData} The data for the given attribute.
573  */
574 DominionSetGenerator.prototype.getAttributeData = function(attr) {
575   if (this.isDefault[attr]) {
576 
577     var min = this.sgd.Min[attr];
578     var max = this.sgd.Max[attr];
579     var minW = this.power_(this.sgd.MinW[attr], this.balanceFactor);
580     var maxW = this.power_(this.sgd.MaxW[attr], this.balanceFactor);
581     var notes = this.sgd.Notes[attr];
582 
583     // all ranges become "dense" (min=0, max=0, minW = 1, maxW > 1)
584     // if balanceFactor < 0;
585    
586     if (this.balanceFactor < 0 && (min != 0 || max != 0)) {
587       min = 0;
588       max = 0;
589       maxW = 1.0/minW;
590       minW = 1;
591     }
592 
593     return (new DominionSetGeneratorAttributeData(min, max, minW, maxW, notes));
594   } else {
595     return (new DominionSetGeneratorAttributeData(
596       this.tMin[attr],
597       this.tMax[attr],
598       this.tMinW[attr],
599       this.tMaxW[attr],
600       this.tNotes[attr]));
601   }
602 };
603 
604 /**
605  * Set the current data for the given attribute.
606  *
607  * @param {String} attr Which attribute to set.
608  * @param {DominionSetGeneratorAttributeData} data The data
609  * for the given attribute.
610  */
611 DominionSetGenerator.prototype.setAttributeData = function(attr, data) {
612   this.tMin[attr] = data.min;
613   this.tMax[attr] = data.max;
614   this.tMinW[attr] = data.minW;
615   this.tMaxW[attr] = data.maxW;
616   this.tNotes[attr] = data.notes;
617   this.isDefault[attr] = false;
618 };
619 
620 /**
621  * Set the given attribute to 'Avoid' -- don't pick these cards
622  * if at all possible.
623  *
624  * @param {String} attr Which attribute to set.
625  */
626 DominionSetGenerator.prototype.attrAvoid = function(attr) {
627   this.tMin[attr] = 10 * this.maxSetSize;
628   this.tMax[attr] = 10 * this.maxSetSize;
629   this.tMinW[attr] = 0.00001;
630   this.tMaxW[attr] = 100000;
631   this.tNotes[attr] = 'User-modified: Avoid';
632   this.isDefault[attr] = false;
633 };
634 
635 /**
636  * Set the given attribute to 'Fewer' -- try to not pick these
637  * cards (by a factor of 10, currently).
638  *
639  * @param {String} attr Which attribute to set.
640  */
641 DominionSetGenerator.prototype.attrFewer = function(attr) {
642   this.tMin[attr] = 10 * this.maxSetSize;
643   this.tMax[attr] = 10 * this.maxSetSize;
644   this.tMinW[attr] = 0.1;
645   this.tMaxW[attr] = 10;
646   this.tNotes[attr] = 'User-modified: Fewer';
647   this.isDefault[attr] = false;
648 };
649 
650 /**
651  * Set the given attribute to 'More' -- don't pick these cards
652  * if at all possible.
653  *
654  * @param {String} attr Which attribute to set.
655  */
656 DominionSetGenerator.prototype.attrMore = function(attr) {
657   this.tMin[attr] = 10 * this.maxSetSize;
658   this.tMax[attr] = 10 * this.maxSetSize;
659   this.tMinW[attr] = 10;
660   this.tMaxW[attr] = 0.1;
661   this.tNotes[attr] = 'User-modified: More';
662   this.isDefault[attr] = false;
663 };
664 
665 /**
666  * Set the given attribute to 'Always' -- try to not pick these
667  * cards (by a factor of 10, currently).
668  *
669  * @param {String} attr Which attribute to set.
670  */
671 DominionSetGenerator.prototype.attrAlways = function(attr) {
672   this.tMin[attr] = 10 * this.maxSetSize;
673   this.tMax[attr] = 10 * this.maxSetSize;
674   this.tMinW[attr] = 100000;
675   this.tMaxW[attr] = 0.00001;
676   this.tNotes[attr] = 'User-modified: Always';
677   this.isDefault[attr] = false;
678 };
679 
680 /**
681  * Set the given attribute to 'Range' -- try to pick so that the
682  * attribute shows up in a certain range.
683  *
684  * @param {String} attr Which attribute to set.
685  * @param {Number} min Try to add cards to make the set exceed this value.
686  * @param {Number} max Try not to add cards that make the set exceed this value.
687  * @param {Number} importance The multiplier for this attribute.
688  */
689 DominionSetGenerator.prototype.attrRange = function(attr, min,
690                                                     max, importance) {
691   this.tMin[attr] = min;
692   this.tMax[attr] = max;
693   this.tMinW[attr] = importance;
694   this.tMaxW[attr] = 1.0 / importance;
695   this.tNotes[attr] = 'User-modified: Range';
696   this.isDefault[attr] = false;
697 };
698 
699 /**
700  * Set the given attribute to 'Dense' -- try to pick so that the
701  * attribute either doesn't show up or shows up en masse.
702  *
703  * @param {String} attr Which attribute to set.
704  * @param {Number} importance The multiplier for this attribute.
705  */
706 DominionSetGenerator.prototype.attrDense = function(attr, importance) {
707   this.tMin[attr] = 0;
708   this.tMax[attr] = 0;
709   this.tMinW[attr] = 1;
710   this.tMaxW[attr] = importance;
711   this.tNotes[attr] = 'User-modified: Dense';
712   this.isDefault[attr] = false;
713 };
714 
715 /**
716  * Set the given attribute to 'Sparse' -- try to pick so that the
717  * attribute either doesn't show up or shows up en masse.
718  *
719  * @param {String} attr Which attribute to set.
720  * @param {Number} importance The multiplier for this attribute.
721  */
722 DominionSetGenerator.prototype.attrSparse = function(attr, importance) {
723   this.tMin[attr] = 0;
724   this.tMax[attr] = 0;
725   this.tMinW[attr] = 1;
726   this.tMaxW[attr] = importance;
727   this.tNotes[attr] = 'User-modified: Sparse';
728   this.isDefault[attr] = false;
729 };
730 
731 /**
732  * Reset the current data for the given attribute to
733  * the original (internal) value.
734  *
735  * @param {String} attr Which attribute to set.
736  */
737 DominionSetGenerator.prototype.resetAttribute = function(attr) {
738   this.isDefault[attr] = true;
739 };
740 
741 /**
742  * Reset the current data for all attributes.
743  */
744 DominionSetGenerator.prototype.resetAllAttributes = function() {
745   for (attr in this.sgd.cardAttributes) {
746     this.resetAttribute(attr);
747   }
748 };
749 
750 /**
751  * Clear the current set.
752  * 
753  */
754 DominionSetGenerator.prototype.clearSet = function() {
755   this.cardset = new Object();
756 };
757 
758 /**
759  * Canonicalize a card string.
760  * Try to find a card in sgd.cardNames that matches the given string.
761  * Right now we just lowercase everything and remove alphanumerics and
762  * see if things match.
763  * 
764  * @private
765  * @param {String} userCard A name to try to match.
766  * @return {String} The matching card, or "" if no match.
767  */
768 DominionSetGenerator.prototype.canonName_ = function(userCard) {
769   for (var realCard in this.sgd.cardNames) {
770     if (userCard.toLowerCase().replace(/[^a-zA-Z0-9_]/g, '') ==
771         realCard.toLowerCase().replace(/[^a-zA-Z0-9_]/g, '')) {
772       return realCard;
773     }
774   }
775   return "";
776 };
777 
778 
779 /**
780  * Tells the generator what cards are 'owned'.
781  * Cards that are
782  * not owned have zero chance of being picked (as opposed to
783  * the 'Try to Exclude' setting, which sets the probability of being
784  * picked to be very small but not zero).
785  * Note that if you own fewer cards than the size of the set, the
786  * generator may give unexpected results!
787  *
788  * @param {Object} data An Object where the keys are the names
789  *   of the cards 'owned'.
790  * @return {Object} An Object where the keys are the cards that weren't
791  *  understodd.
792  */
793 DominionSetGenerator.prototype.setOwned = function(data) {
794   var failed = new Object();
795 
796   this.owned = new Object();
797   for (userCard in data) {
798     var realCard = this.canonName_(userCard);
799     if (realCard != "") {
800       this.owned[realCard] = true;
801     } else {
802       failed[userCard] = true;
803     }
804   }
805 
806   return failed;
807 };
808 
809 /**
810  * Put in some cards into the set.
811  * 
812  * @param {Object} cards An Object where the keys are the cards that have to be
813  *  put into the set.  A card will be considered a "match" if after being
814  *  changed to all lower-case with non-alphanumeric characters removed,
815  *  it is the same string in sgd.cardNames.  For example, "Worker's Village"
816  *  matches "workersvillage".
817  * @return {Object} An Object where the keys are the cards that weren't
818  *  understodd.
819  */
820 DominionSetGenerator.prototype.putIntoSet = function(cards) {
821   var failed = new Object();
822 
823   for (var userCard in cards) {
824     var realCard = this.canonName_(userCard);
825     if (realCard != "") {
826       this.cardset[realCard] = 1;
827     } else {
828       failed[userCard] = true;
829     }
830   }
831 
832   return failed;
833 };
834 
835 /**
836  * Get a random card from the chosen set (uniformly).
837  *
838  * @private
839  */
840 DominionSetGenerator.prototype.randCardInSet_ = function() {
841   var setSize = Object.keys(this.cardset).length;
842   var randValue = Math.floor(setSize * Math.random());
843   for (var card in this.cardset) {
844     if (randValue == 0) {
845       return card;
846     }
847     randValue--;
848   }
849   // shouldn't get here
850   throw("Error in randCardInSet: " + setSize + " " + randValue);
851 };
852 
853 
854 /**
855  * Fill out a set of cards, assuming that the internal set is partially done.
856  *
857  * @param {Number} maxSetSize Number of cards to put in the set.
858  * @return {Object} An Object where all the keys are the cards that are chosen.
859  *   (The values will be 1, but we might change this to something more
860  *    fancy in the future).
861  */
862 DominionSetGenerator.prototype.completeSet = function(maxSetSize) {
863   // generates a set of the given size and returns it.
864 
865   this.setMaxSetSize_(maxSetSize);
866   this.weight = new Object();
867 
868   for (var card in this.sgd.cardNames) {
869     this.weight[card] = 1.0;
870   }
871 
872   while (Object.keys(this.cardset).length < this.getMaxSetSize()) {
873     this.updateWeights_(Object.keys(this.cardset).length == 0);
874     var card = this.randCard_();
875     if (card == 'ERROR') {
876       this.log_('Aborting ... ERROR from randCard_');
877       break;
878     }
879     this.cardset[card] = 1;
880     if (Object.keys(this.cardset).length > 0) {
881       this.logWeights_(10);
882     }
883     this.log_('Adding ' + card + ' (' + this.weight[card] + ') to set.');
884   }
885 
886   this.useShelters = (this.sgd.cardData[this.randCardInSet_()]['DarkAges'] == 1);
887   this.platinumColony = (this.sgd.cardData[this.randCardInSet_()]['Prosperity'] == 1);
888 
889   // get the Bane
890   this.updateWeights_(false);
891   for (var card in this.sgd.cardNames) {
892     if (this.sgd.cardData[card]['Cost2'] != 1 && this.sgd.cardData[card]['Cost3'] != 1) {
893       this.weight[card] = 0;
894     }
895   }
896   this.youngWitchBane = this.randCard_();
897 
898   // get Donline rules
899   for (var card in this.sgd.cardNames) {
900     if (card in this.owned) {
901       this.weight[card] = 1.0;
902     } else {
903       this.weight[card] = 0.0;
904     }
905   }
906   this.useSheltersDonline = (this.sgd.cardData[this.randCard_()]['DarkAges'] == 1);
907   this.platinumColonyDonline = (this.sgd.cardData[this.randCard_()]['Prosperity'] == 1);
908 
909   return this.cardset;
910 };
911 
912 /**
913  * Generate a new set of cards, using the most current values.
914  *
915  * @param {Number} maxSetSize Number of cards to put in the set.
916  * @return {Object} An Object where all the keys are the cards that are chosen.
917  *   (The values will be 1, but we might change this to something more
918  *    fancy in the future).
919  */
920 DominionSetGenerator.prototype.generateSet = function(maxSetSize) {
921   this.clearSet();
922   return this.completeSet(maxSetSize);
923 };
924