mirror of https://github.com/dirtbags/moth.git
Port some example puzzles
This commit is contained in:
parent
490ac78f15
commit
94bc9472b7
|
@ -8,3 +8,4 @@ build/
|
|||
cache/
|
||||
target/
|
||||
puzzles
|
||||
__debug_bin
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
{
|
||||
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><img src='salad.jpg'>",
|
||||
"Attachments": ["salad.jpg"]
|
||||
"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"
|
||||
answer,
|
||||
],
|
||||
"Debug": {
|
||||
"Summary": "Dynamic puzzles",
|
||||
"Hints": [
|
||||
"Check the debug output to get the answer."
|
||||
"Check the debug output to get the answer." ,
|
||||
],
|
||||
"Errors": [],
|
||||
"Log": [
|
||||
"$number is a positive integer"
|
||||
]
|
||||
"%d is a positive integer" % number,
|
||||
],
|
||||
}
|
||||
}
|
||||
EOT
|
||||
;;
|
||||
-file:salad.jpg)
|
||||
cat salad.jpg
|
||||
;;
|
||||
esac
|
||||
json.dump(obj, sys.stdout)
|
||||
|
|
|
@ -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))
|
||||
|
|
@ -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!
|
|
@ -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,8 +24,12 @@ function helperUpdateAnswer(event) {
|
|||
if (join === undefined) {
|
||||
join = ","
|
||||
}
|
||||
if (values.length == 0) {
|
||||
value = "None"
|
||||
} else {
|
||||
value = values.join(join)
|
||||
}
|
||||
}
|
||||
|
||||
// First make any adjustments to the value
|
||||
if (e.classList.contains("lower")) {
|
||||
|
@ -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) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function helperInit(event) {
|
||||
{
|
||||
let init = function(event) {
|
||||
for (let e of document.querySelectorAll(".answer")) {
|
||||
helperActivate(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", helperInit);
|
||||
document.addEventListener("DOMContentLoaded", init)
|
||||
} else {
|
||||
helperInit();
|
||||
init()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
|
@ -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.
|
|
@ -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.
|
|
@ -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 {
|
||||
|
|
|
@ -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 hash of answerHashes) {
|
||||
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
|
||||
}
|
||||
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) {
|
||||
|
|
Loading…
Reference in New Issue