What improvements can I make? Should I organize my functions based on A-Z? For example, find_letter function comes before guessing function.
import json
import random
import re
def find_letter(user_input, vocab, i=None):
return [i.start() for i in re.finditer(user_input, vocab)]
def guessing(tries,vocab,blank_line):
while tries > 0:
user_input = input("Guess:")
if user_input in vocab:
if not(user_input in blank_line):
index = find_letter(user_input=user_input,vocab=vocab)
i = 0
while i < len(index):
blank_line[index[i]] = user_input
i += 1
if not('_' in blank_line):
return "you won!!!"
else:
print("YOU ALREADY TRIED THAT LETTER!")
else:
tries -= 1
print(str(blank_line) + " Number of Tries : " + str(tries))
return "you lost"
def hangman_game(vocab):
'Preliminary Conditions'
print(introductory(vocab=vocab))
tries = num_of_tries(vocab=vocab)
blank_line = produce_blank_lines(vocab=vocab)
print(blank_line)
print(guessing(tries=tries,vocab=vocab,blank_line=blank_line))
def introductory(vocab):
return "HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. " \
"WORD COUNT : " + str(len(vocab))
def num_of_tries(vocab):
if len(vocab) < 7:
return 8
elif 7 <= len(vocab) < 10:
return 10
else:
return 14
def produce_blank_lines(vocab):
blank_line = []
for i in vocab:
if i.isalpha():
blank_line += "_"
elif i == " ":
blank_line += " "
else:
blank_line += i + " "
return blank_line
if __name__ == '__main__':
data = json.load(open("vocabulary.json"))
vocab = random.choice(list(data.keys())).lower()
print(vocab)
print(hangman_game(vocab=vocab))
Snippet of the JSON File
{"abandoned industrial site": ["Site that cannot be used for any purpose, being contaminated by pollutants."], "abandoned vehicle": ["A vehicle that has been discarded in the environment, urban or otherwise, often found wrecked, destroyed, damaged or with a major component part stolen or missing."], "abiotic factor": ["Physical, chemical and other non-living environmental factor."], "access road": ["Any street or narrow stretch of paved surface that leads to a specific destination, such as a main highway."], "access to the sea": ["The ability to bring goods to and from a port that is able to harbor sea faring vessels."], "accident": ["An unexpected, unfortunate mishap, failure or loss with the potential for harming human life, property or the environment.", "An event that happens suddenly or by chance without an apparent cause."], "accumulator": ["A rechargeable device for storing electrical energy in the form of chemical energy, consisting of one or more separate secondary cells.\\n(Source: CED)"], "acidification": ["Addition of an acid to a solution until the pH falls below 7."], "acidity": ["The state of being acid that is of being capable of transferring a hydrogen ion in solution."], "acidity degree": ["The amount of acid present in a solution, often expressed in terms of pH."], "acid rain": ["Rain having a pH less than 5.6."]
1 Answer 1
data = json.load(open("vocabulary.json"))
Unless Python 3 has changed this, I think you're leaking the open file handle here. What you want is more like
with open("vocabulary.json") as f:
data = json.load(f)
which IMHO is even a bit easier to read because it breaks up the mass of nested parentheses.
vocab = random.choice(list(data.keys())).lower()
Same deal with the nested parentheses here. We can save one pair by eliminating the unnecessary materialization into a list
. [EDIT: Graipher points out that my suggestion works only in Python 2. Oops!]
Also, vocab
(short for vocabulary, a set of words) isn't really what this variable represents. data.keys()
is our vocabulary; what we're computing on this particular line is a single vocabulary word. So:
word = random.choice(data.keys()).lower()
print(word)
print(hangman_game(word))
I can tell you're using Python 3 because of the ugly parentheses on these print
s. ;) But what's this? Function hangman_game
doesn't contain any return
statements! So it basically returns None
. Why would we want to print None
to the screen? We shouldn't be printing the result of hangman_game
; we should just call it and discard the None
result.
word = random.choice(data.keys()).lower()
print(word)
hangman_game(word)
def hangman_game(vocab):
Same comment about vocab
versus word
here.
'Preliminary Conditions'
This looks like you maybe forgot a print
? Or maybe you're trying to make a docstring? But single quotes don't make a docstring AFAIK, and even if they did, "Preliminary Conditions" is not a useful comment. I'd just delete this useless line.
print(introductory(vocab=vocab))
In all of these function calls, you don't need to write the argument's name twice. It's a very common convention that if you're calling a function with only one argument, the argument you pass it is precisely the argument it needs. ;) Also, even if the function takes multiple parameters, the reader will generally expect that you pass it the right number of arguments in the right order. You don't have to keyword-argument-ize each one of them unless you're doing something tricky that needs calling out. So:
print(introductory(word))
And then you can inline introductory
since it's only ever used in this one place and it's a one-liner already:
print(
"HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
"WORD COUNT : " + str(len(vocab))
)
Personally, I would avoid stringifying and string-concatenation in Python, and write simply
print(
"HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
"WORD COUNT : %d" % len(vocab)
)
I know plenty of working programmers who would write
print(
"HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
"WORD COUNT : {}".format(len(vocab))
)
instead. (I find that version needlessly verbose, but it's definitely a popular alternative.)
tries = num_of_tries(vocab=vocab)
blank_line = produce_blank_lines(vocab=vocab)
print(blank_line)
print(guessing(tries=tries,vocab=vocab,blank_line=blank_line))
It's odd that you abbreviated number_of_tries
to num_...
, but then left in the word of
. I would expect either number_of_tries
or num_tries
(or ntries
for the C programmers in the audience); num_of_tries
is in a weird no-man's-land.
It's weird that you have a function named produce_blank_lines
that produces actually a single blank_line
. It's even weirder that the value of blank_line
is not a constant "\n"
! This suggests that everything here is misnamed. You probably meant underscores = produce_underscores(word)
or even blanks = shape_of(word)
.
Finally, it is weird that blank_line
is not even a string, but rather a list
of characters.
I would replace this entire dance with something like
blanks = ''.join('_' if c.isalpha() else ch for ch in word)
or
blanks = re.sub('[A-Za-z]', '_', word)
This would lose your addition of a blank space after each non-space-non-alpha character, but IMHO that's actually an improvement over your version. (Your sample JSON doesn't show any examples of words containing non-space-non-alpha characters. My version certainly performs better for words like non-space
or Route 66
.)
if not(user_input in blank_line):
This would be idiomatically expressed (both in Python and in English) as
if user_input not in blank_line:
index = find_letter(user_input=user_input,vocab=vocab)
i = 0
while i < len(index):
blank_line[index[i]] = user_input
i += 1
You have another singular/plural problem here (just like with blank_line[s]
). It doesn't make sense to get the len
of a singular index
. Since the variable actually represents a list of indices, we should name it [list_of_]indices
.
Also, this style of for-loop is very un-Pythonic. In Python we can say simply
for i in find_letter(user_input, word):
blank_line[i] = user_input
And then we can inline the single-use find_letter
:
for m in re.finditer(user_input, word):
blank_line[m.start()] = user_input
(Using m
as the conventional name for a match object.)
And then we can eliminate the regex and just loop over the word
directly:
for i, ch in enumerate(word):
if ch == user_input:
blank_line[i] = user_input
or
for i in range(len(word)):
if word[i] == user_input:
blank_line[i] = user_input
In fact, by not using regexes, we fix a bug in your code: if the chosen vocabulary word contains .
as one of its non-space-non-alpha characters, a user who enters .
as their guess will win the game immediately! :D
Anyway, that's enough for one answer, I think.
-
2\$\begingroup\$ Unfortunately your second recommendation produces the error
TypeError: 'dict_keys' object does not support indexing
when trying to randomly choose a key, so the wrapping withlist
is needed. In Python 2 this worked since the keys were a list already. \$\endgroup\$Graipher– Graipher2018年12月27日 10:13:49 +00:00Commented Dec 27, 2018 at 10:13 -
\$\begingroup\$ But you can get rid of the
.keys()
,random.choice(list(data))
works. \$\endgroup\$Graipher– Graipher2018年12月27日 10:15:35 +00:00Commented Dec 27, 2018 at 10:15 -
2\$\begingroup\$ And nowadays
f"WORD COUNT : {len(vocab)}"
is all the rage, which is at least shorter again :) \$\endgroup\$Graipher– Graipher2018年12月27日 10:17:40 +00:00Commented Dec 27, 2018 at 10:17 -
2\$\begingroup\$ Ah, I mistakenly thought
random.choice
could handle any iterable. If OP had writtenrandom.choice(list(data))
I probably would have complained that that approach was too "cute" and not expressive enough. ;) The problem of arandom.choice
that works on arbitrary iterables is discussed here: stackoverflow.com/questions/12581437/… \$\endgroup\$Quuxplusone– Quuxplusone2018年12月27日 16:09:03 +00:00Commented Dec 27, 2018 at 16:09 -
\$\begingroup\$ If I have many variables to concatenate in a string, isn't it better to use +. But if I only have a variable to concatenate at the end of the string, then is it better to use %s or %d? \$\endgroup\$austingae– austingae2018年12月28日 18:21:53 +00:00Commented Dec 28, 2018 at 18:21
hangman.py
; or once that gets too big, you'd put everything related to the user interface inhangman-ui.py
and everything related to core gameplay inhangman-core.py
; or once that gets too big, you'd split outhangman-dictionary.py
into its own file... And then sure, I'd probably sort those files alphabetically in my manifest or Makefile or whatever. \$\endgroup\$introductory()
) unless I already know how to spell it! And if I know how to spell it (say, my IDE has a sidebar index of all the functions in the current file) then I likely don't care where in the file it is (since I can jump there using my IDE). But everyone has an "IDE" for the filesystem (ls
), and the filenames themselves make a rough "index by topic" — dictionary problem solved! \$\endgroup\$