3
\$\begingroup\$

I have built a Blackjack simulator in Python. I need to decrease the execution time. I have used a profiler to identify key bottlenecks in the execution. The following function is responsible for about 15% of the total run time.

def value(self):
    value = 0
    has_ace = False

    for card in self.cards:
        value += card.value

        if card.rank == "A":
            has_ace = True

    if has_ace and value <= 11:
        value += 10

    return value

The above function is used to calculate the score of a hand. A hand can have multiple cards. A card has a value and a rank.

In Blackjack, an Ace can be worth 1 or 11.

Is there a better way to go?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ The best and biggest optimizations involve changing multiple areas of the code. Yes, this function may be significant, I do think you should share the rest of the program, though. \$\endgroup\$ – AMC Feb 9 '20 at 4:52
  • 1
    \$\begingroup\$ I don't understand the logic behind the condition if has_ace and value <= 11: value += 10. What if the cards include more than a single ace? E.g. 2, A, A, A? Would the value of the hand be 5 or 15? \$\endgroup\$ – Ron Klein Feb 9 '20 at 11:55
  • \$\begingroup\$ @RonKlein Try think of it in regards to black jack, if you have AAAA you'd play it as 4 or 14, not 24, 34 or 44. As all of those other ones are bust. You'd pick 14 over 4 as it's a higher value - making it so you're more likely to win. \$\endgroup\$ – Peilonrayz Feb 9 '20 at 11:59
2
\$\begingroup\$

This function doesn't have any really obvious inefficiencies, so I assume we're looking to shave off every possible clock cycle...

Sometimes it's faster to use Python's built-in aggregation functions than to use a for loop. Here's how you could use sum and any:

def value(self) -> int:
    value = sum(card.value for card in self.cards)
    if value <= 11 and any(card.rank == "A" for card in self.cards):
        value += 10
    return value

Note the ordering of the and expression to make sure that the any iteration only happens if the value condition has already been met!

If you have flexibility over the representation of the cards, you might try making the rank an Enum with integer values. I'd expect comparing two integer enums to be just a smidge faster than comparing two strings.

\$\endgroup\$
5
  • \$\begingroup\$ In addition to Sam's previous answer, the final has_ace test could return immediately without updating local variable value. It could aslo increase your performances to store card ranks as integers instead of strings. \$\endgroup\$ – Davy Gabard Feb 9 '20 at 9:21
  • 5
    \$\begingroup\$ The any expression looks incomplete. \$\endgroup\$ – Roland Illig Feb 9 '20 at 9:33
  • 1
    \$\begingroup\$ It worth noting that your comment about comparing strings being faster than comparing integers is not strictly True, at least for CPython. For short strings such as "A", it is overwhelmingly likely that the interpreter will have interned them. Therefore, card.rank and the literal "A" will refer to precisely the same objects, which is a special case handle explicitly by string_richcompare in stringobject.c. Even if interning isn't performed, it would still be a matter comparing a single character. \$\endgroup\$ – Brian Feb 9 '20 at 15:37
  • \$\begingroup\$ Whoops, fixed that any typo. I'm not deeply familiar with CPython internals, but my thought was that comparing strings requires dereferencing a pointer before you get to the underlying identical byte, whereas comparing values lets you skip that step. If it's cached by the interpreter it's moot though. \$\endgroup\$ – Samwise Feb 9 '20 at 19:08
  • 1
    \$\begingroup\$ Picking ints over chars sounds like a premature optimization. As for if it's actually true, you can just use timeit to verify. For me they perform the same, and so the optimization all around just seems like bad advice. \$\endgroup\$ – Peilonrayz Feb 9 '20 at 20:17
1
\$\begingroup\$

In terms of playing the game, what is the use of the returned value?

For instance, A222 would have the value 17, and at that value, most people would stick and not ask for another card. But if it could be counted as 7, everyone would ask for another card.

To be useful, the result will need to be a list of possible values, (7,17) for this example.

\$\endgroup\$
0
\$\begingroup\$

I think that 10 and 11 are confusing values, and it might be more readable to have them either calculated or more explicitly expressed.

As you say, an ace can be either 1 or 11.

In addition, a valid hand cannot exceed 21.

I'd expect the code to have only those values hard coded (as constants or something similar).

If you want to implement it in an object oriented way (might not be the best idea here), then all of the cards are an instance of a class, which has the following methods:

  • has_alternative_value
  • get_regular_value
  • get_alternative_value

Only the ace would return True to has_alternative_value and so forth.

Having it implemented like that, the code would only have to deal with the magic number 21, and you wouldn't have 10 and 11 magic numbers.

While this sounds like over-engineering, I think that the main idea could be implemented in a simpler way. I don't have the time to write the entire solution myself, but I hope you catch my drift.

Good luck :)

\$\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.