Initial work on new format puzzles, new development server

This commit is contained in:
Neale Pickett 2016-10-14 22:26:47 -06:00
parent 55be0d96b1
commit d9f7efa82b
8 changed files with 1428 additions and 71 deletions

62
README
View File

@ -1,62 +0,0 @@
Dirtbags King Of The Hill Server
=====================
This is a set of thingies to run our KOTH-style contest.
Contests we've run in the past have been called
"Tracer FIRE" and "Project 2".
It serves up puzzles in a manner similar to Jeopardy.
It also track scores,
and comes with a JavaScript-based scoreboard to display team rankings.
How Everything Works
----------------------------
I should fill this in, but I don't feel like anybody would read it.
Send an email to <neale@woozle.org> asking me how it works,
and I'll write this part up and email it back to you :)
How to set it up
--------------------
It's made to be virtualized,
so you can run multiple contests at once if you want.
If you were to want to run it out of `/opt/koth`,
do the following:
$ mkdir -p /opt/koth/mycontest
$ ./install /opt/koth/mycontest
$ cp kothd /opt/koth
Yay, you've got it set up.
Installing Puzzle Categories
------------------------------------
Puzzle categories are distributed in a different way than the server.
After setting up (see above), just run
$ /opt/koth/mycontest/bin/install-category /path/to/my/category
Running It
-------------
Get your web server to serve up files from
`/opt/koth/mycontest/www`.
Then run `/opt/koth/kothd`.
Permissions
----------------
It's up to you not to be a bonehead about permissions.
Install sets it so the web user on your system can write to the files it needs to,
but if you're using Apache,
it plays games with user IDs when running CGI.
You're going to have to figure out how to configure your preferred web server.

9
TODO
View File

@ -1,9 +0,0 @@
* Scoreboard refresh
Test:
* awarding points
* points already awarded
* bad team hash
* category doesn't exist
* puzzle doesn't exist

84
build-puzzles Executable file
View File

@ -0,0 +1,84 @@
#!/usr/bin/python3
import hmac
import base64
import argparse
import glob
import json
import os
import markdown
import random
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
def djb2hash(buf):
h = 5381
for c in buf:
h = ((h * 33) + c) & 0xffffffff
return h
class Puzzle:
def __init__(self, stream):
self.message = bytes(random.choice(messageChars) for i in range(20))
self.fields = {}
self.answers = []
self.hashes = []
body = []
header = True
for line in stream:
if header:
line = line.strip()
if not line.strip():
header = False
continue
key, val = line.split(':', 1)
key = key.lower()
val = val.strip()
self._add_field(key, val)
else:
body.append(line)
self.body = ''.join(body)
def _add_field(self, key, val):
if key == 'answer':
h = djb2hash(val.encode('utf8'))
self.answers.append(val)
self.hashes.append(h)
else:
self.fields[key] = val
def publish(self):
obj = {
'author': self.fields['author'],
'hashes': self.hashes,
'body': markdown.markdown(self.body),
}
return obj
def secrets(self):
obj = {
'answers': self.answers,
'summary': self.fields['summary'],
}
return obj
parser = argparse.ArgumentParser(description='Build a puzzle category')
parser.add_argument('puzzledir', nargs='+', help='Directory of puzzle source')
args = parser.parse_args()
for puzzledir in args.puzzledir:
puzzles = {}
secrets = {}
for puzzlePath in glob.glob(os.path.join(puzzledir, "*.moth")):
filename = os.path.basename(puzzlePath)
points, ext = os.path.splitext(filename)
points = int(points)
puzzle = Puzzle(open(puzzlePath))
puzzles[points] = puzzle
for points in sorted(puzzles):
puzzle = puzzles[points]
print(puzzle.secrets())

98
devel-server.py Executable file
View File

@ -0,0 +1,98 @@
#!/usr/bin/python3
import http.server
import mistune
import pathlib
import socketserver
HTTPStatus = http.server.HTTPStatus
def page(title, body):
return """<!DOCTYPE html>
<html>
<head>
<title>{}</title>
<link rel="stylesheet" href="/files/www/res/style.css">
</head>
<body>
<div id="body" class="terminal">
{}
</div>
</body>
</html>""".format(title, body)
def mdpage(body):
title, _ = body.split('\n', 1)
return page(title, mistune.markdown(body))
class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
pass
class MothHandler(http.server.CGIHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.serve_front()
elif self.path.startswith("/files/"):
self.serve_file()
else:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
def translate_path(self, path):
if path.startswith('/files'):
path = path[7:]
return super().translate_path(path)
def serve_front(self):
page = """
MOTH Development Server Front Page
====================
Yo, it's the front page.
There's stuff you can do here:
* [Available puzzles](/puzzles)
* [Raw filesystem view](/files/)
* [Documentation](/files/doc/)
* [Instructions](/files/doc/devel-server.md) for using this server
If you use this development server to run a contest,
you are a fool.
"""
self.serve_md(page)
def serve_file(self):
if self.path.endswith(".md"):
self.serve_md()
else:
super().do_GET()
def serve_md(self, text=None):
fspathstr = self.translate_path(self.path)
fspath = pathlib.Path(fspathstr)
if not text:
try:
text = fspath.read_text()
except OSError:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
return None
content = mdpage(text)
self.send_response(http.server.HTTPStatus.OK)
self.send_header("Content-type", "text/html; encoding=utf-8")
self.send_header("Content-Length", len(content))
try:
fs = fspath.stat()
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
except:
pass
self.end_headers()
self.wfile.write(content.encode('utf-8'))
def run(address=('', 8080)):
httpd = ThreadingServer(address, MothHandler)
print("=== Listening on http://{}:{}/".format(address[0] or "localhost", address[1]))
httpd.serve_forever()
if __name__ == '__main__':
run()

46
doc/devel-server.md Normal file
View File

@ -0,0 +1,46 @@
Using the MOTH Development Server
======================
To make puzzle development easier,
MOTH comes with a standalone web server written in Python,
which will show you how your puzzles are going to look without making you compile or package anything.
It even works in Windows,
because that is what my career has become.
Starting It Up
-----------------
Just run `devel-server.py` in the top-level MOTH directory.
Installing New Puzzles
-----------------------------
You are meant to have your puzzles checked out into a `puzzles`
directory off the main MOTH directory.
You can do most of your development on this living copy.
In the directory containing `devel-server.py`, you would run something like:
git clone /path/to/my/puzzles-repository puzzles
or
ln -s /path/to/my/puzzles-repository puzzles
The development server wants to see category directories under `puzzles`,
like this:
$ find puzzles -type d
puzzles/
puzzles/category1/
puzzles/category1/10/
puzzles/category1/20/
puzzles/category1/30/
puzzles/category2/
puzzles/category2/100/
puzzles/category2/200/
puzzles/category2/300/

1190
mistune.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -54,6 +54,12 @@ pre, tt {
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
} }
#body {
max-width: 40em;
display: block;
margin: 1% auto;
}
#overview, #messages { #overview, #messages {
width: 47%; width: 47%;
height: 20%; height: 20%;
@ -79,6 +85,10 @@ a:link, .link {
cursor: pointer; cursor: pointer;
} }
a:visited {
color: #999999;
}
.category h2 { .category h2 {
margin: 0; margin: 0;
font-size: 100%; font-size: 100%;