Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
141 changes: 137 additions & 4 deletions adagrams/game.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,144 @@
from multiprocessing import pool

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this import is unused so you can delete it. Perhaps it was accidentally brought in by VSCode. There should be an option to turn off auto completion for imports to avoid bringing in stray, unused imports

import random

def build_letter_pool():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this helper function returns the letter pool which keeps the other function bodies concise. I think when you have a large data structure like this, it is helpful to pull them out of the functions that use them so they don't get dominated by the data structure.

However, since LETTER_POOL is a constant variable, you can have it as a global variable in game.py without wrapping it in a helper function.

LETTER_POOL = {
"A": 9,
"B": 2,
"C": 2,
"D": 4,
"E": 12,
"F": 2,
"G": 3,
"H": 2,
"I": 9,
"J": 1,
"K": 1,
"L": 4,
"M": 2,
"N": 6,
"O": 8,
"P": 2,
"Q": 1,
"R": 6,
"S": 4,
"T": 6,
"U": 4,
"V": 2,
"W": 2,
"X": 1,
"Y": 2,
"Z": 1
}

return LETTER_POOL


def build_available_letters_list(LETTER_POOL):
available_letters = []

for letter,frequency in LETTER_POOL.items():
for i in range(frequency):
available_letters.append(letter)

return available_letters
Comment on lines +38 to +44

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice helper function to keep draw_letters a single responsibility function and more concise 👍



def draw_letters():
pass
hand = []
LETTER_POOL = build_letter_pool()
available_letters = build_available_letters_list(LETTER_POOL)
count_dict = {}

while len(hand) < 10:
random_letter = random.choice(available_letters)

if random_letter not in hand:
hand.append(random_letter)
count_dict[random_letter] = 1
elif LETTER_POOL[random_letter] > count_dict[random_letter]:
hand.append(random_letter)
count_dict[random_letter] += 1

return hand


def uses_available_letters(word, letter_bank):
pass
letter_bank_copy = letter_bank.copy()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another Pythonic way you'll see to make a shallow copy is:

letter_bank_copy = letter_bank[:]

for letter in word.upper():
if letter not in letter_bank_copy:
Comment on lines +68 to +69

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a closer look at the time complexity involved with lines 68-69.

Line 68: a for loop that loops as many times as there are letters in word - O(n)
Line 69: in operator that in the worst case scenario has to check every letter in letter_bank_copy to see if letter is in it - O(n)

Rather than using a list of the letters in the hand, could we build a helper data structure (like a dictionary) that could let us look up if a letter was part of a user's hand?

If the helper dictionary had keys that were the letters and the values were the number of letters in the hand, then you could leverage the fact that key look up in dictionaries is a constant O(1) operation.

So on line 69, you can check if key (the letter in the hand) in helper_dictionary, which has constant look up time.

Additionally, instead of using the remove method, you could decrement the value in the dictionary to keep track of how many letters have been played. Recall that the remove method on a list also has linear run time because it could have to iterate over the entire list before finally removing letter.

return False

letter_bank_copy.remove(letter)

return True


def build_score_chart():
"""
|Letter | Value|
|:----------------------------:|:----:|
|A, E, I, O, U, L, N, R, S, T | 1 |
|D, G | 2 |
|B, C, M, P | 3 |
|F, H, V, W, Y | 4 |
|K | 5 |
|J, X | 8 |
|Q, Z | 10 |
"""

score_chart = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this helper just returns a constant variable, you can make score_chart a global constant called SCORE_CHART that is declared at the top of this file.

("A", "E", "I", "O", "U", "L", "N", "R", "S", "T"): 1,
("D", "G"): 2,
("B", "C", "M", "P"): 3,
("F", "H", "V", "W", "Y"): 4,
("K"): 5,
("J", "X"): 8,
("Q", "Z"): 10
}

return score_chart


def score_word(word):
pass
score_chart = build_score_chart()
score = 0

if 7 <= len(word) <= 10:
score += 8

for letters, points in score_chart.items():
for char in word.upper():
if char in letters:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since letters is a tuple, when you use the in operator you'll encounter linear time complexity.

To avoid increasing time complexity, you could refactor score_chart so that it is a dictionary where the keys are letters and the values are integers (neither keys or values would be list or tuple which both have linear time complexity for in).

If you refactor score_chart as suggested, then you can leverage a dictionary's constant time look up to see if a key (letter) is in the dict. That means your dictionary would be longer, but if you declare it as a constant global variable at the top of the file, it won't clutter your method here and it's worth having a longer dict, but quicker look up time.

score += points

return score


def get_highest_word_score(word_list):
pass
highest_word_score = ("", 0)

for word in word_list:
current_word_score = (word, score_word(word))
if current_word_score[1] > highest_word_score[1]:
highest_word_score = current_word_score

# tie breakers
if current_word_score[1] == highest_word_score[1]:
# if they're the same length pick the first word
if len(current_word_score[0]) == len(highest_word_score[0]):
continue

# or pick the word with a length of 10
elif len(current_word_score[0]) == 10:
highest_word_score = current_word_score

elif len(highest_word_score[0]) == 10:
continue

# or pick the word with the fewest letters
elif len(current_word_score[0]) < len(highest_word_score[0]):
highest_word_score = current_word_score

return highest_word_score

6 changes: 3 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def wave_1_run_game():
display_retry_instructions()
continue_input = input()
game_continue = continue_input == "y"

display_goodbye_message()

def wave_2_run_game():
Expand All @@ -33,7 +33,7 @@ def wave_2_run_game():
display_retry_instructions()
continue_input = input()
game_continue = continue_input == "y"

display_goodbye_message()

