diff --git a/README.md b/README.md index 9b62c5a..00f9d95 100644 --- a/README.md +++ b/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! - diff --git a/docs/overview.md b/docs/overview.md index 94d2885..5242aed 100644 --- a/docs/overview.md +++ b/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". diff --git a/src/maintenance.go b/src/maintenance.go index cba6a04..e6d7be3 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -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 { - ctx.tidy() - ctx.collectPoints() - ctx.generatePuzzleList() - ctx.generatePointsLog() + // 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") diff --git a/src/mothd.go b/src/mothd.go index 790bbf4..acfda71 100644 --- a/src/mothd.go +++ b/src/mothd.go @@ -47,7 +47,7 @@ func main() { ) listen := flag.String( "listen", - ":8000", + ":8080", "[host]:port to bind and listen", ) flag.Parse()