Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Darts] : Add Approaches #3386

Merged
merged 5 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions exercises/practice/darts/.approaches/booleans-as-ints/content.md
BethanyG marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Using Boolean Values as Integers


```python
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return (radius<=1)*5 + (radius<=25)*4 + (radius<=100)*1
```


In Python, the [Boolean values `True` and `False` are _subclasses_ of `int`][bools-as-ints] and can be interpreted as `0` (False) and `1` (True) in a mathematical context.
This approach leverages that interpretation by checking which areas the toss falls into and multiplying each Boolean `int` by a scoring multiple.
For example, a toss that lands on the 25 (_or 5 if using `math.sqrt(x**2 + y**2)`_) circle should have a score of 5:

```python
>>> (False)*5 + (True)*4 + (True)*1
5
```


This makes for very compact code and has the added boost of not requiring any `loops` or additional data structures.
However, it is considered bad form to rely on Boolean interpretation.
Instead, the Python documentation recommends an explicit conversion to `int`:


```python
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return int(radius<=1)*5 + int(radius<=25)*4 + int(radius<=100)*1
```

Beyond that recommendation, the terseness of this approach might be harder to reason about or decode — especially if a programmer is coming from a programming langauge that does not treat Boolean values as `ints`.
Despite the "radius" variable name, is also more difficult to relate the scoring "rings" of the Dartboard to the values being checked and calculated in the `return` statement.
If using this code in a larger program, it would be strongly recommended that a docstring be provided to explain the Dartboard rings, scoring rules, and the corresponding scores.

[bools-as-ints]: https://docs.python.org/3/library/stdtypes.html#boolean-type-bool
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return (radius<=1)*5 + (radius<=25)*4 +(radius<=100)*1
50 changes: 50 additions & 0 deletions exercises/practice/darts/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"introduction": {
"authors": ["bethanyg"],
"contributors": []
},
"approaches": [
{
"uuid": "7d78f598-8b4c-4f7f-89e1-e8644e934a4c",
"slug": "if-statements",
"title": "Use If Statements",
"blurb": "Use if-statements to check scoring boundaries for a dart toss.",
"authors": ["bethanyg"]
},
{
"uuid": "f8f5533a-09d2-4b7b-9dec-90f268bfc03b",
"slug": "tuple-and-loop",
"title": "Use a Tuple & Loop through Scores",
"blurb": "Score the Dart toss by looping through a tuple of scores.",
"authors": ["bethanyg"]
},
{
"uuid": "a324f99e-15bb-43e0-9181-c1652094bc4f",
"slug": "match-case",
"title": "Use Structural Pattern Matching ('Match-Case')",
"blurb": "Use a Match-Case (Structural Pattern Matching) to score the dart toss.)",
"authors": ["bethanyg"]
},
{
"uuid": "966bd2dd-c4fd-430b-ad77-3a304dedd82e",
"slug": "dict-and-generator",
"title": "Use a Dictionary with a Generator Expression",
"blurb": "Use a generator expression looping over a scoring dictionary, getting the max score for the dart toss.",
"authors": ["bethanyg"]
},
{
"uuid": "5b087f50-31c5-4b84-9116-baafd3a30ed6",
"slug": "booleans-as-ints",
"title": "Use Boolean Values as Integers",
"blurb": "Use True and False as integer values to calculate the score of the dart toss.",
"authors": ["bethanyg"]
},
{
"uuid": "0b2dbcd3-f0ac-45f7-af75-3451751fd21f",
"slug": "dict-and-dict-get",
"title": "Use a Dictionary with dict.get",
"blurb": "Loop over a dictionary and retrieve score via dct.get.",
"authors": ["bethanyg"]
}
]
}
72 changes: 72 additions & 0 deletions exercises/practice/darts/.approaches/dict-and-dict-get/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Using a Dictionary and `dict.get()`


```python
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 100: 1,
point <= 25: 5,
point <= 1: 10
}

return scores.get(True, 0)
```

At first glance, this approach looks similar to the [Booleans as Integers][approach-boolean-values-as-integers] approach, due to the Boolean evaluation used in the dictionary keys.
However, this approach is **not** interpreting Booleans as integers and is instead exploiting three key properties of [dictionaries][dicts]:


1. [Keys must be hashable][hashable-keys] — in other words, keys have to be _unique_.
2. Insertion order is preserved (_as of `Python 3.7`_), and evaluation/iteration happens in insertion order.
3. Duplicate keys _overwrite_ existing keys.
If the first key is `True` and the third key is `True`, the _value_ from the third key will overwrite the value from the first key.

Finally, the `return` line uses [`dict.get()`][dict-get] to `return` a default value of 0 when a toss is outside the existing circle radii.
To see this in action, you can view this code on [Python Tutor][dict-get-python-tutor].


Because of the listed dictionary qualities, **_order matters_**.
This approach depends on the outermost scoring circle containing all smaller circles and that
checks proceed from largest --> smallest circle.
Iterating in the opposite direction will not resolve to the correct score.
The following code variations do not pass the exercise tests:


```python

def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 1: 10,
point <= 25: 5,
point <= 100: 1,
}

return scores.get(True, 0)

#OR#

def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 25: 5,
point <= 1: 10,
point <= 100: 1,
}

return scores.get(True, 0)

```

While this approach is a _very clever_ use of dictionary properties, it is likely to be very hard to reason about for those who are not deeply knowledgeable.
Even those experienced in Python might take longer than usual to figure out what is happening in the code.
Extensibility could also be error-prone due to needing a strict order for the `dict` keys.

