Skip to content Skip to sidebar Skip to footer

Javascript Recursive Dice Combination And Store The Result In A Matrix

what I'm trying to achieve is one of the classical results of the combination of the launch of N-dices,but saving the results in a matrix with M-N fields (where N is the number of

Solution 1:

Fixed-length combinations

Recursion is a functional heritage and so using it with functional style will yield the best results. Recursion is all about breaking a large problem down into smaller sub problems until a base case is reached.

Below, we use the proposed Array.prototype.flatMap but include a polyfill for environments that don't support it yet. When n = 0 our base case has been reached and we return the empty result. The inductive case is n > 0 where choices will be added to the result of the smaller problem combination (choices, n - 1) – We say this problem is smaller here because n - 1 is closer to the base case of n = 0

Array.prototype.flatMap = function (f)
{
  returnthis.reduce ((acc, x) => acc.concat (f (x)), [])
}

constcombinations = (choices, n = 1) =>
  n === 0
    ? [[]]
    : combinations (choices, n - 1) .flatMap (comb =>
        choices .map (c => [ c, ...comb ]))
        
const faces =
  [ 1, 2, 3 ]
  
// roll 2 diceconsole.log (combinations (faces, 2))
// [ [ 1, 1 ], [ 2, 1 ], [ 3, 1 ], [ 1, 2 ], ..., [ 2, 3 ], [ 3, 3 ] ]// roll 3 diceconsole.log (combinations (faces, 3))
// [ [ 1, 1, 1 ], [ 2, 1, 1 ], [ 3, 1, 1 ], [ 1, 2, 1 ], ..., [ 2, 3, 3 ], [ 3, 3, 3 ] ]

Using combinations with your program

Writing rollDice would look something like this

const rollDice = (dice, numberOfDice) =>
  combinations (dice.diceFaces, numberOfDice)

console.log (rollDice (attackDice, 2))
// [ [ 'critical', 'critical' ]
// , [ 'smash', 'critical' ]
// , [ 'smash', 'critical' ]
// , [ 'fury', 'critical' ]
// , [ 'support1', 'critical' ]
// , [ 'support2', 'critical' ]
// , [ 'critical', 'smash' ]
// , [ 'smash', 'smash' ]
// , ...
// , [ 'critical', 'support2' ]
// , [ 'smash', 'support2' ]
// , [ 'smash', 'support2' ]
// , [ 'fury', 'support2' ]
// , [ 'support1', 'support2' ]
// , [ 'support2', 'support2' ]
// ]

Without dependencies

If you're curious how flatMap and map are working, we can implement them on our own. Pure recursion, through and through.

constNone =
  Symbol ()

constmap = (f, [ x = None, ...xs ]) =>
  x === None
    ? []
    : [ f (x), ...map (f, xs) ]
  
constflatMap = (f, [ x = None, ...xs ]) =>
  x === None
    ? []
    : [ ...f (x), ...flatMap (f, xs) ]

constcombinations = (choices = [], n = 1) =>
  n === 0
    ? [[]]
    : flatMap ( comb => map (c => [ c, ...comb ], choices)
              , combinations (choices, n - 1)
              )
          
const faces =
  [ 1, 2, 3 ]
  
// roll 2 diceconsole.log (combinations (faces, 2))
// [ [ 1, 1 ], [ 2, 1 ], [ 3, 1 ], [ 1, 2 ], ..., [ 2, 3 ], [ 3, 3 ] ]// roll 3 diceconsole.log (combinations (faces, 3))
// [ [ 1, 1, 1 ], [ 2, 1, 1 ], [ 3, 1, 1 ], [ 1, 2, 1 ], ..., [ 2, 3, 3 ], [ 3, 3, 3 ] ]

Nerfed

Ok, so combinations allows us to determine the possible combinations of a repeated, fixed set of choices. What if we had 2 unique dice and wanted to get the all the possible rolls?

const results = 
  rollDice (attackDice, defenceDice) ???

