mirror of https://github.com/dirtbags/moth.git
Merged in some changes.
This commit is contained in:
commit
f2c12aa3ea
|
@ -0,0 +1,3 @@
|
||||||
|
Neale Pickett
|
||||||
|
Patrick Avery
|
||||||
|
Shannon Steinfadt
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import cgi
|
||||||
import glob
|
import glob
|
||||||
import http.server
|
import http.server
|
||||||
import mistune
|
import mistune
|
||||||
|
@ -7,6 +8,8 @@ import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import puzzles
|
import puzzles
|
||||||
import socketserver
|
import socketserver
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from http.server import HTTPStatus
|
from http.server import HTTPStatus
|
||||||
|
@ -14,7 +17,10 @@ except ImportError:
|
||||||
class HTTPStatus:
|
class HTTPStatus:
|
||||||
NOT_FOUND = 404
|
NOT_FOUND = 404
|
||||||
OK = 200
|
OK = 200
|
||||||
|
|
||||||
|
# XXX: This will eventually cause a problem. Do something more clever here.
|
||||||
|
seed = 1
|
||||||
|
|
||||||
|
|
||||||
def page(title, body):
|
def page(title, body):
|
||||||
return """<!DOCTYPE html>
|
return """<!DOCTYPE html>
|
||||||
|
@ -45,7 +51,20 @@ class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MothHandler(http.server.CGIHTTPRequestHandler):
|
class MothHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def handle_one_request(self):
|
||||||
|
try:
|
||||||
|
super().handle_one_request()
|
||||||
|
except:
|
||||||
|
tbtype, value, tb = sys.exc_info()
|
||||||
|
tblist = traceback.format_tb(tb, None) + traceback.format_exception_only(tbtype, value)
|
||||||
|
page = ("# Traceback (most recent call last)\n" +
|
||||||
|
" " +
|
||||||
|
" ".join(tblist[:-1]) +
|
||||||
|
tblist[-1])
|
||||||
|
self.serve_md(page)
|
||||||
|
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/":
|
if self.path == "/":
|
||||||
self.serve_front()
|
self.serve_front()
|
||||||
|
@ -91,25 +110,22 @@ you are a fool.
|
||||||
elif len(parts) == 3:
|
elif len(parts) == 3:
|
||||||
# List all point values in a category
|
# List all point values in a category
|
||||||
body.append("# Puzzles in category `{}`".format(parts[2]))
|
body.append("# Puzzles in category `{}`".format(parts[2]))
|
||||||
puzz = []
|
fpath = os.path.join("puzzles", parts[2])
|
||||||
for i in glob.glob(os.path.join("puzzles", parts[2], "*.moth")):
|
cat = puzzles.Category(fpath, seed)
|
||||||
base = os.path.basename(i)
|
for points in cat.pointvals:
|
||||||
root, _ = os.path.splitext(base)
|
body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points))
|
||||||
points = int(root)
|
|
||||||
puzz.append(points)
|
|
||||||
for puzzle in sorted(puzz):
|
|
||||||
body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=puzzle))
|
|
||||||
elif len(parts) == 4:
|
elif len(parts) == 4:
|
||||||
body.append("# {} puzzle {}".format(parts[2], parts[3]))
|
body.append("# {} puzzle {}".format(parts[2], parts[3]))
|
||||||
with open("puzzles/{}/{}.moth".format(parts[2], parts[3])) as f:
|
fpath = os.path.join("puzzles", parts[2])
|
||||||
p = puzzles.Puzzle(f)
|
cat = puzzles.Category(fpath, seed)
|
||||||
body.append("* Author: `{}`".format(p.fields.get("author")))
|
p = cat.puzzle(int(parts[3]))
|
||||||
body.append("* Summary: `{}`".format(p.fields.get("summary")))
|
body.append("* Author: `{}`".format(p['author']))
|
||||||
|
body.append("* Summary: `{}`".format(p['summary']))
|
||||||
body.append('')
|
body.append('')
|
||||||
body.append("## Body")
|
body.append("## Body")
|
||||||
body.append(p.body)
|
body.append(p.body)
|
||||||
body.append("## Answers:")
|
body.append("## Answers")
|
||||||
for a in p.answers:
|
for a in p['answers']:
|
||||||
body.append("* `{}`".format(a))
|
body.append("* `{}`".format(a))
|
||||||
body.append("")
|
body.append("")
|
||||||
else:
|
else:
|
||||||
|
@ -121,7 +137,7 @@ you are a fool.
|
||||||
self.serve_md()
|
self.serve_md()
|
||||||
else:
|
else:
|
||||||
super().do_GET()
|
super().do_GET()
|
||||||
|
|
||||||
def serve_md(self, text=None):
|
def serve_md(self, text=None):
|
||||||
fspathstr = self.translate_path(self.path)
|
fspathstr = self.translate_path(self.path)
|
||||||
fspath = pathlib.Path(fspathstr)
|
fspath = pathlib.Path(fspathstr)
|
||||||
|
@ -133,8 +149,9 @@ you are a fool.
|
||||||
return None
|
return None
|
||||||
content = mdpage(text)
|
content = mdpage(text)
|
||||||
|
|
||||||
self.send_response(http.server.HTTPStatus.OK)
|
self.send_response(HTTPStatus.OK)
|
||||||
self.send_header("Content-type", "text/html; encoding=utf-8")
|
|
||||||
|
self.send_header("Content-type", "text/html; charset=utf-8")
|
||||||
self.send_header("Content-Length", len(content))
|
self.send_header("Content-Length", len(content))
|
||||||
try:
|
try:
|
||||||
fs = fspath.stat()
|
fs = fspath.stat()
|
||||||
|
@ -145,9 +162,9 @@ you are a fool.
|
||||||
self.wfile.write(content.encode('utf-8'))
|
self.wfile.write(content.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def run(address=('', 8080)):
|
def run(address=('localhost', 8080)):
|
||||||
httpd = ThreadingServer(address, MothHandler)
|
httpd = ThreadingServer(address, MothHandler)
|
||||||
print("=== Listening on http://{}:{}/".format(address[0] or "localhost", address[1]))
|
print("=== Listening on http://{}:{}/".format(address[0], address[1]))
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -335,7 +335,7 @@ class BlockLexer(object):
|
||||||
|
|
||||||
rest = len(item)
|
rest = len(item)
|
||||||
if i != length - 1 and rest:
|
if i != length - 1 and rest:
|
||||||
_next = item[rest-1] == '\n'
|
_next = item[rest - 1] == '\n'
|
||||||
if not loose:
|
if not loose:
|
||||||
loose = _next
|
loose = _next
|
||||||
|
|
||||||
|
|
66
puzzles.py
66
puzzles.py
|
@ -56,7 +56,7 @@ class Puzzle:
|
||||||
ANSWER_WORDS = [w.strip() for w in open(os.path.join(os.path.dirname(__file__),
|
ANSWER_WORDS = [w.strip() for w in open(os.path.join(os.path.dirname(__file__),
|
||||||
'answer_words.txt'))]
|
'answer_words.txt'))]
|
||||||
|
|
||||||
def __init__(self, category_seed, path=None):
|
def __init__(self, category_seed, path=None, points=None):
|
||||||
"""A MOTH Puzzle
|
"""A MOTH Puzzle
|
||||||
:param category_seed: A byte string to use as a seed for random numbers for this puzzle.
|
:param category_seed: A byte string to use as a seed for random numbers for this puzzle.
|
||||||
It is combined with the puzzle points.
|
It is combined with the puzzle points.
|
||||||
|
@ -69,12 +69,16 @@ class Puzzle:
|
||||||
(optional) A puzzle.py file. This is expected to have a callable called make
|
(optional) A puzzle.py file. This is expected to have a callable called make
|
||||||
that takes a single positional argument (this puzzle object).
|
that takes a single positional argument (this puzzle object).
|
||||||
This callable can then do whatever it needs to with this object.
|
This callable can then do whatever it needs to with this object.
|
||||||
|
:param points: The point value of the puzzle. Mutually exclusive with path.
|
||||||
If neither of the above are given, the point value for the puzzle will have to
|
If neither of the above are given, the point value for the puzzle will have to
|
||||||
be set manually.
|
be set at instantiation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
if (points is None and path is None) or (points is not None and path is not None):
|
||||||
|
raise ValueError("Either points or path must be set, but not both.")
|
||||||
|
|
||||||
self._dict = defaultdict(lambda: [])
|
self._dict = defaultdict(lambda: [])
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
self._puzzle_dir = path
|
self._puzzle_dir = path
|
||||||
|
@ -83,25 +87,24 @@ class Puzzle:
|
||||||
self.message = bytes(random.choice(messageChars) for i in range(20))
|
self.message = bytes(random.choice(messageChars) for i in range(20))
|
||||||
self.body = ''
|
self.body = ''
|
||||||
|
|
||||||
self._seed = hashlib.sha1(category_seed + bytes(self['points'])).digest()
|
|
||||||
self.rand = random.Random(self._seed)
|
|
||||||
|
|
||||||
# Set our 'files' as a dict, since we want register them uniquely by name.
|
|
||||||
self['files'] = dict()
|
|
||||||
|
|
||||||
# A list of temporary files we've created that will need to be deleted.
|
# A list of temporary files we've created that will need to be deleted.
|
||||||
self._temp_files = []
|
self._temp_files = []
|
||||||
|
if path is not None:
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
raise ValueError("No such directory: {}".format(path))
|
||||||
|
|
||||||
# All internal variables must be initialized before the following runs
|
|
||||||
if not os.path.exists(path):
|
|
||||||
raise ValueError("No puzzle at path: {]".format(path))
|
|
||||||
elif os.path.isdir(path):
|
|
||||||
# Expected format is path/<points_int>.moth
|
|
||||||
pathname = os.path.split(path)[-1]
|
pathname = os.path.split(path)[-1]
|
||||||
try:
|
try:
|
||||||
self.points = int(pathname)
|
self.points = int(pathname)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
raise ValueError("Directory name must be a point value: {}".format(path))
|
||||||
|
elif points is not None:
|
||||||
|
self.points = points
|
||||||
|
|
||||||
|
self._seed = category_seed * self.points
|
||||||
|
self.rand = random.Random(self._seed)
|
||||||
|
|
||||||
|
if path is not None:
|
||||||
files = os.listdir(path)
|
files = os.listdir(path)
|
||||||
|
|
||||||
if 'puzzle.moth' in files:
|
if 'puzzle.moth' in files:
|
||||||
|
@ -112,9 +115,10 @@ class Puzzle:
|
||||||
loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py'))
|
loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py'))
|
||||||
puzzle_mod = loader.load_module()
|
puzzle_mod = loader.load_module()
|
||||||
if hasattr(puzzle_mod, 'make'):
|
if hasattr(puzzle_mod, 'make'):
|
||||||
|
self.body = '# `puzzle.body` was not set by the `make` function'
|
||||||
puzzle_mod.make(self)
|
puzzle_mod.make(self)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unacceptable file type for puzzle at {}".format(path))
|
self.body = '# `puzzle.py` does not define a `make` function'
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Cleanup any outstanding temporary files."""
|
"""Cleanup any outstanding temporary files."""
|
||||||
|
@ -238,16 +242,13 @@ class Puzzle:
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
return self._dict[item.lower()]
|
return self._dict[item.lower()]
|
||||||
|
|
||||||
def make_answer(self, word_count, sep=b' '):
|
def make_answer(self, word_count, sep=' '):
|
||||||
"""Generate and return a new answer. It's automatically added to the puzzle answer list.
|
"""Generate and return a new answer. It's automatically added to the puzzle answer list.
|
||||||
:param int word_count: The number of words to include in the answer.
|
:param int word_count: The number of words to include in the answer.
|
||||||
:param str|bytes sep: The word separator.
|
:param str|bytes sep: The word separator.
|
||||||
:returns: The answer bytes
|
:returns: The answer string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if type(sep) == str:
|
|
||||||
sep = sep.encode('ascii')
|
|
||||||
|
|
||||||
answer = sep.join(self.rand.sample(self.ANSWER_WORDS, word_count))
|
answer = sep.join(self.rand.sample(self.ANSWER_WORDS, word_count))
|
||||||
self['answer'] = answer
|
self['answer'] = answer
|
||||||
|
|
||||||
|
@ -271,8 +272,8 @@ class Puzzle:
|
||||||
'summary': self['summary'],
|
'summary': self['summary'],
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description='Build a puzzle category')
|
parser = argparse.ArgumentParser(description='Build a puzzle category')
|
||||||
parser.add_argument('puzzledir', nargs='+', help='Directory of puzzle source')
|
parser.add_argument('puzzledir', nargs='+', help='Directory of puzzle source')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -291,3 +292,22 @@ if __name__ == '__main__':
|
||||||
puzzle = puzzles[points]
|
puzzle = puzzles[points]
|
||||||
print(puzzle.secrets())
|
print(puzzle.secrets())
|
||||||
|
|
||||||
|
|
||||||
|
class Category:
|
||||||
|
def __init__(self, path, seed):
|
||||||
|
self.path = path
|
||||||
|
self.seed = seed
|
||||||
|
self.pointvals = []
|
||||||
|
for fpath in glob.glob(os.path.join(path, "[0-9]*")):
|
||||||
|
pn = os.path.basename(fpath)
|
||||||
|
points = int(pn)
|
||||||
|
self.pointvals.append(points)
|
||||||
|
self.pointvals.sort()
|
||||||
|
|
||||||
|
def puzzle(self, points):
|
||||||
|
path = os.path.join(self.path, str(points))
|
||||||
|
return Puzzle(path, self.seed)
|
||||||
|
|
||||||
|
def puzzles(self):
|
||||||
|
for points in self.pointvals:
|
||||||
|
yield self.puzzle(points)
|
||||||
|
|
Loading…
Reference in New Issue