This approach offers no space or speed advantages over using `if-statements` or other strategies, so is not recommended for use beyond a learning context.

[approach-boolean-values-as-integers]: https://exercism.org/tracks/python/exercises/darts/approaches/boolean-values-as-integers
[dicts]: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get
[dict-get-python-tutor]: https://pythontutor.com/render.html#code=def%20score%28x_coord,%20y_coord%29%3A%0A%20%20%20%20point%20%3D%20%28x_coord**2%20%2B%20y_coord**2%29%0A%20%20%20%20scores%20%3D%20%7B%0A%20%20%20%20%20%20%20%20point%20%3C%3D%20100%3A%201,%0A%20%20%20%20%20%20%20%20point%20%3C%3D%2025%3A%205,%0A%20%20%20%20%20%20%20%20point%20%3C%3D%201%3A%2010%0A%20%20%20%20%7D%0A%20%20%20%20%0A%20%20%20%20return%20scores.get%28True,%200%29%0A%20%20%20%20%0Aprint%28score%281,3%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
[hashable-keys]: https://www.pythonmorsels.com/what-are-hashable-objects/#dictionary-keys-must-be-hashable
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {point <= 100: 1, point <= 25: 5, point <= 1: 10}

return scores.get(True, 0)
69 changes: 69 additions & 0 deletions exercises/practice/darts/.approaches/dict-and-generator/content.md
BethanyG marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Use a Dictionary and a Generator Expression

```python
def score(x_coord, y_coord):
toss = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}

return max(point for distance, point in
rules.items() if toss <= distance)
```


This approach is very similar to the [tuple and loop][approach-tuple-and-loop] approach, but iterates over [`dict.items()`][dict-items] and writes the `loop` as a [`generator-expression`][generator-expression] inside `max()`.
In cases where the scoring circles overlap, `max()` will return the maximum score available for the toss.
The generator expression inside `max()` is the equivalent of using a `for-loop` and a variable to determine the max score:


```python
def score(x_coord, y_coord):
toss = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1}
max_score = 0

for distance, point in rules.items():
if toss <= distance and point > max_score:
max_score = point
return max_score
```


A `list` or `tuple` can also be used in place of `max()`, but then requires and index to return the max score:

```python
def score(x_coord, y_coord):
toss = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}

return [point for distance, point in
rules.items() if toss <= distance][0] #<-- have to specify index 0.

#OR#

def score(x_coord, y_coord):
toss = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}

return tuple(point for distance, point in
rules.items() if toss <= distance)[0]
```


This solution can even be reduced to a "one-liner".
However, this is not performant, and is difficult to read:

```python
def score(x_coord, y_coord):
return max(point for distance, point in
{1: 10, 25: 5, 100: 1, 200: 0}.items() if
(x_coord**2 + y_coord**2) <= distance)
```

While all of these variations do pass the tests, they suffer from even more over-engineering/performance caution than the earlier tuple and loop approach.
Additionally, the dictionary will take much more space in memory than using a `tuple` of tuples to hold scoring values.
In some circumstances, these variations might also be harder to reason about for those not familiar with `generator-expressions` or `list comprehensions`.


[approach-tuple-and-loop]: https://exercism.org/tracks/python/exercises/darts/approaches/tuple-and-loop
[dict-items]: https://docs.python.org/3/library/stdtypes.html#dict.items
[generator-expression]: https://dbader.org/blog/python-generator-expressions
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def score(x_coord, y_coord):
length = x_coord**2 + y_coord**2
rules = {1.0: 10, 25.0: 5, 100.0: 1, 200: 0}
score = max(point for
distance, point in
rules.items() if length <= distance)

return score
73 changes: 73 additions & 0 deletions exercises/practice/darts/.approaches/if-statements/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Use `if-statements`


```python
import math

# Checks scores from the center --> edge.
def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)

if distance <= 1: return 10
if distance <= 5: return 5
if distance <= 10: return 1

return 0
```

This approach uses [concept:python/conditionals]() to check the boundaries for each scoring ring, returning the corresponding score.
Calculating the euclidian distance is assigned to the variable "distance" to avoid having to re-calculate it for every if check.
Because the `if-statements` are simple and readable, they're written on one line to shorten the function body.
Zero is returned if no other check is true.


To avoid importing the `math` module (_for a very very slight speedup_), (x**2 +y**2) can be calculated instead, and the scoring rings can be adjusted to 1, 25, and 100:


```python
# Checks scores from the center --> edge.
def score(x_coord, y_coord):
distance = x_coord**2 + y_coord**2

if distance <= 1: return 10
if distance <= 25: return 5
if distance <= 100: return 1

return 0
```


# Variation 1: Check from Edge to Center Using Upper and Lower Bounds


```python
import math

# Checks scores from the edge --> center
def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)

if distance > 10: return 0
if 5 < distance <= 10: return 1
if 1 < distance <= 5: return 5

return 10
```

This variant checks from the edge moving inward, checking both a lower and upper bound due to the overlapping scoring circles in this direction.

Scores for any of these solutions can also be assigned to a variable to avoid multiple `returns`, but this isn't really necessary:

```python
# Checks scores from the edge --> center
def score(x_coord, y_coord):
distance = x_coord**2 + y_coord**2
points = 10

if distance > 100: points = 0
if 25 < distance <= 100: points = 1
if 1 < distance <= 25: points = 5

return points
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import math

def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)
if distance <= 1: return 10
if distance <= 5: return 5
if distance <= 10: return 1
return 0
Loading
Loading