18
\$\begingroup\$

For my CS class, we were tasked with writing a program that calculates all the possible results from rolling two n-sided dice. For example, the input of 4 would print [2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7, 8]. Or, if the input was 3, it would print [2, 3, 3, 4, 4, 4, 5, 5, 6].


I was given enough test cases that I noticed the pattern that the number range was [2,n] and the test cases had a pattern where the pattern was always could be represented by printing the number in [2,2n+1] a number in the set [1,n] U (n,1] a corresponding number of times. For example, for 4, the set would be [1,2,3,4,3,2,1] and the output would be [(one 2), (two 3s), (three 4), (four 5s), (three 6s), (two 7s), and (one 8). This explanation may be horrible, but see the code and you will understand.


The code:

def allRolls(sides):
    nums = list(range(1,sides+1)) + reverseIt(list(range(1,sides)))
    index = sides-1
    #the return array
    retVal = []
    #The index to look for in the list of nuts
    cIndex = 0
    for i in range(2,sides*2+1):
        for b in range(nums[cIndex]):
            retVal.append(i)
        cIndex+=1
    retVal.sort()
    return retVal

#Reverses the list
def reverseIt(liste):
    newb =[]
    index = len(liste)-1
    while index>=0:
        newb.append(liste[index])
        index= index-1
    return newb

The issue: everyone in my class got it in many fewer lines of code. What did I do wrong?


Code test cases:

Input: 2
Output:[2, 3, 3, 4]

Input: 3
Output:[2, 3, 3, 4, 4, 4, 5, 5, 6]

Input: 4
Output: [2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7, 8]

\$\endgroup\$
  • \$\begingroup\$ Was it a requirement to output the numbers in ascending order? \$\endgroup\$ – Carsten S Dec 21 '15 at 17:03
  • \$\begingroup\$ Yes it was @CarstenS \$\endgroup\$ – intboolstring Dec 21 '15 at 17:04
15
\$\begingroup\$

As @BenC mostly put a code dump, I'll explain ways that your code could have improved.


reverseIt

Programming this is just wrong in Python. This is as there are two ways to do this.

>>> list(reversed([1, 2, 3]))
[3, 2, 1]
>>> [1, 2, 3][::-1]
[3, 2, 1]

As an alternate way to this you could just calculate the range backwards off the bat.

>>> list(range(10, 0, -1))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

In short you could halve your code just by removing this.


If you wanted to keep it, then you would want to use the reversed range. And do pretty much what you are now. To get:

def reverseIt(indexable):
    lst = []
    for index in range(len(indexable) - 1, -1, -1):
        lst.append(indexable[index])
    return lst

You could then use a list comprehension, if amount of lines dictates how good your code is.

def reverseIt(indexable):
    return [indexable[index] for index in range(len(indexable) - 1, -1, -1))]

allRolls

  • nums is pointless if you use a different algorithm. (Which is also more intuitive.)
  • index is never used.
  • retVal can be changed to a comprehension.
  • Manually counting cIndex is a frowned upon in Python.

In short, use a different algorithm. The pseudo-code for this could be:

fn allRolls(sides):
    list = List()
    for dice1-face in dice-faces:
        for dice2-face in dice-faces:
            add dice1-face + dice2-face to list
    return sorted list

Or in Python:

fn allRoles(sides):
    ret = []
    for a in range(1, sides + 1):
        for b in range(1, sides + 1):
            ret.append(a + b)
    ret.sort()
    return ret

If you take it the next step then you would get @BenC's answer.


Misc

In short, you just used the wrong algorithm.

And in Python you should:

\$\endgroup\$
12
\$\begingroup\$

For your specific case of two rolls of n-sided dice:

def sumsFromTwoRolls(sides):
    return sorted(a + b for a in range(1, sides + 1) for b in range(1, sides + 1))

As sides or number of rolls gets big, you would want a more generic solution, and also probably an algorithm that sorts as you go, instead of looping through again afterwards with sort().

