mkpuzzle working in dev mode

This commit is contained in:
Neale Pickett 2020-09-11 13:03:19 -06:00
parent f1f6140eea
commit 490ac78f15
19 changed files with 317 additions and 306 deletions

30
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"env": {},
"args": []
},
{
"name": "MOTHd",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/mothd",
"env": {},
"args": [
"--state", "/tmp/state",
"--puzzles", "${workspaceFolder}/example-puzzles",
"--theme", "${workspaceFolder}/theme",
]
}
]
}

View File

@ -1,3 +1,5 @@
* Figure out how to log JSend short text in addition to HTTP code * Figure out how to log JSend short text in addition to HTTP code
* We've got logic in state.go and httpd.go that is neither httpd nor state specific. * We've got logic in state.go and httpd.go that is neither httpd nor state specific.
Pull this into some other file that means "here are the brains of the server". Pull this into some other file that means "here are the brains of the server".
* Get Bo's answer pattern anchors working again
* Are we logging every transaction now?

BIN
cmd/mothd/__debug_bin Executable file

Binary file not shown.

View File

@ -27,7 +27,7 @@ func main() {
puzzlePath := flag.String( puzzlePath := flag.String(
"puzzles", "puzzles",
"", "",
"Path to puzzles tree; if specified, enables development mode", "Path to puzzles tree (enables development mode)",
) )
refreshInterval := flag.Duration( refreshInterval := flag.Duration(
"refresh", "refresh",

View File

@ -14,84 +14,101 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
) )
// T contains everything required for a transpilation invocation (across the nation). // T represents the state of things
type T struct { type T struct {
// What action to take Stdout io.Writer
w io.Writer Stderr io.Writer
Cat string Args []string
Points int BaseFs afero.Fs
Answer string fs afero.Fs
Filename string filename string
Fs afero.Fs answer string
} }
// ParseArgs parses command-line arguments into T, returning the action to take. // Command is a function invoked by the user
// BUG(neale): CLI arguments are not related to how the CLI will be used. type Command func() error
func (t *T) ParseArgs() string {
action := flag.String("action", "inventory", "Action to take: must be 'inventory', 'open', 'answer', or 'mothball'")
flag.StringVar(&t.Cat, "cat", "", "Puzzle category")
flag.IntVar(&t.Points, "points", 0, "Puzzle point value")
flag.StringVar(&t.Answer, "answer", "", "Answer to check for correctness, for 'answer' action")
flag.StringVar(&t.Filename, "filename", "", "Filename, for 'open' action")
basedir := flag.String("basedir", ".", "Base directory containing all puzzles")
flag.Parse()
osfs := afero.NewOsFs() func nothing() error {
t.Fs = afero.NewBasePathFs(osfs, *basedir) return nil
return *action
} }
// Handle performs the requested action // ParseArgs parses arguments and runs the appropriate action.
func (t *T) Handle(action string) error { func (t *T) ParseArgs() (Command, error) {
switch action { var cmd Command
case "inventory":
return t.PrintInventory() if len(t.Args) == 1 {
case "open": fmt.Fprintln(t.Stderr, "Usage: transpile COMMAND [flags]")
return t.Open() fmt.Fprintln(t.Stderr, "")
case "mothball": fmt.Fprintln(t.Stderr, " mothball: Compile a mothball")
return t.Mothball() fmt.Fprintln(t.Stderr, " inventory: Show category inventory")
default: fmt.Fprintln(t.Stderr, " open: Open a file for a puzzle")
return fmt.Errorf("Unimplemented action: %s", action) fmt.Fprintln(t.Stderr, " answer: Check correctness of an answer")
return nothing, nil
} }
flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError)
directory := flags.String("dir", "", "Work directory")
switch t.Args[1] {
case "mothball":
cmd = t.DumpMothball
case "inventory":
cmd = t.PrintInventory
case "open":
flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
cmd = t.DumpFile
case "answer":
flags.StringVar(&t.answer, "answer", "", "Answer to check")
cmd = t.CheckAnswer
default:
return nothing, fmt.Errorf("%s is not a valid command", t.Args[1])
}
flags.SetOutput(t.Stderr)
if err := flags.Parse(t.Args[2:]); err != nil {
return nothing, err
}
if *directory != "" {
t.fs = afero.NewBasePathFs(t.BaseFs, *directory)
} else {
t.fs = t.BaseFs
}
log.Println(t.Args, t.fs)
return cmd, nil
} }
// PrintInventory prints a puzzle inventory to stdout // PrintInventory prints a puzzle inventory to stdout
func (t *T) PrintInventory() error { func (t *T) PrintInventory() error {
inv := make(map[string][]int) inv, err := transpile.FsInventory(t.fs)
dirEnts, err := afero.ReadDir(t.Fs, ".")
if err != nil { if err != nil {
return err return err
} }
for _, ent := range dirEnts {
if ent.IsDir() {
c := t.NewCategory(ent.Name())
if puzzles, err := c.Inventory(); err != nil {
log.Print(err)
continue
} else {
sort.Ints(puzzles)
inv[ent.Name()] = puzzles
}
}
}
m := json.NewEncoder(t.w) cats := make([]string, 0, len(inv))
if err := m.Encode(inv); err != nil { for cat := range inv {
return err cats = append(cats, cat)
}
sort.Strings(cats)
for _, cat := range cats {
puzzles := inv[cat]
fmt.Fprint(t.Stdout, cat)
for _, p := range puzzles {
fmt.Fprint(t.Stdout, " ", p)
}
fmt.Fprintln(t.Stdout)
} }
return nil return nil
} }
// Open writes a file to the writer. // DumpFile writes a file to the writer.
func (t *T) Open() error { func (t *T) DumpFile() error {
c := t.NewCategory(t.Cat) puzzle := transpile.NewFsPuzzle(t.fs)
switch t.Filename { switch t.filename {
case "puzzle.json", "": case "puzzle.json", "":
// BUG(neale): we need a way to tell the transpiler to strip answers // BUG(neale): we need a way to tell the transpiler to strip answers
p, err := c.Puzzle(t.Points) p, err := puzzle.Puzzle()
if err != nil { if err != nil {
return err return err
} }
@ -99,14 +116,14 @@ func (t *T) Open() error {
if err != nil { if err != nil {
return err return err
} }
t.w.Write(jp) t.Stdout.Write(jp)
default: default:
f, err := c.Open(t.Points, t.Filename) f, err := puzzle.Open(t.filename)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
if _, err := io.Copy(t.w, f); err != nil { if _, err := io.Copy(t.Stdout, f); err != nil {
return err return err
} }
} }
@ -114,32 +131,43 @@ func (t *T) Open() error {
return nil return nil
} }
// Mothball writes a mothball to the writer. // DumpMothball writes a mothball to the writer.
func (t *T) Mothball() error { func (t *T) DumpMothball() error {
c := t.NewCategory(t.Cat) c := transpile.NewFsCategory(t.fs, "")
mb, err := transpile.Mothball(c) mb, err := transpile.Mothball(c)
if err != nil { if err != nil {
return err return err
} }
if _, err := io.Copy(t.w, mb); err != nil { if _, err := io.Copy(t.Stdout, mb); err != nil {
return err return err
} }
return nil return nil
} }
// NewCategory returns a new Fs-backed category. // CheckAnswer prints whether an answer is correct.
func (t *T) NewCategory(name string) transpile.Category { func (t *T) CheckAnswer() error {
return transpile.NewFsCategory(t.Fs, name) c := transpile.NewFsPuzzle(t.fs)
if c.Answer(t.answer) {
fmt.Fprintln(t.Stdout, "correct")
} else {
fmt.Fprintln(t.Stdout, "wrong")
}
return nil
} }
func main() { func main() {
// XXX: Convert puzzle.py to standalone thingies // XXX: Convert puzzle.py to standalone thingies
t := &T{ t := &T{
w: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr,
Args: os.Args,
} }
action := t.ParseArgs() cmd, err := t.ParseArgs()
if err := t.Handle(action); err != nil { if err != nil {
log.Fatal(err)
}
if err := cmd(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }

View File

@ -3,7 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"strings" "log"
"testing" "testing"
"github.com/dirtbags/moth/pkg/transpile" "github.com/dirtbags/moth/pkg/transpile"
@ -23,62 +23,51 @@ pre:
--- ---
YAML body YAML body
`) `)
var testMothRfc822 = []byte(`author: test
Author: Arthur
author: Fred Flintstone
answer: RFC822 answer
RFC822 body
`)
func newTestFs() afero.Fs { func newTestFs() afero.Fs {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
afero.WriteFile(fs, "cat0/1/puzzle.md", testMothYaml, 0644) afero.WriteFile(fs, "cat0/1/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/1/moo.txt", []byte("Moo."), 0644) afero.WriteFile(fs, "cat0/1/moo.txt", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/2/puzzle.md", testMothRfc822, 0644) afero.WriteFile(fs, "cat0/2/puzzle.moth", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/3/puzzle.moth", testMothYaml, 0644) afero.WriteFile(fs, "cat0/3/puzzle.moth", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/4/puzzle.md", testMothYaml, 0644) afero.WriteFile(fs, "cat0/4/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/5/puzzle.md", testMothYaml, 0644) afero.WriteFile(fs, "cat0/5/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/10/puzzle.md", []byte(`--- afero.WriteFile(fs, "cat0/10/puzzle.md", testMothYaml, 0644)
Answers:
- moo
Authors:
- bad field
---
body
`), 0644)
afero.WriteFile(fs, "cat0/20/puzzle.md", []byte("Answer: no\nBadField: yes\n\nbody\n"), 0644)
afero.WriteFile(fs, "cat0/21/puzzle.md", []byte("Answer: broken\nSpooon\n"), 0644)
afero.WriteFile(fs, "cat0/22/puzzle.md", []byte("---\nanswers:\n - pencil\npre:\n unused-field: Spooon\n---\nSpoon?\n"), 0644)
afero.WriteFile(fs, "cat1/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644)
afero.WriteFile(fs, "cat1/barney/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "unbroken/1/puzzle.md", testMothYaml, 0644) afero.WriteFile(fs, "unbroken/1/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "unbroken/1/moo.txt", []byte("Moo."), 0644) afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothRfc822, 0644)
return fs return fs
} }
func (tp T) Run(args ...string) error {
tp.Args = append([]string{"transpile"}, args...)
command, err := tp.ParseArgs()
log.Println(tp.fs)
if err != nil {
return err
}
return command()
}
func TestEverything(t *testing.T) { func TestEverything(t *testing.T) {
stdout := new(bytes.Buffer) stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
tp := T{ tp := T{
w: stdout, Stdout: stdout,
Fs: newTestFs(), Stderr: stderr,
BaseFs: newTestFs(),
} }
if err := tp.Handle("inventory"); err != nil { if err := tp.Run("inventory"); err != nil {
t.Error(err) t.Error(err)
} }
if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93],"unbroken":[1,2]}` { if stdout.String() != "cat0 1 2 3 4 5 10\nunbroken 1 2\n" {
t.Errorf("Bad inventory: %#v", stdout.String()) t.Errorf("Bad inventory: %#v", stdout.String())
} }
stdout.Reset() stdout.Reset()
tp.Cat = "cat0" if err := tp.Run("open", "-dir=cat0/1"); err != nil {
tp.Points = 1
if err := tp.Handle("open"); err != nil {
t.Error(err) t.Error(err)
} }
p := transpile.Puzzle{} p := transpile.Puzzle{}
if err := json.Unmarshal(stdout.Bytes(), &p); err != nil { if err := json.Unmarshal(stdout.Bytes(), &p); err != nil {
t.Error(err) t.Error(err)
@ -88,8 +77,7 @@ func TestEverything(t *testing.T) {
} }
stdout.Reset() stdout.Reset()
tp.Filename = "moo.txt" if err := tp.Run("open", "-dir=cat0/1", "-file=moo.txt"); err != nil {
if err := tp.Handle("open"); err != nil {
t.Error(err) t.Error(err)
} }
if stdout.String() != "Moo." { if stdout.String() != "Moo." {
@ -97,8 +85,9 @@ func TestEverything(t *testing.T) {
} }
stdout.Reset() stdout.Reset()
tp.Cat = "unbroken" if err := tp.Run("mothball", "-dir=cat0"); err != nil {
if err := tp.Handle("mothball"); err != nil { t.Log(tp.BaseFs)
t.Log(tp.fs)
t.Error(err) t.Error(err)
} }
if stdout.Len() < 200 { if stdout.Len() < 200 {

View File

@ -0,0 +1,34 @@
#! /bin/sh
number=$(seq 20 500 | shuf -n 1)
answer=$(echo $(grep -v "['A-Z]" /usr/share/dict/words | shuf -n 4))
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."
],
"Errors": [],
"Log": [
"$number is a positive integer"
]
}
}
EOT
;;
-file:salad.jpg)
cat salad.jpg
;;
esac

View File

@ -90,12 +90,12 @@ func (c FsCategory) Inventory() ([]int, error) {
// Puzzle returns a Puzzle structure for the given point value. // Puzzle returns a Puzzle structure for the given point value.
func (c FsCategory) Puzzle(points int) (Puzzle, error) { func (c FsCategory) Puzzle(points int) (Puzzle, error) {
return NewFsPuzzle(c.fs, points).Puzzle() return NewFsPuzzlePoints(c.fs, points).Puzzle()
} }
// Open returns an io.ReadCloser for the given filename. // Open returns an io.ReadCloser for the given filename.
func (c FsCategory) Open(points int, filename string) (ReadSeekCloser, error) { func (c FsCategory) Open(points int, filename string) (ReadSeekCloser, error) {
return NewFsPuzzle(c.fs, points).Open(filename) return NewFsPuzzlePoints(c.fs, points).Open(filename)
} }
// Answer checks whether an answer is correct. // Answer checks whether an answer is correct.

View File

@ -12,7 +12,7 @@ type Inventory map[string][]int
// FsInventory returns a mapping of category names to puzzle point values. // FsInventory returns a mapping of category names to puzzle point values.
func FsInventory(fs afero.Fs) (Inventory, error) { func FsInventory(fs afero.Fs) (Inventory, error) {
dirEnts, err := afero.ReadDir(fs, ".") dirEnts, err := afero.ReadDir(fs, "")
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return nil, err return nil, err

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
) )
// Mothball packages a Category up for a production server run. // Mothball packages a Category up for a production server run.
@ -35,6 +36,7 @@ func Mothball(c Category) (*bytes.Reader, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Println(puzzlePath)
puzzle, err := c.Puzzle(points) puzzle, err := c.Puzzle(points)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -11,6 +11,7 @@ import (
"log" "log"
"net/mail" "net/mail"
"os/exec" "os/exec"
"path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -110,24 +111,40 @@ type PuzzleProvider interface {
Answer(answer string) bool Answer(answer string) bool
} }
// NewFsPuzzle returns a new FsPuzzle for points. // NewFsPuzzle returns a new FsPuzzle.
func NewFsPuzzle(fs afero.Fs, points int) PuzzleProvider { func NewFsPuzzle(fs afero.Fs) PuzzleProvider {
pfs := NewRecursiveBasePathFs(fs, strconv.Itoa(points)) var command string
if info, err := pfs.Stat("mkpuzzle"); (err == nil) && (info.Mode()&0100 != 0) {
if command, err := pfs.RealPath(info.Name()); err != nil { if info, err := fs.Stat("mkpuzzle"); (err == nil) && (info.Mode()&0100 != 0) {
log.Println("Unable to resolve full path to", info.Name(), pfs) // Try to get the actual path to the executable
} else { if pfs, ok := fs.(*RecursiveBasePathFs); ok {
return FsCommandPuzzle{ if command, err = pfs.RealPath(info.Name()); err != nil {
fs: pfs, log.Println("Unable to resolve full path to", info.Name(), pfs)
command: command, }
timeout: 2 * time.Second, } else if pfs, ok := fs.(*afero.BasePathFs); ok {
if command, err = pfs.RealPath(info.Name()); err != nil {
log.Println("Unable to resolve full path to", info.Name(), pfs)
} }
} }
} }
return FsPuzzle{ if command != "" {
fs: pfs, return FsCommandPuzzle{
fs: fs,
command: command,
timeout: 2 * time.Second,
}
} }
return FsPuzzle{
fs: fs,
}
}
// NewFsPuzzlePoints returns a new FsPuzzle for points.
func NewFsPuzzlePoints(fs afero.Fs, points int) PuzzleProvider {
return NewFsPuzzle(NewRecursiveBasePathFs(fs, strconv.Itoa(points)))
} }
// FsPuzzle is a single puzzle's directory. // FsPuzzle is a single puzzle's directory.
@ -332,6 +349,7 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, fp.command) cmd := exec.CommandContext(ctx, fp.command)
cmd.Dir = path.Dir(fp.command)
stdout, err := cmd.Output() stdout, err := cmd.Output()
if err != nil { if err != nil {
return Puzzle{}, err return Puzzle{}, err
@ -364,6 +382,7 @@ func (fp FsCommandPuzzle) Open(filename string) (ReadSeekCloser, error) {
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)
out, err := cmd.Output() out, err := cmd.Output()
buf := nopCloser{bytes.NewReader(out)} buf := nopCloser{bytes.NewReader(out)}
if err != nil { if err != nil {
@ -379,6 +398,7 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
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)
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
log.Printf("ERROR: checking answer: %s", err) log.Printf("ERROR: checking answer: %s", err)

View File

@ -13,7 +13,7 @@ func TestPuzzle(t *testing.T) {
catFs := NewRecursiveBasePathFs(puzzleFs, "cat0") catFs := NewRecursiveBasePathFs(puzzleFs, "cat0")
{ {
pd := NewFsPuzzle(catFs, 1) pd := NewFsPuzzlePoints(catFs, 1)
p, err := pd.Puzzle() p, err := pd.Puzzle()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -31,7 +31,7 @@ func TestPuzzle(t *testing.T) {
} }
{ {
p, err := NewFsPuzzle(catFs, 2).Puzzle() p, err := NewFsPuzzlePoints(catFs, 2).Puzzle()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -46,21 +46,21 @@ func TestPuzzle(t *testing.T) {
} }
} }
if _, err := NewFsPuzzle(catFs, 3).Puzzle(); err != nil { if _, err := NewFsPuzzlePoints(catFs, 3).Puzzle(); err != nil {
t.Error("Legacy `puzzle.moth` file:", err) t.Error("Legacy `puzzle.moth` file:", err)
} }
if _, err := NewFsPuzzle(catFs, 99).Puzzle(); err == nil { if _, err := NewFsPuzzlePoints(catFs, 99).Puzzle(); err == nil {
t.Error("Non-existent puzzle", err) t.Error("Non-existent puzzle", err)
} }
if _, err := NewFsPuzzle(catFs, 10).Puzzle(); err == nil { if _, err := NewFsPuzzlePoints(catFs, 10).Puzzle(); err == nil {
t.Error("Broken YAML") t.Error("Broken YAML")
} }
if _, err := NewFsPuzzle(catFs, 20).Puzzle(); err == nil { if _, err := NewFsPuzzlePoints(catFs, 20).Puzzle(); err == nil {
t.Error("Bad RFC822 header") t.Error("Bad RFC822 header")
} }
if _, err := NewFsPuzzle(catFs, 21).Puzzle(); err == nil { if _, err := NewFsPuzzlePoints(catFs, 21).Puzzle(); err == nil {
t.Error("Boken RFC822 header") t.Error("Boken RFC822 header")
} }
@ -69,7 +69,7 @@ func TestPuzzle(t *testing.T) {
if err := afero.WriteFile(fs, "1/mkpuzzle", []byte("bleat"), 0755); err != nil { if err := afero.WriteFile(fs, "1/mkpuzzle", []byte("bleat"), 0755); err != nil {
t.Error(err) t.Error(err)
} }
p := NewFsPuzzle(fs, 1) p := NewFsPuzzlePoints(fs, 1)
if _, ok := p.(FsCommandPuzzle); !ok { if _, ok := p.(FsCommandPuzzle); !ok {
t.Error("We didn't get an FsCommandPuzzle") t.Error("We didn't get an FsCommandPuzzle")
} }
@ -82,15 +82,15 @@ func TestPuzzle(t *testing.T) {
func TestFsPuzzle(t *testing.T) { func TestFsPuzzle(t *testing.T) {
catFs := NewRecursiveBasePathFs(NewRecursiveBasePathFs(afero.NewOsFs(), "testdata"), "static") catFs := NewRecursiveBasePathFs(NewRecursiveBasePathFs(afero.NewOsFs(), "testdata"), "static")
if _, err := NewFsPuzzle(catFs, 1).Puzzle(); err != nil { if _, err := NewFsPuzzlePoints(catFs, 1).Puzzle(); err != nil {
t.Error(err) t.Error(err)
} }
if _, err := NewFsPuzzle(catFs, 2).Puzzle(); err != nil { if _, err := NewFsPuzzlePoints(catFs, 2).Puzzle(); err != nil {
t.Error(err) t.Error(err)
} }
mkpuzzleDir := NewFsPuzzle(catFs, 3) mkpuzzleDir := NewFsPuzzlePoints(catFs, 3)
if _, err := mkpuzzleDir.Puzzle(); err != nil { if _, err := mkpuzzleDir.Puzzle(); err != nil {
t.Error(err) t.Error(err)
} }

View File

@ -91,9 +91,15 @@ input:invalid {
#devel { #devel {
background-color: #c88; background-color: #eee;
color: black; color: black;
} }
#devel .string {
color: #9c27b0;
}
#devel .body {
background-color: #ffc107;
}
.kvpair { .kvpair {
border: solid black 2px; border: solid black 2px;
} }

View File

@ -5,7 +5,6 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="basic.css"> <link rel="stylesheet" href="basic.css">
<script src="moth-pwa.js" type="text/javascript"></script>
<script src="moth.js"></script> <script src="moth.js"></script>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>

View File

@ -1,17 +0,0 @@
function pwa_init() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register("./sw.js").then(function(reg) {
})
.catch(err => {
console.warn("Error while registering service worker", err)
})
} else {
console.log("Service workers not supported. Some offline functionality may not work")
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", pwa_init)
} else {
pwa_init()
}

View File

@ -5,7 +5,6 @@
<link rel="stylesheet" href="basic.css"> <link rel="stylesheet" href="basic.css">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<meta charset="utf-8"> <meta charset="utf-8">
<script src="moth-pwa.js"></script>
<script src="puzzle.js"></script> <script src="puzzle.js"></script>
<script> <script>

View File

@ -1,8 +1,18 @@
// jshint asi:true // jshint asi:true
// prettify adds classes to various types, returning an HTML string.
function prettify(key, val) {
console.log(key, val)
switch (key) {
case "Body":
return '[HTML]'
}
return val
}
// devel_addin drops a bunch of development extensions into element e. // devel_addin drops a bunch of development extensions into element e.
// It will only modify stuff inside e. // It will only modify stuff inside e.
function devel_addin(obj, e) { function devel_addin(e) {
let h = document.createElement("h2") let h = document.createElement("h2")
e.appendChild(h) e.appendChild(h)
h.textContent = "Development Options" h.textContent = "Development Options"
@ -11,44 +21,10 @@ function devel_addin(obj, e) {
e.appendChild(g) e.appendChild(g)
g.innerText = "This section will not appear for participants." g.innerText = "This section will not appear for participants."
let keys = Object.keys(obj) let hobj = JSON.stringify(window.puzzle, prettify, 2)
keys.sort() let d = e.appendChild(document.createElement("pre"))
for (let key of keys) { d.classList.add("object")
switch (key) { d.innerHTML = hobj
case "body":
continue
}
let val = obj[key]
if ((! val) || (val.length === 0)) {
// Empty, skip it
continue
}
let d = document.createElement("div")
e.appendChild(d)
d.classList.add("kvpair")
let ktxt = document.createElement("span")
d.appendChild(ktxt)
ktxt.textContent = key
if (Array.isArray(val)) {
let vi = document.createElement("select")
d.appendChild(vi)
vi.multiple = true
for (let a of val) {
let opt = document.createElement("option")
vi.appendChild(opt)
opt.innerText = a
}
} else {
let vi = document.createElement("input")
d.appendChild(vi)
vi.value = val
vi.disabled = true
}
}
} }
// Hash routine used in v3.4 and earlier // Hash routine used in v3.4 and earlier
@ -73,22 +49,18 @@ async function sha256Hash(message) {
// Is the provided answer possibly correct? // Is the provided answer possibly correct?
async function possiblyCorrect(answer) { async function possiblyCorrect(answer) {
for (let correctHash of window.puzzle.hashes) { let pattern = window.puzzle.Pre.AnswerPattern || []
// CPU time is cheap. Especially if it's not our server's time.
// So we'll just try absolutely everything and see what happens. for (let correctHash of window.puzzle.Pre.AnswerHashes) {
// We're counting on hash collisions being extremely rare with the algorithm we use.
// And honestly, this pales in comparison to the amount of CPU being eaten by
// something like the github 404 page.
if (djb2hash(answer) == correctHash) { if (djb2hash(answer) == correctHash) {
return answer return answer
} }
for (let end = 0; end <= answer.length; end += 1) { for (let end = 0; end <= answer.length; end += 1) {
if (window.puzzle.xAnchors && window.puzzle.xAnchors.includes("end") && (end != answer.length)) { if (pattern.includes("end") && (end != answer.length)) {
continue continue
} }
for (let beg = 0; beg < answer.length; beg += 1) { for (let beg = 0; beg < answer.length; beg += 1) {
if (window.puzzle.xAnchors && window.puzzle.xAnchors.includes("begin") && (beg != 0)) { if (pattern.includes("begin") && (beg != 0)) {
continue continue
} }
let sub = answer.substring(beg, end) let sub = answer.substring(beg, end)
@ -148,66 +120,63 @@ function submit(e) {
}) })
} }
function loadPuzzle(categoryName, points, puzzleId) { async function loadPuzzle(categoryName, points, puzzleId) {
let puzzle = document.getElementById("puzzle") let puzzle = document.getElementById("puzzle")
let base = "content/" + categoryName + "/" + puzzleId + "/" let base = "content/" + categoryName + "/" + puzzleId + "/"
fetch(base + "puzzle.json") let resp = await fetch(base + "puzzle.json")
.then(resp => { if (! resp.ok) {
return resp.json() console.log(resp)
}) let err = await resp.text()
.then(obj => {
// Populate authors
document.getElementById("authors").textContent = obj.authors.join(", ")
// Make the whole puzzle available
window.puzzle = obj
// If answers are provided, this is the devel server
if (obj.answers) {
devel_addin(obj, document.getElementById("devel"))
}
// Load scripts
for (let script of obj.scripts) {
let st = document.createElement("script")
document.head.appendChild(st)
st.src = base + script
}
// List associated files
for (let fn of obj.files) {
let li = document.createElement("li")
let a = document.createElement("a")
a.href = base + fn
a.innerText = fn
li.appendChild(a)
document.getElementById("files").appendChild(li)
}
// Prefix `base` to relative URLs in the puzzle body
let doc = new DOMParser().parseFromString(obj.body, "text/html")
for (let se of doc.querySelectorAll("[src],[href]")) {
se.outerHTML = se.outerHTML.replace(/(src|href)="([^/]+)"/i, "$1=\"" + base + "$2\"")
}
// If a validation pattern was provided, set that
if (obj.pattern) {
document.querySelector("#answer").pattern = obj.pattern
}
// Replace puzzle children with what's in `doc`
Array.from(puzzle.childNodes).map(e => e.remove()) Array.from(puzzle.childNodes).map(e => e.remove())
Array.from(doc.body.childNodes).map(e => puzzle.appendChild(e)) p = puzzle.appendChild(document.createElement("p"))
})
.catch(err => {
// Show error to the user
Array.from(puzzle.childNodes).map(e => e.remove())
let p = document.createElement("p")
puzzle.appendChild(p)
p.classList.add("Error") p.classList.add("Error")
p.textContent = err p.textContent = err
}) return
}
// Make the whole puzzle available
window.puzzle = await resp.json()
// Populate authors
document.getElementById("authors").textContent = window.puzzle.Pre.Authors.join(", ")
// If answers are provided, this is the devel server
if (window.puzzle.Answers) {
devel_addin(document.getElementById("devel"))
}
// Load scripts
for (let script of (window.puzzle.Pre.Scripts || [])) {
let st = document.createElement("script")
document.head.appendChild(st)
st.src = base + script
}
// List associated files
for (let fn of (window.puzzle.Pre.Attachments || [])) {
let li = document.createElement("li")
let a = document.createElement("a")
a.href = base + fn
a.innerText = fn
li.appendChild(a)
document.getElementById("files").appendChild(li)
}
// Prefix `base` to relative URLs in the puzzle body
let doc = new DOMParser().parseFromString(window.puzzle.Pre.Body, "text/html")
for (let se of doc.querySelectorAll("[src],[href]")) {
se.outerHTML = se.outerHTML.replace(/(src|href)="([^/]+)"/i, "$1=\"" + base + "$2\"")
}
// If a validation pattern was provided, set that
if (window.puzzle.Pre.AnswerPattern) {
document.querySelector("#answer").pattern = window.puzzle.Pre.AnswerPattern
}
// Replace puzzle children with what's in `doc`
Array.from(puzzle.childNodes).map(e => e.remove())
Array.from(doc.body.childNodes).map(e => puzzle.appendChild(e))
document.title = categoryName + " " + points document.title = categoryName + " " + points
document.querySelector("body > h1").innerText = document.title document.querySelector("body > h1").innerText = document.title

View File

@ -4,7 +4,6 @@
<title>Scoreboard</title> <title>Scoreboard</title>
<link rel="stylesheet" href="basic.css"> <link rel="stylesheet" href="basic.css">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<script src="moth-pwa.js"></script>
<script src="moment.min.js" async></script> <script src="moment.min.js" async></script>
<script src="Chart.min.js" async></script> <script src="Chart.min.js" async></script>
<script src="scoreboard.js" async></script> <script src="scoreboard.js" async></script>

View File

@ -1,49 +0,0 @@
var cacheName = "moth:v1"
var content = [
"index.html",
"basic.css",
"puzzle.js",
"puzzle.html",
"scoreboard.html",
"moth.js",
"sw.js",
"points.json",
]
self.addEventListener("install", function(e) {
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(content).then(
function() {
self.skipWaiting()
})
})
)
})
/* Attempt to fetch live resources, first, then fall back to cache */
self.addEventListener('fetch', function(event) {
let cache_used = false
event.respondWith(
fetch(event.request)
.catch(function(evt) {
//console.log("Falling back to cache for " + event.request.url)
cache_used = true
return caches.match(event.request, {ignoreSearch: true})
}).then(function(res) {
if (res && res.ok) {
let res_clone = res.clone()
if (! cache_used && event.request.method == "GET" ) {
caches.open(cacheName).then(function(cache) {
cache.put(event.request, res_clone)
//console.log("Storing " + event.request.url + " in cache")
})
}
return res
} else {
console.log("Failed to retrieve resource")
}
})
)
})