clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/regenerate-vhs-gifs
puzzletea / cmd / new_export.go
new_export.go 4.9K
1package cmd23import (4 "encoding/json"5 "fmt"6 "math/rand/v2"7 "path/filepath"8 "strings"9 "time"1011 "github.com/FelineStateMachine/puzzletea/builtinprint"12 "github.com/FelineStateMachine/puzzletea/game"13 "github.com/FelineStateMachine/puzzletea/namegen"14 "github.com/FelineStateMachine/puzzletea/packexport"15 "github.com/FelineStateMachine/puzzletea/pdfexport"16 "github.com/FelineStateMachine/puzzletea/registry"17 "github.com/FelineStateMachine/puzzletea/resolve"1819 "github.com/spf13/cobra"20)2122var exportNow = time.Now2324type exportModeEntry struct {25 spawner game.Spawner26 mode string27}2829func runNewExport(cmd *cobra.Command, args []string) error {30 builtinprint.Register()3132 if err := validateNewExportFlags(cmd, args); err != nil {33 return err34 }3536 entry, ok := registry.Resolve(args[0])37 if !ok {38 return fmt.Errorf("unknown game %q", args[0])39 }40 if !pdfexport.HasPrintAdapter(entry.Definition.Name) {41 return fmt.Errorf("game %q does not support export", entry.Definition.Name)42 }4344 modeArg := ""45 if len(args) > 1 {46 modeArg = args[1]47 }4849 entries, modeSelection, err := collectExportModes(entry, modeArg)50 if err != nil {51 return err52 }5354 generatedAt := exportNow()55 records, err := buildExportRecords(entry.Definition.Name, modeSelection, entries, flagExport, flagWithSeed, generatedAt)56 if err != nil {57 return err58 }5960 if err := writeExportJSONL(cmd, flagOutput, records); err != nil {61 return err62 }6364 return nil65}6667func validateNewExportFlags(_ *cobra.Command, args []string) error {68 if flagExport < 1 {69 return fmt.Errorf("--export must be at least 1")70 }71 if strings.TrimSpace(flagOutput) != "" && !strings.EqualFold(filepath.Ext(flagOutput), ".jsonl") {72 return fmt.Errorf("--output must use a .jsonl extension")73 }74 if flagSetSeed != "" {75 return fmt.Errorf("--set-seed cannot be combined with export (--export/--output)")76 }77 if len(args) == 0 {78 return fmt.Errorf("requires at least 1 arg(s), only received 0")79 }80 if len(args) > 2 {81 return fmt.Errorf("accepts at most 2 arg(s), received %d", len(args))82 }83 return nil84}8586func collectExportModes(entry registry.Entry, modeArg string) ([]exportModeEntry, string, error) {87 if modeArg != "" {88 spawner, modeTitle, err := resolve.Mode(entry, modeArg)89 if err != nil {90 return nil, "", err91 }92 return []exportModeEntry{{spawner: spawner, mode: modeTitle}}, modeTitle, nil93 }9495 entries := make([]exportModeEntry, 0, len(entry.Modes))96 for _, mode := range entry.Modes {97 entries = append(entries, exportModeEntry{98 spawner: mode.Spawner,99 mode: mode.Definition.Title,100 })101 }102 if len(entries) == 0 {103 return nil, "", fmt.Errorf("game %q has no exportable modes", entry.Definition.Name)104 }105106 return entries, "mixed modes", nil107}108109func buildExportRecords(110 gameType, modeSelection string,111 entries []exportModeEntry,112 count int,113 seed string,114 generatedAt time.Time,115) ([]pdfexport.JSONLRecord, error) {116 var rng *rand.Rand117 if seed != "" {118 rng = resolve.RNGFromString(seed)119 }120121 nameSeed := seed122 if strings.TrimSpace(nameSeed) == "" {123 nameSeed = generatedAt.Format(time.RFC3339Nano)124 }125 nameRNG := resolve.RNGFromString("export-names:" + nameSeed)126127 records := make([]pdfexport.JSONLRecord, 0, count)128 for i := range count {129 entry := entries[0]130 if len(entries) > 1 {131 var modeIndex int132 if rng != nil {133 modeIndex = rng.IntN(len(entries))134 } else {135 modeIndex = rand.IntN(len(entries))136 }137 entry = entries[modeIndex]138 }139140 puzzle, err := spawnExportPuzzle(entry.spawner, rng)141 if err != nil {142 return nil, fmt.Errorf("generate puzzle %d: %w", i+1, err)143 }144145 save, err := puzzle.GetSave()146 if err != nil {147 return nil, fmt.Errorf("serialize puzzle %d: %w", i+1, err)148 }149 if !json.Valid(save) {150 return nil, fmt.Errorf("serialize puzzle %d: save payload is not valid JSON", i+1)151 }152153 records = append(records, pdfexport.JSONLRecord{154 Schema: pdfexport.ExportSchemaV1,155 Pack: pdfexport.JSONLPackMeta{156 Generated: generatedAt.Format(time.RFC3339),157 Version: Version,158 Category: gameType,159 ModeSelection: modeSelection,160 Count: count,161 Seed: seed,162 },163 Puzzle: pdfexport.JSONLPuzzle{164 Index: i + 1,165 Name: namegen.GenerateSeeded(nameRNG),166 Game: gameType,167 Mode: entry.mode,168 Save: json.RawMessage(save),169 },170 })171 }172173 return records, nil174}175176func spawnExportPuzzle(spawner game.Spawner, rng *rand.Rand) (game.Gamer, error) {177 if rng == nil {178 return spawner.Spawn()179 }180181 seeded, ok := spawner.(game.SeededSpawner)182 if !ok {183 return nil, fmt.Errorf("mode does not support deterministic spawning")184 }185 return seeded.SpawnSeeded(rng)186}187188func writeExportJSONL(cmd *cobra.Command, path string, records []pdfexport.JSONLRecord) error {189 content, err := packexport.EncodeJSONL(records)190 if err != nil {191 return err192 }193 if strings.TrimSpace(path) == "" {194 if _, err := cmd.OutOrStdout().Write(content); err != nil {195 return fmt.Errorf("write export jsonl to stdout: %w", err)196 }197 return nil198 }199 return packexport.WriteJSONL(path, records)200}