mirror of https://github.com/dirtbags/moth.git
Documentation updates
This commit is contained in:
parent
e460f5b6b3
commit
e147839966
67
README.md
67
README.md
|
@ -20,12 +20,33 @@ It also tracks scores,
|
|||
and comes with a JavaScript-based scoreboard to display team rankings.
|
||||
|
||||
|
||||
How everything works
|
||||
---------------------------
|
||||
Running a Development Server
|
||||
============================
|
||||
|
||||
docker run --rm -it -p 8080:8080 dirtbags/moth-devel
|
||||
|
||||
And point a browser to http://localhost:8080/ (or whatever host is running the server).
|
||||
|
||||
When you're ready to create your own puzzles,
|
||||
read [the devel server documentation](docs/devel-server.md).
|
||||
|
||||
Click the `[mb]` link by a puzzle category to compile and download a mothball that the production server can read.
|
||||
|
||||
|
||||
Running a Production Server
|
||||
===========================
|
||||
|
||||
docker run --rm -it -p 8080:8080 -v /path/to/moth:/moth dirtbags/mothd
|
||||
|
||||
You can be more fine-grained about directories, if you like.
|
||||
Inside the container, you need the following paths:
|
||||
|
||||
* `/moth/state` (rw) Where state is stored. Read [the overview](docs/overview.md) to learn what's what in here.
|
||||
* `/moth/mothballs` (ro) Mothballs (puzzle bundles) as provided by the development server.
|
||||
* `/moth/resources` (ro) Overrides for built-in HTML/CSS resources.
|
||||
|
||||
|
||||
|
||||
This section wound up being pretty long.
|
||||
Please check out [the overview](docs/overview.md)
|
||||
for details.
|
||||
|
||||
|
||||
Getting Started Developing
|
||||
|
@ -121,39 +142,3 @@ the category will vanish,
|
|||
but points scored in that category won't!
|
||||
|
||||
|
||||
|
||||
Resources Directory
|
||||
===================
|
||||
|
||||
|
||||
Making it look better
|
||||
-------------------
|
||||
|
||||
`mothd` provides some built-in HTML for rendering a complete contest,
|
||||
but it's rather bland.
|
||||
You can override everything by dropping a new file into the `resources` directory:
|
||||
|
||||
* `basic.css` is used by the default HTML to pretty things up
|
||||
* `index.html` is the landing page, which asks to register a team
|
||||
* `puzzle.html` and `puzzle.js` render a puzzle from JSON
|
||||
* `puzzle-list.html` and `puzzle-list.js` render the list of active puzzles from JSON
|
||||
* `scoreboard.html` and `scoreboard.js` render the current scoreboard from JSON
|
||||
* Any other file in the `resources` directory will be served up, too.
|
||||
|
||||
If you don't want to read through the source code, I don't blame you.
|
||||
Run a `mothd` server and pull the various static resources into your `resources` directory,
|
||||
and then you can start hacking away at them.
|
||||
|
||||
|
||||
Changing scoring
|
||||
--------------
|
||||
|
||||
Believe it or not,
|
||||
scoring is determined client-side in the scoreboard,
|
||||
from the points log.
|
||||
You can hack in whatever algorithm you like.
|
||||
|
||||
If you do hack in a new algorithm,
|
||||
please be a dear and email it to us.
|
||||
We'd love to see it!
|
||||
|
||||
|
|
273
docs/overview.md
273
docs/overview.md
|
@ -16,8 +16,142 @@ indicating score within each category,
|
|||
and overall ranking.
|
||||
|
||||
|
||||
How Scores are Calculated
|
||||
-------------------------
|
||||
State Directory
|
||||
===============
|
||||
|
||||
The state directory is written to by the server to preserve state.
|
||||
At no point is anything only in memory:
|
||||
if it's not on the filesystem,
|
||||
mothd doesn't think it exists.
|
||||
|
||||
The state directory is also used to communicate actions to mothd.
|
||||
|
||||
|
||||
`initialized`
|
||||
-------------
|
||||
|
||||
Remove this file to reset the state. This will blow away team assignments and the points log.
|
||||
|
||||
|
||||
`disabled`
|
||||
----------
|
||||
|
||||
Create this file to pause collection of points and other maintenance.
|
||||
Contestants can still submit answers,
|
||||
but they won't show up on the scoreboard until you remove this file.
|
||||
|
||||
This file does not normally exist.
|
||||
|
||||
|
||||
`until`
|
||||
-------
|
||||
|
||||
Put an RFC3337 date/time stamp in here to have the server pause itself at a given time.
|
||||
Remember that time zones exist!
|
||||
I recommend always using Zulu time.
|
||||
|
||||
This file does not normally exist.
|
||||
|
||||
|
||||
`teamids.txt`
|
||||
-------------
|
||||
|
||||
A list of valid Team IDs, one per line.
|
||||
It defaults to all 4-digit natural numbers,
|
||||
but you can put whatever you want in here.
|
||||
|
||||
|
||||
`points.log`
|
||||
------------
|
||||
|
||||
The log of awarded points:
|
||||
|
||||
EpochTime TeamId Category Points
|
||||
|
||||
Do not write to this file, unless you have disabled the contest. You will lose points!
|
||||
|
||||
|
||||
`points.tmp`
|
||||
------------
|
||||
|
||||
Drop points logs here.
|
||||
Filenames can be anything.
|
||||
|
||||
When the file is complete and written out,
|
||||
move it into `points.new`,
|
||||
where a non-disabled event's maintenance loop will eventually move it into the main log.
|
||||
|
||||
`points.new`
|
||||
------------
|
||||
|
||||
Complete points logs should be atomically moved here.
|
||||
This is to avoid needing locks.
|
||||
[Read about Maildir](https://en.wikipedia.org/wiki/Maildir)
|
||||
if you care about the technical reasons we do things this way.
|
||||
|
||||
|
||||
Mothball Directory
|
||||
==================
|
||||
|
||||
Put a mothball in this directory to open that category.
|
||||
Remove a mothball to disable that category.
|
||||
|
||||
Overwriting a mothball with a newer version will be noticed by the server within one maintenance interval
|
||||
(20 seconds by default).
|
||||
Be sure to use the same compilation seed in the development server if you compile a new version!
|
||||
|
||||
Removing a category does not remove points that have been scored in the category.
|
||||
|
||||
|
||||
Resources Directory
|
||||
===================
|
||||
|
||||
|
||||
Making it look better
|
||||
-------------------
|
||||
|
||||
`mothd` provides some built-in HTML for rendering a complete contest,
|
||||
but it's rather bland.
|
||||
You can override everything by dropping a new file into the `resources` directory:
|
||||
|
||||
* `basic.css` is used by the default HTML to pretty things up
|
||||
* `index.html` is the landing page, which asks to register a team
|
||||
* `puzzle.html` renders a puzzle from JSON
|
||||
* `puzzle-list.html` renders the list of active puzzles from JSON
|
||||
* `scoreboard.html` renders the current scoreboard from JSON
|
||||
* Any other file in the `resources` directory will be served up, too.
|
||||
|
||||
If you don't want to read through the source code, I don't blame you.
|
||||
Run a `mothd` server and pull the various static resources into your `resources` directory,
|
||||
and then you can start hacking away at them.
|
||||
|
||||
|
||||
Making it look totally different
|
||||
---------------------
|
||||
|
||||
Every handler can serve its answers up in JSON format,
|
||||
just add `application/json` to the `Accept` header of your request.
|
||||
|
||||
This means you could completely ignore the file structure in the previous section,
|
||||
and write something like a web app that only loads static resources at startup.
|
||||
|
||||
|
||||
Changing scoring
|
||||
--------------
|
||||
|
||||
Scoring is determined client-side in the scoreboard,
|
||||
from the points log.
|
||||
You can hack in whatever algorithm you like,
|
||||
and provide your own scoreboard(s).
|
||||
|
||||
If you do hack in a new algorithm,
|
||||
please be a dear and email it to us.
|
||||
We'd love to see it!
|
||||
|
||||
|
||||
|
||||
How Scores are Calculated by Default
|
||||
------------------------------------
|
||||
|
||||
The per-category score for team `t` is computed as:
|
||||
|
||||
|
@ -38,138 +172,3 @@ Because we don't award extra points for quick responses,
|
|||
teams always feel like they have the possibility to catch up if they are skilled enough.
|
||||
|
||||
|
||||
Requirements
|
||||
-------------
|
||||
|
||||
MOTH was written to run on a wide range of Linux systems.
|
||||
We are very careful not to require exotic extensions:
|
||||
you can run MOTH equally well on OpenWRT and Ubuntu Server.
|
||||
It might even run on BSD: if you've tried this, please email us!
|
||||
|
||||
Its architecture also limits permissions,
|
||||
to make it easier to lock things down very tight.
|
||||
Since it writes to the filesystem slowly and atomically,
|
||||
it can be run from a USB flash drive formatted with VFAT.
|
||||
|
||||
|
||||
On the server, it requires:
|
||||
|
||||
* Bourne shell (POSIX 1003.2: BASH is okay but not required)
|
||||
* Awk (POSIX 1003.2: gawk is okay but not required)
|
||||
* Lua 5.1
|
||||
|
||||
|
||||
On the client, it requires:
|
||||
|
||||
* A modern web browser with JavaScript
|
||||
* Categories might add other requirements (like domain-specific tools to solve the puzzles)
|
||||
|
||||
|
||||
Filesystem Layout
|
||||
=================
|
||||
|
||||
The system is set up to make it simple to run one or more contests on a single machine.
|
||||
|
||||
I like to use `/srv/moth` as the base directory for all instances.
|
||||
So if I were running an instance called "hack",
|
||||
the instance directory would be `/srv/moth/hack`.
|
||||
|
||||
There are five entries in each instance directory, described in detail below:
|
||||
|
||||
/srv/moth/hack # (r-x) Instance directory
|
||||
/srv/moth/hack/assigned.txt # (r--) List of assigned team tokens
|
||||
/srv/moth/hack/bin/ # (r-x) Per-instance binaries
|
||||
/srv/moth/hack/categories/ # (r-x) Installed categories
|
||||
/srv/moth/hack/state/ # (rwx) Contest state
|
||||
/srv/moth/hack/www/ # (r-x) Web server documentroot
|
||||
|
||||
|
||||
|
||||
`state/assigned.txt`
|
||||
----------------
|
||||
|
||||
This is just a list of tokens that have been assigned.
|
||||
One token per line, and tokens can be anything you want.
|
||||
|
||||
For my middle school events, I make tokens all possible 4-digit numbers,
|
||||
and tell kids to use any number they want: it makes it quicker to start.
|
||||
For more advanced events,
|
||||
this doesn't work as well because people start guessing other teams' numbers to confuse each other.
|
||||
So I use hex representations of random 32-bit ints.
|
||||
But you could use anything you want in here (for specifics on allowed characters, read the registration CGI).
|
||||
|
||||
The registration CGI checks this list to see if a token has already assigned to a team name.
|
||||
Teams enter points by token,
|
||||
which lets them use any text they want for a team name.
|
||||
Since we don't read their team name anywhere else than the registration and scoreboard generator,
|
||||
it allows some assumptions about what kind of strings tokens can be,
|
||||
resulting in simpler code.
|
||||
|
||||
|
||||
`categories/`
|
||||
--------------
|
||||
|
||||
`categories/` contains read-only category packages.
|
||||
Within each subdirectory there is:
|
||||
|
||||
* `map.txt` mapping point values to directory names
|
||||
* `answers.txt` a list of answers for each point value
|
||||
* `salt` used to generate directory names (so people can't guess them to skip ahead)
|
||||
* `summary.txt` a compliation of `00summary.txt` files for puzzles, to give you a quick reference point when someone says "I need help on js 40".
|
||||
* `puzzles` is all the HTML that needs to be served up for the category
|
||||
|
||||
|
||||
`bin/`
|
||||
------
|
||||
|
||||
Contains all the binaries you'll need to run an event.
|
||||
These are probably just copies from the `base` package (where this README lives).
|
||||
They're copied over in case you need to hack on them during an event.
|
||||
|
||||
`bin/once` is of particular interest:
|
||||
it gets run periodically to do everything, including:
|
||||
|
||||
* Gather points from `points.new` and append them to the points log.
|
||||
* Generate a new `puzzles.html` listing all open puzzles.
|
||||
* Generate a new `points.json` for the scoreboard
|
||||
|
||||
### Pausing `once`
|
||||
|
||||
You can pause everything `bin/once` does by touching a file in the root directory
|
||||
called `disabled`.
|
||||
This doesn't stop the game:
|
||||
it just stops points collection and generation of the files listed above.
|
||||
|
||||
This is extremely helpful when, inevitably,
|
||||
you need to hack the points log,
|
||||
or do other maintenance tasks.
|
||||
Most times you don't even need to announce that you're doing anything:
|
||||
people can keep playing the game and their points keep collecting,
|
||||
ready to be appended to the log when you're done and you re-enable `once`.
|
||||
|
||||
|
||||
`www/`
|
||||
-----------
|
||||
|
||||
HTML root for an event.
|
||||
It is possible to make this read-only,
|
||||
after you've set up your packages.
|
||||
You will need to symlink a few things into the `state` directory, though.
|
||||
|
||||
|
||||
`state/`
|
||||
---------
|
||||
|
||||
Where all game state is stored.
|
||||
This is the only part of the contest directory setup that needs to be writable,
|
||||
and tarring it up preserves exactly the entire contest.
|
||||
|
||||
Notable, it contains the mapping from team hash to name,
|
||||
and the points log.
|
||||
|
||||
`points.log` is replayed by the scoreboard generator to calculate the current score for each team.
|
||||
|
||||
New points are written to `points.new`, and picked up by `bin/once` to append to `points.log`.
|
||||
When `once` is disabled (by touching a file called `disabled` at the top level for a game),
|
||||
the various points-awarding things can keep writing files into `points.new`,
|
||||
with no need for locking or "bringing down the game for maintenance".
|
||||
|
|
|
@ -119,12 +119,6 @@ func (ctx *Instance) tidy() {
|
|||
// Do they want to reset everything?
|
||||
ctx.MaybeInitialize()
|
||||
|
||||
// Skip if we've been disabled
|
||||
if _, err := os.Stat(ctx.StatePath("disabled")); err == nil {
|
||||
log.Print("disabled file found, suspending maintenance")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if we've expired
|
||||
untilspec, err := ioutil.ReadFile(ctx.StatePath("until"))
|
||||
if err == nil {
|
||||
|
@ -225,10 +219,15 @@ func (ctx *Instance) collectPoints() {
|
|||
// maintenance is the goroutine that runs a periodic maintenance task
|
||||
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||
for {
|
||||
// Skip if we've been disabled
|
||||
if _, err := os.Stat(ctx.StatePath("disabled")); err == nil {
|
||||
log.Print("disabled file found, suspending maintenance")
|
||||
} else {
|
||||
ctx.tidy()
|
||||
ctx.collectPoints()
|
||||
ctx.generatePuzzleList()
|
||||
ctx.generatePointsLog()
|
||||
}
|
||||
select {
|
||||
case <-ctx.update:
|
||||
// log.Print("Forced update")
|
||||
|
|
|
@ -47,7 +47,7 @@ func main() {
|
|||
)
|
||||
listen := flag.String(
|
||||
"listen",
|
||||
":8000",
|
||||
":8080",
|
||||
"[host]:port to bind and listen",
|
||||
)
|
||||
flag.Parse()
|
||||
|
|
Loading…
Reference in New Issue