Programming Forums
User Name Password Register
 

RSS Feed
FORUM INDEX | TODAY'S POSTS | UNANSWERED THREADS | ADVANCED SEARCH

Reply
 
Thread Tools Display Modes
Old Jan 22nd, 2006, 4:21 PM   #1
Jessehk
The Oblivious One
 
Jessehk's Avatar
 
Join Date: May 2005
Location: Ontario, Canada
Posts: 644
Rep Power: 4 Jessehk is on a distinguished road
[Python] Simple Hangman game

I'm still learning and experimenting with Python, and in the process, I made a little hangman game.

As I am still new to Python, comments would be welcome.

Here is the source:


#!/usr/bin/python

class Hangman:
	def __init__(self, word):
		self.word = word
		self.progress = '_' * len(word)
		self.guessed = []
		self.attempts = 0
		self.chances = 10
	
	def guess_letter(self, letter):
		"""Guess a letter

		Guess a letter towards the word. Returns True on a good guess, and False
		otherwise
		"""
		
		self.guessed.sort()
		letter = letter.lower()
		
		if len(letter) != 1:
			raise ValueError("Input must be a single letter")
		
		if letter in self.guessed:
			print "You already guessed the letter \'%s\'" % letter
			return False
		
		else:
			self.guessed.append(letter) #add the letter to the list of guessed letters
			self.attempts += 1 #increment the number of attempts made

			if letter in self.word:	
				#with word: "joe", would create [(j, 0), (o, 1), (e, 2)]
				positions = [(l, p) for l, p in zip(self.word, range(len(self.word)))]

				#x cycles through the positions of the letters that match the word
				for x in [p for l, p in positions if l == letter]:
					#add the letter to self.progess
					if x == 0:
						self.progress = letter + self.progress[1:]
					else:
						self.progress = self.progress[:x] + letter + self.progress[x + 1:]

				return True
			
			else:
				self.chances -= 1
				return False
	
	def check_winner(self):
		"""Check for a winner

		Returns True if there is a winner, False otherwise
		"""
		return self.word == self.progress


def main():
	import random
	import sys
	
	try:
		words = open("words.txt", 'r')
		count = 0

		for line in words:
			count += 1

		words.seek(0)
		wordlist = words.readlines()
		word = wordlist[random.randint(1, count)] #fetch a random word from the word file
		word = word[:len(word) - 1] #get rid of space at the end of the word
		
	except IOError, ioe:
		print "Necessary file \"words.txt\" not found."
		sys.exit()
	
	else:
		h = Hangman(word)
		while h.chances  > -1:
			try:
				print "\n\n\t%s" % h.progress
				
				if h.check_winner():
					print "Excellent! You guessed the secret word in %d attempts!" % h.attempts
					sys.exit()

				print "\nGuessed letters", h.guessed
				print "Bad guesses left: ", h.chances
				
				letter = raw_input("\n\tEnter letter to guess: ")
				if h.guess_letter(letter):
					print "\nGood guess!"
					continue
				else:
					print "\nBad Guess. Try again."
				
			except ValueError, ve:
				print ve
				continue

		print "\n\nYou ran out of guesses! The word was \"%s\"" % h.word
		sys.exit()
		
if __name__ == "__main__":
	main()

And the words.txt file which the program gets its words from:

http://hakuch.tripod.com/jesse/words.txt
__________________
Dr. Zoidberg: [ecstatic] I'm going to a movie... with FRIENDS!
Jessehk is offline   Reply With Quote
Old Jan 22nd, 2006, 5:00 PM   #2
Arevos
Programming Guru
 
Arevos's Avatar
 
Join Date: Aug 2005
Location: England
Posts: 1,499
Rep Power: 5 Arevos is on a distinguished road
Quote:
Originally Posted by Jessehk
As I am still new to Python, comments would be welcome.
It's a very good program for someone new to Python! But there are a few improvements that could be made. Firstly, there are a few functions in the standard python library that could help you out, and secondly, you don't need to keep a separate 'count' for arrays.

For instance, take the file loading:
words = open("words.txt", 'r')
count = 0

for line in words:
    count += 1

words.seek(0)
wordlist = words.readlines()
word = wordlist[random.randint(1, count)] #fetch a random word from the word file
word = word[:len(word) - 1] #get rid of space at the end of the word
Lists in Python have length, so there's no need to use count when you could use len(wordlist).

The functions random.choice and string.strip can also cut down your workload: random.choice will select an item at random from a list, and str.strip will strip a word of beginning and ending whitespace.

