Add 'WordGames/' from commit '854e77a6d4d576aad9da8aa9852a1939e205fc4e'

git-subtree-dir: WordGames
git-subtree-mainline: a8c0694be8909bdc34ab3f23579b911545b94df6
git-subtree-split: 854e77a6d4d576aad9da8aa9852a1939e205fc4e
This commit is contained in:
oddluck 2019-02-24 15:32:20 -05:00
commit 5e74a4a10e
6 changed files with 1483 additions and 0 deletions

188
WordGames/README.md Normal file
View File

@ -0,0 +1,188 @@
Supybot Word Games Plugin
=========================
This is a plugin for the popular IRC bot Supybot that implements a few
word games to play in your IRC channel.
Typical installation process:
1. Clone this git repository in your downloads directory.
2. Create a symlink called `Wordgames` in supybot/plugins pointing to it.
3. In IRC, load Wordgames.
**Note:** These games rely on a dictionary file (not included). On
Ubuntu, you can normally just install the `wamerican` package. See
the Configurable Variables section to customize.
Commands
--------
The following commands are exposed by this plugin:
`worddle [easy|medium|hard|evil | stop|stats]`
> Play a Worddle game. With no argument, a new game will start with the
> default difficulty (see worddleDifficulty in the Configuration section).
> If a game is already started, you will join the game in progress.
>
> In addition, you may use the following subcommands:
>
> easy minimum acceptable word length: 3 (overrides config)
> medium minimum acceptable word length: 4 " "
> hard minimum acceptable word length: 5 " "
> evil minimum acceptable word length: 6 " "
> stop Stop a currently running game (alias for @wordquit)
> stats Display some post-game statistics about the board
`wordshrink [difficulty]`
> Start a new WordShrink game. Difficulty values: easy [medium] hard evil
`wordtwist [difficulty]`
> Start a new WordTwist game. Difficulty values: easy [medium] hard evil
`wordquit`
> Shut down any currently running game. One solution will be displayed for
> the word chain games, to satisfy your curiosity.
Game Rules
----------
### Worddle
Worddle is a clone of a well-known puzzle game involving a 4x4 grid of
randomly-placed letters. It's a timed, multiplayer game where you compete
to find words that your opponents didn't find.
When you start a new game, or join an existing game, the bot will send you a
private message indicating that you are playing. All your guesses must be
sent to the bot in this private conversation, so your opponents can't see your
guesses. At the end of the game, the results will be presented in the channel
where the game was started.
To be a valid guess, words must:
* be made of adjacent letters on the board (in all 8 directions, diagonals ok)
* be at least 3 letters in length (or 4, or 5, etc. depending on the level)
* appear in the dictionary file.
At the end of the game, if a word was found by multiple players, it is not
counted. The remaining words contribute to your score using these values:
Length | Value
--------+-------
3, 4 | 1 point
5 | 2 points
6 | 3 points
7 | 5 points
8+ | 11 points
### WordShrink
WordShrink and WordTwist are word chain (or word ladder) style games.
A puzzle will be presented in the form:
word1 > --- > --- > word4
... and your job is to come up with a response of the form `word2 > word3`
that completes the puzzle. (You can optionally include the start and end
words in your response, as long as each word is separated by a greater-than
sign.)
In WordShrink, each word must be made by removing one letter from the
preceding word and rearranging the letters to form a new word. Example
session:
<mike> @wordshrink
<supybot> WordShrink: lights > ----- > ---- > sit
<supybot> (12 possible solutions)
<mike> sight > this
<supybot> WordShrink: mike got it!
<supybot> WordShrink: lights > sight > this > sit
<ben> lights > hilts > hits > sit
<supybot> ben: Your solution is also valid.
### WordTwist
WordTwist is very similar to WordShrink, except that the way you manipulate
the words to solve the puzzle is changed. In WordTwist, you change exactly
one letter in each successive word to form a new word (no rearranging is
allowed). Example session:
<mike> @wordtwist medium
<supybot> WordTwist: mass > ---- > ---- > ---- > jade
<supybot> (5 possible solutions)
<mike> mars > mare > made
<supybot> WordTwist: mike got it!
<supybot> WordTwist: mass > mars > mare > made > jade
Configuration Variables
-----------------------
`plugins.Wordgames.wordFile`
> Path to the dictionary file.
>
> Default: `/usr/share/dict/american-english`
`plugins.Wordgames.wordRegexp`
> A regular expression defining what a valid word looks like. This will
> be used to filter words from the dictionary file that contain undesirable
> characters (proper names, hyphens, accents, etc.). You will probably have
> to quote the string when setting, e.g.:
>
> @config plugins.Wordgames.wordRegexp "^[a-x]+$"
>
> (No words containing 'y' or 'z' would be allowed by this.)
>
> Default: `^[a-z]+$`
`plugins.Wordgames.worddleDelay`
> The length (in seconds) of the pre-game period where players can join a
> new Worddle game.
>
> Default: `15`
`plugins.Wordgames.worddleDuration`
> The length (in seconds) of the active period of a Worddle game, when
> players can submit guesses.
>
> Default: `90`
`plugins.Wordgames.worddleDifficulty`
> The default difficulty for a Worddle game.
>
> Default: `easy` (words must be 3 letters or longer)
A Technical Note About Worddle
------------------------------
This game sends a lot of PRIVMSGs (between the channel and all the players,
the messages add up). It attempts to send them in a single PRIVMSG if
possible to combine targets.
Supybot: I run Supybot with the latest git (there are show-stopper bugs in
0.83.4.1) and the Twisted driver (but Socket should work as well).
IRC Server: I had to tune my IRC server to handle this game, due to the large
amount of messages it sends. You may find that it has problems on your server
due to flood controls (bot may be either fake lagged or kicked from the
server). If the game seems extremely slow, it is either an old Supybot or the
server is throttling you.
I would like to add an option to tune the verbosity of the game to mitigate
this for restrictive servers.
Credit
------
Copyright 2012 Mike Mueller <mike@subfocal.net>
Released under the WTF public license: http://sam.zoy.org/wtfpl/
Thanks to Ben Schomp <ben@benschomp.com> for the inspiration and QA testing.

