Wordgames -> WordGames

This commit is contained in:
oddluck 2020-03-01 08:18:43 +00:00
parent 366da60c58
commit fe6541cac0
2 changed files with 75 additions and 77 deletions

View File

@ -39,31 +39,31 @@ def configure(advanced):
# user or not. You should effect your configuration by manipulating the # user or not. You should effect your configuration by manipulating the
# registry as appropriate. # registry as appropriate.
from supybot.questions import expect, anything, something, yn from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Wordgames', True) conf.registerPlugin('WordGames', True)
Wordgames = conf.registerPlugin('Wordgames') WordGames = conf.registerPlugin('WordGames')
conf.registerGlobalValue(Wordgames, 'wordFile', conf.registerGlobalValue(WordGames, 'wordFile',
registry.String('/usr/share/dict/american-english', registry.String('/usr/share/dict/american-english',
'Path to the dictionary file.')) 'Path to the dictionary file.'))
conf.registerGlobalValue(Wordgames, 'wordRegexp', conf.registerGlobalValue(WordGames, 'wordRegexp',
registry.String('^[a-z]+$', registry.String('^[a-z]+$',
'Regular expression defining what a valid word looks ' + 'Regular expression defining what a valid word looks ' +
'like (i.e. ignore proper names, contractions, etc. ' + 'like (i.e. ignore proper names, contractions, etc. ' +
'Modify this if you need to allow non-English chars.')) 'Modify this if you need to allow non-English chars.'))
conf.registerGlobalValue(Wordgames, 'worddleDelay', conf.registerGlobalValue(WordGames, 'boggleDelay',
registry.NonNegativeInteger(15, registry.NonNegativeInteger(15,
'Delay (in seconds) before a Worddle game ' + 'Delay (in seconds) before a Boggle game ' +
'begins.')) 'begins.'))
conf.registerGlobalValue(Wordgames, 'worddleDuration', conf.registerGlobalValue(WordGames, 'boggleDuration',
registry.NonNegativeInteger(90, registry.NonNegativeInteger(120,
'Duration (in seconds) of a Worddle game ' + 'Duration (in seconds) of a Boggle game ' +
'(not including the initial delay).')) '(not including the initial delay).'))
conf.registerGlobalValue(Wordgames, 'worddleDifficulty', conf.registerGlobalValue(WordGames, 'boggleDifficulty',
registry.String('easy', 'Default difficulty for Worddle games.')) registry.String('easy', 'Default difficulty for Boggle games.'))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -62,13 +62,13 @@ GRAY = '\x0314'
LGRAY = '\x0315' LGRAY = '\x0315'
def debug(message): def debug(message):
log.debug('Wordgames: ' + message) log.debug('WordGames: ' + message)
def info(message): def info(message):
log.info('Wordgames: ' + message) log.info('WordGames: ' + message)
def error(message): def error(message):
log.error('Wordgames: ' + message) log.error('WordGames: ' + message)
def point_str(value): def point_str(value):
"Return 'point' or 'points' depending on value." "Return 'point' or 'points' depending on value."
@ -97,7 +97,7 @@ def get_max_targets(irc):
error('Detecting max targets: %s. Using default (1).' % str(e)) error('Detecting max targets: %s. Using default (1).' % str(e))
return result return result
class WordgamesError(Exception): pass class WordGamesError(Exception): pass
class Difficulty: class Difficulty:
EASY = 0 EASY = 0
@ -117,9 +117,9 @@ class Difficulty:
try: try:
return Difficulty.VALUES[Difficulty.NAMES.index(name)] return Difficulty.VALUES[Difficulty.NAMES.index(name)]
except ValueError: except ValueError:
raise WordgamesError('Unrecognized difficulty value: %s' % name) raise WordGamesError('Unrecognized difficulty value: %s' % name)
class Wordgames(callbacks.Plugin): class WordGames(callbacks.Plugin):
"Please see the README file to configure and use this plugin." "Please see the README file to configure and use this plugin."
def inFilter(self, irc, msg): def inFilter(self, irc, msg):
@ -135,16 +135,16 @@ class Wordgames(callbacks.Plugin):
game.guess(msg.nick, msg.args[1]) game.guess(msg.nick, msg.args[1])
return None return None
except: except:
return pass
# In all other cases, default to normal message handling # In all other cases, default to normal message handling
return self.parent.inFilter(irc, msg) return msg
def __init__(self, irc): def __init__(self, irc):
# Tech note: Save a reference to my parent class because Supybot's # Tech note: Save a reference to my parent class because Supybot's
# Owner plugin will reload this module BEFORE calling die(), which # Owner plugin will reload this module BEFORE calling die(), which
# means super() calls will fail with a TypeError. I consider this a # means super() calls will fail with a TypeError. I consider this a
# bug in Supybot. # bug in Supybot.
self.parent = super(Wordgames, self) self.parent = super(WordGames, self)
self.parent.__init__(irc) self.parent.__init__(irc)
self.games = {} self.games = {}
@ -170,55 +170,53 @@ class Wordgames(callbacks.Plugin):
irc.reply('No game is currently running.') irc.reply('No game is currently running.')
wordsolve = wrap(wordsolve, ['channel']) wordsolve = wrap(wordsolve, ['channel'])
def worddle(self, irc, msgs, args, channel, command): def boggle(self, irc, msgs, args, channel, command):
"""[command] """[command]
Play a Worddle game. Commands: [easy|medium|hard|evil | stop|stats] Play a game of Boggle. Commands: [easy|medium|hard|evil] [stop|stats]
(default: start with configured difficulty). (default: start with configured difficulty).
""" """
try: try:
# Allow deprecated 'join' command: # Allow deprecated 'join' command:
if not command or command == 'join' or command in Difficulty.NAMES: if not command or command == 'join' or command in Difficulty.NAMES:
difficulty = Difficulty.value( difficulty = Difficulty.value(
self.registryValue('worddleDifficulty')) self.registryValue('boggleDifficulty'))
if command in Difficulty.NAMES: if command in Difficulty.NAMES:
difficulty = Difficulty.value(command) difficulty = Difficulty.value(command)
game = self.games.get(channel) game = self.games.get(channel)
if game and game.is_running(): if game and game.is_running():
if game.__class__ == Worddle: if game.__class__ == Boggle:
if command: if command:
irc.reply('Joining the game. (Ignored "%s".)' % irc.reply('Joining the game. (Ignored "%s".)' %
command) command)
game.join(msgs.nick) game.join(msgs.nick)
else: else:
irc.reply('Current word game is not Worddle!') irc.reply('Current word game is not Boggle!')
else: else:
delay = self.registryValue('worddleDelay') delay = self.registryValue('boggleDelay')
duration = self.registryValue('worddleDuration') duration = self.registryValue('boggleDuration')
self._start_game(Worddle, irc, channel, msgs.nick, self._start_game(Boggle, irc, channel, msgs.nick,
delay, duration, difficulty) delay, duration, difficulty)
elif command == 'stop': elif command == 'stop':
# Alias for @wordquit # Alias for @wordquit
self._stop_game(irc, channel) self._stop_game(irc, channel)
elif command == 'stats': elif command == 'stats':
game = self.games.get(channel) game = self.games.get(channel)
if not game or game.__class__ != Worddle: if not game or game.__class__ != Boggle:
irc.reply('No Worddle game available for stats.') irc.reply('No Boggle game available for stats.')
elif game.is_running(): elif game.is_running():
irc.reply('Please wait until the game finishes.') irc.reply('Please wait until the game finishes.')
else: else:
game.stats() game.stats()
else: else:
irc.reply('Unrecognized command to worddle.') irc.reply('Unrecognized command to Boggle.')
except WordgamesError as e: except WordGamesError as e:
irc.reply('Wordgames error: %s' % str(e)) irc.reply('WordGames error: %s' % str(e))
irc.reply('Please check the configuration and try again. ' + irc.reply('Please check the configuration and try again. ' +
'See README for help.') 'See README for help.')
worddle = wrap(worddle, boggle = wrap(boggle,
['channel', optional(('literal', ['channel', optional(('literal',
Difficulty.NAMES + ['join', 'stop', 'stats']))]) Difficulty.NAMES + ['join', 'stop', 'stats']))])
# Alias for misspelling of the game name
wordle = worddle
def wordshrink(self, irc, msgs, args, channel, difficulty): def wordshrink(self, irc, msgs, args, channel, difficulty):
"""[easy|medium|hard|evil] (default: medium) """[easy|medium|hard|evil] (default: medium)
@ -268,12 +266,12 @@ class Wordgames(callbacks.Plugin):
try: try:
regexp = re.compile(self.registryValue('wordRegexp')) regexp = re.compile(self.registryValue('wordRegexp'))
except Exception as e: except Exception as e:
raise WordgamesError("Bad value for wordRegexp: %s" % str(e)) raise WordGamesError("Bad value for wordRegexp: %s" % str(e))
path = self.registryValue('wordFile') path = self.registryValue('wordFile')
try: try:
wordFile = open(path) wordFile = open(path)
except Exception as e: except Exception as e:
raise WordgamesError("Unable to open word file: %s" % path) raise WordGamesError("Unable to open word file: %s" % path)
return list(filter(regexp.match, list(map(str.strip, wordFile.readlines())))) return list(filter(regexp.match, list(map(str.strip, wordFile.readlines()))))
def _start_game(self, Game, irc, channel, *args, **kwargs): def _start_game(self, Game, irc, channel, *args, **kwargs):
@ -286,10 +284,10 @@ class Wordgames(callbacks.Plugin):
words = self._get_words() words = self._get_words()
self.games[channel] = Game(words, irc, channel, *args, **kwargs) self.games[channel] = Game(words, irc, channel, *args, **kwargs)
self.games[channel].start() self.games[channel].start()
except WordgamesError as e: except WordGamesError as e:
# Get rid of the game in case it's in an indeterminate state # Get rid of the game in case it's in an indeterminate state
if channel in self.games: del self.games[channel] if channel in self.games: del self.games[channel]
irc.reply('Wordgames error: %s' % str(e)) irc.reply('WordGames error: %s' % str(e))
irc.reply('Please check the configuration and try again. ' + irc.reply('Please check the configuration and try again. ' +
'See README for help.') 'See README for help.')
@ -364,8 +362,8 @@ class BaseGame(object):
"Handle incoming messages on the channel." "Handle incoming messages on the channel."
pass pass
class Worddle(BaseGame): class Boggle(BaseGame):
"The Worddle game implementation." "The Boggle game implementation."
BOARD_SIZE = 4 BOARD_SIZE = 4
FREQUENCY_TABLE = { FREQUENCY_TABLE = {
@ -401,7 +399,7 @@ class Worddle(BaseGame):
'point%%(plural)s (%%(words)s)') % 'point%%(plural)s (%%(words)s)') %
(WHITE, LGRAY, LGREEN, LGRAY), (WHITE, LGRAY, LGREEN, LGRAY),
'startup': ('Starting in %%(seconds)d seconds, ' + 'startup': ('Starting in %%(seconds)d seconds, ' +
'use "%s%%(commandChar)sworddle%s" to play!') % 'use "%s%%(commandChar)sboggle%s" to play!') %
(WHITE, LGRAY), (WHITE, LGRAY),
'stopped': 'Game stopped.', 'stopped': 'Game stopped.',
'stopped2': ('%s::: Game Stopped :::%s') % (LRED, LGRAY), 'stopped2': ('%s::: Game Stopped :::%s') % (LRED, LGRAY),
@ -451,7 +449,7 @@ class Worddle(BaseGame):
def get_score(self): def get_score(self):
score = 0 score = 0
for word in self.unique: for word in self.unique:
score += Worddle.POINT_VALUES.get(len(word), Worddle.MAX_POINTS) score += Boggle.POINT_VALUES.get(len(word), Boggle.MAX_POINTS)
return score return score
def render_words(self, longest_len=0): def render_words(self, longest_len=0):
@ -496,14 +494,14 @@ class Worddle(BaseGame):
else: else:
unique.add(word) unique.add(word)
self.player_results[player] = \ self.player_results[player] = \
Worddle.PlayerResult(player, unique, dup) Boggle.PlayerResult(player, unique, dup)
def sorted_results(self): def sorted_results(self):
return sorted(list(self.player_results.values()), reverse=True) return sorted(list(self.player_results.values()), reverse=True)
def __init__(self, words, irc, channel, nick, delay, duration, difficulty): def __init__(self, words, irc, channel, nick, delay, duration, difficulty):
# See tech note in the Wordgames class. # See tech note in the WordGames class.
self.parent = super(Worddle, self) self.parent = super(Boggle, self)
self.parent.__init__(words, irc, channel) self.parent.__init__(words, irc, channel)
self.delay = delay self.delay = delay
self.duration = duration self.duration = duration
@ -511,11 +509,11 @@ class Worddle(BaseGame):
self.max_targets = get_max_targets(irc) self.max_targets = get_max_targets(irc)
self._handle_difficulty() self._handle_difficulty()
self.board = self._generate_board() self.board = self._generate_board()
self.event_name = 'Worddle.%d' % id(self) self.event_name = 'Boggle.%d' % id(self)
self.init_time = time.time() self.init_time = time.time()
self.longest_len = len(max(self.board.solutions, key=len)) self.longest_len = len(max(self.board.solutions, key=len))
self.starter = nick self.starter = nick
self.state = Worddle.State.PREGAME self.state = Boggle.State.PREGAME
self.players = [] self.players = []
self.player_answers = {} self.player_answers = {}
self.warnings = [30, 10, 5] self.warnings = [30, 10, 5]
@ -527,7 +525,7 @@ class Worddle(BaseGame):
if nick not in self.players: if nick not in self.players:
self.join(nick) self.join(nick)
# Pre-game messages are relayed as chatter (not treated as guesses) # Pre-game messages are relayed as chatter (not treated as guesses)
if self.state < Worddle.State.ACTIVE: if self.state < Boggle.State.ACTIVE:
self._broadcast('chat', self.players, nick=nick, text=text) self._broadcast('chat', self.players, nick=nick, text=text)
return return
guesses = set(map(str.lower, text.split())) guesses = set(map(str.lower, text.split()))
@ -548,7 +546,7 @@ class Worddle(BaseGame):
def join(self, nick): def join(self, nick):
assert self.is_running() assert self.is_running()
assert self.state != Worddle.State.DONE assert self.state != Boggle.State.DONE
if nick not in self.players: if nick not in self.players:
self._broadcast('welcome1', [nick], now=True, self._broadcast('welcome1', [nick], now=True,
difficulty=Difficulty.name(self.difficulty), difficulty=Difficulty.name(self.difficulty),
@ -557,12 +555,12 @@ class Worddle(BaseGame):
self._broadcast('joined', self.players, nick=nick) self._broadcast('joined', self.players, nick=nick)
self.players.append(nick) self.players.append(nick)
self.player_answers[nick] = set() self.player_answers[nick] = set()
if self.state == Worddle.State.ACTIVE: if self.state == Boggle.State.ACTIVE:
self._display_board(nick) self._display_board(nick)
else: else:
self._broadcast('players', [nick]) self._broadcast('players', [nick])
# Keep at least 5 seconds on the pre-game clock if someone joins # Keep at least 5 seconds on the pre-game clock if someone joins
if self.state == Worddle.State.PREGAME: if self.state == Boggle.State.PREGAME:
time_left = self.init_time + self.delay - time.time() time_left = self.init_time + self.delay - time.time()
if time_left < 5: if time_left < 5:
self.delay += (5 - time_left) self.delay += (5 - time_left)
@ -572,7 +570,7 @@ class Worddle(BaseGame):
def show(self): def show(self):
# Not sure if this is really useful. # Not sure if this is really useful.
#if self.state == Worddle.State.ACTIVE: #if self.state == Boggle.State.ACTIVE:
# self._display_board(self.channel) # self._display_board(self.channel)
pass pass
@ -587,7 +585,7 @@ class Worddle(BaseGame):
def stop(self, now=False): def stop(self, now=False):
self.parent.stop() self.parent.stop()
self.state = Worddle.State.DONE self.state = Boggle.State.DONE
try: try:
schedule.removeEvent(self.event_name) schedule.removeEvent(self.event_name)
except KeyError: except KeyError:
@ -597,10 +595,10 @@ class Worddle(BaseGame):
self._broadcast('stopped2', self.players) self._broadcast('stopped2', self.players)
def stats(self): def stats(self):
assert self.state == Worddle.State.DONE assert self.state == Boggle.State.DONE
points = 0 points = 0
for word in self.board.solutions: for word in self.board.solutions:
points += Worddle.POINT_VALUES.get(len(word), Worddle.MAX_POINTS) points += Boggle.POINT_VALUES.get(len(word), Boggle.MAX_POINTS)
longest_words = [w for w in self.board.solutions if len(w) == self.longest_len] longest_words = [w for w in self.board.solutions if len(w) == self.longest_len]
self.announce(('There were %s%d%s possible words, with total point' self.announce(('There were %s%d%s possible words, with total point'
' value %s%d%s. The longest word%s: %s%s%s.') % ' value %s%d%s. The longest word%s: %s%s%s.') %
@ -635,7 +633,7 @@ class Worddle(BaseGame):
(WHITE, (LGRAY + ', ' + WHITE).join(self.players), LGRAY) (WHITE, (LGRAY + ', ' + WHITE).join(self.players), LGRAY)
if 'points' in kwargs: if 'points' in kwargs:
kwargs['plural'] = '' if kwargs['points'] == 1 else 's' kwargs['plural'] = '' if kwargs['points'] == 1 else 's'
formatted = Worddle.MESSAGES[name] % kwargs formatted = Boggle.MESSAGES[name] % kwargs
self._broadcast_text(formatted, recipients, now) self._broadcast_text(formatted, recipients, now)
def _handle_difficulty(self): def _handle_difficulty(self):
@ -647,12 +645,12 @@ class Worddle(BaseGame):
}[self.difficulty] }[self.difficulty]
def _get_ready(self): def _get_ready(self):
self.state = Worddle.State.READY self.state = Boggle.State.READY
self._broadcast('ready', now=True) self._broadcast('ready', now=True)
self._schedule_next_event() self._schedule_next_event()
def _begin_game(self): def _begin_game(self):
self.state = Worddle.State.ACTIVE self.state = Boggle.State.ACTIVE
self.start_time = time.time() self.start_time = time.time()
self.end_time = self.start_time + self.duration self.end_time = self.start_time + self.duration
self._display_board() self._display_board()
@ -668,15 +666,15 @@ class Worddle(BaseGame):
schedule.removeEvent(self.event_name) schedule.removeEvent(self.event_name)
except KeyError: except KeyError:
pass pass
if self.state == Worddle.State.PREGAME: if self.state == Boggle.State.PREGAME:
# Schedule "get ready" message # Schedule "get ready" message
schedule.addEvent(self._get_ready, schedule.addEvent(self._get_ready,
self.init_time + self.delay, self.event_name) self.init_time + self.delay, self.event_name)
elif self.state == Worddle.State.READY: elif self.state == Boggle.State.READY:
# Schedule game start # Schedule game start
schedule.addEvent(self._begin_game, schedule.addEvent(self._begin_game,
self.init_time + self.delay + 3, self.event_name) self.init_time + self.delay + 3, self.event_name)
elif self.state == Worddle.State.ACTIVE: elif self.state == Boggle.State.ACTIVE:
if self.warnings: if self.warnings:
# Warn almost half a second early, in case there is a little # Warn almost half a second early, in case there is a little
# latency before the event is triggered. (Otherwise a 30 second # latency before the event is triggered. (Otherwise a 30 second
@ -697,10 +695,10 @@ class Worddle(BaseGame):
def _end_game(self): def _end_game(self):
self.gameover() self.gameover()
self.state = Worddle.State.DONE self.state = Boggle.State.DONE
# Compute results # Compute results
results = Worddle.Results() results = Boggle.Results()
for player, answers in self.player_answers.items(): for player, answers in self.player_answers.items():
results.add_player_words(player, answers) results.add_player_words(player, answers)
@ -728,7 +726,7 @@ class Worddle(BaseGame):
def _display_board(self, nick=None): def _display_board(self, nick=None):
"Display the board to everyone or just one nick if specified." "Display the board to everyone or just one nick if specified."
commandChar = str(conf.supybot.reply.whenAddressedBy.chars)[0] commandChar = str(conf.supybot.reply.whenAddressedBy.chars)[0]
help_msgs = [''] * Worddle.BOARD_SIZE help_msgs = [''] * Boggle.BOARD_SIZE
help_msgs[1] = '%sLet\'s GO!' % (WHITE) help_msgs[1] = '%sLet\'s GO!' % (WHITE)
help_msgs[2] = '%s%s%s seconds left!' % \ help_msgs[2] = '%s%s%s seconds left!' % \
(LYELLOW, int(round(self.end_time - time.time())), LGRAY) (LYELLOW, int(round(self.end_time - time.time())), LGRAY)
@ -744,16 +742,16 @@ class Worddle(BaseGame):
attempts = 5 attempts = 5
wordtrie = Trie() wordtrie = Trie()
list(map(wordtrie.add, self.words)) list(map(wordtrie.add, self.words))
boards = [WorddleBoard(wordtrie, Worddle.BOARD_SIZE, self.min_length) boards = [BoggleBoard(wordtrie, Boggle.BOARD_SIZE, self.min_length)
for i in range(0, attempts)] for i in range(0, attempts)]
board_quality = lambda b: len(b.solutions) board_quality = lambda b: len(b.solutions)
return max(boards, key=board_quality) return max(boards, key=board_quality)
class WorddleBoard(object): class BoggleBoard(object):
"Represents the board in a Worddle game." "Represents the board in a Boggle game."
def __init__(self, wordtrie, n, min_length): def __init__(self, wordtrie, n, min_length):
"Generate a new n x n Worddle board." "Generate a new n x n Boggle board."
self.size = n self.size = n
self.min_length = min_length self.min_length = min_length
self.rows = self._generate_rows() self.rows = self._generate_rows()
@ -798,10 +796,10 @@ class WorddleBoard(object):
return result return result
def _generate_rows(self): def _generate_rows(self):
"Randomly generate a Worddle board (a list of lists)." "Randomly generate a Boggle board (a list of lists)."
letters = reduce(add, (list(map(mul, letters = reduce(add, (list(map(mul,
list(Worddle.FREQUENCY_TABLE.keys()), list(Boggle.FREQUENCY_TABLE.keys()),
list(Worddle.FREQUENCY_TABLE.values()))))) list(Boggle.FREQUENCY_TABLE.values())))))
rows = [] rows = []
values = random.sample(letters, self.size**2) values = random.sample(letters, self.size**2)
for i in range(0, self.size): for i in range(0, self.size):
@ -831,7 +829,7 @@ class WordChain(BaseGame):
self.num_solutions = num_solutions self.num_solutions = num_solutions
def __init__(self, words, irc, channel, settings): def __init__(self, words, irc, channel, settings):
# See tech note in the Wordgames class. # See tech note in the WordGames class.
self.parent = super(WordChain, self) self.parent = super(WordChain, self)
self.parent.__init__(words, irc, channel) self.parent.__init__(words, irc, channel)
self.settings = settings self.settings = settings
@ -853,7 +851,7 @@ class WordChain(BaseGame):
while len(self.solution) < self.solution_length: while len(self.solution) < self.solution_length:
attempts -= 1 attempts -= 1
if attempts == 0: if attempts == 0:
raise WordgamesError(('Unable to generate %s puzzle. This' + raise WordGamesError(('Unable to generate %s puzzle. This' +
' is either a bug, or the word file is too small.') % ' is either a bug, or the word file is too small.') %
self.__class__.__name__) self.__class__.__name__)
self.solution = [random.choice(self.words)] self.solution = [random.choice(self.words)]
@ -877,7 +875,7 @@ class WordChain(BaseGame):
if happy: if happy:
break break
if not happy: if not happy:
raise WordgamesError(('Unable to generate %s puzzle meeting the ' + raise WordGamesError(('Unable to generate %s puzzle meeting the ' +
'game parameters. This is probably a bug.') % 'game parameters. This is probably a bug.') %
self.__class__.__name__) self.__class__.__name__)
@ -1064,6 +1062,6 @@ class WordTwist(WordChain):
"If it's possible to get there in fewer hops, this is trivial." "If it's possible to get there in fewer hops, this is trivial."
return len(solution) < self.solution_length return len(solution) < self.solution_length
Class = Wordgames Class = WordGames
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: