Refactored not to use SQLite; we lose anagrams, but crossword and hangman become much easier to use.

This commit is contained in:
Jeremy Fincher 2004-07-27 17:43:17 +00:00
parent 43013d4cc0
commit 82161ee386

View File

@ -39,83 +39,59 @@ import supybot
import supybot.plugins as plugins import supybot.plugins as plugins
import os import os
import re
import copy import copy
import string
import time import time
import random import random
import string
import sqlite
import supybot.conf as conf import supybot.conf as conf
import supybot.utils as utils import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry import supybot.registry as registry
import supybot.callbacks as callbacks
conf.registerPlugin('Words')
conf.registerGlobalValue(conf.supybot.plugins.Words, 'file',
conf.DataFilename('words', """Determines what file in your data directory
will be used by this plugin as its list of words."""))
conf.registerGroup(conf.supybot.plugins.Words, 'hangman')
conf.registerChannelValue(conf.supybot.plugins.Words.hangman, 'maxTries',
registry.Integer(6, """Determines how many oppurtunities users will have to
guess letters in the hangman game."""))
conf.registerChannelValue(conf.supybot.plugins.Words.hangman, 'prefix',
registry.StringWithSpaceOnRight('-= HANGMAN =- ', """Determines what prefix
string is placed in front of hangman-related messages sent to the
channel."""))
conf.registerChannelValue(conf.supybot.plugins.Words.hangman, 'timeout',
registry.Integer(300, """Determines how long a game must be idle before it
will be replaced with a new game."""))
class WordsDB(plugins.DBHandler): def wordsFile():
def makeDb(self, filename): return file(conf.supybot.plugins.Words.file())
if os.path.exists(filename):
db = sqlite.connect(filename)
else:
db = sqlite.connect(filename, converters={'bool': bool})
cursor = db.cursor()
cursor.execute("""CREATE TABLE words (
id INTEGER PRIMARY KEY,
word TEXT UNIQUE ON CONFLICT IGNORE,
sorted_word_id INTEGER)""")
cursor.execute("""CREATE TABLE sorted_words (
id INTEGER PRIMARY KEY,
word TEXT UNIQUE ON CONFLICT IGNORE)""")
cursor.execute("""CREATE INDEX sorted_word_id
ON words (sorted_word_id)""")
cursor.execute("""CREATE INDEX sorted_words_word
ON sorted_words (word)""")
db.commit()
return db
def addWord(db, word, commit=False):
word = word.strip().lower()
if word.translate(string.ascii, string.ascii_letters):
raise ValueError, 'Invalid word: %r' % word
L = list(word)
L.sort()
sorted = ''.join(L)
cursor = db.cursor()
cursor.execute("""INSERT INTO sorted_words VALUES (NULL, %s)""", sorted)
cursor.execute("""INSERT INTO words VALUES (NULL, %s,
(SELECT id FROM sorted_words
WHERE word=%s))""", word, sorted)
if commit:
db.commit()
class HangmanGame: class HangmanGame:
def __init__(self): def __init__(self):
self.timeout = 0
self.timeGuess = 0
self.tries = 0 self.tries = 0
self.guess = ''
self.prefix = '' self.prefix = ''
self.guessed = False
self.unused = '' self.unused = ''
self.hidden = '' self.hidden = ''
self.guess = '' self.timeout = 0
self.timeGuess = 0
self.guessed = False
def getWord(self, dbHandler): def getWord(self):
db = dbHandler.getDb() fd = wordsFile()
cursor = db.cursor() try:
cursor.execute("""SELECT word FROM words ORDER BY random() LIMIT 1""") return random.choice(fd).strip().lower()
if cursor.rowcount != 0: finally:
word = cursor.fetchone()[0] fd.close()
return word.lower()
else:
raise callbacks.Error, 'My words database is currently empty.'
def letterPositions(self, letter, word): def letterPositions(self, letter, word):
""" """Returns a list containing the positions of letter in word."""
Returns a list containing the positions of letter in word.
"""
lst = [] lst = []
for (i, c) in enumerate(word): for (i, c) in enumerate(word):
if c == letter: if c == letter:
@ -135,101 +111,55 @@ class HangmanGame:
newWord.append(c) newWord.append(c)
return ''.join(newWord) return ''.join(newWord)
def triesLeft(self, n): def triesLeft(self, n=None):
""" """
Returns the number of tries and the correctly pluralized try/tries Returns the number of tries and the correctly pluralized try/tries
""" """
if n is None:
n = self.tries
return utils.nItems('try', n) return utils.nItems('try', n)
def letterArticle(self, letter): def letterArticle(self, letter):
""" """Returns 'a' or 'an' to match the letter that will come after."""
Returns 'a' or 'an' to match the letter that will come after
"""
anLetters = 'aefhilmnorsx' anLetters = 'aefhilmnorsx'
if letter in anLetters: if letter in anLetters:
return 'an' return 'an'
else: else:
return 'a' return 'a'
conf.registerPlugin('Words')
conf.registerChannelValue(conf.supybot.plugins.Words, 'hangmanMaxTries',
registry.Integer(6, """Determines how many oppurtunities users will have to
guess letters in the hangman game."""))
conf.registerChannelValue(conf.supybot.plugins.Words, 'hangmanPrefix',
registry.StringWithSpaceOnRight('-= HANGMAN -= ', """Determines what prefix
string is placed in front of hangman-related messages sent to the
channel."""))
conf.registerChannelValue(conf.supybot.plugins.Words, 'hangmanTimeout',
registry.Integer(300, """Determines how long a game must be idle before it
will be replaced with a new game."""))
class Words(callbacks.Privmsg): class Words(callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
dataDir = conf.supybot.directories.data()
self.dbHandler = WordsDB(os.path.join(dataDir, 'Words'))
def add(self, irc, msg, args):
"""<word> [<word>]
Adds a word or words to the database of words. This database is used
for the other commands in this plugin.
"""
if not args:
raise callbacks.ArgumentError
for word in args:
if word.translate(string.ascii, string.ascii_letters):
irc.error('Word must contain only letters.')
return
else:
addWord(self.dbHandler.getDb(), word, commit=True)
irc.replySuccess()
def crossword(self, irc, msg, args): def crossword(self, irc, msg, args):
"""<word> """<word>
Gives the possible crossword completions for <word>; use underscores Gives the possible crossword completions for <word>; use underscores
('_') to denote blank spaces. ('_') to denote blank spaces.
""" """
# XXX: Should we somehow disable this during a hangman game?
word = privmsgs.getArgs(args).lower() word = privmsgs.getArgs(args).lower()
db = self.dbHandler.getDb() word = re.escape(word)
cursor = db.cursor() word = word.replace('\\_', '_') # Stupid re.escape escapes underscores!
if '%' in word: word = word.replace('_', '.')
irc.error('"%" isn\'t allowed in the word.') word = '^%s$' % word
return wordRe = re.compile(word, re.I)
cursor.execute("""SELECT word FROM words words = []
WHERE word LIKE %s fd = wordsFile()
ORDER BY word""", word) try:
words = [t[0] for t in cursor.fetchall()] for line in fd:
line = line.strip()
if wordRe.match(line):
words.append(line)
if len(words) > 100:
irc.reply('More than 100 words matched.')
return
finally:
fd.close()
if words: if words:
words.sort()
irc.reply(utils.commaAndify(words)) irc.reply(utils.commaAndify(words))
else: else:
irc.reply('No matching words were found.') irc.reply('No matching words were found.')
def anagram(self, irc, msg, args):
"""<word>
Using the words database, determines if a word has any anagrams.
"""
word = privmsgs.getArgs(args).strip().lower()
db = self.dbHandler.getDb()
cursor = db.cursor()
L = list(word.lower())
L.sort()
sorted = ''.join(L)
cursor.execute("""SELECT id FROM sorted_words WHERE word=%s""", sorted)
if cursor.rowcount == 0:
irc.reply('That word has no anagrams I could find.')
else:
id = cursor.fetchone()[0]
cursor.execute("""SELECT words.word FROM words
WHERE sorted_word_id=%s""", id)
if cursor.rowcount > 1:
words = [t[0] for t in cursor.fetchall()]
irc.reply(utils.commaAndify(words))
else:
irc.reply('That word has no anagrams I could find.')
### ###
# HANGMAN # HANGMAN
### ###
@ -237,7 +167,7 @@ class Words(callbacks.Privmsg):
validLetters = list(string.ascii_lowercase) validLetters = list(string.ascii_lowercase)
def endGame(self, channel): def endGame(self, channel):
self.games[channel] = None del self.games[channel]
def letters(self, irc, msg, args): def letters(self, irc, msg, args):
"""[<channel>] """[<channel>]
@ -250,9 +180,13 @@ class Words(callbacks.Privmsg):
if channel in self.games: if channel in self.games:
game = self.games[channel] game = self.games[channel]
if game is not None: if game is not None:
irc.reply('%s%s' % (game.prefix, ' '.join(game.unused))) self._hangmanReply(irc, channel, ' '.join(game.unused))
return return
irc.error('There is no hangman game going on right now.') irc.error('There is currently no hangman game in %s.' % channel)
def _hangmanReply(self, irc, channel, s):
s = self.registryValue('hangman.prefix', channel=channel) + s
irc.reply(s, prefixName=False)
def hangman(self, irc, msg, args): def hangman(self, irc, msg, args):
"""[<channel>] """[<channel>]
@ -268,31 +202,30 @@ class Words(callbacks.Privmsg):
if self.games[channel] is None: if self.games[channel] is None:
self.games[channel] = HangmanGame() self.games[channel] = HangmanGame()
game = self.games[channel] game = self.games[channel]
game.timeout = self.registryValue('hangmanTimeout', channel) game.timeout = self.registryValue('hangman.timeout', channel)
game.timeGuess = time.time() game.timeGuess = time.time()
game.tries = self.registryValue('hangmanMaxTries', channel) game.tries = self.registryValue('hangman.maxTries', channel)
game.prefix = self.registryValue('hangmanPrefix', channel) game.prefix = self.registryValue('hangman.prefix', channel)
game.guessed = False game.guessed = False
game.unused = copy.copy(self.validLetters) game.unused = copy.copy(self.validLetters)
game.hidden = game.getWord(self.dbHandler) game.hidden = game.getWord()
game.guess = '_' * len(game.hidden) game.guess = '_' * len(game.hidden)
irc.reply('%sOkay ladies and gentlemen, you have ' self._hangmanReply(irc, channel,
'a %s-letter word to find, you have %s!' % 'Okay ladies and gentlemen, you have '
(game.prefix, len(game.hidden), 'a %s-letter word to find, you have %s!' %
game.triesLeft(game.tries)), prefixName=False) (len(game.hidden), game.triesLeft()))
# So, a game is going on, but let's see if it's timed out. If it is # So, a game is going on, but let's see if it's timed out. If it is
# we create a new one, otherwise we inform the user # we create a new one, otherwise we inform the user
else: else:
game = self.games[channel] game = self.games[channel]
secondsEllapsed = time.time() - game.timeGuess secondsElapsed = time.time() - game.timeGuess
if secondsEllapsed > game.timeout: if secondsElapsed > game.timeout:
self.endGame(channel) self.endGame(channel)
self.hangman(irc, msg, args) self.hangman(irc, msg, args)
else: else:
irc.error('Sorry, there is already a game going on. ' irc.reply('Sorry, there is already a game going on. '
'%s left before timeout.' % \ '%s left before the game times out.' % \
utils.nItems('second', utils.timeElapsed(game.timeout - secondsElapsed))
int(game.timeout - secondsEllapsed)))
def guess(self, irc, msg, args): def guess(self, irc, msg, args):
"""[<channel>] <letter|word> """[<channel>] <letter|word>
@ -314,18 +247,19 @@ class Words(callbacks.Privmsg):
if letter in game.unused: if letter in game.unused:
del game.unused[game.unused.index(letter)] del game.unused[game.unused.index(letter)]
if letter in game.hidden: if letter in game.hidden:
irc.reply('%sYes, there is %s %r' % (game.prefix, self._hangmanReply(irc, channel,
game.letterArticle(letter), letter), prefixName=False) 'Yes, there is %s %r.' %
(game.letterArticle(letter), letter))
game.guess = game.addLetter(letter, game.guess, game.guess = game.addLetter(letter, game.guess,
game.letterPositions(letter, game.hidden)) game.letterPositions(letter,
game.hidden))
if game.guess == game.hidden: if game.guess == game.hidden:
game.guessed = True game.guessed = True
else: else:
irc.reply('%sNo, there is no %s' % (game.prefix,`letter`), self._hangmanReply(irc, channel, 'No, there is no %r.' % letter)
prefixName=False)
game.tries -= 1 game.tries -= 1
irc.reply('%s%s (%s left)' % (game.prefix, game.guess, self._hangmanReply(irc, channel,
game.triesLeft(game.tries)), prefixName=False) '%s (%s left)' % (game.guess, game.triesLeft()))
# User input a valid character that has already been tried # User input a valid character that has already been tried
elif letter in self.validLetters: elif letter in self.validLetters:
irc.error('That letter has already been tried.') irc.error('That letter has already been tried.')
@ -338,24 +272,25 @@ class Words(callbacks.Privmsg):
if letter == game.hidden: if letter == game.hidden:
game.guessed = True game.guessed = True
else: else:
irc.reply('%syou did not guess the correct word ' self._hangmanReply(irc, channel,
'and you lose a try' % game.prefix, prefixName=False) 'You did not guess the word, so you '
'lose a try.')
game.tries -= 1 game.tries -= 1
else: else:
# User input an invalid character # User input an invalid character
if len(letter) == 1: if len(letter) == 1:
irc.error('That is not a valid character.') irc.error('That is not a valid letter.')
# User input an invalid word (len(try) != len(hidden)) # User input an invalid word (len(try) != len(hidden))
else: else:
irc.error('That is not a valid word guess.') irc.error('That is not a valid word guess.')
# Verify if the user won or lost # Verify if the user won or lost
if game.guessed and game.tries > 0: if game.guessed and game.tries > 0:
irc.reply('%sYou win! The word was indeed %s' % self._hangmanReply(irc, channel,
(game.prefix, game.hidden), prefixName=False) 'You win! The was indeed %r.' % game.hidden)
self.endGame(channel) self.endGame(channel)
elif not game.guessed and game.tries == 0: elif not game.guessed and game.tries == 0:
irc.reply('%sYou lose! The word was %s' % self._hangmanReply(irc, channel,
(game.prefix, game.hidden), prefixName=False) 'You lose! The was %r.' % game.hidden)
self.endGame(channel) self.endGame(channel)
### ###
# END HANGMAN # END HANGMAN