mirror of https://github.com/dirtbags/moth.git
139 lines
2.8 KiB
Go
139 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"hash/fnv"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"encoding/hex"
|
|
"strconv"
|
|
"math/rand"
|
|
"context"
|
|
"time"
|
|
"os/exec"
|
|
"bytes"
|
|
)
|
|
|
|
|
|
type PuzzleEntry struct {
|
|
Id string
|
|
Points int
|
|
Puzzle Puzzle
|
|
}
|
|
|
|
func PrngOfStrings(input ...string) (*rand.Rand) {
|
|
hasher := fnv.New64()
|
|
for _, s := range input {
|
|
fmt.Fprint(hasher, s, "\n")
|
|
}
|
|
seed := binary.BigEndian.Uint64(hasher.Sum(nil))
|
|
source := rand.NewSource(int64(seed))
|
|
return rand.New(source)
|
|
}
|
|
|
|
|
|
func runPuzzleGen(puzzlePath string, seed string) (*Puzzle, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, puzzlePath)
|
|
cmd.Env = append(
|
|
os.Environ(),
|
|
fmt.Sprintf("MOTH_PUZZLE_SEED=%s", seed),
|
|
)
|
|
stdout, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jsdec := json.NewDecoder(bytes.NewReader(stdout))
|
|
jsdec.DisallowUnknownFields()
|
|
puzzle := new(Puzzle)
|
|
err = jsdec.Decode(puzzle)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return puzzle, nil
|
|
}
|
|
|
|
func ParsePuzzle(puzzlePath string, puzzleSeed string) (*Puzzle, error) {
|
|
var puzzle *Puzzle
|
|
|
|
// Try the .moth file first
|
|
puzzleMothPath := filepath.Join(puzzlePath, "puzzle.moth")
|
|
puzzleFd, err := os.Open(puzzleMothPath)
|
|
if err == nil {
|
|
defer puzzleFd.Close()
|
|
puzzle, err = ParseMoth(puzzleFd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if os.IsNotExist(err) {
|
|
var genErr error
|
|
|
|
puzzleGenPath := filepath.Join(puzzlePath, "mkpuzzle")
|
|
puzzle, genErr = runPuzzleGen(puzzleGenPath, puzzlePath)
|
|
if genErr != nil {
|
|
bigErr := fmt.Errorf(
|
|
"%v; (%s: %v)",
|
|
genErr,
|
|
filepath.Base(puzzleMothPath), err,
|
|
)
|
|
return nil, bigErr
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
|
|
return puzzle, nil
|
|
}
|
|
|
|
|
|
func ParseCategory(categoryPath string, seed string) ([]PuzzleEntry, error) {
|
|
categoryFd, err := os.Open(categoryPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer categoryFd.Close()
|
|
|
|
puzzleDirs, err := categoryFd.Readdirnames(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
puzzleEntries := make([]PuzzleEntry, 0, len(puzzleDirs))
|
|
for _, puzzleDir := range puzzleDirs {
|
|
puzzlePath := filepath.Join(categoryPath, puzzleDir)
|
|
puzzleSeed := fmt.Sprintf("%s/%s", seed, puzzleDir)
|
|
puzzle, err := ParsePuzzle(puzzlePath, puzzleSeed)
|
|
if err != nil {
|
|
log.Printf("Skipping %s: %v", puzzlePath, err)
|
|
continue
|
|
}
|
|
|
|
// Determine point value from directory name
|
|
points, err := strconv.Atoi(puzzleDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create a category entry for this
|
|
prng := PrngOfStrings(puzzlePath)
|
|
idBytes := make([]byte, 16)
|
|
prng.Read(idBytes)
|
|
id := hex.EncodeToString(idBytes)
|
|
puzzleEntry := PuzzleEntry{
|
|
Id: id,
|
|
Puzzle: *puzzle,
|
|
Points: points,
|
|
}
|
|
puzzleEntries = append(puzzleEntries, puzzleEntry)
|
|
}
|
|
|
|
return puzzleEntries, nil
|
|
}
|