def wave_3_run_game():
Expand Down Expand Up @@ -101,4 +101,4 @@ def main(wave):
wave = int(args[1])
else:
wave = "ERROR"
main(wave)
main(wave)
4 changes: 3 additions & 1 deletion tests/test_wave_01.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@
'Y': 2,
'Z': 1
}

#@pytest.mark.skip()
def test_draw_letters_draws_ten():
# Arrange/Act
letters = draw_letters()

# Assert
assert len(letters) == 10

#@pytest.mark.skip()
def test_draw_letters_is_list_of_letter_strings():
# Arrange/Act
letters = draw_letters()
Expand All @@ -49,6 +50,7 @@ def test_draw_letters_is_list_of_letter_strings():
assert type(elem) == str
assert len(elem) == 1

#@pytest.mark.skip()
def test_letter_not_selected_too_many_times():

for i in range(1000):
Expand Down
5 changes: 5 additions & 0 deletions tests/test_wave_02.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from adagrams.game import uses_available_letters

#@pytest.mark.skip()
def test_uses_available_letters_true_word_in_letter_bank():
# Arrange
letters = ["D", "O", "G", "X", "X", "X", "X", "X", "X", "X"]
Expand All @@ -13,6 +14,7 @@ def test_uses_available_letters_true_word_in_letter_bank():
# Assert
assert is_valid == True

#@pytest.mark.skip()
def test_uses_available_letters_false_word_in_letter_bank():
# Arrange
letters = ["D", "O", "X", "X", "X", "X", "X", "X", "X", "X"]
Expand All @@ -24,6 +26,7 @@ def test_uses_available_letters_false_word_in_letter_bank():
# Assert
assert is_valid == False

#@pytest.mark.skip()
def test_uses_available_letters_false_word_overuses_letter():
# Arrange
letters = ["A", "X", "X", "X", "X", "X", "X", "X", "X", "X"]
Expand All @@ -35,6 +38,7 @@ def test_uses_available_letters_false_word_overuses_letter():
# Assert
assert is_valid == False

#@pytest.mark.skip()
def test_uses_available_letters_does_not_change_letter_bank():
# Arrange
letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
Expand All @@ -48,6 +52,7 @@ def test_uses_available_letters_does_not_change_letter_bank():
assert is_valid == True
assert letters == letters_copy

#@pytest.mark.skip()
def test_uses_available_letters_ignores_case():
# Arrange
letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
Expand Down
4 changes: 4 additions & 0 deletions tests/test_wave_03.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@

from adagrams.game import score_word

#@pytest.mark.skip()
def test_score_word_accurate():
# Assert
assert score_word("A") == 1
assert score_word("DOG") == 5
assert score_word("WHIMSY") == 17

#@pytest.mark.skip()
def test_score_word_accurate_ignores_case():
# Assert
assert score_word("a") == 1
assert score_word("dog") == 5
assert score_word("wHiMsY") == 17

#@pytest.mark.skip()
def test_score_zero_for_empty():
# Assert
assert score_word("") == 0

#@pytest.mark.skip()
def test_score_extra_points_for_seven_or_longer():
# Assert
assert score_word("XXXXXXX") == 64
Expand Down
7 changes: 7 additions & 0 deletions tests/test_wave_04.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from adagrams.game import score_word, get_highest_word_score

#@pytest.mark.skip()
def test_get_highest_word_score_accurate():
# Arrange
words = ["X", "XX", "XXX", "XXXX"]
Expand All @@ -14,6 +15,7 @@ def test_get_highest_word_score_accurate():
assert best_word[0] == "XXXX"
assert best_word[1] == 32

#@pytest.mark.skip()
def test_get_highest_word_score_accurate_unsorted_list():
# Arrange
words = ["XXX", "XXXX", "XX", "X"]
Expand All @@ -25,6 +27,7 @@ def test_get_highest_word_score_accurate_unsorted_list():
assert best_word[0] == "XXXX"
assert best_word[1] == 32

#@pytest.mark.skip()
def test_get_highest_word_tie_prefers_shorter_word():
# Arrange
words = ["MMMM", "WWW"]
Expand All @@ -38,6 +41,7 @@ def test_get_highest_word_tie_prefers_shorter_word():
assert best_word[0] == "WWW"
assert best_word[1] == 12

#@pytest.mark.skip()
def test_get_highest_word_tie_prefers_shorter_word_unsorted_list():
# Arrange
words = ["WWW", "MMMM"]
Expand All @@ -51,6 +55,7 @@ def test_get_highest_word_tie_prefers_shorter_word_unsorted_list():
assert best_word[0] == "WWW"
assert best_word[1] == 12

#@pytest.mark.skip()
def test_get_highest_word_tie_prefers_ten_letters():
# Arrange
words = ["AAAAAAAAAA", "BBBBBB"]
Expand All @@ -62,6 +67,7 @@ def test_get_highest_word_tie_prefers_ten_letters():
assert best_word[0] == "AAAAAAAAAA"
assert best_word[1] == 18

#@pytest.mark.skip()
def test_get_highest_word_tie_prefers_ten_letters_unsorted_list():
# Arrange
words = ["BBBBBB", "AAAAAAAAAA"]
Expand All @@ -73,6 +79,7 @@ def test_get_highest_word_tie_prefers_ten_letters_unsorted_list():
assert best_word[0] == "AAAAAAAAAA"
assert best_word[1] == 18

#@pytest.mark.skip()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the changes in your test files are just to comment out the skip test decorate, you don't technically need to add and commit these changes (but it doesn't hurt anything to do so). You could just add and commit the changes in game.py and that would make your pull request more concise with just the logical changes you made.

def test_get_highest_word_tie_same_length_prefers_first():
# Arrange
words = ["AAAAAAAAAA", "EEEEEEEEEE"]
Expand Down