Another little trick is that (depending on how much you're supposed to be implementing yourself), reverseIt(arr) is just arr[::-1] (slicing with a negative step starts from the end).

\$\endgroup\$
11
\$\begingroup\$

I've got a suggestion to an alternative way of generating your list based on your own thoughts on how the sequence grows. But first a few general remarks related to the style of your code.

  • Naming of functions and variables in Python are usually snake_case – This indicates that it should be all_rolls, reverse_list, and so on
  • Avoid abbreviations for variable names – Use better descriptive names for all variables. Stuff like cIndex, retVal, nums and newb doesn't describe much on the use and why you have this variables. Better names could be secondary_index, result, dice_numbers and reversed_list...
  • Add more spaces both horizontally and vertically – Be a little more generous with your spaceing. I.e. Use range(2, sides * 2 + 1), or cIndex += 1, or while index >= 0. In addition you could add vertical space (or blank lines) within a function before new loops or blocks, and the guidelines dictates two blank lines before functions (and classes)
  • Look into slices and use of these –  Slices can solve your problem a lot more elegant, based on your own thinking. The basic examples as some have mentioned already is to use [::-1] to reverse an entire list, in one swift go.

Alternate solutions

If we look at the case for a 4 sided die, we get the following lists, as based on your suggestion:

2 3 4 5 6 7 8
  3 4 5 6 7    = [ 2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7, 8 ]
    4 5 6
      5

This pattern can easily be regenerated using slicing. The first iteration of the list is simply list(range(2, 2*sides + 1). The next iteration we skip the first and last element, which can be expressed as the slice [1:-1]. And this we can continue doing until there are no more elements left in the temporary list.

In code this look the following:

def all_rolls(sides):
    """Return sum of all combinations of two dice with given number of sides."""
    result = []
    temp_list = list(range(2, 2*sides+1))

    while temp_list:
        result.extend(temp_list)
        temp_list = temp_list[1:-1]

    return sorted(result)

Note how I also added a simple, but hopefully sufficient docstring to state what the function is meant to do, and have removed some of the unneeded loops and ranges.

\$\endgroup\$
8
\$\begingroup\$

On top of @BenC answer which provide an efficient writting for a straitforward algorithm, I'll provide a version where you build the resulting list already sorted, saving you some computation.

The main thing is that @BenC answer can easily be extended to provide more than 2 rolls of the dice, but by further examinating the structure of the desired output we can craft the required list specificaly for 2 rolls:

  • Each number between 2 and 2 * num_sides appears in the output sequence;
  • The first number appear 1 time, the second number appear 2 times, …, the nth number appear n time until a number appears num_sides times;
  • After that, the next number appears one time less than the previous one, with the number 2 * num_sides appearing once.

Given that, we can iterate through range(1, num_sides) to get the number of ascending items, and then through range(num_sides, 0, -1) to get the number of descending items. The value of the items starts at 2 and end when we finish iterating.

First implementation

def sum_of_two_rolls(num_sides):
    result = []
    value = 2

    for num_repeat in range(1, num_sides):
        for _ in range(num_repeat):
            result.append(value)
        value += 1

    for num_repeat in range(num_sides, 0, -1):
        for _ in range(num_repeat):
            result.append(value)
        value += 1

    return result

Note the use of _ when we do not care about the variable used to iterate and just want to loop a certain amount of times.

But this solution is not very elegant since it contains two loops that are very similar.

Second implementation

We now want to merge both for loops since they perform the same operation. But we cannot use + to concatenate two range objects (assuming Python 3, since in Python 2 range returns lists and you can concatenate lists using +). However, since range objects are iterators, we can make good use of itertools.chain which concatenates any number of iterators:

from itertools import chain

def sum_of_two_rolls(num_sides):
    result = []
    value = 2

    for num_repeat in chain(range(1, num_sides), range(num_sides, 0, -1)):
        for _ in range(num_repeat):
            result.append(value)
        value += 1

    return result

But we can make this code even more pythonic.

Third implementation

The first thing to note is that using some sort of counter value being incremented inside a for loop is a common anti-pattern and is best served with enumerate. In our case, we need to start at 2 instead of the default 0 usualy used with enumerate. Luckily, enumerate's optional second argument let us do that:

from itertools import chain

def sum_of_two_rolls(num_sides):
    result = []

    for value, repeats in enumerate(chain(range(1, num_sides), range(num_sides, 0, -1)), 2):
        for _ in range(repeats):
            result.append(value)

     return result

Last improvement: the one-liner

Lastly, creating an empty list and appending to it in a loop is also an anti-pattern since a list-comprehension is most likely possible to use and has better performances.

Since this is the only computation that the function is doing, you can directly return the list comprehension:

from itertools import chain

def sum_of_two_rolls(num_sides):
    return [v for v, r in enumerate(chain(range(1, num_sides), range(num_sides, 0, -1)), 2) for _ in range(r)]

Or splitted over several lines for better readability:

from itertools import chain

def sum_of_two_rolls(num_sides):
    return [v
            for v, r in enumerate(
                chain(range(1, num_sides), range(num_sides, 0, -1)),
                2)
            for _ in range(r)]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.