Port some example puzzles

This commit is contained in:
Neale Pickett 2020-09-11 17:33:43 -06:00
parent 490ac78f15
commit 94bc9472b7
13 changed files with 222 additions and 205 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ build/
cache/ cache/
target/ target/
puzzles puzzles
__debug_bin

View File

@ -1,21 +1,26 @@
Author: neale ---
Summary: static puzzles pre:
Answer: puzzle.moth authors:
- neale
debug:
summary: static puzzles
answers:
- puzzle.md
---
Puzzle categories are laid out on the filesystem: Puzzle categories are laid out on the filesystem:
example/ example/
├─1 ├─1
│ └─puzzle.moth │ └─puzzle.md
├─2 ├─2
│ ├─puzzle.moth │ ├─puzzle.md
│ └─salad.jpg │ └─salad.jpg
├─3 ├─3
│ └─puzzle.py │ └─mkpuzzle
├─10 ├─10
│ └─puzzle.moth │ └─puzzle.md
└─100 └─100
└─puzzle.py └─mkpuzzle
In this example, In this example,
there are puzzles with point values 1, 2, 3, 10, and 100. there are puzzles with point values 1, 2, 3, 10, and 100.
@ -24,17 +29,22 @@ Puzzles 1, 2, and 10 are "static" puzzles:
their content was written by hand. their content was written by hand.
Puzzles 3 and 100 are "dynamic" puzzles: Puzzles 3 and 100 are "dynamic" puzzles:
they are generated from a Python module. their content is generated by `mkpuzzle`.
To create a static puzzle, all you must have is a To create a static puzzle, all you must have is a
`puzzle.moth` file in the puzzle's directory. `puzzle.md` file in the puzzle's directory.
This file is in the following format: This file is in the following format:
Author: [name of the person who wrote this puzzle] ---
Summary: [brief description of the puzzle] pre:
Answer: [answer to this puzzle] authors:
Answer: [second acceptable answer to this puzzle] - name of the person who wrote this puzzle
debug:
summary: brief description of the puzzle
answers:
- answer to this puzzle
- second acceptable answer to this puzzle
---
This is the puzzle body. This is the puzzle body.
It is Markdown formatted: It is Markdown formatted:
you can read more about Markdown on the Internet. you can read more about Markdown on the Internet.

View File

@ -0,0 +1,36 @@
---
pre:
authors:
- neale
attachments:
- filename: salad.jpg
- filename: s2.jpg
filesystempath: salad2.jpg
debug:
summary: Static puzzle resource files
answers:
- salad
---
You can include additional resources in a static puzzle,
by dropping them in the directory and listing them under `attachments`.
If the puzzle compiler sees both `filename` and `filesystempath`,
it changes the filename when the puzzle category is built.
You can use this to give good filenames while building,
but obscure them during build.
On this page, we obscure
`salad2.jpg` to `s2.jpg`,
so that people can't guess the answer based on filename.
Check the source to this puzzle to see how this is done!
You can refer to resources directly in your Markdown,
or use them however else you see fit.
They will appear in the same directory on the web server once the exercise is running.
Check the source for this puzzle to see how it was created.
![Leafy Green Deliciousness](salad.jpg)
![Mmm so good](s2.jpg)
The answer for this page is what is featured in the photograph.

View File

@ -1,39 +0,0 @@
Author: neale
Summary: Static puzzle resource files
File: salad.jpg s.jpg
File: salad2.jpg s2.jpg hidden
Answer: salad
X-Answer-Pattern: *pong
You can include additional resources in a static puzzle,
by dropping them in the directory and listing them in a `File:` header field.
The format is:
File: filename [translatedname] [hidden]
If `translatedname` is provided,
the filename is changed to it when the puzzle category is built.
You can use this to give good filenames while building,
but obscure them during build.
On this page, we obscure `salad.jpg` to `s.jpg`,
and `salad2.jpg` to `s2.jpg`,
so that people can't guess the answer based on filename.
The word `hidden`, if present,
prevents a file from being listed at the bottom of the page.
Here are the `File:` fields in this page:
File: salad.jpg s.jpg
File: salad2.jpg s2.jpg hidden
You can refer to resources directly in your Markdown,
or use them however else you see fit.
They will appear in the same directory on the web server once the exercise is running.
Check the source for this puzzle to see how it was created.
![Leafy Green Deliciousness](s.jpg)
![Mmm so good](s2.jpg)
The answer for this page is what is featured in the photograph.

View File

