Repository: exercism/python
Pull request #2592 (2021-09-21 12:11:34)

Implement `black-jack` concept exercise
Partially addresses #2428

Implemented 
- instructions
- code skeleton
- tests
- example solution
- hints

as seemed appropriate.

Relied on https://bicyclecards.com/how-to-play/blackjack for some typical house rules.

Overall, seems in-line with teaching python operators.

BethanyG: @pranasziaukas  -- in relation to this, would you also like to pick up [PR2456](https://github.com/exercism/python/issues/2456), which is the concept write-up for this exercise?  
It is perfectly fine if you do not want to, but we also can't mark this concept exercise live without it, so I am noting that fact here.
pranasziaukas: Thank you for reviewing @BethanyG. Since I'm not a native English speaker I gladly accepted the suggested phrasing changes.

> in relation to this, would you also like to pick up PR2456, which is the concept write-up for this exercise?

I might pick that one up a bit later. In any case
1. Should the concept write-up work be part of this or a separate PR?
2. Should this remain as a draft PR?
BethanyG: > Should the concept write-up work be part of this or a separate PR?

It doesn't have to be.  It depends on how you want to work.  If it feels better to do it in a follow-on PR, that's completely fine.  If you do decide to include that work in _**this**_ PR, I would then make this a `large-size` PR, and keep it in draft form until we get those concept write-up pieces done.

>Should this remain as a draft PR?

Only if you want to include the concept write-up.  If not, we can change this to full PR and get it reviewed and merged.  The exercise won't go "live" on the site however until the concept `introduction.md` and `about.md` write-ups are complete, so we don't have `TODO` showing on tooltips and pages. 

Just let me know how you want to proceed.  😄 

pranasziaukas: Great!

I guess I prefer working in smaller chunks. Therefore, I'm marking this PR as ready for review as it is.
Once the code is merged, it's only the concept write-ups that need to be done, and those can be naturally addressed in a separate PR.
BethanyG: aaand we have conflicts.  SAD FACE.  @pranasziaukas -- can you try fetching from head and rebasing this locally?  If all else fails, we'll have to force it, but I am hoping you can resolve this without that.  Thanks!  😄 
pranasziaukas: Should be good now.

The conflict was `class BlackJackTest(unittest.TestCase)` vs `class TestBlackJack(unittest.TestCase)` -- I don't quite know/remember where that came from :)
BethanyG: It was me.  😞    I changed the name to `BlackJackTest` to make the output in the UI prettier.  It was easier to change the place where `Test` was in the name than to make a regex that would reliably detect and replace it wherever it went.  Now the results will print out with **`Blackjack > test _____`**  Apologies for that.  

Thank you again for resolving things and for working on this!  

Patch:
diff --git a/exercises/concept/black-jack/.docs/hints.md b/exercises/concept/black-jack/.docs/hints.md
index 48d5446c..f91a51a1 100644
--- a/exercises/concept/black-jack/.docs/hints.md
+++ b/exercises/concept/black-jack/.docs/hints.md
@@ -1,21 +1,33 @@
 # General
 
-- [The Python Comparisons Tutorial][the python comparisons tutorial] and [Python comparisons examples][python comparisons examples] can be a great introduction.
+[The Python comparisons tutorial][python comparisons tutorial] and [Python comparisons examples][python comparisons examples] are a great introduction covering the content of this exercise.
 
-## 1. Calculate the number of card
+## 1. Calculate the value of a card
 
-- You can use the [equality comparison operator][equality comparison operator] to get the number of the card.
+- You can use the equality comparison operator `==` to determine specific cards, e.g. `card == 'J'`.
+- You can use the [`int` constructor][int constructor] to get an integer number from an integer literal, e.g. `int(card)`.
 
-## 2. Calculate the number of Ace
+## 2. Calculate the value of an ace
 
-- You can use the [order comparisons operator][order comparisons operator]to decide the value of ace without the sum of hand exceeding 21.
+- You can use the order comparison operator `>` to decide the appropriate course of action, e.g. `hand_value + 11 > 21`.
 
-## 3. Judge Blackjack
+## 3. Determine Blackjack
 
-- You can use the [membership test operations][membership test operations] in `if` or `elif` syntax to find black-jack from the first two cards in your hand.
+- You can use the [`if`/`elif`/`else` syntax][if syntax] to handle different combinations of cards.
+- You can reuse the already implemented `value_of_card` function.
 
-[the python comparisons tutorial]: https://docs.python.org/3/reference/expressions.html#comparisons
+## 4. Splitting pairs
+
+- You can handle the `A` case (when at least one of the cards in an ace) separately.
+
+## 5. Doubling down
+
+- You can chain comparison operators, e.g. `9 <= hand_value <= 11`.
+- You can use the [conditional expression][conditional expression] (sometimes called a "ternary operator")
+to shorten simple `if`/`else` statements, e.g. `1 if card == 'A' else value_of_card(card)`.
+
+[python comparisons tutorial]: https://docs.python.org/3/reference/expressions.html#comparisons
 [python comparisons examples]: https://www.tutorialspoint.com/python/comparison_operators_example.htm
-[equality comparison operator]: https://docs.python.org/3/reference/expressions.html#comparisons
-[order comparisons operator]: https://docs.python.org/3/reference/expressions.html#comparisons
-[membership test operations]: https://docs.python.org/3/reference/expressions.html#comparisons
\ No newline at end of file
+[int constructor]: https://docs.python.org/3/library/functions.html#int
+[if syntax]: https://docs.python.org/3/tutorial/controlflow.html#if-statements
+[conditional expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions
diff --git a/exercises/concept/black-jack/.docs/instructions.md b/exercises/concept/black-jack/.docs/instructions.md
index b9069239..774b35fd 100644
--- a/exercises/concept/black-jack/.docs/instructions.md
+++ b/exercises/concept/black-jack/.docs/instructions.md
@@ -1,50 +1,91 @@
-In this exercise, you're going to implement some rules from [Blackjack][blackjack]
+# Instructions
 
-You have some rules to implement for judging the result of the game.
+In this exercise you are going to implement some rules of [Blackjack][blackjack],
+such as the way the game is played and scored.
 
-**Note** : In this exercise, _A_ means an ace, _J_ means jack, _Q_ means queen, and _K_ means king card.
+**Note** : In this exercise, _A_ means ace, _J_ means jack, _Q_ means queen, and _K_ means king cards.
+A [standard 52-card deck][standard_deck] is assumed.
 
-## 1. Calculate the number of card
+## 1. Calculate the value of a card
 
-Create the `number_of_card()` function with a parameter `card`. The value of _J, Q_ or _K_ is 10. If the `card` is _A_, then just return "ace".
+In Blackjack, it is up to each individual player if an ace is worth 1 or 11 points.
+Face cards (_J_, _Q_, _K_) are worth 10 points and any other card is worth its pip (numerical) value.
+
+Define the `value_of_card` function with a parameter `card`.
+The value of _J_, _Q_ or _K_ is 10.
+Otherwise, return the numerical value of a card.
+An ace can take on multiple values so let's ignore _A_ for the time being.
 
 ```python
->>> number_of_card('K')
+>>> value_of_card('K')
 10
->>> number_of_card('A')
-ace
+>>> value_of_card('4')
+4
 ```
 
-## 2. Calculate the number of Ace
-
-Create the `number_of_ace()` function with a parameter `hand`.
+## 2. Calculate the value of an ace
 
-1. `hand` : the sum of cards in hand with an ace.
+As mentioned before, an ace can be worth _either_ 1 or 11 points.
+At the same time, players are trying to get as close to 21 as possible, without going _over_ 21.
 
-Ace is 1 or 11. You have to decide the value of ace without the sum of hand exceeding 21.
+Define the `value_of_ace(<hand_value>)` function with a parameter `hand_value`,
+which is the total hand value before getting an ace card.
+You now have to decide if the upcoming ace card will be scored as a 1 or 11.
+The value of the hand with the ace needs to be as high as possible _without_ going over 21.
 
 ```python
->>> number_of_ace(19)
+>>> value_of_ace(19)
 1
->>> number_of_ace(7)
+>>> value_of_ace(7)
 11
 ```
 
-## 3. Judge Blackjack
+## 3. Determine Blackjack
+
+If the first two cards are an ace and a ten-card, giving a count of 21 in two cards, it is known as blackjack.
+
+Define the `is_blackjack(<card_one>, <card_two>)` function with parameters `card_one` and `card_two`, which are a pair of cards.
+Determine if the two-card hand is a blackjack.
 
-Create the `blackjack()` function with a parameter `hand`.
+**Note** : This calculation can be done in many ways.
+ If possible, check if there is an ace and a ten-card in the hand.
 
-1. `hand` : first two cards in hand.
+```python
+>>> is_blackjack('A', 'K')
+True
+>>> is_blackjack('10', '9')
+False
+```
+
+## 4. Splitting pairs
+
+If the first two cards are of the same value,
+such as two sixes, or a _Q_ and _K_ a player may choose to treat them as two separate hands.
+This is known as "splitting pairs".
+
+Define the `can_split_pairs(<card_one>, <card_two>)` function with parameters `card_one` and `card_two`, which are a pair of cards.
+Determine if this two-card hand can be split into two pairs.
+```python
+>>> can_split_pairs('Q', 'K')
+True
+>>> can_split_pairs('10', 'A')
+False
+```
 
-This function should return if the hand is blackjack. There's must be an ace card in `hand`.
+## 5. Doubling down
 
-**Note** : If the player has an Ace and a ten-value card, it is called a _Blackjack_. Ten-value cards include _10, J, Q, K_. I think you may have many ways. But if you can, use a way to check if there are an ace and a ten-value in the list.
+When the original two cards dealt total 9, 10, or 11 points
+a player can place an additional bet equal to the original bet.
+This is known as "doubling down".
 
+Define the `can_double_down(<card_one>, <card_two>)` function with parameters `card_one` and `card_two`, which are a pair of cards.
+Determine if the two-card hand can be "doubled down".
 ```python
->>> blackjack(['A', 'K'])
+>>> can_double_down('A', '9')
 True
->>> blackjack([10, 9])
+>>> can_double_down('10', '2')
 False
 ```
 
-[blackjack]: https://en.wikipedia.org/wiki/Blackjack
\ No newline at end of file
+[blackjack]: https://en.wikipedia.org/wiki/Blackjack
+[standard_deck]: https://en.wikipedia.org/wiki/Standard_52-card_deck
diff --git a/exercises/concept/black-jack/.meta/config.json b/exercises/concept/black-jack/.meta/config.json
index e5e5971e..c120dafa 100644
--- a/exercises/concept/black-jack/.meta/config.json
+++ b/exercises/concept/black-jack/.meta/config.json
@@ -1,6 +1,6 @@
 {
   "blurb": "Learn about comparisons by implementing some Black Jack judging rules.",
-  "authors": ["Ticktakto", "Yabby1997", "limm-jk", "OMEGA-Y", "wnstj2007"],
+  "authors": ["Ticktakto", "Yabby1997", "limm-jk", "OMEGA-Y", "wnstj2007", "pranasziaukas"],
   "icon": "poker",
   "contributors": ["bethanyg"],
   "files": {
diff --git a/exercises/concept/black-jack/.meta/exemplar.py b/exercises/concept/black-jack/.meta/exemplar.py
index 7276260e..0b6c7b32 100644
--- a/exercises/concept/black-jack/.meta/exemplar.py
+++ b/exercises/concept/black-jack/.meta/exemplar.py
@@ -1,23 +1,72 @@
-def number_of_card(card):
-    if card == 'A':
-        return "ace"
-    elif card == 'J' or card == 'Q' or card == 'K':
-        return 10
+def value_of_card(card):
+    """
+
+    :param card: str - given card.
+    :return: int - value of a given card (J, Q, K = 10, numerical value otherwise).
+    """
+
+    if card == 'J' or card == 'Q' or card == 'K':
+        value = 10
     else:
-        return card
+        value = int(card)
+    return value
+
+
+def value_of_ace(hand_value):
+    """
 
+    :param hand_value: int - current hand value.
+    :return: int - value of the upcoming ace card (either 1 or 11).
+    """
 
-def number_of_ace(hand):
-    if hand + 11 <= 21:
-        return 11
+    if hand_value + 11 > 21:
+        value = 1
     else:
-        return 1
+        value = 11
+    return value
 
 
-def blackjack(hand):
-    if 'A' not in hand:
-        return False
-    elif 'J' in hand or 'Q' in hand or 'K' in hand or 10 in hand:
-        return True
+def is_blackjack(card_one, card_two):
+    """
+
+    :param card_one: str - first card in hand.
+    :param card_two: str - second card in hand.
+    :return: bool - if the hand is a blackjack (two cards worth 21).
+    """
+
+    if card_one == 'A' and card_two != 'A':
+        blackjack = value_of_card(card_two) == 10
+    elif card_one != 'A' and card_two == 'A':
+        blackjack = value_of_card(card_one) == 10
     else:
-        False
+        blackjack = False
+    return blackjack
+
+
+def can_split_pairs(card_one, card_two):
+    """
+
+    :param card_one: str - first card in hand.
+    :param card_two: str - second card in hand.
+    :return: bool - if the hand can be split into two pairs (i.e. cards are of the same value).
+    """
+
+    if card_one == 'A' or card_two == 'A':
+        split_pairs = card_one == card_two
+    else:
+        split_pairs = value_of_card(card_one) == value_of_card(card_two)
+    return split_pairs
+
+
+def can_double_down(card_one, card_two):
+    """
+
+    :param card_one: str - first card in hand.
+    :param card_two: str - second card in hand.
+    :return: bool - if the hand can be doubled down (i.e. totals 9, 10 or 11 points).
+    """
+
+    card_one_value = 1 if card_one == 'A' else value_of_card(card_one)
+    card_two_value = 1 if card_two == 'A' else value_of_card(card_two)
+    hand_value = card_one_value + card_two_value
+    return 9 <= hand_value <= 11
diff --git a/exercises/concept/black-jack/black_jack.py b/exercises/concept/black-jack/black_jack.py
index 5c954d2d..7343184f 100644
--- a/exercises/concept/black-jack/black_jack.py
+++ b/exercises/concept/black-jack/black_jack.py
@@ -1,10 +1,51 @@
-def number_of_card(card):
+def value_of_card(card):
+    """
+
+    :param card: str - given card.
+    :return: int - value of a given card (J, Q, K = 10, numerical value otherwise).
+    """
+
+    pass
+
+
+def value_of_ace(hand_value):
+    """
+
+    :param hand_value: int - current hand value.
+    :return: int - value of the upcoming ace card (either 1 or 11).
+    """
+
+    pass
+
+
+def is_blackjack(card_one, card_two):
+    """
+
+    :param card_one: str - first card in hand.
+    :param card_two: str - second card in hand.
+    :return: bool - if the hand is a blackjack (two cards worth 21).
+    """
+
     pass
 
 
-def number_of_ace(hand):
+def can_split_pairs(card_one, card_two):
+    """
+
+    :param card_one: str - first card in hand.
+    :param card_two: str - second card in hand.
+    :return: bool - if the hand can be split into two pairs (i.e. cards are of the same value).
+    """
+
     pass
 
 
-def blackjack(hand):
+def can_double_down(card_one, card_two):
+    """
+
+    :param card_one: str - first card in hand.
+    :param card_two: str - second card in hand.
+    :return: bool - if the hand can be doubled down (i.e. totals 9, 10 or 11 points).
+    """
+
     pass


Test patch:
diff --git a/exercises/concept/black-jack/black_jack_test.py b/exercises/concept/black-jack/black_jack_test.py
index 0e12d271..bfef90bf 100644
--- a/exercises/concept/black-jack/black_jack_test.py
+++ b/exercises/concept/black-jack/black_jack_test.py
@@ -1,43 +1,109 @@
 import unittest
 import pytest
 from black_jack import (
-    number_of_card,
-    number_of_ace,
-    blackjack
+    value_of_card,
+    value_of_ace,
+    is_blackjack,
+    can_split_pairs,
+    can_double_down
 )
 
 
 class BlackJackTest(unittest.TestCase):
 
     @pytest.mark.task(taskno=1)
-    def test_number_of_card(self):
-        input_data = ['K', 'A']
-        output_data = [10, "ace"]
-        number_of_variants = range(1, len(input_data) + 1)
+    def test_value_of_card(self):
+        data = [
+            ('2', 2),
+            ('5', 5),
+            ('8', 8),
+            ('10', 10),
+            ('J', 10),
+            ('Q', 10),
+            ('K', 10),
+        ]
 
-        for variant, input, output in zip(number_of_variants, input_data, output_data):
-            with self.subTest(f"variation #{variant}", input=input, output=output):
-                self.assertEqual(number_of_card(input[0], input[1]), output,
-                                 msg=f'Expected: {output} but the number of cards was calculated incorrectly.')
+        for variant, (card, value) in enumerate(data, 1):
+            with self.subTest(f'variation #{variant}', input=card, output=value):
+                self.assertEqual(
+                    value,
+                    value_of_card(card),
+                    msg=f'Expected {value} as the value of {card}.'
+                )
 
     @pytest.mark.task(taskno=2)
-    def test_number_of_ace(self):
-        input_data = [19, 7]
-        output_data = [1, 11]
-        number_of_variants = range(1, len(input_data) + 1)
+    def test_value_of_ace(self):
+        data = [
+            (2, 11),
+            (5, 11),
+            (7, 11),
+            (9, 11),
+            (10, 11),
+            (11, 1),
+            (12, 1),
+            (15, 1),
+            (19, 1),
+            (20, 1),
+        ]
 
-        for variant, input, output in zip(number_of_variants, input_data, output_data):
-            with self.subTest(f"variation #{variant}", input=input, output=output):
-                self.assertEqual(number_of_ace(input[0], input[1]), output,
-                                 msg=f'Expected: {output} but the number of Ace cards was calculated incorrectly.')
+        for variant, (hand_value, ace_value) in enumerate(data, 1):
+            with self.subTest(f'variation #{variant}', input=hand_value, output=ace_value):
+                self.assertEqual(
+                    ace_value,
+                    value_of_ace(hand_value),
+                    msg=f'Expected {ace_value} as the value of ace when the hand is worth {hand_value}.'
+                )
 
     @pytest.mark.task(taskno=3)
-    def test_blackjack(self):
-        input_data = [['A', 'J'], [10, 9]]
-        output_data = [True, False]
-        number_of_variants = range(1, len(input_data) + 1)
-
-        for variant, input, output in zip(number_of_variants, input_data, output_data):
-            with self.subTest(f"variation #{variant}", input=input, output=output):
-                self.assertEqual(blackjack(input[0], input[1]), output,
-                                 msg=f'Expected: {output} but the value returned was incorrect,')
+    def test_is_blackjack(self):
+        data = [
+            (('A', 'K'), True),
+            (('10', 'A'), True),
+            (('10', '9'), False),
+            (('A', 'A'), False),
+        ]
+
+        for variant, (hand, blackjack) in enumerate(data, 1):
+            with self.subTest(f'variation #{variant}', input=hand, output=blackjack):
+                self.assertEqual(
+                    blackjack,
+                    is_blackjack(*hand),
+                    msg=f'Hand {hand} {"is" if blackjack else "is not"} a blackjack.'
+                )
+
+    @pytest.mark.task(taskno=4)
+    def test_can_split_pairs(self):
+        data = [
+            (('Q', 'K'), True),
+            (('6', '6'), True),
+            (('A', 'A'), True),
+            (('10', 'A'), False),
+            (('10', '9'), False),
+        ]
+
+        for variant, (hand, split_pairs) in enumerate(data, 1):
+            with self.subTest(f'variation #{variant}', input=hand, output=split_pairs):
+                self.assertEqual(
+                    split_pairs,
+                    can_split_pairs(*hand),
+                    msg=f'Hand {hand} {"can" if split_pairs else "cannot"} be split into pairs.'
+                )
+
+    @pytest.mark.task(taskno=5)
+    def test_can_double_down(self):
+        data = [
+            (('A', '9'), True),
+            (('K', 'A'), True),
+            (('4', '5'), True),
+            (('A', 'A'), False),
+            (('10', '2'), False),
+            (('10', '9'), False),
+        ]
+
+        for variant, (hand, double_down) in enumerate(data, 1):
+            with self.subTest(f'variation #{variant}', input=hand, output=double_down):
+                self.assertEqual(
+                    double_down,
+                    can_double_down(*hand),
+                    msg=f'Hand {hand} {"can" if double_down else "cannot"} be doubled down.'
+                )