<file_sep>let debug = require('debug')('generateSymbols');

let generateSymbols = (number) => {
  if (typeof number != 'number') {
    throw new Error("You must specify the number of symbols to generate.");
  }

  let symbols = new Set();

  for (let i = 0; i < number; i++) {
    symbols.add("S" + i);
  }

  return symbols;
};

module.exports = generateSymbols;
<file_sep>.PHONY: test

install:
	npm install

test:
	@NODE_ENV=test ./node_modules/mocha/bin/mocha --compilers js:babel/register
<file_sep><table>
        <tr>
            <td><img width="120" src="https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/rocket.svg" alt="onboarding" /></td>
            <td><strong>Archived Repository</strong><br />
            The code of this repository was written during a <a href="https://marmelab.com/blog/2018/09/05/agile-integration.html">Marmelab agile integration</a>. It illustrates the efforts of a new hiree, who had to implement a board game in several languages and platforms as part of his initial learning. Some of these efforts end up in failure, but failure is part of our learning process, so the code remains publicly visible.<br />
        <strong>This code is not intended to be used in production, and is not maintained.</strong>
        </td>
        </tr>
</table>

# Dobble

Dobble is an experiment to implementing mathematics behind the
[board game](http://www.asmodee.com/ressources/jeux_versions/dobble.php) of the same name.

## Installation

```make install```

## Tests

```make test```

*You'll notice that tests for 5, 7 and 9 symbols per card do not pass.
Feel free to contribute.*

In order to see the generated cards in the console, run:
```
DEBUG=deckGeneratorTest make test
```

## Resources (french)
- [Les mathématiques du Dobble](http://www.bibmath.net/forums/viewtopic.php?id=5134)
- [Générer un jeu de dobble](http://www.quirysse.com/informatique/generer-un-jeu-de-dobble/)
- [Dobble et la géométrie finie](http://images.math.cnrs.fr/spip.php?page=forum&id_article=927&id_forum=4233)
- [Dobble et modéle mathématique](http://www.trictrac.net/forum/viewtopic.php?p=1197354)
- [Dobble et théorie mathématique](https://www.scribd.com/fullscreen/32602239?access_key=key-290ldts3eb15fhdyt1mf)


## License

Dobble is open-source, sponsored by [marmelab](marmelab.com). You can use it freely under the terms of the [MIT License](LICENSE).
<file_sep>let assert = require("assert");
let Card = require("../lib/Card");
let Pair = require("../lib/Pair");
let Deck = require("../lib/Deck");
let deckGenerator = require("../lib/deckGenerator");
let debug = require('debug')('deckGeneratorTest');


describe('deckGenerator()', () => {
  it(`should return a function when called with no arguments`, () => {
    let generateDeck = deckGenerator();
    assert.equal('function', typeof generateDeck);
  });

  it(`should throw an error when called with anything other than a function`, () => {
    assert.throws(() => deckGenerator(42));
    assert.throws(() => deckGenerator("42"));
    assert.throws(() => deckGenerator([42]));
  });

  describe('deckGenerator().generateSymbol', () => {
    it(`should return a function when called without arguments`, () => {
      let generateDeck = deckGenerator();
      assert.equal('function', typeof generateDeck.generateSymbol);
    });
  });

  describe('deckGenerator()()', () => {
    let tests = [{
      dimensions: 2,
      expectedCards: 3,
      expectedSymbols: 2
    }, {
      dimensions: 3,
      expectedCards: 7,
      expectedSymbols: 3
    }, {
      dimensions: 4,
      expectedCards: 13,
      expectedSymbols: 4
    }, {
      dimensions: 5,
      expectedCards: 21,
      expectedSymbols: 5
    }, {
      dimensions: 6,
      expectedCards: 31,
      expectedSymbols: 6
    }, {
      dimensions: 7,
      expectedCards: 43,
      expectedSymbols: 7
    }, {
      dimensions: 8,
      expectedCards: 57,
      expectedSymbols: 8
    }, {
      dimensions: 9,
      expectedCards: 73,
      expectedSymbols: 9
    }];

    tests.forEach(test => {
      it(`should return a deck with cards containing ${test.expectedSymbols} symbols when called with ${test.dimensions}`, () => {
        let generateDeck = deckGenerator();
        let deck = generateDeck(test.dimensions);

        debug(`${deck}`);

        let cards = deck.cards;

        let invalidCards = cards.filter(c => {
          return c.symbols.length != test.expectedSymbols;
        });

        assert.equal(0, invalidCards.length, `Found ${invalidCards.length} invalid cards\n${invalidCards.join('\n')}`);
      });
    });

    tests.forEach(test => {
      it(`should return a deck with ${test.expectedCards} cards when called with ${test.dimensions}`, () => {
        let generateDeck = deckGenerator();
        let deck = generateDeck(test.dimensions);

        debug(`${deck}`);

        let cards = deck.cards;

        assert.equal(test.expectedCards, cards.length, `Expected ${test.expectedCards} cards, found ${cards.length}`);
      });
    });

    tests.forEach(test => {
      it(`should return a deck with valid cards and pairs when called with ${test.dimensions}`, () => {
        let generateDeck = deckGenerator();
        let deck = generateDeck(test.dimensions);

        debug(`${deck}`);

        assert.ok(deck.isValid(), "Deck is invalid");
      });
    });
  });
});
<file_sep>let assert = require("assert");
let Card = require("../lib/Card");
let Deck = require("../lib/Deck");

describe('Deck', () => {
  describe('constructor()', () => {
    it('should throw an error when called with no values', () => {
      assert.throws(() => new Deck());
    });

    it('does not accept anything that is not an array', () => {
      assert.throws(() => new Deck(42));
    });

    it('does not accept an array which contain something else than cards', () => {
      assert.throws(() => new Deck([
        41, 
        new Card([41, 42])
      ]));
    });

    it('accepts an array of cards', () => {
      let deck = new Deck([ 
        new Card(["A", "B"]), 
        new Card(["A", "A"])
      ]);

      assert.equal(2, deck.cards.length);
    });
  });

  describe('isValid()', () => {

    it('should return false when deck is empty', () => {
      let deck = new Deck([]);
      assert.ok(!deck.isValid());
    });

    it('should return false when any card is invalid', () => {
      let card1 = new Card(["A", "B", "C", "D", "E", "F", "G", "H", "I"]);
      let card2 = new Card(["A", "A", "K", "L", "M", "N", "O", "P", "Q"]);
      let deck = new Deck([card1, card2]);

      assert.ok(!deck.isValid());
    });

    it('should return false when any possible pair is invalid', () => {
      let card1 = new Card(["A", "B", "C", "D", "E", "F", "G", "H", "I"]);
      let card2 = new Card(["A", "B", "L", "M", "N", "O", "P", "Q", "R"]);
      let deck = new Deck([card1, card2]);

      assert.ok(!deck.isValid());
    });

    it('should return true when cards and pairs are valid', () => {
      let card1 = new Card(["A", "B"  , "C", "D", "E", "F", "G", "H", "I"]);
      let card2 = new Card(["A", "K", "L", "M", "N", "O", "P", "Q", "R"]);
      let deck = new Deck([card1, card2]);

      assert.ok(deck.isValid());
    });

  });
});
<file_sep>let assert = require("assert");
let generateSymbol = require("../lib/generateSymbol");

describe('generateSymbol', () => {
  it('should throw an error when called anything but a number', () => {
    assert.throws(() => generateSymbol());
    assert.throws(() => generateSymbol('g'));
    assert.throws(() => generateSymbol(null));
    assert.throws(() => generateSymbol([]));
  });

  it('should returns a Set of 10 symbols when called with 10', () => {
    let symbols = generateSymbol(10);
    assert.ok(symbols instanceof Set);
  });

  it('should returns a Set of 20 symbols when called with 20', () => {
    let symbols = generateSymbol(20);
    assert.ok(symbols instanceof Set);
  });
});
<file_sep>let debug = require('debug')('deckGenerator');
let Card = require("./Card");
let Deck = require("./Deck");
let generateSymbol = require("./generateSymbol");

let _computeMinfactor = (dimensions) => {
  let minFactor = dimensions;
  let poweredDimensions = 1 + Math.floor(Math.pow(dimensions, 0.5));

  debug(`poweredDimensions = ${poweredDimensions}`)
  for (let i = 2; i < poweredDimensions; ++i) {
    if (dimensions % i == 0) {
      minFactor = i;
      break;
    }
  }

  return minFactor;
}

let deckGenerator = (generateSymbolFunc = generateSymbol) => {
  if (typeof generateSymbolFunc != 'function') {
    throw new Error('generateSymbolFunc must be a function.');
  }

  let generator = (dimensions = 8) => {
    let dimensionsForComputations = dimensions - 1;
    debug(`dimensions: ${dimensions}`);
    if (typeof dimensions != 'number') {
      throw new Error('dimensions must be a number.');
    }

    let numberOfDifferentCards = (dimensions * dimensions) - dimensions + 1;
    let numberOfDifferentSymbols = (((dimensions - 1) * (numberOfDifferentCards - 1)) / dimensions) + dimensions;

    let symbols = Array.from(generateSymbolFunc(numberOfDifferentSymbols));
    let cards = [];
    let minFactor = _computeMinfactor(dimensionsForComputations);

    debug(`minFactor: ${minFactor}`);
    debug(`numberOfDifferentCards: ${numberOfDifferentCards}`);
    debug(`symbols (${numberOfDifferentSymbols}): ${Array.from(symbols)}`);

    for (let i = 0; i < dimensionsForComputations; ++i) {
      let cardSymbols = [];

      for (let j = 0; j < dimensionsForComputations; ++j) {
        cardSymbols.push(symbols[i * dimensionsForComputations + j]);
      }
      cardSymbols.push(symbols[dimensionsForComputations * dimensionsForComputations]);
      let card = new Card(cardSymbols);
      debug(`card: ${card}`);
      cards.push(card);
    }

    for (let i = 0; i < dimensionsForComputations; ++i) {
      for (let j = 0; j < dimensionsForComputations; ++j) {
        let cardSymbols = [];
        for (let k = 0; k < dimensionsForComputations; ++k) {
          cardSymbols.push(symbols[k * dimensionsForComputations + (j + i * k) % dimensionsForComputations]);
        }

        cardSymbols.push(symbols[dimensionsForComputations * dimensionsForComputations + 1 + i]);
        let card = new Card(cardSymbols);
        debug(`card: ${card}`);
        cards.push(card);
      }
    }

    let cardSymbols = [];

    for (let i = 0; i < (minFactor + 1); ++i) {
      cardSymbols.push(symbols[dimensionsForComputations * dimensionsForComputations + i]);
    }

    if (cardSymbols.length < dimensions) {
      for (let i = cardSymbols.length; i < dimensions; ++i) {
        cardSymbols.push(symbols[dimensionsForComputations * dimensionsForComputations + i]);
      }
    }

    let card = new Card(cardSymbols);
    debug(`card: ${card}`);
    cards.push(card);

    return new Deck(cards);
  };

  generator.generateSymbol = (newGenerateSymbolFunc) => {
    // getter if no arguments
    if (arguments.length == 0) return generateSymbolFunc;

    generateSymbolFunc = newGenerateSymbolFunc
    return generator;
  }

  return generator;
};

module.exports = deckGenerator;
<file_sep>let debug = require('debug')('Pair');
let Card = require("../lib/Card");

class Pair {
  constructor(card1, card2) {
    if(!(card1 instanceof Card) || !(card2 instanceof Card)){
      throw new Error("Pair must be instantiated with two instances of Card");
    }

    this._card1 = card1;
    this._card2 = card2;
  }

  toString() {
    return `\tCard 1 = ${this._card1} \n\tCard 2 = ${this._card2}`;
  }

  isValid() {
    if(this._card1 == null || !(this._card1 instanceof Card)){
      return false;
    }

    if(this._card2 == null || !(this._card2 instanceof Card)){
      return false;
    }

    if(!this._card1.isValid() || !this._card2.isValid()){
      return false;
    }

    let total = this._card1.symbols.reduce((count, symbolFromCard1) => {
      let found = this._card2.symbols.reduce((count, symbolFromCard2) => {
        let result = symbolFromCard1 === symbolFromCard2;
        debug(`${symbolFromCard1} === ${symbolFromCard2} => ${result}`);

        return (result) ? ++count : count;
      }, 0);

      return count += found;
    }, 0);

    return total === 1;
  }
}

module.exports = Pair;
