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
* 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?

BIN
cmd/mothd/__debug_bin Executable file

Binary file not shown.

View File

@ -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",

View File

@ -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)
}
}

View File

@ -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 {

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.
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.

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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;
}

View File

@ -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>

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">
<meta name="viewport" content="width=device-width">
<meta charset="utf-8">
<script src="moth-pwa.js"></script>
<script src="puzzle.js"></script>
<script>

View File

@ -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

View File

@ -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>

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")
}
})
)
})