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;
|
||||
}
|
||||
|
||||
#body {
|
||||
max-width: 40em;
|
||||
display: block;
|
||||
margin: 1% auto;
|
||||
}
|
||||
|
||||
#overview, #messages {
|
||||
width: 47%;
|
||||
height: 20%;
|
||||
|
@ -79,6 +85,10 @@ a:link, .link {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.category h2 {
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
|
|
Loading…
Reference in New Issue