Skip to content

Commit db94cb0

Browse files
authored
[Darts] : Add Approaches (#3386)
* Initial docs for darts approaches. * Initial rough draft of darts approaches. * Removing .articles for now. * Wrapped up approaches content details and removed performance info. * Cleaned up typos and changed toss to throw.
1 parent 01fb257 commit db94cb0

File tree

14 files changed

+625
-0
lines changed

14 files changed

+625
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Using Boolean Values as Integers
2+
3+
4+
```python
5+
def score(x_coord, y_coord):
6+
radius = (x_coord**2 + y_coord**2)
7+
return (radius<=1)*5 + (radius<=25)*4 + (radius<=100)*1
8+
```
9+
10+
11+
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.
12+
This approach leverages that interpretation by checking which areas the throw falls into and multiplying each Boolean `int` by a scoring multiple.
13+
For example, a throw that lands on the 25 (_or 5 if using `math.sqrt(x**2 + y**2)`_) circle should have a score of 5:
14+
15+
```python
16+
>>> (False)*5 + (True)*4 + (True)*1
17+
5
18+
```
19+
20+
21+
This makes for very compact code and has the added boost of not requiring any `loops` or additional data structures.
22+
However, it is considered bad form to rely on Boolean interpretation.
23+
Instead, the Python documentation recommends an explicit conversion to `int`:
24+
25+
26+
```python
27+
def score(x_coord, y_coord):
28+
radius = (x_coord**2 + y_coord**2)
29+
return int(radius<=1)*5 + int(radius<=25)*4 + int(radius<=100)*1
30+
```
31+
32+
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`.
33+
Despite the "radius" variable name, it is also more difficult to relate the scoring "rings" of the Dartboard to the values being checked and calculated in the `return` statement.
34+
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.
35+
36+
[bools-as-ints]: https://docs.python.org/3/library/stdtypes.html#boolean-type-bool
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def score(x_coord, y_coord):
2+
radius = (x_coord**2 + y_coord**2)
3+
return (radius<=1)*5 + (radius<=25)*4 +(radius<=100)*1
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"introduction": {
3+
"authors": ["bethanyg"],
4+
"contributors": []
5+
},
6+
"approaches": [
7+
{
8+
"uuid": "7d78f598-8b4c-4f7f-89e1-e8644e934a4c",
9+
"slug": "if-statements",
10+
"title": "Use If Statements",
11+
"blurb": "Use if-statements to check scoring boundaries for a dart throw.",
12+
"authors": ["bethanyg"]
13+
},
14+
{
15+
"uuid": "f8f5533a-09d2-4b7b-9dec-90f268bfc03b",
16+
"slug": "tuple-and-loop",
17+
"title": "Use a Tuple & Loop through Scores",
18+
"blurb": "Score the Dart throw by looping through a tuple of scores.",
19+
"authors": ["bethanyg"]
20+
},
21+
{
22+
"uuid": "a324f99e-15bb-43e0-9181-c1652094bc4f",
23+
"slug": "match-case",
24+
"title": "Use Structural Pattern Matching ('Match-Case')",
25+
"blurb": "Use a Match-Case (Structural Pattern Matching) to score the dart throw.)",
26+
"authors": ["bethanyg"]
27+
},
28+
{
29+
"uuid": "966bd2dd-c4fd-430b-ad77-3a304dedd82e",
30+
"slug": "dict-and-generator",
31+
"title": "Use a Dictionary with a Generator Expression",
32+
"blurb": "Use a generator expression looping over a scoring dictionary, getting the max score for the dart throw.",
33+
"authors": ["bethanyg"]
34+
},
35+
{
36+
"uuid": "5b087f50-31c5-4b84-9116-baafd3a30ed6",
37+
"slug": "booleans-as-ints",
38+
"title": "Use Boolean Values as Integers",
39+
"blurb": "Use True and False as integer values to calculate the score of the dart throw.",
40+
"authors": ["bethanyg"]
41+
},
42+
{
43+
"uuid": "0b2dbcd3-f0ac-45f7-af75-3451751fd21f",
44+
"slug": "dict-and-dict-get",
45+
"title": "Use a Dictionary with dict.get",
46+
"blurb": "Loop over a dictionary and retrieve score via dct.get.",
47+
"authors": ["bethanyg"]
48+
}
49+
]
50+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Using a Dictionary and `dict.get()`
2+
3+
4+
```python
5+
def score(x_coord, y_coord):
6+
point = (x_coord**2 + y_coord**2)
7+
scores = {
8+
point <= 100: 1,
9+
point <= 25: 5,
10+
point <= 1: 10
11+
}
12+
13+
return scores.get(True, 0)
14+
```
15+
16+
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.
17+
However, this approach is **not** interpreting Booleans as integers and is instead exploiting three key properties of [dictionaries][dicts]:
18+
19+
20+
1. [Keys must be hashable][hashable-keys] — in other words, keys have to be _unique_.
21+
2. Insertion order is preserved (_as of `Python 3.7`_), and evaluation/iteration happens in insertion order.
22+
3. Duplicate keys _overwrite_ existing keys.
23+
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.
24+
25+
Finally, the `return` line uses [`dict.get()`][dict-get] to `return` a default value of 0 when a throw is outside the existing circle radii.
26+
To see this in action, you can view this code on [Python Tutor][dict-get-python-tutor].
27+
28+
29+
Because of the listed dictionary qualities, **_order matters_**.
30+
This approach depends on the outermost scoring circle containing all smaller circles and that
31+
checks proceed from largest --> smallest circle.
32+
Iterating in the opposite direction will not resolve to the correct score.
33+
The following code variations do not pass the exercise tests:
34+
35+
36+
```python
37+
38+
def score(x_coord, y_coord):
39+
point = (x_coord**2 + y_coord**2)
40+
scores = {
41+
point <= 1: 10,
42+
point <= 25: 5,
43+
point <= 100: 1,
44+
}
45+
46+
return scores.get(True, 0)
47+
48+
#OR#
49+
50+
def score(x_coord, y_coord):
51+
point = (x_coord**2 + y_coord**2)
52+
scores = {
53+
point <= 25: 5,
54+
point <= 1: 10,
55+
point <= 100: 1,
56+
}
57+
58+
return scores.get(True, 0)
59+
60+
```
61+
62+
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.
63+
Even those experienced in Python might take longer than usual to figure out what is happening in the code.
64+
Extensibility could also be error-prone due to needing a strict order for the `dict` keys.
65+
66+
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.
67+
68+
[approach-boolean-values-as-integers]: https://exercism.org/tracks/python/exercises/darts/approaches/boolean-values-as-integers
69+
[dicts]: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
70+
[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get
71+
[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
72+
[hashable-keys]: https://www.pythonmorsels.com/what-are-hashable-objects/#dictionary-keys-must-be-hashable
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def score(x_coord, y_coord):
2+
point = (x_coord**2 + y_coord**2)
3+
scores = {point <= 100: 1, point <= 25: 5, point <= 1: 10}
4+
5+
return scores.get(True, 0)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Use a Dictionary and a Generator Expression
2+
3+
```python
4+
def score(x_coord, y_coord):
5+
throw = x_coord**2 + y_coord**2
6+
rules = {1: 10, 25: 5, 100: 1, 200: 0}
7+
8+
return max(point for distance, point in
9+
rules.items() if throw <= distance)
10+
```
11+
12+
13+
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()`.
14+
In cases where the scoring circles overlap, `max()` will return the maximum score available for the throw.
15+
The generator expression inside `max()` is the equivalent of using a `for-loop` and a variable to determine the max score:
16+
17+
18+
```python
19+
def score(x_coord, y_coord):
20+
throw = x_coord**2 + y_coord**2
21+
rules = {1: 10, 25: 5, 100: 1}
22+
max_score = 0
23+
24+
for distance, point in rules.items():
25+
if throw <= distance and point > max_score:
26+
max_score = point
27+
return max_score
28+
```
29+
30+
31+
A `list` or `tuple` can also be used in place of `max()`, but then requires an index to return the max score:
32+
33+
```python
34+
def score(x_coord, y_coord):
35+
throw = x_coord**2 + y_coord**2
36+
rules = {1: 10, 25: 5, 100: 1, 200: 0}
37+
38+
return [point for distance, point in
39+
rules.items() if throw <= distance][0] #<-- have to specify index 0.
40+
41+
#OR#
42+
43+
def score(x_coord, y_coord):
44+
throw = x_coord**2 + y_coord**2
45+
rules = {1: 10, 25: 5, 100: 1, 200: 0}
46+
47+
return tuple(point for distance, point in
48+
rules.items() if throw <= distance)[0]
49+
```
50+
51+
52+
This solution can even be reduced to a "one-liner".
53+
However, this is not performant, and is difficult to read:
54+
55+
```python
56+
def score(x_coord, y_coord):
57+
return max(point for distance, point in
58+
{1: 10, 25: 5, 100: 1, 200: 0}.items() if
59+
(x_coord**2 + y_coord**2) <= distance)
60+
```
61+
62+
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 (_although for the data in this problem, the performance hit is slight_).
63+
Additionally, the dictionary will take much more space in memory than using a `tuple` of tuples to hold scoring values.
64+
In some circumstances, these variations might also be harder to reason about for those not familiar with `generator-expressions` or `list comprehensions`.
65+
66+
67+
[approach-tuple-and-loop]: https://exercism.org/tracks/python/exercises/darts/approaches/tuple-and-loop
68+
[dict-items]: https://docs.python.org/3/library/stdtypes.html#dict.items
69+
[generator-expression]: https://dbader.org/blog/python-generator-expressions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def score(x_coord, y_coord):
2+
length = x_coord**2 + y_coord**2
3+
rules = {1.0: 10, 25.0: 5, 100.0: 1, 200: 0}
4+
score = max(point for
5+
distance, point in
6+
rules.items() if length <= distance)
7+
8+
return score
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Use `if-statements`
2+
3+
4+
```python
5+
import math
6+
7+
# Checks scores from the center --> edge.
8+
def score(x_coord, y_coord):
9+
distance = math.sqrt(x_coord**2 + y_coord**2)
10+
11+
if distance <= 1: return 10
12+
if distance <= 5: return 5
13+
if distance <= 10: return 1
14+
15+
return 0
16+
```
17+
18+
This approach uses [concept:python/conditionals]() to check the boundaries for each scoring ring, returning the corresponding score.
19+
Calculating the euclidian distance is assigned to the variable "distance" to avoid having to re-calculate it for every if check.
20+
Because the `if-statements` are simple and readable, they're written on one line to shorten the function body.
21+
Zero is returned if no other check is true.
22+
23+
24+
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:
25+
26+
27+
```python
28+
# Checks scores from the center --> edge.
29+
def score(x_coord, y_coord):
30+
distance = x_coord**2 + y_coord**2
31+
32+
if distance <= 1: return 10
33+
if distance <= 25: return 5
34+
if distance <= 100: return 1
35+
36+
return 0
37+
```
38+
39+
40+
# Variation 1: Check from Edge to Center Using Upper and Lower Bounds
41+
42+
43+
```python
44+
import math
45+
46+
# Checks scores from the edge --> center
47+
def score(x_coord, y_coord):
48+
distance = math.sqrt(x_coord**2 + y_coord**2)
49+
50+
if distance > 10: return 0
51+
if 5 < distance <= 10: return 1
52+
if 1 < distance <= 5: return 5
53+
54+
return 10
55+
```
56+
57+
This variant checks from the edge moving inward, checking both a lower and upper bound due to the overlapping scoring circles in this direction.
58+
59+
Scores for any of these solutions can also be assigned to a variable to avoid multiple `returns`, but this isn't really necessary:
60+
61+
```python
62+
# Checks scores from the edge --> center
63+
def score(x_coord, y_coord):
64+
distance = x_coord**2 + y_coord**2
65+
points = 10
66+
67+
if distance > 100: points = 0
68+
if 25 < distance <= 100: points = 1
69+
if 1 < distance <= 25: points = 5
70+
71+
return points
72+
```
73+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import math
2+
3+
def score(x_coord, y_coord):
4+
distance = math.sqrt(x_coord**2 + y_coord**2)
5+
if distance <= 1: return 10
6+
if distance <= 5: return 5
7+
if distance <= 10: return 1
8+
return 0

0 commit comments

Comments
 (0)