mirror of https://github.com/dirtbags/moth.git
mkpuzzle working in dev mode
This commit is contained in:
parent
f1f6140eea
commit
490ac78f15
|
@ -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",
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
2
TODO.md
2
TODO.md
|
@ -1,3 +1,5 @@
|
|||
* 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.
|
||||
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?
|
||||
|
|
Binary file not shown.
|
@ -27,7 +27,7 @@ func main() {
|
|||
puzzlePath := flag.String(
|
||||
"puzzles",
|
||||
"",
|
||||
"Path to puzzles tree; if specified, enables development mode",
|
||||
"Path to puzzles tree (enables development mode)",
|
||||
)
|
||||
refreshInterval := flag.Duration(
|
||||
"refresh",
|
||||
|
|
|
@ -14,84 +14,101 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// T contains everything required for a transpilation invocation (across the nation).
|
||||
// T represents the state of things
|
||||
type T struct {
|
||||
// What action to take
|
||||
w io.Writer
|
||||
Cat string
|
||||
Points int
|
||||
Answer string
|
||||
Filename string
|
||||
Fs afero.Fs
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Args []string
|
||||
BaseFs afero.Fs
|
||||
fs afero.Fs
|
||||
filename string
|
||||
answer string
|
||||
}
|
||||
|
||||
// ParseArgs parses command-line arguments into T, returning the action to take.
|
||||
// BUG(neale): CLI arguments are not related to how the CLI will be used.
|
||||
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()
|
||||
// Command is a function invoked by the user
|
||||
type Command func() error
|
||||
|
||||
osfs := afero.NewOsFs()
|
||||
t.Fs = afero.NewBasePathFs(osfs, *basedir)
|
||||
|
||||
return *action
|
||||
func nothing() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle performs the requested action
|
||||
func (t *T) Handle(action string) error {
|
||||
switch action {
|
||||
case "inventory":
|
||||
return t.PrintInventory()
|
||||
case "open":
|
||||
return t.Open()
|
||||
case "mothball":
|
||||
return t.Mothball()
|
||||
default:
|
||||
return fmt.Errorf("Unimplemented action: %s", action)
|
||||
// ParseArgs parses arguments and runs the appropriate action.
|
||||
func (t *T) ParseArgs() (Command, error) {
|
||||
var cmd Command
|
||||
|
||||
if len(t.Args) == 1 {
|
||||
fmt.Fprintln(t.Stderr, "Usage: transpile COMMAND [flags]")
|
||||
fmt.Fprintln(t.Stderr, "")
|
||||
fmt.Fprintln(t.Stderr, " mothball: Compile a mothball")
|
||||
fmt.Fprintln(t.Stderr, " inventory: Show category inventory")
|
||||
fmt.Fprintln(t.Stderr, " open: Open a file for a puzzle")
|
||||
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
|
||||
func (t *T) PrintInventory() error {
|
||||
inv := make(map[string][]int)
|
||||
|
||||
dirEnts, err := afero.ReadDir(t.Fs, ".")
|
||||
inv, err := transpile.FsInventory(t.fs)
|
||||
if err != nil {
|
||||
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)
|
||||
if err := m.Encode(inv); err != nil {
|
||||
return err
|
||||
cats := make([]string, 0, len(inv))
|
||||
for cat := range inv {
|
||||
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
|
||||
}
|
||||
|
||||
// Open writes a file to the writer.
|
||||
func (t *T) Open() error {
|
||||
c := t.NewCategory(t.Cat)
|
||||
// DumpFile writes a file to the writer.
|
||||
func (t *T) DumpFile() error {
|
||||
puzzle := transpile.NewFsPuzzle(t.fs)
|
||||
|
||||
switch t.Filename {
|
||||
switch t.filename {
|
||||
case "puzzle.json", "":
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -99,14 +116,14 @@ func (t *T) Open() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.w.Write(jp)
|
||||
t.Stdout.Write(jp)
|
||||
default:
|
||||
f, err := c.Open(t.Points, t.Filename)
|
||||
f, err := puzzle.Open(t.filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(t.w, f); err != nil {
|
||||
if _, err := io.Copy(t.Stdout, f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -114,32 +131,43 @@ func (t *T) Open() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Mothball writes a mothball to the writer.
|
||||
func (t *T) Mothball() error {
|
||||
c := t.NewCategory(t.Cat)
|
||||
// DumpMothball writes a mothball to the writer.
|
||||
func (t *T) DumpMothball() error {
|
||||
c := transpile.NewFsCategory(t.fs, "")
|
||||
mb, err := transpile.Mothball(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(t.w, mb); err != nil {
|
||||
if _, err := io.Copy(t.Stdout, mb); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCategory returns a new Fs-backed category.
|
||||
func (t *T) NewCategory(name string) transpile.Category {
|
||||
return transpile.NewFsCategory(t.Fs, name)
|
||||
// CheckAnswer prints whether an answer is correct.
|
||||
func (t *T) CheckAnswer() error {
|
||||
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() {
|
||||
// XXX: Convert puzzle.py to standalone thingies
|
||||
|
||||
t := &T{
|
||||
w: os.Stdout,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Args: os.Args,
|
||||
}
|
||||
action := t.ParseArgs()
|
||||
if err := t.Handle(action); err != nil {
|
||||
cmd, err := t.ParseArgs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := cmd(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/transpile"
|
||||
|
@ -23,62 +23,51 @@ pre:
|
|||
---
|
||||
YAML body
|
||||
`)
|
||||
var testMothRfc822 = []byte(`author: test
|
||||
Author: Arthur
|
||||
author: Fred Flintstone
|
||||
answer: RFC822 answer
|
||||
|
||||
RFC822 body
|
||||
`)
|
||||
|
||||
func newTestFs() afero.Fs {
|
||||
fs := afero.NewMemMapFs()
|
||||
afero.WriteFile(fs, "cat0/1/puzzle.md", testMothYaml, 0644)
|
||||
afero.WriteFile(fs, "cat0/1/moo.txt", []byte("Moo."), 0644)
|
||||
afero.WriteFile(fs, "cat0/2/puzzle.md", testMothRfc822, 0644)
|
||||
afero.WriteFile(fs, "cat0/1/moo.txt", testMothYaml, 0644)
|
||||
afero.WriteFile(fs, "cat0/2/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/5/puzzle.md", testMothYaml, 0644)
|
||||
afero.WriteFile(fs, "cat0/10/puzzle.md", []byte(`---
|
||||
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, "cat0/10/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", testMothRfc822, 0644)
|
||||
afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothYaml, 0644)
|
||||
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) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
tp := T{
|
||||
w: stdout,
|
||||
Fs: newTestFs(),
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
BaseFs: newTestFs(),
|
||||
}
|
||||
|
||||
if err := tp.Handle("inventory"); err != nil {
|
||||
if err := tp.Run("inventory"); err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
tp.Cat = "cat0"
|
||||
tp.Points = 1
|
||||
if err := tp.Handle("open"); err != nil {
|
||||
if err := tp.Run("open", "-dir=cat0/1"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
p := transpile.Puzzle{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &p); err != nil {
|
||||
t.Error(err)
|
||||
|
@ -88,8 +77,7 @@ func TestEverything(t *testing.T) {
|
|||
}
|
||||
|
||||
stdout.Reset()
|
||||
tp.Filename = "moo.txt"
|
||||
if err := tp.Handle("open"); err != nil {
|
||||
if err := tp.Run("open", "-dir=cat0/1", "-file=moo.txt"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if stdout.String() != "Moo." {
|
||||
|
@ -97,8 +85,9 @@ func TestEverything(t *testing.T) {
|
|||
}
|
||||
|
||||
stdout.Reset()
|
||||
tp.Cat = "unbroken"
|
||||
if err := tp.Handle("mothball"); err != nil {
|
||||
if err := tp.Run("mothball", "-dir=cat0"); err != nil {
|
||||
t.Log(tp.BaseFs)
|
||||
t.Log(tp.fs)
|
||||
t.Error(err)
|
||||
}
|
||||
if stdout.Len() < 200 {
|
||||
|
|
|
@ -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
|
|
@ -90,12 +90,12 @@ func (c FsCategory) Inventory() ([]int, error) {
|
|||
|
||||
// Puzzle returns a Puzzle structure for the given point value.
|
||||
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.
|
||||
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.
|
||||
|
|
|
@ -12,7 +12,7 @@ type Inventory map[string][]int
|
|||
|
||||
// FsInventory returns a mapping of category names to puzzle point values.
|
||||
func FsInventory(fs afero.Fs) (Inventory, error) {
|
||||
dirEnts, err := afero.ReadDir(fs, ".")
|
||||
dirEnts, err := afero.ReadDir(fs, "")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil, err
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Mothball packages a Category up for a production server run.
|
||||
|
@ -35,6 +36,7 @@ func Mothball(c Category) (*bytes.Reader, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Println(puzzlePath)
|
||||
puzzle, err := c.Puzzle(points)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"log"
|
||||
"net/mail"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -110,24 +111,40 @@ type PuzzleProvider interface {
|
|||
Answer(answer string) bool
|
||||
}
|
||||
|
||||
// NewFsPuzzle returns a new FsPuzzle for points.
|
||||
func NewFsPuzzle(fs afero.Fs, points int) PuzzleProvider {
|
||||
pfs := NewRecursiveBasePathFs(fs, strconv.Itoa(points))
|
||||
if info, err := pfs.Stat("mkpuzzle"); (err == nil) && (info.Mode()&0100 != 0) {
|
||||
if command, err := pfs.RealPath(info.Name()); err != nil {
|
||||
log.Println("Unable to resolve full path to", info.Name(), pfs)
|
||||
} else {
|
||||
return FsCommandPuzzle{
|
||||
fs: pfs,
|
||||
command: command,
|
||||
timeout: 2 * time.Second,
|
||||
// NewFsPuzzle returns a new FsPuzzle.
|
||||
func NewFsPuzzle(fs afero.Fs) PuzzleProvider {
|
||||
var command string
|
||||
|
||||
if info, err := fs.Stat("mkpuzzle"); (err == nil) && (info.Mode()&0100 != 0) {
|
||||
// Try to get the actual path to the executable
|
||||
if pfs, ok := fs.(*RecursiveBasePathFs); ok {
|
||||
if command, err = pfs.RealPath(info.Name()); err != nil {
|
||||
log.Println("Unable to resolve full path to", info.Name(), pfs)
|
||||
}
|
||||
} 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{
|
||||
fs: pfs,
|
||||
if command != "" {
|
||||
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.
|
||||
|
@ -332,6 +349,7 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
|
|||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, fp.command)
|
||||
cmd.Dir = path.Dir(fp.command)
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return Puzzle{}, err
|
||||
|
@ -364,6 +382,7 @@ func (fp FsCommandPuzzle) Open(filename string) (ReadSeekCloser, error) {
|
|||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, fp.command, "-file", filename)
|
||||
cmd.Dir = path.Dir(fp.command)
|
||||
out, err := cmd.Output()
|
||||
buf := nopCloser{bytes.NewReader(out)}
|
||||
if err != nil {
|
||||
|
@ -379,6 +398,7 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
|
|||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, fp.command, "-answer", answer)
|
||||
cmd.Dir = path.Dir(fp.command)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Printf("ERROR: checking answer: %s", err)
|
||||
|
|
|
@ -13,7 +13,7 @@ func TestPuzzle(t *testing.T) {
|
|||
catFs := NewRecursiveBasePathFs(puzzleFs, "cat0")
|
||||
|
||||
{
|
||||
pd := NewFsPuzzle(catFs, 1)
|
||||
pd := NewFsPuzzlePoints(catFs, 1)
|
||||
p, err := pd.Puzzle()
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
if _, err := NewFsPuzzle(catFs, 99).Puzzle(); err == nil {
|
||||
if _, err := NewFsPuzzlePoints(catFs, 99).Puzzle(); err == nil {
|
||||
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")
|
||||
}
|
||||
if _, err := NewFsPuzzle(catFs, 20).Puzzle(); err == nil {
|
||||
if _, err := NewFsPuzzlePoints(catFs, 20).Puzzle(); err == nil {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ func TestPuzzle(t *testing.T) {
|
|||
if err := afero.WriteFile(fs, "1/mkpuzzle", []byte("bleat"), 0755); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p := NewFsPuzzle(fs, 1)
|
||||
p := NewFsPuzzlePoints(fs, 1)
|
||||
if _, ok := p.(FsCommandPuzzle); !ok {
|
||||
t.Error("We didn't get an FsCommandPuzzle")
|
||||
}
|
||||
|
@ -82,15 +82,15 @@ func TestPuzzle(t *testing.T) {
|
|||
func TestFsPuzzle(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
if _, err := NewFsPuzzle(catFs, 2).Puzzle(); err != nil {
|
||||
if _, err := NewFsPuzzlePoints(catFs, 2).Puzzle(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
mkpuzzleDir := NewFsPuzzle(catFs, 3)
|
||||
mkpuzzleDir := NewFsPuzzlePoints(catFs, 3)
|
||||
if _, err := mkpuzzleDir.Puzzle(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
|
@ -91,9 +91,15 @@ input:invalid {
|
|||
|
||||
|
||||
#devel {
|
||||
background-color: #c88;
|
||||
background-color: #eee;
|
||||
color: black;
|
||||
}
|
||||
#devel .string {
|
||||
color: #9c27b0;
|
||||
}
|
||||
#devel .body {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
.kvpair {
|
||||
border: solid black 2px;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="basic.css">
|
||||
<script src="moth-pwa.js" type="text/javascript"></script>
|
||||
<script src="moth.js"></script>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
<link rel="stylesheet" href="basic.css">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta charset="utf-8">
|
||||
<script src="moth-pwa.js"></script>
|
||||
<script src="puzzle.js"></script>
|
||||
<script>
|
||||
|
||||
|
|
173
theme/puzzle.js
173
theme/puzzle.js
|
@ -1,8 +1,18 @@
|
|||
// 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.
|
||||
// It will only modify stuff inside e.
|
||||
function devel_addin(obj, e) {
|
||||
function devel_addin(e) {
|
||||
let h = document.createElement("h2")
|
||||
e.appendChild(h)
|
||||
h.textContent = "Development Options"
|
||||
|
@ -11,44 +21,10 @@ function devel_addin(obj, e) {
|
|||
e.appendChild(g)
|
||||
g.innerText = "This section will not appear for participants."
|
||||
|
||||
let keys = Object.keys(obj)
|
||||
keys.sort()
|
||||
for (let key of keys) {
|
||||
switch (key) {
|
||||
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
|
||||
}
|
||||
}
|
||||
let hobj = JSON.stringify(window.puzzle, prettify, 2)
|
||||
let d = e.appendChild(document.createElement("pre"))
|
||||
d.classList.add("object")
|
||||
d.innerHTML = hobj
|
||||
}
|
||||
|
||||
// Hash routine used in v3.4 and earlier
|
||||
|
@ -73,22 +49,18 @@ async function sha256Hash(message) {
|
|||
|
||||
// Is the provided answer possibly correct?
|
||||
async function possiblyCorrect(answer) {
|
||||
for (let correctHash of window.puzzle.hashes) {
|
||||
// CPU time is cheap. Especially if it's not our server's time.
|
||||
// So we'll just try absolutely everything and see what happens.
|
||||
// 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.
|
||||
|
||||
let pattern = window.puzzle.Pre.AnswerPattern || []
|
||||
|
||||
for (let correctHash of window.puzzle.Pre.AnswerHashes) {
|
||||
if (djb2hash(answer) == correctHash) {
|
||||
return answer
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 base = "content/" + categoryName + "/" + puzzleId + "/"
|
||||
|
||||
fetch(base + "puzzle.json")
|
||||
.then(resp => {
|
||||
return resp.json()
|
||||
})
|
||||
.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`
|
||||
let resp = await fetch(base + "puzzle.json")
|
||||
if (! resp.ok) {
|
||||
console.log(resp)
|
||||
let err = await resp.text()
|
||||
Array.from(puzzle.childNodes).map(e => e.remove())
|
||||
Array.from(doc.body.childNodes).map(e => puzzle.appendChild(e))
|
||||
})
|
||||
.catch(err => {
|
||||
// Show error to the user
|
||||
Array.from(puzzle.childNodes).map(e => e.remove())
|
||||
let p = document.createElement("p")
|
||||
puzzle.appendChild(p)
|
||||
p = puzzle.appendChild(document.createElement("p"))
|
||||
p.classList.add("Error")
|
||||
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.querySelector("body > h1").innerText = document.title
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
<title>Scoreboard</title>
|
||||
<link rel="stylesheet" href="basic.css">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<script src="moth-pwa.js"></script>
|
||||
<script src="moment.min.js" async></script>
|
||||
<script src="Chart.min.js" async></script>
|
||||
<script src="scoreboard.js" async></script>
|
||||
|
|
49
theme/sw.js
49
theme/sw.js
|
@ -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")
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
Loading…
Reference in New Issue