With this in mind, your above code can be written:
file = open("words.txt")
word = random.choice(file.readlines()).strip()
file.close()
The other main part of the program is the part that handles a user inputting a letter:
self.guessed.append(letter) #add the letter to the list of guessed letters
self.attempts += 1 #increment the number of attempts made

if letter in self.word:	
    #with word: "joe", would create [(j, 0), (o, 1), (e, 2)]
    positions = [(l, p) for l, p in zip(self.word, range(len(self.word)))]

    #x cycles through the positions of the letters that match the word
    for x in [p for l, p in positions if l == letter]:
        #add the letter to self.progess
        if x == 0:
            self.progress = letter + self.progress[1:]
        else:
            self.progress = self.progress[:x] + letter + self.progress[x + 1:]

        return True
			
else:
    self.chances -= 1
    return False
Your use of zip is clever, but in Python 2.4 there's the handy "enumerate" function that does almost the same thing.

Secondly, you don't need self.attempts when you have len(self.guessed).

And finally, if self.progess was a list, you'd have things a bit easier
self.guessed.append(letter)

if letter in self.word:	
    for p, l in enumerate(self.word):
        if l == letter:
            self.progress[p] = letter
    return True
			
else:
    self.chances -= 1
    return False
Then, to convert self.progress into a string:
"".join(self.progress)
Also, you might consider using exceptions to handle errors in user input - that's what they're there for, after all. You use this approach when the user enters in more than one letter:
if len(letter) != 1:
    raise ValueError("Input must be a single letter")
But then give up on this approach and and print out the error straight from the function when the user enters a letter twice.
if letter in self.guessed:
    print "You already guessed the letter \'%s\'" % letter
    return False
If you don't think that ValueError is appropriate here, you can always roll your own:
class RepeatError(Exception):
    "Raised when a user enters a letter twice."
Or just use ValueError again. Exceptions are good, because they don't tie your classes to a particular form of output. Separating the functionality in your program is a good habit to get into. That way, if something goes wrong, it's usually all in the same place. And it means that programs can quickly be adapted; for instance, changing your CLI-based hangman game to a GUI one
Arevos is offline   Reply With Quote
Old Jan 22nd, 2006, 5:24 PM   #3
Jessehk
The Oblivious One
 
Jessehk's Avatar
 
Join Date: May 2005
Location: Ontario, Canada
Posts: 644
Rep Power: 4 Jessehk is on a distinguished road
Thanks for the great advice Arevos. I'll definitely be using it.
__________________
Dr. Zoidberg: [ecstatic] I'm going to a movie... with FRIENDS!
Jessehk is offline   Reply With Quote
Old Jan 23rd, 2006, 10:50 PM   #4
Jessehk
The Oblivious One
 
Jessehk's Avatar
 
Join Date: May 2005
Location: Ontario, Canada
Posts: 644
Rep Power: 4 Jessehk is on a distinguished road
I took the advice of Arevos and made a few changes.

Enjoy! (its actually kind-of fun :p)

Note: change the directory on line 89 to suit your system, or the program will not work.

and the location of words.txt is given at the beginning of this thread.

#!/usr/bin/python

#Copyright 2006 H-K, Jesse E.
#
#This program is free software; you can redistribute it and/or modify 
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

class Hangman:
	"""A game of Hangman
	
	This class provides the means to play a simple
	game of hangman. The game is text-based, where properties
	such as a list of bad letters, and the number of guesses
	are supplied by this class
	
	"""
	class RepeatLetterError(ValueError):
		""""Raised when a repeat letter is guessed"""
		def __init__(self, what):
			self.message = str(what)
		
		def __str__(self):
			return self.message
		
	def __init__(self, word):
		self.word = word
		self.progress = ['_' for x in range(len(self.word))]
		self.guessed = []
		self.chances = 8
	
	def guess_letter(self, letter):
		"""Guess a letter

		Guess a letter towards the word. Returns True on a good guess, and False
		otherwise

		Raises:
			ValueError --> when input is not a single letter.
			Hangman.RepeatLetterError --> when letter has been previously guessed.
		"""
		
		letter = letter.lower()
		
		if len(letter) != 1:
			raise ValueError("Input must be a single letter.")
		
		if letter in self.guessed:
			raise Hangman.RepeatLetterError("You already guessed the letter \'%s\'." % letter)
		
		else: #no problems with input...
			self.guessed.append(letter) #add the letter to the list of guessed letters
			self.guessed.sort(); #sort the guessed letters alphabetically
			if letter in self.word:	
				for p, l in enumerate(self.word): #word = "apple": [(0, a), (1, p), (2, p), (3, l), (4, e)]
					if l == letter:
						self.progress[p] = l
						

				return True #good guess
			
			else: #the guessed letter is not in the word...
				self.chances -= 1 #decrement the number of bad guesses left
				return False #bad guess
	
	def check_winner(self):
		"""Check for a winner

		Returns True if there is a winner, False otherwise
		"""
		return self.word == ("".join(self.progress))