@ -1,34 +1,55 @@
#! /bin/sh #! /usr/bin/python3
number=$(seq 20 500 | shuf -n 1) import argparse
answer=$(echo $(grep -v "['A-Z]" /usr/share/dict/words | shuf -n 4)) import json
import os
import random
import shutil
import sys
case "$1:$2" in parser = argparse.ArgumentParser("Generate a puzzle")
:) parser.add_argument("--file", dest="file", help="File to provide, instead of puzzle")
cat <<EOT parser.add_argument("--answer", dest="answer", help="Answer to check, instead of providing puzzle")
{ args = parser.parse_args()
seed = hash(os.getenv("SEED"))
random.seed(seed)
words = ["apple", "pear", "peach", "tangerine", "orange", "potato", "carrot", "pea"]
answer = ' '.join(random.sample(words, 4))
if args.file:
f = open(args.file, "rb")
shutil.copyfileobj(f, sys.stdout.buffer)
elif args.answer:
if args.answer == answer:
print("correct")
else:
print("incorrect")
else:
number = random.randint(20, 500)
obj = {
"Pre": { "Pre": {
"Authors": ["neale"], "Authors": ["neale"],
"Body": "<p>Dynamic puzzles are provided with a JSON-generating <code>mkpuzzles</code> program in the puzzle directory.</p><img src='salad.jpg'>", "Body": (
"Attachments": ["salad.jpg"] "<p>Dynamic puzzles are provided with a JSON-generating <code>mkpuzzles</code> program in the puzzle directory.</p>"
"<p>You can write <code>mkpuzzles</code> in any language you like. This puzzle was written in Python 3.</p>"
"<p>Here is some salad:<img src='salad.jpg'></p>"
),
"Attachments": ["salad.jpg"],
}, },
"Answers": [ "Answers": [
"$answer" answer,
], ],
"Debug": { "Debug": {
"Summary": "Dynamic puzzles", "Summary": "Dynamic puzzles",
"Hints": [ "Hints": [
"Check the debug output to get the answer." "Check the debug output to get the answer." ,
], ],
"Errors": [], "Errors": [],
"Log": [ "Log": [
"$number is a positive integer" "%d is a positive integer" % number,
] ],
} }
} }
EOT json.dump(obj, sys.stdout)
;;
-file:salad.jpg)
cat salad.jpg
;;
esac

View File

@ -1,27 +0,0 @@
#!/usr/bin/python3
def make(puzzle):
puzzle.author = 'neale'
puzzle.summary = 'dynamic puzzles'
answer = puzzle.randword()
puzzle.answers.append(answer)
puzzle.body.write("To generate a dynamic puzzle, you need to write a Python module.\n")
puzzle.body.write("\n")
puzzle.body.write("The passed-in puzzle object provides some handy methods.\n")
puzzle.body.write("In particular, please use the `puzzle.rand` object to guarantee that rebuilding a category\n")
puzzle.body.write("won't change puzzles and answers.\n")
puzzle.body.write("(Participants don't like it when puzzles and answers change.)\n")
puzzle.body.write("\n")
puzzle.add_file('salad.jpg')
puzzle.body.write("Here are some more pictures of salad:\n")
puzzle.body.write("<img src='salad.jpg' alt='Markdown lets you insert raw HTML if you want'>")
puzzle.body.write("![salad](salad.jpg)")
puzzle.body.write("\n\n")
number = puzzle.rand.randint(20, 500)
puzzle.log("One is the loneliest number, but {} is the saddest number.".format(number))
puzzle.body.write("The answer for this page is `{}`.\n".format(answer))

View File

@ -1,20 +0,0 @@
Summary: Answer patterns
Answer: command.com
Answer: COMMAND.COM
X-Answer-Pattern: PINBALL.*
X-Answer-Pattern: pinball.*
Author: neale
Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3}
This puzzle features answer input pattern checking.
Sometimes you need to provide a hint about whether the user has entered the answer in the right format.
By providing a `Pattern` value (a regular expression),
the browser will (hopefully) provide a visual hint when an answer is incorrectly formatted.
It will also (hopefully) prevent the user from submitting,
which will (hopefully) inform the participant that they may have the right solution technique,
but there's a problem with the format of the answer.
This will (hopefully) keep people from getting overly-frustrated with difficult-to-enter answers.
This answer field will validate only FAT 8+3 filenames.
Try it!

View File