59
WordGames/__init__.py Normal file
View File

@ -0,0 +1,59 @@
###
# Copyright (c) 2012, Mike Mueller
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Do whatever you want.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Implements some word games.
"""
import supybot
import supybot.world as world
import importlib
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = ""
# Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.Author('Mike Mueller', 'mmueller', 'mike@subfocal.net')
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = 'http://github.com/mmueller/supybot-wordgames'
from . import config
from . import plugin
importlib.reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

61
WordGames/config.py Normal file
View File

@ -0,0 +1,61 @@
###
# Copyright (c) 2012, Mike Mueller
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Do whatever you want
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
import re
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Wordgames', True)
Wordgames = conf.registerPlugin('Wordgames')
conf.registerGlobalValue(Wordgames, 'wordFile',
registry.String('/usr/share/dict/american-english',
'Path to the dictionary file.'))
conf.registerGlobalValue(Wordgames, 'wordRegexp',
registry.String('^[a-z]+$',
'Regular expression defining what a valid word looks ' +
'like (i.e. ignore proper names, contractions, etc. ' +
'Modify this if you need to allow non-English chars.'))
conf.registerGlobalValue(Wordgames, 'worddleDelay',
registry.NonNegativeInteger(15,
'Delay (in seconds) before a Worddle game ' +
'begins.'))
conf.registerGlobalValue(Wordgames, 'worddleDuration',
registry.NonNegativeInteger(90,
'Duration (in seconds) of a Worddle game ' +
'(not including the initial delay).'))
conf.registerGlobalValue(Wordgames, 'worddleDifficulty',
registry.String('easy', 'Default difficulty for Worddle games.'))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

1040
WordGames/plugin.py Normal file

File diff suppressed because it is too large Load Diff

27
WordGames/test.py Normal file
View File

@ -0,0 +1,27 @@
# Copyright (c) 2012, Mike Mueller
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Do whatever you want.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from supybot.test import *
class WordgamesTestCase(PluginTestCase):
plugins = ('Wordgames',)
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

108
WordGames/trie.py Normal file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env python
"""
Quick & dirty prefix tree (aka trie).
"""
# This got a little uglier because using a nice object-oriented approach took
# too much time and memory on big trees. Now every node is simply a dict,
# with the special '*' field meaning that the word is complete at that node.
class Trie(object):
def __init__(self):
self.contents = {'*': False}
def add(self, value, contents=None):
if contents is None:
contents = self.contents
if not value:
contents['*'] = True
return
prefix = value[0]
remainder = value[1:]
child_contents = contents.get(prefix, None)
if not child_contents:
child_contents = {'*': False}
contents[prefix] = child_contents
self.add(remainder, child_contents)
def find(self, value):
"Return true if the value appears, false otherwise."
x = self.find_prefix(value)
return x and x['*'] == True
def find_prefix(self, value, contents=None):
"Return true if the given prefix appears in the tree."
if contents is None:
contents = self.contents
if not value:
return contents
child_contents = contents.get(value[0], None)
if not child_contents:
return None
return self.find_prefix(value[1:], child_contents)
def dump(self, indent=0, contents=None):
"Dump the trie to stdout."
if contents is None:
contents = self.contents
for key in sorted(contents.keys()):
if key == '*':
continue
text = indent * ' '
text += key
child_contents = contents[key]
if child_contents['*']:
text += '*'
print(text)
self.dump(indent+2, child_contents)
if __name__ == '__main__':
import resource
import sys
import time
if '--perf' in sys.argv:
# Performance test, last arg should be input file
start = time.time()
t = Trie()
f = open(sys.argv[-1], 'r')
for line in f:
t.add(line.strip())
mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
elapsed = time.time() - start
print(('Trie created in %g seconds.' % elapsed))
print(('Used %dMB RAM.' % (mem/1024)))
else:
# Regular sanity test
t = Trie()
t.add('hell')
t.add('hello')
t.add('he')
t.add('world')
t.add('alphabet')
t.add('foo')
t.add('food')
t.add('foodie')
t.add('bar')
t.add('alphanumeric')
t.dump()
assert not t.find('h')
assert t.find('he')
assert not t.find('hel')
assert t.find('hell')
assert t.find('hello')
assert not t.find('r')
assert t.find('world')
assert not t.find('ba')
assert t.find('bar')
assert t.find('alphabet')
assert t.find('alphanumeric')
assert not t.find('alpha')
assert not t.find('f')
assert not t.find('fo')
assert t.find('foo')
assert t.find('food')
assert not t.find('foodi')
assert t.find('foodie')