def main():
	import random
	import sys
	
	try:
		words = open("/home/jesse/bin/hangman-files/words.txt", 'r')
		word = (random.choice(words.readlines())).strip()
		
	except IOError, ioe:
		print "Necessary file \"words.txt\" not found."
		sys.exit()
	
	else:
		h = Hangman(word)
		while h.chances  > -1:
			try:
				print "\n\n\t\t\t%s" % " ".join(h.progress)
				
				if h.check_winner():
					print "\nExcellent! You guessed the secret word with %d chances left!" % h.chances
					sys.exit()

				print "\n%20s" % "Bad letters:" , h.guessed
				print "\n%20s" % "Bad guesses left:", h.chances
				
				letter = raw_input("\n\tEnter letter to guess: ")
				if h.guess_letter(letter):
					print "\nGood guess!"
					continue
				else:
					print "\nBad Guess. Try again."
				
			except ValueError, ve:
				print ve
				continue

			except Hangman.RepeatLetterError, rle:
				print rle
				continue
				

		print "\n\nYou ran out of guesses! The word was \"%s\"" % h.word
		sys.exit()
		
if __name__ == "__main__":
	main()
__________________
Dr. Zoidberg: [ecstatic] I'm going to a movie... with FRIENDS!
Jessehk is offline   Reply With Quote
Old Jan 24th, 2006, 2:54 AM   #5
Arevos
Programming Guru
 
Arevos's Avatar
 
Join Date: Aug 2005
Location: England
Posts: 1,499
Rep Power: 5 Arevos is on a distinguished road
Looking good - there's not really much more that could be improved on - though there are a few small tricks you could use to reduce the length of your code further.

Your RepeatLetterError inherits from ValueError, so it already takes in a message and returns it through "__str__". I believe you can change your RepeatLetterError to just this, and it'll do exactly the same thing (correct me if I'm wrong!):
class RepeatLetterError(ValueError):
	"Raised when a repeat letter is guessed"
Next, a handy thing to know is that "for x in string" will iterate over the letters in a string. So you can change your progress assignment to:
self.progress = ['_' for x in self.word]
The only other things I might suggest changing is the use of sys.exit() (you might want to use 'return' instead), and when you catch your raised exceptions:
except ValueError, ve:
	print ve
	continue

except Hangman.RepeatLetterError, rle:
	print rle
	continue
Because Hangman.RepeatLetterError inherits from ValueError, you don't need to have two 'excepts'. Both RepeatLetterError and ValueError will be caught by the first condition.

But these are pretty minor things
Arevos is offline   Reply With Quote
Old Jan 24th, 2006, 8:36 AM   #6
Jessehk
The Oblivious One
 
Jessehk's Avatar
 
Join Date: May 2005
Location: Ontario, Canada
Posts: 644
Rep Power: 4 Jessehk is on a distinguished road
A lot of the things you mentioned involve me getting out of the C++ mindset.

for example, to inherit from std::exception:

#include <exception>
#include <string>

class BadLetterError : public std::exception
{
     pivate:
          std::string message;
     public:
          BadLetterError(const std::string &str)
               : std::exception(), message(str) {}
          virtual const char *what() const throw() {return message.c_str();} 
          ~BadLetterError() throw() {}
};

I have to remember that Python has no private members, and therefore, everything is inherited.

Thanks again.
__________________
Dr. Zoidberg: [ecstatic] I'm going to a movie... with FRIENDS!

Last edited by Jessehk; Jan 24th, 2006 at 8:49 AM.
Jessehk is offline   Reply With Quote
Reply

Bookmarks

« Previous Thread in Forum | Next Thread in Forum »

Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off
Forum Jump




DaniWeb IT Discussion Community
All times are GMT -5. The time now is 11:50 PM.

Powered by vBulletin® Version 3.7.0, Copyright ©2000 - 2008, Jelsoft Enterprises Ltd.
Copyright ©2007 DaniWeb® LLC