mirror of https://github.com/dirtbags/moth.git
Initial work on new format puzzles, new development server
This commit is contained in:
parent
55be0d96b1
commit
d9f7efa82b
62
README
62
README
|
@ -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
9
TODO
|
@ -1,9 +0,0 @@
|
||||||
* Scoreboard refresh
|
|
||||||
|
|
||||||
Test:
|
|
||||||
|
|
||||||
* awarding points
|
|
||||||
* points already awarded
|
|
||||||
* bad team hash
|
|
||||||
* category doesn't exist
|
|
||||||
* puzzle doesn't exist
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()
|
|
@ -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/
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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%;
|
||||||
|
|
Loading…
Reference in New Issue