We could call rollDice (attackDice, 1) and then rollDice (defenceDice, 1) and then somehow combine the answers. But there's a better way; a way that allows for any number of unique dice, even with a varying number of sides on each die. Below, I show you the two versions of combinations we wrote alongside the necessary changes to access untapped potential

// version 1: using JS nativesconstcombinations = (choices, n = 1) =>
constcombinations = (choices = None, ...rest) =>
  n === 0
  choices === None
    ? [[]]
    : combinations (choices, n - 1) .flatMap (comb =>
    : combinations (...rest) .flatMap (comb =>
        choices .map (c => [ c, ...comb ]))

// version 2: without dependenciesconstcombinations = (choices = [], n = 1) =>
constcombinations = (choices = None, ...rest) =>
  n === 0
  choices === None
    ? [[]]
    : flatMap ( comb => map (c => [ c, ...comb ], choices)
              , combinations (choices, n - 1)
              , combinations (...rest)
              )

With this new version of combinations, we can roll any number of dice of any size – even the physically impossible 3-sided die is possible in this program ^_^

// version 3: variadic dice
const combinations = (choices = None, ...rest) =>
  choices === None
    ? [[]]
    : flatMap ( comb => map (c => [ c, ...comb ], choices)
              , combinations (...rest)
              )

const d1 =
  [ 'J', 'Q', 'K' ]

const d2 =
  [ '♤', '♡', '♧', '♢' ]

console.log (combinations (d1, d2))
// [ [ 'J', '♤' ], [ 'Q', '♤' ], [ 'K', '♤' ]
// , [ 'J', '♡' ], [ 'Q', '♡' ], [ 'K', '♡' ]
// , [ 'J', '♧' ], [ 'Q', '♧' ], [ 'K', '♧' ]
// , [ 'J', '♢' ], [ 'Q', '♢' ], [ 'K', '♢' ]
// ]

And of course you can roll a collection of the same dice

console.log (combinations (d1, d1, d1))
// [ [ 'J', 'J', 'J' ]
// , [ 'Q', 'J', 'J' ]
// , [ 'K', 'J', 'J' ]
// , [ 'J', 'Q', 'J' ]
// , [ 'Q', 'Q', 'J' ]
// , [ 'K', 'Q', 'J' ]
// , [ 'J', 'K', 'J' ]
// , ...
// , [ 'K', 'Q', 'K' ]
// , [ 'J', 'K', 'K' ]
// , [ 'Q', 'K', 'K' ]
// , [ 'K', 'K', 'K' ]
// ]

Tapping into this potential with your program, you can write rollDice as

const rollDice = (...dice) =>
  combinations (...dice.map (d => d.diceFaces))

console.log (rollDice (attackDice, defenceDice))
// [ [ 'critical', 'critical' ]
// , [ 'smash', 'critical' ]
// , [ 'smash', 'critical' ]
// , [ 'fury', 'critical' ]
// , [ 'support1', 'critical' ]
// , [ 'support2', 'critical' ]
// , [ 'critical', 'block' ]
// , [ 'smash', 'block' ]
// , ...
// , [ 'support2', 'support1' ]
// , [ 'critical', 'support2' ]
// , [ 'smash', 'support2' ]
// , [ 'smash', 'support2' ]
// , [ 'fury', 'support2' ]
// , [ 'support1', 'support2' ]
// , [ 'support2', 'support2' ]
// ]

Or with all sorts of dice

const rollDice = (...dice) =>
  combinations (...dice.map (d => d.diceFaces))

console.log (rollDice (defenceDice, attackDice, attackDice, attackDice))
// [ [ 'critical', 'critical', 'critical', 'critical' ]
// , [ 'block', 'critical', 'critical', 'critical' ]
// , [ 'block', 'critical', 'critical', 'critical' ]
// , [ 'dodge', 'critical', 'critical', 'critical' ]
// , [ 'support1', 'critical', 'critical', 'critical' ]
// , [ 'support2', 'critical', 'critical', 'critical' ]
// , [ 'critical', 'smash', 'critical', 'critical' ]
// , [ 'block', 'smash', 'critical', 'critical' ]
// , [ 'block', 'smash', 'critical', 'critical' ]
// , [ 'dodge', 'smash', 'critical', 'critical' ]
// , [ 'support1', 'smash', 'critical', 'critical' ]
// , ...
// ]

Going high-level

It's cool to see how we can accomplish so much with just a few pure functions in JavaScript. However, the above implementation is slow af and severely limited in terms of how many combinations it could produce.

Below, we try to determine the combinations for seven 6-sided dice. We expect 6^7 to result in 279936 combinations

const dice =
  [ attackDice, attackDice, attackDice, attackDice, attackDice, attackDice, attackDice ]

rollDice (...dice)
// => ...

Depending on the implementation of combinations you picked above, if it doesn't cause your environment to hang indefinitely, it will result in a stack overflow error

To increase performance here, we reach for a high-level feature provided by Javascript: generators. Below, we rewrite combinations but this time using some imperative style required to interact with generators.

constNone =
  Symbol ()

const combinations = function* (...all)
{
  const loop = function* (comb, [ choices = None, ...rest ])
  {
    if (choices === None)
      returnelseif (rest.length === 0)
      for (const c of choices)
        yield [ ...comb, c ]
    elsefor (const c of choices)
        yield* loop ([ ...comb, c], rest)
  }
  yield* loop ([], all)
}

const d1 =
  [ 'J', 'Q', 'K', 'A' ]
  
const d2 =
  [ '♤', '♡', '♧', '♢' ]

const result =
  Array.from (combinations (d1, d2))

console.log (result)
// [ [ 'J', '♤' ], [ 'J', '♡' ], [ 'J', '♧' ], [ 'J', '♢' ]// , [ 'Q', '♤' ], [ 'Q', '♡' ], [ 'Q', '♧' ], [ 'Q', '♢' ]// , [ 'K', '♤' ], [ 'K', '♡' ], [ 'K', '♧' ], [ 'K', '♢' ]// , [ 'A', '♤' ], [ 'A', '♡' ], [ 'A', '♧' ], [ 'A', '♢' ]// ]

Above, we use Array.from to eagerly collect all the combinations into a single result. This is often not necessary when working with generators. Instead, we can use the values as they're being generated

Below, we use for...of to interact with each combination directly as it comes out of the generator. In this example, we show any combination that includes a J or a

const d1 =
  [ 'J', 'Q', 'K', 'A' ]

const d2 =
  [ '♤', '♡', '♧', '♢' ]

for (const [ rank, suit ] of combinations (d1, d2))
{
  if (rank === 'J' || suit === '♡' )
    console.log (rank, suit)
}
// J ♤ <-- all Jacks// J ♡// J ♧// J ♢// Q ♡ <-- or non-Jacks with Hearts// K ♡// A ♡

But of course there's more potential here. We can write whatever we want in the for block. Below, we add an additional condition to skip Queens Q using continue

const d1 =
  [ 'J', 'Q', 'K', 'A' ]

const d2 =
  [ '♤', '♡', '♧', '♢' ]

for (const [ rank, suit ] of combinations (d1, d2))
{
  if (rank === 'Q')
    continueif (rank === 'J' || suit === '♡' )
    console.log (rank, suit)
}
// J ♤// J ♡// J ♧// J ♢// K ♡ <--- Queens dropped from the output// A ♡

And perhaps the most powerful thing here is we can stop generating combinations with break. Below, if a King K is encountered, we halt the generator immediately

const d1 =
  [ 'J', 'Q', 'K', 'A' ]

const d2 =
  [ '♤', '♡', '♧', '♢' ]

for (const [ rank, suit ] of combinations (d1, d2))
{
  if (rank === 'K')
    breakif (rank === 'J' || suit === '♡' )
    console.log (rank, suit)
}
// J ♤// J ♡// J ♧// J ♢// Q ♡ <-- no Kings or Aces; generator stopped at K

You can get pretty creative with the conditions. How about all the combinations that begin or end in a Heart

for (const [ a, b, c, d, e ] of combinations (d2, d2, d2, d2, d2))
{
  if (a === '♡' && e === '♡')
    console.log (a, b, c, d, e)
}

// ♡ ♤ ♤ ♤ ♡// ♡ ♤ ♤ ♡ ♡// ♡ ♤ ♤ ♧ ♡// ...// ♡ ♢ ♢ ♡ ♡// ♡ ♢ ♢ ♧ ♡// ♡ ♢ ♢ ♢ ♡

And to show you generators work for large data sets

const d1 =
  [ 1, 2, 3, 4, 5, 6 ]

Array.from (combinations (d1, d1, d1, d1, d1, d1, d1)) .length
// 6^7 = 279936Array.from (combinations (d1, d1, d1, d1, d1, d1, d1, d1)) .length
// 6^8 = 1679616

We can even write higher-order functions to work with generators such as our own filter function. Below, we find all combinations of three 20-sided dice that form a Pythagorean triple - 3 whole numbers that make up the side lengths of a valid right triangle

const filter = function* (f, iterable)
{
  for (const x of iterable)
    if (f (x))
      yield x
}

const d20 =
  [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]

const combs =
  combinations (d20, d20, d20)

const pythagoreanTriple = ([ a, b, c ]) =>
  (a * a) + (b * b) === (c * c)

for (const c of filter (pythagoreanTriple, combs))
  console.log (c)

// [ 3, 4, 5 ]// [ 4, 3, 5 ]// [ 5, 12, 13 ]// [ 6, 8, 10 ]// [ 8, 6, 10 ]// [ 8, 15, 17 ]// [ 9, 12, 15 ]// [ 12, 5, 13 ]// [ 12, 9, 15 ]// [ 12, 16, 20 ]// [ 15, 8, 17 ]// [ 16, 12, 20 ]

Or use Array.from with a mapping function to simultaneously transform each combination into a new result and collect all results in an array

const allResults =
  Array.from ( filter (pythagoreanTriple, combs)
             , ([ a, b, c ], index) => ({ result: index + 1, solution: `${a}² + ${b}² = ${c}²`})
             )

console.log (allResults)
// [ { result: 1, solution: '3² + 4² = 5²' }// , { result: 2, solution: '4² + 3² = 5²' }// , { result: 3, solution: '5² + 12² = 13²' }// , ...// , { result: 10, solution: '12² + 16² = 20²' }// , { result: 11, solution: '15² + 8² = 17²' }// , { result: 12, solution: '16² + 12² = 20²' }// ]

What the func?

Functional programming is deep. Dive in!

constNone =
  Symbol ()

// Array ApplicativeArray.prototype.ap = function (args)
  {
    constloop = (acc, [ x = None, ...xs ]) =>
      x === None
        ? this.map (f => f (acc))
        : x.chain (a => loop ([ ...acc, a ], xs))
    return loop ([], args)
  }
 
// Array Monad (this is the same as flatMap above)Array.prototype.chain = functionchain (f)
  {
    returnthis.reduce ((acc, x) => [ ...acc, ...f (x) ], [])
  }

// Identity functionconstidentity = x =>
  x

// math is programming is math is ...constcombinations = (...arrs) =>
  [ identity ] .ap (arrs)

console.log (combinations ([ 0, 1 ], [ 'A', 'B' ], [ '♡', '♢' ]))
// [ [ 0, 'A', '♡' ]// , [ 0, 'A', '♢' ]// , [ 0, 'B', '♡' ]// , [ 0, 'B', '♢' ]// , [ 1, 'A', '♡' ]// , [ 1, 'A', '♢' ]// , [ 1, 'B', '♡' ]// , [ 1, 'B', '♢' ]// ]

Post a Comment for "Javascript Recursive Dice Combination And Store The Result In A Matrix"