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/
target/
puzzles
__debug_bin

View File

@ -1,21 +1,26 @@
Author: neale
Summary: static puzzles
Answer: puzzle.moth
---
pre:
authors:
- neale
debug:
summary: static puzzles
answers:
- puzzle.md
---
Puzzle categories are laid out on the filesystem:
example/
├─1
│ └─puzzle.moth
│ └─puzzle.md
├─2
│ ├─puzzle.moth
│ ├─puzzle.md
│ └─salad.jpg
├─3
│ └─puzzle.py
│ └─mkpuzzle
├─10
│ └─puzzle.moth
│ └─puzzle.md
└─100
└─puzzle.py
└─mkpuzzle
In this example,
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.
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
`puzzle.moth` file in the puzzle's directory.
`puzzle.md` file in the puzzle's directory.
This file is in the following format:
Author: [name of the person who wrote this puzzle]
Summary: [brief description of the puzzle]
Answer: [answer to this puzzle]
Answer: [second acceptable answer to this puzzle]
---
pre:
authors:
- 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.
It is Markdown formatted:
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)
answer=$(echo $(grep -v "['A-Z]" /usr/share/dict/words | shuf -n 4))
import argparse
import json
import os
import random
import shutil
import sys
case "$1:$2" in
:)
cat <<EOT
{
"Pre": {
"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'>",
"Attachments": ["salad.jpg"]
},
"Answers": [
"$answer"
],
"Debug": {
"Summary": "Dynamic puzzles",
"Hints": [
"Check the debug output to get the answer."
parser = argparse.ArgumentParser("Generate a puzzle")
parser.add_argument("--file", dest="file", help="File to provide, instead of puzzle")
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": {
"Authors": ["neale"],
"Body": (
"<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": [
answer,
],
"Errors": [],
"Log": [
"$number is a positive integer"
]
"Debug": {
"Summary": "Dynamic puzzles",
"Hints": [
"Check the debug output to get the answer." ,
],
"Errors": [],
"Log": [
"%d is a positive integer" % number,
],
}
}
}
EOT
;;
-file:salad.jpg)
cat salad.jpg
;;
esac
json.dump(obj, sys.stdout)

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
function helperUpdateAnswer(event) {
async function helperUpdateAnswer(event) {
let e = event.currentTarget
let value = e.value
let inputs = e.querySelectorAll("input")
@ -24,7 +24,11 @@ function helperUpdateAnswer(event) {
if (join === undefined) {
join = ","
}
value = values.join(join)
if (values.length == 0) {
value = "None"
} else {
value = values.join(join)
}
}
// First make any adjustments to the value
@ -35,6 +39,35 @@ function helperUpdateAnswer(event) {
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")
answer.value = value
answer.dispatchEvent(new InputEvent("input"))
@ -78,15 +111,16 @@ function helperActivate(e) {
}
}
{
let init = function(event) {
for (let e of document.querySelectorAll(".answer")) {
helperActivate(e)
}
}
function helperInit(event) {
for (let e of document.querySelectorAll(".answer")) {
helperActivate(e)
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", helperInit);
} else {
helperInit();
}

View File

@ -1,9 +1,15 @@
Summary: Using JavaScript Input Helpers
Author: neale
Script: helpers.js
Script: draggable.js
Answer: helper
---
pre:
authors:
- neale
scripts:
- filename: helpers.js
- filename: draggable.js
answers:
- helper
debug:
summary: Using JavaScript Input Helpers
---
MOTH only takes static answers:
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.
@ -18,7 +24,7 @@ This is just a demonstration page.
You will probably only want one of these in a page,
to avoid confusing people.
RFC3339 Timestamp
### RFC3339 Timestamp
<div class="answer" data-join="">
<input type="date">
<input type="hidden" value="T">
@ -26,10 +32,10 @@ RFC3339 Timestamp
<input type="hidden" value="Z">
</div>
All lower-case letters
### All lower-case letters
<input class="answer lower">
Multiple concatenated values
### Multiple concatenated values
<div class="answer lower">
<input type="color">
<input type="number">
@ -37,22 +43,32 @@ Multiple concatenated values
<input>
</div>
Free input, sorted, concatenated values
### Free input, sorted, concatenated values
<ul class="answer lower sort">
<li><input></li>
<li><button class="expand" title="Add another input"></button><l/i>
</ul>
User-draggable values
### User-draggable values
<ul class="answer">
<li draggable="true"><input value="First" readonly></li>
<li draggable="true"><input value="Third" readonly></li>
<li draggable="true"><input value="Second" readonly></li>
</ul>
Select from an ordered list of options
### Select from an ordered list of options
<ul class="answer">
<li><input type="checkbox" value="horn">Horns</li>
<li><input type="checkbox" value="hoof">Hooves</li>
<li><input type="checkbox" value="antler">Antlers</li>
</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"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -27,9 +28,9 @@ type Puzzle struct {
Authors []string
Attachments []string
Scripts []string
AnswerHashes []string
AnswerPattern string
Body string
AnswerPattern string
AnswerHashes []string
}
Post struct {
Objective string
@ -89,7 +90,6 @@ type StaticPuzzle struct {
type StaticAttachment struct {
Filename string // Filename presented as part of puzzle
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.
@ -270,11 +270,6 @@ func legacyAttachmentParser(val []string) []StaticAttachment {
} else {
cur.Filename = cur.FilesystemPath
}
if (len(parts) > 2) && (parts[2] == "hidden") {
cur.Listed = false
} else {
cur.Listed = true
}
ret[idx] = cur
}
return ret
@ -351,7 +346,9 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
cmd := exec.CommandContext(ctx, fp.command)
cmd.Dir = path.Dir(fp.command)
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
}
@ -381,7 +378,7 @@ func (fp FsCommandPuzzle) Open(filename string) (ReadSeekCloser, error) {
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, fp.command, "-file", filename)
cmd := exec.CommandContext(ctx, fp.command, "--file", filename)
cmd.Dir = path.Dir(fp.command)
out, err := cmd.Output()
buf := nopCloser{bytes.NewReader(out)}
@ -397,7 +394,7 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, fp.command, "-answer", answer)
cmd := exec.CommandContext(ctx, fp.command, "--answer", answer)
cmd.Dir = path.Dir(fp.command)
out, err := cmd.Output()
if err != nil {

View File

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