@ -1,6 +1,6 @@
// jshint asi:true // jshint asi:true
function helperUpdateAnswer(event) { async function helperUpdateAnswer(event) {
let e = event.currentTarget let e = event.currentTarget
let value = e.value let value = e.value
let inputs = e.querySelectorAll("input") let inputs = e.querySelectorAll("input")
@ -24,8 +24,12 @@ function helperUpdateAnswer(event) {
if (join === undefined) { if (join === undefined) {
join = "," join = ","
} }
if (values.length == 0) {
value = "None"
} else {
value = values.join(join) value = values.join(join)
} }
}
// First make any adjustments to the value // First make any adjustments to the value
if (e.classList.contains("lower")) { if (e.classList.contains("lower")) {
@ -35,6 +39,35 @@ function helperUpdateAnswer(event) {
value = value.toUpperCase() value = value.toUpperCase()
} }
// "substrings" answers try all substrings. If any are the answer, they're filled in.
if (e.classList.contains("substring")) {
let validated = null
let anchorEnd = e.classList.contains("anchor-end")
let anchorBeg = e.classList.contains("anchor-beg")
for (let end = 0; end <= value.length; end += 1) {
for (let beg = 0; beg < value.length; beg += 1) {
if (anchorEnd && (end != value.length)) {
continue
}
if (anchorBeg && (beg != 0)) {
continue
}
let sub = value.substring(beg, end)
if (await checkAnswer(sub)) {
validated = sub
}
}
}
value = validated
}
// If anything zeroed out value, don't update the answer field
if (!value) {
return
}
let answer = document.querySelector("#answer") let answer = document.querySelector("#answer")
answer.value = value answer.value = value
answer.dispatchEvent(new InputEvent("input")) answer.dispatchEvent(new InputEvent("input"))
@ -78,15 +111,16 @@ function helperActivate(e) {
} }
} }
{
function helperInit(event) { let init = function(event) {
for (let e of document.querySelectorAll(".answer")) { for (let e of document.querySelectorAll(".answer")) {
helperActivate(e) helperActivate(e)
} }
} }
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", helperInit); document.addEventListener("DOMContentLoaded", init)
} else { } else {
helperInit(); init()
}
} }

View File

@ -1,9 +1,15 @@
Summary: Using JavaScript Input Helpers ---
Author: neale pre:
Script: helpers.js authors:
Script: draggable.js - neale
Answer: helper scripts:
- filename: helpers.js
- filename: draggable.js
answers:
- helper
debug:
summary: Using JavaScript Input Helpers
---
MOTH only takes static answers: MOTH only takes static answers:
you can't, for instance, write code to check answer correctness. you can't, for instance, write code to check answer correctness.
But you can provide as many correct answers as you like in a single puzzle. But you can provide as many correct answers as you like in a single puzzle.
@ -18,7 +24,7 @@ This is just a demonstration page.
You will probably only want one of these in a page, You will probably only want one of these in a page,
to avoid confusing people. to avoid confusing people.
RFC3339 Timestamp ### RFC3339 Timestamp
<div class="answer" data-join=""> <div class="answer" data-join="">
<input type="date"> <input type="date">
<input type="hidden" value="T"> <input type="hidden" value="T">
@ -26,10 +32,10 @@ RFC3339 Timestamp
<input type="hidden" value="Z"> <input type="hidden" value="Z">
</div> </div>
All lower-case letters ### All lower-case letters
<input class="answer lower"> <input class="answer lower">
Multiple concatenated values ### Multiple concatenated values
<div class="answer lower"> <div class="answer lower">
<input type="color"> <input type="color">
<input type="number"> <input type="number">
@ -37,22 +43,32 @@ Multiple concatenated values
<input> <input>
</div> </div>
Free input, sorted, concatenated values ### Free input, sorted, concatenated values
<ul class="answer lower sort"> <ul class="answer lower sort">
<li><input></li> <li><input></li>
<li><button class="expand" title="Add another input"></button><l/i> <li><button class="expand" title="Add another input"></button><l/i>
</ul> </ul>
User-draggable values ### User-draggable values
<ul class="answer"> <ul class="answer">
<li draggable="true"><input value="First" readonly></li> <li draggable="true"><input value="First" readonly></li>
<li draggable="true"><input value="Third" readonly></li> <li draggable="true"><input value="Third" readonly></li>
<li draggable="true"><input value="Second" readonly></li> <li draggable="true"><input value="Second" readonly></li>
</ul> </ul>
Select from an ordered list of options ### Select from an ordered list of options
<ul class="answer"> <ul class="answer">
<li><input type="checkbox" value="horn">Horns</li> <li><input type="checkbox" value="horn">Horns</li>
<li><input type="checkbox" value="hoof">Hooves</li> <li><input type="checkbox" value="hoof">Hooves</li>
<li><input type="checkbox" value="antler">Antlers</li> <li><input type="checkbox" value="antler">Antlers</li>
</ul> </ul>
### Substring matches
#### Any substring
<input class="answer substring">
#### Only if at the beginning
<input class="answer substring anchor-beg">
#### Only if at the end
<input class="answer substring anchor-end">

View File

@ -0,0 +1,18 @@
---
pre:
authors:
- neale
answers:
- YAML
- yaml
debug:
summary: YAML Metadata
post:
objective: Understand how YAML metadata can be used in a `.moth` file
success:
acceptable: Enter the answer and move on
mastery: Create a `.md` file using YAML metadata and serve it through the devel server.
---
You can also provide metadata in YAML format.
This puzzle's `.md` file serves as an example.

View File

@ -1,17 +0,0 @@
---
Summary: YAML Metadata
Author: neale
Answer: YAML
Answer: yaml
Objective: |
Understand how YAML metadata can be used in a `.moth` file
Success:
Acceptable: |
Enter the answer and move on
Mastery: |
Create a `.moth` file using YAML metadata and serve it
through the devel server.
---
You can also provide metadata in YAML format.
This puzzle's `.moth` file serves as an example.

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -27,9 +28,9 @@ type Puzzle struct {
Authors []string Authors []string
Attachments []string Attachments []string
Scripts []string Scripts []string
AnswerHashes []string
AnswerPattern string
Body string Body string
AnswerPattern string
AnswerHashes []string
} }
Post struct { Post struct {
Objective string Objective string
@ -89,7 +90,6 @@ type StaticPuzzle struct {
type StaticAttachment struct { type StaticAttachment struct {
Filename string // Filename presented as part of puzzle Filename string // Filename presented as part of puzzle
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS) FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
Listed bool // Whether this file is listed as an attachment
} }
// ReadSeekCloser provides io.Reader, io.Seeker, and io.Closer. // ReadSeekCloser provides io.Reader, io.Seeker, and io.Closer.
@ -270,11 +270,6 @@ func legacyAttachmentParser(val []string) []StaticAttachment {
} else { } else {
cur.Filename = cur.FilesystemPath cur.Filename = cur.FilesystemPath
} }
if (len(parts) > 2) && (parts[2] == "hidden") {
cur.Listed = false
} else {
cur.Listed = true
}
ret[idx] = cur ret[idx] = cur
} }
return ret return ret
@ -351,7 +346,9 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
cmd := exec.CommandContext(ctx, fp.command) cmd := exec.CommandContext(ctx, fp.command)
cmd.Dir = path.Dir(fp.command) cmd.Dir = path.Dir(fp.command)
stdout, err := cmd.Output() stdout, err := cmd.Output()
if err != nil { if exiterr, ok := err.(*exec.ExitError); ok {
return Puzzle{}, errors.New(string(exiterr.Stderr))
} else if err != nil {
return Puzzle{}, err return Puzzle{}, err
} }
@ -381,7 +378,7 @@ func (fp FsCommandPuzzle) Open(filename string) (ReadSeekCloser, error) {
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout) ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, fp.command, "-file", filename) cmd := exec.CommandContext(ctx, fp.command, "--file", filename)
cmd.Dir = path.Dir(fp.command) cmd.Dir = path.Dir(fp.command)
out, err := cmd.Output() out, err := cmd.Output()
buf := nopCloser{bytes.NewReader(out)} buf := nopCloser{bytes.NewReader(out)}
@ -397,7 +394,7 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout) ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, fp.command, "-answer", answer) cmd := exec.CommandContext(ctx, fp.command, "--answer", answer)
cmd.Dir = path.Dir(fp.command) cmd.Dir = path.Dir(fp.command)
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {

View File

@ -48,34 +48,21 @@ async function sha256Hash(message) {
} }
// Is the provided answer possibly correct? // Is the provided answer possibly correct?
async function possiblyCorrect(answer) { async function checkAnswer(answer) {
let pattern = window.puzzle.Pre.AnswerPattern || [] let answerHashes = []
answerHashes.push(djb2hash(answer))
answerHashes.push(await sha256Hash(answer))
for (let hash of answerHashes) {
for (let correctHash of window.puzzle.Pre.AnswerHashes) { for (let correctHash of window.puzzle.Pre.AnswerHashes) {
if (djb2hash(answer) == correctHash) { if (hash == correctHash) {
return answer return true
}
for (let end = 0; end <= answer.length; end += 1) {
if (pattern.includes("end") && (end != answer.length)) {
continue
}
for (let beg = 0; beg < answer.length; beg += 1) {
if (pattern.includes("begin") && (beg != 0)) {
continue
}
let sub = answer.substring(beg, end)
let digest = await sha256Hash(sub)
if (digest == correctHash) {
return sub
}
} }
} }
} }
return false return false
} }
// Pop up a message // Pop up a message
function toast(message, timeout=5000) { function toast(message, timeout=5000) {
let p = document.createElement("p") let p = document.createElement("p")
@ -196,7 +183,7 @@ function answerCheck(e) {
return return
} }
possiblyCorrect(answer) checkAnswer(answer)
.then (correct => { .then (correct => {
document.querySelector("[name=xAnswer").value = correct || answer document.querySelector("[name=xAnswer").value = correct || answer
if (correct) { if (correct) {