clone

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

step 01 commit

Dami committed 0c5cbc3
commit
0c5cbc33eac807a0baf0a01376882acad1026633
parent
d260f2e00012bf3d2207281fc8b6b2c3d1ca48ed
author
Dami <dami.etoile@gmail.com>
28 files changed, +4576 -10
MREADME.md16+
Mapp/keys.go18+-
Mapp/update.go5+
Mapp/update_test.go101++++
Aapp/yank.go42++
Acmd/export_pdf.go178+++++++
Mcmd/new.go8+
Acmd/new_export.go201++++++++
Acmd/new_export_test.go175+++++++
Mcmd/root.go2+-
Mgo.mod5+-
Mgo.sum2+
Amarkdownexport/detect.go41++
Amarkdownexport/document.go71+++
Amarkdownexport/render.go467++++++++++++++++++
Amarkdownexport/render_test.go188+++++++
Amarkdownexport/support.go26+
Anonogram-pack-01.md776++++++++++++++++++++++++++++++
Apack-01/issue-01.pdfbin
Apack-01/nonogram.md332+++++++++++++
Apack-01/shikaku.md368++++++++++++++
Apack-01/takuzu.md252++++++++++
Apdfexport/order.go104++++
Apdfexport/order_test.go36+
Apdfexport/parse.go496+++++++++++++++++++
Apdfexport/parse_test.go135+++++
Apdfexport/render.go477++++++++++++++++++
Apdfexport/types.go64++
M README.md +16 -0
M app/keys.go +11 -7
M app/update.go +5 -0
M app/update_test.go +101 -0
@@ -1,7 +1,9 @@
11 package app22 33 import (4+ "errors"45 "path/filepath"6+ "strings"57 "testing"68 "time"79
@@ -10,9 +12,11 @@ import (
1012 tea "charm.land/bubbletea/v2"1113 "github.com/FelineStateMachine/puzzletea/daily"1214 "github.com/FelineStateMachine/puzzletea/game"15+ "github.com/FelineStateMachine/puzzletea/lightsout"1316 "github.com/FelineStateMachine/puzzletea/namegen"1417 "github.com/FelineStateMachine/puzzletea/resolve"1518 "github.com/FelineStateMachine/puzzletea/store"19+ "github.com/FelineStateMachine/puzzletea/sudoku"1620 )1721 1822 type escapeTrackingGame struct {
@@ -238,3 +242,100 @@ func openAppTestStore(t *testing.T) *store.Store {
238242 t.Cleanup(func() { _ = s.Close() })239243 return s240244 }245+246+func TestCtrlYYanksSupportedPuzzle(t *testing.T) {247+ previousCopy := copyToClipboard248+ t.Cleanup(func() { copyToClipboard = previousCopy })249+250+ var copied string251+ copyToClipboard = func(s string) error {252+ copied = s253+ return nil254+ }255+256+ m := model{257+ state: gameView,258+ game: sudoku.Model{},259+ }260+261+ _, cmd := m.Update(tea.KeyPressMsg{Code: 'y', Mod: tea.ModCtrl})262+ if cmd != nil {263+ t.Fatal("expected nil command when native clipboard copy succeeds")264+ }265+ if copied == "" {266+ t.Fatal("expected markdown snippet to be copied")267+ }268+ if !strings.Contains(copied, "Given Grid") {269+ t.Fatalf("expected sudoku markdown snippet, got:\n%s", copied)270+ }271+}272+273+func TestCtrlYYankUnsupportedGameIsNoOp(t *testing.T) {274+ previousCopy := copyToClipboard275+ t.Cleanup(func() { copyToClipboard = previousCopy })276+277+ calls := 0278+ copyToClipboard = func(string) error {279+ calls++280+ return nil281+ }282+283+ m := model{284+ state: gameView,285+ game: lightsout.Model{},286+ }287+288+ _, cmd := m.Update(tea.KeyPressMsg{Code: 'y', Mod: tea.ModCtrl})289+ if cmd != nil {290+ t.Fatal("expected nil command for unsupported game yank")291+ }292+ if calls != 0 {293+ t.Fatalf("clipboard should not be called, got %d calls", calls)294+ }295+}296+297+func TestCtrlYYankOutsideGameViewIsNoOp(t *testing.T) {298+ previousCopy := copyToClipboard299+ t.Cleanup(func() { copyToClipboard = previousCopy })300+301+ calls := 0302+ copyToClipboard = func(string) error {303+ calls++304+ return nil305+ }306+307+ m := model{308+ state: mainMenuView,309+ game: sudoku.Model{},310+ }311+312+ _, cmd := m.Update(tea.KeyPressMsg{Code: 'y', Mod: tea.ModCtrl})313+ if cmd != nil {314+ t.Fatal("expected nil command outside game view")315+ }316+ if calls != 0 {317+ t.Fatalf("clipboard should not be called outside game view, got %d calls", calls)318+ }319+}320+321+func TestCtrlYYankFallsBackToOSC52(t *testing.T) {322+ previousCopy := copyToClipboard323+ t.Cleanup(func() { copyToClipboard = previousCopy })324+325+ copyToClipboard = func(string) error {326+ return errors.New("clipboard unavailable")327+ }328+329+ m := model{330+ state: gameView,331+ game: sudoku.Model{},332+ }333+334+ _, cmd := m.Update(tea.KeyPressMsg{Code: 'y', Mod: tea.ModCtrl})335+ if cmd == nil {336+ t.Fatal("expected fallback clipboard command")337+ }338+ if msg := cmd(); msg == nil {339+ t.Fatal("expected fallback clipboard message")340+ }341+}
A app/yank.go +42 -0
A cmd/export_pdf.go +178 -0
@@ -0,0 +1,178 @@
1+package cmd2+3+import (4+ "fmt"5+ "path/filepath"6+ "strings"7+ "time"8+9+ "github.com/FelineStateMachine/puzzletea/app"10+ "github.com/FelineStateMachine/puzzletea/game"11+ "github.com/FelineStateMachine/puzzletea/pdfexport"12+13+ "github.com/spf13/cobra"14+)15+16+var (17+ flagPDFOutput string18+ flagPDFTitle string19+ flagPDFAdvert string20+ flagPDFShuffleSeed string21+)22+23+var exportPDFCmd = &cobra.Command{24+ Use: "export-pdf <input.md> [more.md ...]",25+ Short: "Convert one or more PuzzleTea markdown exports into an A5 printable PDF",26+ Long: "Parse one or more markdown export files, order puzzles by progressive difficulty with seeded mixing, and render an A5 PDF with a title page and one puzzle per page.",27+ Args: cobra.MinimumNArgs(1),28+ RunE: runExportPDF,29+}30+31+func init() {32+ exportPDFCmd.Flags().StringVarP(&flagPDFOutput, "output", "o", "", "write output PDF path (defaults to <first-input>-print.pdf)")33+ exportPDFCmd.Flags().StringVar(&flagPDFTitle, "title", "", "title shown on the generated title page")34+ exportPDFCmd.Flags().StringVar(&flagPDFAdvert, "advert", "Find more puzzles at github.com/FelineStateMachine/puzzletea", "advert text shown on the title page")35+ exportPDFCmd.Flags().StringVar(&flagPDFShuffleSeed, "shuffle-seed", "", "seed for deterministic within-band difficulty mixing")36+}37+38+func runExportPDF(cmd *cobra.Command, args []string) error {39+ docs, err := pdfexport.ParseFiles(args)40+ if err != nil {41+ return err42+ }43+44+ puzzles := flattenPuzzles(docs)45+ if len(puzzles) == 0 {46+ return fmt.Errorf("no puzzles found in input markdown files")47+ }48+49+ lookup := buildModeDifficultyLookup(app.Categories)50+ annotateDifficulty(puzzles, lookup)51+52+ shuffleSeed := strings.TrimSpace(flagPDFShuffleSeed)53+ if shuffleSeed == "" {54+ shuffleSeed = time.Now().Format(time.RFC3339Nano)55+ }56+ ordered := pdfexport.OrderPuzzlesForPrint(puzzles, shuffleSeed)57+58+ output := strings.TrimSpace(flagPDFOutput)59+ if output == "" {60+ base := filepath.Base(args[0])61+ output = strings.TrimSuffix(base, filepath.Ext(base)) + "-print.pdf"62+ }63+ if !strings.EqualFold(filepath.Ext(output), ".pdf") {64+ return fmt.Errorf("--output must use a .pdf extension")65+ }66+67+ title := strings.TrimSpace(flagPDFTitle)68+ if title == "" {69+ title = defaultPDFTitle(docs)70+ }71+72+ cfg := pdfexport.RenderConfig{73+ Title: title,74+ AdvertText: flagPDFAdvert,75+ GeneratedAt: time.Now(),76+ ShuffleSeed: shuffleSeed,77+ }78+ if err := pdfexport.WritePDF(output, docs, ordered, cfg); err != nil {79+ return err80+ }81+82+ _, err = fmt.Fprintf(cmd.OutOrStdout(), "wrote %s with %d puzzles\n", output, len(ordered))83+ return err84+}85+86+func flattenPuzzles(docs []pdfexport.PackDocument) []pdfexport.Puzzle {87+ puzzles := []pdfexport.Puzzle{}88+ for _, doc := range docs {89+ puzzles = append(puzzles, doc.Puzzles...)90+ }91+ return puzzles92+}93+94+func defaultPDFTitle(docs []pdfexport.PackDocument) string {95+ if len(docs) == 1 {96+ category := strings.TrimSpace(docs[0].Metadata.Category)97+ if category != "" {98+ return fmt.Sprintf("%s Puzzle Pack", category)99+ }100+ }101+ return "PuzzleTea Mixed Puzzle Pack"102+}103+104+func buildModeDifficultyLookup(categories []game.Category) map[string]map[string]float64 {105+ lookup := make(map[string]map[string]float64, len(categories))106+107+ for _, cat := range categories {108+ titles := []string{}109+ for _, item := range cat.Modes {110+ mode, ok := item.(game.Mode)111+ if !ok {112+ continue113+ }114+ title := strings.TrimSpace(mode.Title())115+ if title == "" {116+ continue117+ }118+ titles = append(titles, title)119+ }120+121+ if len(titles) == 0 {122+ continue123+ }124+125+ scores := make(map[string]float64, len(titles))126+ if len(titles) == 1 {127+ scores[normalizeDifficultyToken(titles[0])] = 0.5128+ } else {129+ for i, title := range titles {130+ scores[normalizeDifficultyToken(title)] = float64(i) / float64(len(titles)-1)131+ }132+ }133+134+ lookup[normalizeDifficultyToken(cat.Name)] = scores135+ }136+137+ return lookup138+}139+140+func annotateDifficulty(puzzles []pdfexport.Puzzle, lookup map[string]map[string]float64) {141+ for i := range puzzles {142+ mode := normalizeDifficultyToken(puzzles[i].ModeSelection)143+ if mode == "" || strings.Contains(mode, "mixed modes") {144+ puzzles[i].DifficultyScore = 0.5145+ puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium146+ puzzles[i].DifficultySource = "mixed-mode fallback"147+ continue148+ }149+150+ category := normalizeDifficultyToken(puzzles[i].Category)151+ modes, ok := lookup[category]152+ if !ok {153+ puzzles[i].DifficultyScore = 0.5154+ puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium155+ puzzles[i].DifficultySource = "category lookup fallback"156+ continue157+ }158+159+ score, ok := modes[mode]160+ if !ok {161+ puzzles[i].DifficultyScore = 0.5162+ puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium163+ puzzles[i].DifficultySource = "mode lookup fallback"164+ continue165+ }166+167+ puzzles[i].DifficultyScore = score168+ puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceHigh169+ puzzles[i].DifficultySource = "mode-order"170+ }171+}172+173+func normalizeDifficultyToken(s string) string {174+ s = strings.ToLower(strings.TrimSpace(s))175+ s = strings.ReplaceAll(s, "-", " ")176+ s = strings.ReplaceAll(s, "_", " ")177+ return strings.Join(strings.Fields(s), " ")178+}
M cmd/new.go +8 -0
A cmd/new_export.go +201 -0
@@ -0,0 +1,201 @@
1+package cmd2+3+import (4+ "errors"5+ "fmt"6+ "io"7+ "math/rand/v2"8+ "os"9+ "path/filepath"10+ "strings"11+ "time"12+13+ "github.com/FelineStateMachine/puzzletea/app"14+ "github.com/FelineStateMachine/puzzletea/game"15+ "github.com/FelineStateMachine/puzzletea/markdownexport"16+ "github.com/FelineStateMachine/puzzletea/resolve"17+18+ "github.com/spf13/cobra"19+)20+21+var exportNow = time.Now22+23+type exportModeEntry struct {24+ spawner game.Spawner25+ mode string26+}27+28+func runNewExport(cmd *cobra.Command, args []string) error {29+ if err := validateNewExportFlags(cmd, args); err != nil {30+ return err31+ }32+33+ cat, err := resolve.Category(args[0], app.GameCategories)34+ if err != nil {35+ return err36+ }37+ if !markdownexport.SupportsGameType(cat.Name) {38+ return fmt.Errorf("game %q does not support markdown export", cat.Name)39+ }40+41+ modeArg := ""42+ if len(args) > 1 {43+ modeArg = args[1]44+ }45+46+ entries, modeSelection, err := collectExportModes(cat, modeArg)47+ if err != nil {48+ return err49+ }50+51+ sections, err := buildExportSections(cat.Name, entries, flagExport, flagWithSeed)52+ if err != nil {53+ return err54+ }55+56+ doc := markdownexport.BuildDocument(markdownexport.DocumentConfig{57+ Version: Version,58+ Category: cat.Name,59+ ModeSelection: modeSelection,60+ Count: flagExport,61+ Seed: flagWithSeed,62+ GeneratedAt: exportNow(),63+ }, sections)64+65+ if err := writeExportMarkdown(cmd, flagOutput, doc); err != nil {66+ return err67+ }68+69+ return nil70+}71+72+func validateNewExportFlags(cmd *cobra.Command, args []string) error {73+ if flagExport < 1 {74+ return fmt.Errorf("--export must be at least 1")75+ }76+ if strings.TrimSpace(flagOutput) != "" && !strings.EqualFold(filepath.Ext(flagOutput), ".md") {77+ return fmt.Errorf("--output must use a .md extension")78+ }79+ if flagSetSeed != "" {80+ return fmt.Errorf("--set-seed cannot be combined with markdown export (--export/--output)")81+ }82+ if len(args) == 0 {83+ return fmt.Errorf("requires at least 1 arg(s), only received 0")84+ }85+ if len(args) > 2 {86+ return fmt.Errorf("accepts at most 2 arg(s), received %d", len(args))87+ }88+ return nil89+}90+91+func collectExportModes(cat game.Category, modeArg string) ([]exportModeEntry, string, error) {92+ if modeArg != "" {93+ spawner, modeTitle, err := resolve.Mode(cat, modeArg)94+ if err != nil {95+ return nil, "", err96+ }97+ return []exportModeEntry{{spawner: spawner, mode: modeTitle}}, modeTitle, nil98+ }99+100+ entries := make([]exportModeEntry, 0, len(cat.Modes))101+ for _, item := range cat.Modes {102+ mode, ok := item.(game.Mode)103+ if !ok {104+ continue105+ }106+ spawner, ok := item.(game.Spawner)107+ if !ok {108+ continue109+ }110+ entries = append(entries, exportModeEntry{111+ spawner: spawner,112+ mode: mode.Title(),113+ })114+ }115+ if len(entries) == 0 {116+ return nil, "", fmt.Errorf("game %q has no exportable modes", cat.Name)117+ }118+119+ return entries, "mixed modes", nil120+}121+122+func buildExportSections(gameType string, entries []exportModeEntry, count int, seed string) ([]markdownexport.PuzzleSection, error) {123+ var rng *rand.Rand124+ if seed != "" {125+ rng = resolve.RNGFromString(seed)126+ }127+128+ sections := make([]markdownexport.PuzzleSection, 0, count)129+ for i := 0; i < count; i++ {130+ entry := entries[0]131+ if len(entries) > 1 {132+ var modeIndex int133+ if rng != nil {134+ modeIndex = rng.IntN(len(entries))135+ } else {136+ modeIndex = rand.IntN(len(entries))137+ }138+ entry = entries[modeIndex]139+ }140+141+ puzzle, err := spawnExportPuzzle(entry.spawner, rng)142+ if err != nil {143+ return nil, fmt.Errorf("generate puzzle %d: %w", i+1, err)144+ }145+146+ save, err := puzzle.GetSave()147+ if err != nil {148+ return nil, fmt.Errorf("serialize puzzle %d: %w", i+1, err)149+ }150+151+ snippet, err := markdownexport.RenderPuzzleSnippet(gameType, entry.mode, save)152+ if err != nil {153+ if errors.Is(err, markdownexport.ErrUnsupportedGame) {154+ return nil, fmt.Errorf("game %q does not support markdown export", gameType)155+ }156+ return nil, fmt.Errorf("render puzzle %d: %w", i+1, err)157+ }158+159+ sections = append(sections, markdownexport.PuzzleSection{160+ Index: i + 1,161+ GameType: gameType,162+ Mode: entry.mode,163+ Body: snippet,164+ })165+ }166+167+ return sections, nil168+}169+170+func spawnExportPuzzle(spawner game.Spawner, rng *rand.Rand) (game.Gamer, error) {171+ if rng == nil {172+ return spawner.Spawn()173+ }174+175+ seeded, ok := spawner.(game.SeededSpawner)176+ if !ok {177+ return nil, fmt.Errorf("mode does not support deterministic spawning")178+ }179+ return seeded.SpawnSeeded(rng)180+}181+182+func writeExportMarkdown(cmd *cobra.Command, path, content string) error {183+ if strings.TrimSpace(path) == "" {184+ if _, err := io.WriteString(cmd.OutOrStdout(), content); err != nil {185+ return fmt.Errorf("write export markdown to stdout: %w", err)186+ }187+ return nil188+ }189+190+ dir := filepath.Dir(path)191+ if dir != "" && dir != "." {192+ if err := os.MkdirAll(dir, 0o755); err != nil {193+ return fmt.Errorf("create output directory: %w", err)194+ }195+ }196+197+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {198+ return fmt.Errorf("write output markdown: %w", err)199+ }200+ return nil201+}
A cmd/new_export_test.go +175 -0
@@ -0,0 +1,175 @@
1+package cmd2+3+import (4+ "bytes"5+ "os"6+ "path/filepath"7+ "strconv"8+ "strings"9+ "testing"10+ "time"11+12+ "github.com/spf13/cobra"13+)14+15+func TestRunNewExportRejectsUnsupportedGame(t *testing.T) {16+ withExportFlagReset(t)17+ flagOutput = filepath.Join(t.TempDir(), "lights.md")18+19+ cmd, _ := newExportTestCmd(t, false)20+ err := runNewExport(cmd, []string{"lights-out"})21+ if err == nil {22+ t.Fatal("expected unsupported game error")23+ }24+ if !strings.Contains(err.Error(), "does not support markdown export") {25+ t.Fatalf("unexpected error: %v", err)26+ }27+}28+29+func TestRunNewExportValidation(t *testing.T) {30+ t.Run("writes to stdout when output omitted", func(t *testing.T) {31+ withExportFlagReset(t)32+ flagExport = 233+34+ cmd, out := newExportTestCmd(t, true)35+ err := runNewExport(cmd, []string{"nonogram", "mini"})36+ if err != nil {37+ t.Fatalf("expected stdout export success, got error: %v", err)38+ }39+ if !strings.Contains(out.String(), "# PuzzleTea Export") {40+ t.Fatalf("expected markdown output on stdout, got:\n%s", out.String())41+ }42+ })43+44+ t.Run("output extension must be markdown", func(t *testing.T) {45+ withExportFlagReset(t)46+ flagOutput = filepath.Join(t.TempDir(), "out.txt")47+48+ cmd, _ := newExportTestCmd(t, false)49+ err := runNewExport(cmd, []string{"nonogram", "mini"})50+ if err == nil {51+ t.Fatal("expected extension validation error")52+ }53+ if !strings.Contains(err.Error(), ".md extension") {54+ t.Fatalf("unexpected error: %v", err)55+ }56+ })57+58+ t.Run("set-seed cannot be combined with output", func(t *testing.T) {59+ withExportFlagReset(t)60+ flagSetSeed = "abc"61+ flagOutput = filepath.Join(t.TempDir(), "out.md")62+63+ cmd, _ := newExportTestCmd(t, false)64+ err := runNewExport(cmd, []string{"nonogram", "mini"})65+ if err == nil {66+ t.Fatal("expected set-seed validation error")67+ }68+ if !strings.Contains(err.Error(), "--set-seed cannot be combined with markdown export (--export/--output)") {69+ t.Fatalf("unexpected error: %v", err)70+ }71+ })72+}73+74+func TestRunNewExportReproducibleWithSeed(t *testing.T) {75+ withExportFlagReset(t)76+77+ fixedNow := time.Date(2026, 2, 22, 11, 0, 0, 0, time.UTC)78+ previousNow := exportNow79+ exportNow = func() time.Time { return fixedNow }80+ t.Cleanup(func() { exportNow = previousNow })81+82+ fileA := filepath.Join(t.TempDir(), "a.md")83+ fileB := filepath.Join(t.TempDir(), "b.md")84+85+ flagExport = 386+ flagWithSeed = "zine-seed-01"87+ flagOutput = fileA88+ cmdA, _ := newExportTestCmd(t, false)89+ if err := runNewExport(cmdA, []string{"nonogram", "mini"}); err != nil {90+ t.Fatalf("first export failed: %v", err)91+ }92+93+ flagOutput = fileB94+ cmdB, _ := newExportTestCmd(t, false)95+ if err := runNewExport(cmdB, []string{"nonogram", "mini"}); err != nil {96+ t.Fatalf("second export failed: %v", err)97+ }98+99+ a, err := os.ReadFile(fileA)100+ if err != nil {101+ t.Fatal(err)102+ }103+ b, err := os.ReadFile(fileB)104+ if err != nil {105+ t.Fatal(err)106+ }107+ if string(a) != string(b) {108+ t.Fatal("expected deterministic markdown output for identical seed and args")109+ }110+}111+112+func TestRunNewExportOverwritesOutputFile(t *testing.T) {113+ withExportFlagReset(t)114+115+ file := filepath.Join(t.TempDir(), "out.md")116+ if err := os.WriteFile(file, []byte("old"), 0o644); err != nil {117+ t.Fatal(err)118+ }119+120+ flagExport = 1121+ flagWithSeed = "overwrite-seed"122+ flagOutput = file123+124+ cmd, _ := newExportTestCmd(t, false)125+ if err := runNewExport(cmd, []string{"nonogram", "mini"}); err != nil {126+ t.Fatalf("export failed: %v", err)127+ }128+129+ data, err := os.ReadFile(file)130+ if err != nil {131+ t.Fatal(err)132+ }133+ if string(data) == "old" {134+ t.Fatal("expected output file to be overwritten")135+ }136+ if !strings.Contains(string(data), "# PuzzleTea Export") {137+ t.Fatal("expected markdown export header")138+ }139+}140+141+func withExportFlagReset(t *testing.T) {142+ t.Helper()143+144+ prevSetSeed := flagSetSeed145+ prevWithSeed := flagWithSeed146+ prevExport := flagExport147+ prevOutput := flagOutput148+149+ flagSetSeed = ""150+ flagWithSeed = ""151+ flagExport = 1152+ flagOutput = ""153+154+ t.Cleanup(func() {155+ flagSetSeed = prevSetSeed156+ flagWithSeed = prevWithSeed157+ flagExport = prevExport158+ flagOutput = prevOutput159+ })160+}161+162+func newExportTestCmd(t *testing.T, exportChanged bool) (*cobra.Command, *bytes.Buffer) {163+ t.Helper()164+165+ cmd := &cobra.Command{}166+ var out bytes.Buffer167+ cmd.SetOut(&out)168+ cmd.Flags().Int("export", 1, "")169+ if exportChanged {170+ if err := cmd.Flags().Set("export", strconv.Itoa(flagExport)); err != nil {171+ t.Fatal(err)172+ }173+ }174+ return cmd, &out175+}
M cmd/root.go +1 -1
M go.mod +3 -2
M go.sum +2 -0
A markdownexport/detect.go +41 -0
A markdownexport/document.go +71 -0
@@ -0,0 +1,71 @@
1+package markdownexport2+3+import (4+ "fmt"5+ "hash/fnv"6+ "math/rand/v2"7+ "strings"8+ "time"9+10+ "github.com/FelineStateMachine/puzzletea/namegen"11+)12+13+type DocumentConfig struct {14+ Version string15+ Category string16+ ModeSelection string17+ Count int18+ Seed string19+ GeneratedAt time.Time20+}21+22+type PuzzleSection struct {23+ Index int24+ GameType string25+ Mode string26+ Body string27+}28+29+func BuildDocument(cfg DocumentConfig, puzzles []PuzzleSection) string {30+ var b strings.Builder31+32+ seed := cfg.Seed33+ if strings.TrimSpace(seed) == "" {34+ seed = "none"35+ }36+ nameRNG := exportNameRNG(cfg)37+38+ fmt.Fprintf(&b, "# PuzzleTea Export\n\n")39+ fmt.Fprintf(&b, "- Generated: %s\n", cfg.GeneratedAt.Format(time.RFC3339))40+ fmt.Fprintf(&b, "- Version: %s\n", cfg.Version)41+ fmt.Fprintf(&b, "- Category: %s\n", cfg.Category)42+ fmt.Fprintf(&b, "- Mode Selection: %s\n", cfg.ModeSelection)43+ fmt.Fprintf(&b, "- Count: %d\n", cfg.Count)44+ fmt.Fprintf(&b, "- Seed: %s\n\n", seed)45+46+ for i, puzzle := range puzzles {47+ if i > 0 {48+ b.WriteString("\n---\n\n")49+ }50+ fmt.Fprintf(&b, "## %s - %d\n\n", namegen.GenerateSeeded(nameRNG), puzzle.Index)51+ b.WriteString(strings.TrimSpace(puzzle.Body))52+ b.WriteString("\n")53+ }54+55+ return b.String()56+}57+58+func exportNameRNG(cfg DocumentConfig) *rand.Rand {59+ seed := cfg.Seed60+ if strings.TrimSpace(seed) == "" {61+ seed = cfg.GeneratedAt.Format(time.RFC3339Nano)62+ }63+ return rngFromString("export-names:" + seed)64+}65+66+func rngFromString(seed string) *rand.Rand {67+ h := fnv.New64a()68+ h.Write([]byte(seed))69+ s := h.Sum64()70+ return rand.New(rand.NewPCG(s, ^s))71+}
A markdownexport/render.go +467 -0
@@ -0,0 +1,467 @@
1+package markdownexport2+3+import (4+ "encoding/json"5+ "fmt"6+ "strconv"7+ "strings"8+9+ "github.com/FelineStateMachine/puzzletea/hashiwokakero"10+ "github.com/FelineStateMachine/puzzletea/hitori"11+ "github.com/FelineStateMachine/puzzletea/nonogram"12+ "github.com/FelineStateMachine/puzzletea/nurikabe"13+ "github.com/FelineStateMachine/puzzletea/shikaku"14+ "github.com/FelineStateMachine/puzzletea/sudoku"15+ "github.com/FelineStateMachine/puzzletea/takuzu"16+ "github.com/FelineStateMachine/puzzletea/wordsearch"17+)18+19+func RenderPuzzleSnippet(gameType, _ string, save []byte) (string, error) {20+ switch normalizeGameType(gameType) {21+ case "hashiwokakero":22+ return renderHashi(save)23+ case "hitori":24+ return renderHitori(save)25+ case "nonogram":26+ return renderNonogram(save)27+ case "nurikabe":28+ return renderNurikabe(save)29+ case "shikaku":30+ return renderShikaku(save)31+ case "sudoku":32+ return renderSudoku(save)33+ case "takuzu":34+ return renderTakuzu(save)35+ case "word search", "wordsearch":36+ return renderWordSearch(save)37+ case "lights out", "lightsout":38+ return "", ErrUnsupportedGame39+ default:40+ return "", ErrUnsupportedGame41+ }42+}43+44+func renderHashi(data []byte) (string, error) {45+ var save hashiwokakero.Save46+ if err := json.Unmarshal(data, &save); err != nil {47+ return "", fmt.Errorf("decode hashiwokakero save: %w", err)48+ }49+50+ cells := makeGrid(save.Width, save.Height, ".")51+ for _, island := range save.Islands {52+ if island.Y >= 0 && island.Y < len(cells) && island.X >= 0 && island.X < len(cells[island.Y]) {53+ cells[island.Y][island.X] = strconv.Itoa(island.Required)54+ }55+ }56+57+ var b strings.Builder58+ b.WriteString("### Grid\n\n")59+ b.WriteString(renderGridTable(cells))60+ b.WriteString("\n\n")61+ b.WriteString("Rules: connect numbered islands with horizontal/vertical bridges. ")62+ b.WriteString("Use up to two bridges per connection and never cross bridges.")63+ return b.String(), nil64+}65+66+func renderHitori(data []byte) (string, error) {67+ var save hitori.Save68+ if err := json.Unmarshal(data, &save); err != nil {69+ return "", fmt.Errorf("decode hitori save: %w", err)70+ }71+72+ rows := splitLines(save.Numbers)73+ cells := make([][]string, 0, max(len(rows), save.Size))74+ targetHeight := max(len(rows), save.Size)75+ for y := range targetHeight {76+ row := []rune{}77+ if y < len(rows) {78+ row = []rune(rows[y])79+ }80+ cellsRow := make([]string, max(len(row), save.Size))81+ if len(cellsRow) == 0 {82+ cellsRow = []string{"."}83+ }84+ for x := range len(cellsRow) {85+ if x < len(row) {86+ cellsRow[x] = string(row[x])87+ } else {88+ cellsRow[x] = "."89+ }90+ }91+ cells = append(cells, cellsRow)92+ }93+ if len(cells) == 0 {94+ cells = [][]string{{"."}}95+ }96+97+ var b strings.Builder98+ b.WriteString("### Grid\n\n")99+ b.WriteString(renderGridTable(cells))100+ b.WriteString("\n\n")101+ b.WriteString("Goal: shade cells so no row or column has duplicate unshaded values, ")102+ b.WriteString("shaded cells do not touch orthogonally, and all unshaded cells stay connected.")103+ return b.String(), nil104+}105+106+func renderNonogram(data []byte) (string, error) {107+ var save nonogram.Save108+ if err := json.Unmarshal(data, &save); err != nil {109+ return "", fmt.Errorf("decode nonogram save: %w", err)110+ }111+112+ width := save.Width113+ height := save.Height114+ if width <= 0 {115+ width = len(save.ColHints)116+ }117+ if height <= 0 {118+ height = len(save.RowHints)119+ }120+ if width <= 0 || height <= 0 {121+ return "### Puzzle Grid with Integrated Hints\n\n_(empty grid)_", nil122+ }123+124+ rowHints := normalizeNonogramHints(save.RowHints, height)125+ colHints := normalizeNonogramHints(save.ColHints, width)126+127+ rowHintCols := maxNonogramHintLen(rowHints)128+ colHintRows := maxNonogramHintLen(colHints)129+ if rowHintCols < 1 {130+ rowHintCols = 1131+ }132+ if colHintRows < 1 {133+ colHintRows = 1134+ }135+136+ var b strings.Builder137+ b.WriteString("### Puzzle Grid with Integrated Hints\n\n")138+ b.WriteString(renderNonogramTable(rowHints, colHints, width, height, rowHintCols, colHintRows))139+ b.WriteString("\n\n")140+ b.WriteString("Row hints are right-aligned beside each row. ")141+ b.WriteString("Column hints are stacked above each column and bottom-aligned to the grid.")142+ return b.String(), nil143+}144+145+func renderNurikabe(data []byte) (string, error) {146+ var save nurikabe.Save147+ if err := json.Unmarshal(data, &save); err != nil {148+ return "", fmt.Errorf("decode nurikabe save: %w", err)149+ }150+151+ clues := parseNurikabeClues(save.Clues, save.Width, save.Height)152+ cells := makeGrid(save.Width, save.Height, ".")153+ for y := range len(cells) {154+ for x := range len(cells[y]) {155+ if y < len(clues) && x < len(clues[y]) && clues[y][x] > 0 {156+ cells[y][x] = strconv.Itoa(clues[y][x])157+ }158+ }159+ }160+161+ var b strings.Builder162+ b.WriteString("### Clue Grid\n\n")163+ b.WriteString(renderGridTable(cells))164+ b.WriteString("\n\n")165+ b.WriteString("Goal: build one connected sea while each numbered island has the exact size of its clue.")166+ return b.String(), nil167+}168+169+func renderShikaku(data []byte) (string, error) {170+ var save shikaku.Save171+ if err := json.Unmarshal(data, &save); err != nil {172+ return "", fmt.Errorf("decode shikaku save: %w", err)173+ }174+175+ cells := makeGrid(save.Width, save.Height, ".")176+ for _, clue := range save.Clues {177+ if clue.Y >= 0 && clue.Y < len(cells) && clue.X >= 0 && clue.X < len(cells[clue.Y]) {178+ cells[clue.Y][clue.X] = strconv.Itoa(clue.Value)179+ }180+ }181+182+ var b strings.Builder183+ b.WriteString("### Clue Grid\n\n")184+ b.WriteString(renderGridTable(cells))185+ b.WriteString("\n\n")186+ b.WriteString("Goal: partition the grid into rectangles so each rectangle contains one clue and its area matches that clue.")187+ return b.String(), nil188+}189+190+func renderSudoku(data []byte) (string, error) {191+ var save sudoku.Save192+ if err := json.Unmarshal(data, &save); err != nil {193+ return "", fmt.Errorf("decode sudoku save: %w", err)194+ }195+196+ cells := makeGrid(9, 9, ".")197+ for _, provided := range save.Provided {198+ if provided.Y >= 0 && provided.Y < len(cells) && provided.X >= 0 && provided.X < len(cells[provided.Y]) {199+ cells[provided.Y][provided.X] = strconv.Itoa(provided.V)200+ }201+ }202+203+ var b strings.Builder204+ b.WriteString("### Given Grid\n\n")205+ b.WriteString(renderGridTable(cells))206+ b.WriteString("\n\n")207+ b.WriteString("Goal: fill each row, column, and 3x3 box with digits 1-9 exactly once.")208+ return b.String(), nil209+}210+211+func renderTakuzu(data []byte) (string, error) {212+ var save takuzu.Save213+ if err := json.Unmarshal(data, &save); err != nil {214+ return "", fmt.Errorf("decode takuzu save: %w", err)215+ }216+217+ cells := makeGrid(save.Size, save.Size, ".")218+ stateRows := splitLines(save.State)219+ providedRows := splitLines(save.Provided)220+221+ for y := range len(cells) {222+ for x := range len(cells[y]) {223+ provided := y < len(providedRows) && x < len(providedRows[y]) && providedRows[y][x] == '#'224+ if !provided {225+ continue226+ }227+228+ if y < len(stateRows) && x < len(stateRows[y]) {229+ switch stateRows[y][x] {230+ case '0', '1':231+ cells[y][x] = string(stateRows[y][x])232+ default:233+ cells[y][x] = "."234+ }235+ }236+ }237+ }238+239+ var b strings.Builder240+ b.WriteString("### Given Grid\n\n")241+ b.WriteString(renderGridTable(cells))242+ b.WriteString("\n\n")243+ b.WriteString("Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.")244+ return b.String(), nil245+}246+247+func renderWordSearch(data []byte) (string, error) {248+ var save wordsearch.Save249+ if err := json.Unmarshal(data, &save); err != nil {250+ return "", fmt.Errorf("decode word search save: %w", err)251+ }252+253+ rows := splitLines(save.Grid)254+ height := max(save.Height, len(rows))255+ width := save.Width256+ if width <= 0 {257+ for _, row := range rows {258+ if len([]rune(row)) > width {259+ width = len([]rune(row))260+ }261+ }262+ }263+264+ cells := makeGrid(width, height, ".")265+ for y := range len(cells) {266+ if y >= len(rows) {267+ continue268+ }269+ row := []rune(rows[y])270+ for x := range len(cells[y]) {271+ if x < len(row) {272+ cells[y][x] = string(row[x])273+ }274+ }275+ }276+277+ var b strings.Builder278+ b.WriteString("### Grid\n\n")279+ b.WriteString(renderGridTable(cells))280+ b.WriteString("\n\n")281+282+ b.WriteString("### Word List\n\n")283+ b.WriteString("| # | Word |\n")284+ b.WriteString("| --- | --- |\n")285+ for i, word := range save.Words {286+ fmt.Fprintf(&b, "| %d | %s |\n", i+1, escapeCell(word.Text))287+ }288+ if len(save.Words) == 0 {289+ b.WriteString("| 1 | (none) |\n")290+ }291+292+ b.WriteString("\nGoal: find all listed words in the grid.")293+ return b.String(), nil294+}295+296+func makeGrid(width, height int, fill string) [][]string {297+ if width <= 0 || height <= 0 {298+ return [][]string{}299+ }300+ grid := make([][]string, height)301+ for y := range height {302+ grid[y] = make([]string, width)303+ for x := range width {304+ grid[y][x] = fill305+ }306+ }307+ return grid308+}309+310+func renderGridTable(cells [][]string) string {311+ if len(cells) == 0 {312+ return "_(empty grid)_"313+ }314+ width := 0315+ for _, row := range cells {316+ if len(row) > width {317+ width = len(row)318+ }319+ }320+ if width == 0 {321+ return "_(empty grid)_"322+ }323+324+ var b strings.Builder325+ b.WriteString("| |")326+ for x := range width {327+ fmt.Fprintf(&b, " %d |", x+1)328+ }329+ b.WriteString("\n| --- |")330+ for range width {331+ b.WriteString(" --- |")332+ }333+ b.WriteString("\n")334+335+ for y := range len(cells) {336+ fmt.Fprintf(&b, "| %d |", y+1)337+ for x := range width {338+ cell := "."339+ if x < len(cells[y]) && strings.TrimSpace(cells[y][x]) != "" {340+ cell = cells[y][x]341+ }342+ fmt.Fprintf(&b, " %s |", escapeCell(cell))343+ }344+ b.WriteString("\n")345+ }346+347+ return strings.TrimRight(b.String(), "\n")348+}349+350+func parseNurikabeClues(raw string, width, height int) [][]int {351+ clues := make([][]int, max(height, 0))352+ for y := range len(clues) {353+ clues[y] = make([]int, max(width, 0))354+ }355+356+ rows := splitLines(raw)357+ for y := range min(len(rows), len(clues)) {358+ parts := strings.Split(rows[y], ",")359+ for x := range min(len(parts), len(clues[y])) {360+ val, err := strconv.Atoi(strings.TrimSpace(parts[x]))361+ if err != nil {362+ continue363+ }364+ clues[y][x] = val365+ }366+ }367+368+ return clues369+}370+371+func splitLines(s string) []string {372+ if strings.TrimSpace(s) == "" {373+ return []string{}374+ }375+ return strings.Split(s, "\n")376+}377+378+func escapeCell(s string) string {379+ return strings.ReplaceAll(s, "|", "\\|")380+}381+382+func normalizeNonogramHints(src nonogram.TomographyDefinition, size int) [][]int {383+ hints := make([][]int, max(size, 0))384+ for i := range len(hints) {385+ if i >= len(src) {386+ hints[i] = []int{0}387+ continue388+ }389+ if len(src[i]) == 0 {390+ hints[i] = []int{0}391+ continue392+ }393+ hints[i] = append([]int(nil), src[i]...)394+ }395+ return hints396+}397+398+func maxNonogramHintLen(hints [][]int) int {399+ maxLen := 0400+ for _, hint := range hints {401+ if len(hint) > maxLen {402+ maxLen = len(hint)403+ }404+ }405+ return maxLen406+}407+408+func renderNonogramTable(409+ rowHints, colHints [][]int,410+ width, height, rowHintCols, colHintRows int,411+) string {412+ var b strings.Builder413+414+ b.WriteString("|")415+ for i := range rowHintCols {416+ fmt.Fprintf(&b, " R%d |", i+1)417+ }418+ for x := range width {419+ fmt.Fprintf(&b, " C%d |", x+1)420+ }421+ b.WriteString("\n|")422+ for range rowHintCols + width {423+ b.WriteString(" --- |")424+ }425+ b.WriteString("\n")426+427+ for hintRow := range colHintRows {428+ b.WriteString("|")429+ for range rowHintCols {430+ b.WriteString(" . |")431+ }432+ for x := range width {433+ b.WriteString(" ")434+ b.WriteString(renderColumnHintCell(colHints[x], colHintRows, hintRow))435+ b.WriteString(" |")436+ }437+ b.WriteString("\n")438+ }439+440+ for y := range height {441+ rowHint := rowHints[y]442+ hintStart := rowHintCols - len(rowHint)443+444+ b.WriteString("|")445+ for hintCol := range rowHintCols {446+ if hintCol < hintStart {447+ b.WriteString(" . |")448+ continue449+ }450+ fmt.Fprintf(&b, " %d |", rowHint[hintCol-hintStart])451+ }452+ for range width {453+ b.WriteString(" . |")454+ }455+ b.WriteString("\n")456+ }457+458+ return strings.TrimRight(b.String(), "\n")459+}460+461+func renderColumnHintCell(hint []int, depth, row int) string {462+ start := depth - len(hint)463+ if row < start {464+ return "."465+ }466+ return strconv.Itoa(hint[row-start])467+}
A markdownexport/render_test.go +188 -0
@@ -0,0 +1,188 @@
1+package markdownexport2+3+import (4+ "errors"5+ "regexp"6+ "strings"7+ "testing"8+ "time"9+10+ "github.com/FelineStateMachine/puzzletea/game"11+ "github.com/FelineStateMachine/puzzletea/sudoku"12+ "github.com/FelineStateMachine/puzzletea/wordsearch"13+14+ "charm.land/bubbles/v2/key"15+ tea "charm.land/bubbletea/v2"16+)17+18+func TestSupportsGameType(t *testing.T) {19+ if !SupportsGameType("Sudoku") {20+ t.Fatal("expected sudoku to be supported")21+ }22+ if SupportsGameType("Lights Out") {23+ t.Fatal("expected lights out to be unsupported")24+ }25+}26+27+func TestRenderPuzzleSnippetUnsupported(t *testing.T) {28+ _, err := RenderPuzzleSnippet("Lights Out", "", []byte(`{}`))29+ if !errors.Is(err, ErrUnsupportedGame) {30+ t.Fatalf("expected ErrUnsupportedGame, got %v", err)31+ }32+}33+34+func TestRenderPuzzleSnippetSudokuUsesProvidedOnly(t *testing.T) {35+ data := []byte(`{36+ "grid":"500000000\n600000000\n000000000\n000000000\n000000000\n000000000\n000000000\n000000000\n000000000",37+ "provided":[{"x":0,"y":0,"v":5}]38+ }`)39+40+ snippet, err := RenderPuzzleSnippet("Sudoku", "", data)41+ if err != nil {42+ t.Fatal(err)43+ }44+45+ if !strings.Contains(snippet, "| 1 | 5 | . | . | . | . | . | . | . | . |") {46+ t.Fatalf("expected given row in snippet, got:\n%s", snippet)47+ }48+ if !strings.Contains(snippet, "| 2 | . | . | . | . | . | . | . | . | . |") {49+ t.Fatalf("expected user-entered row to be blank in snippet, got:\n%s", snippet)50+ }51+}52+53+func TestRenderPuzzleSnippetTakuzuUsesProvidedOnly(t *testing.T) {54+ data := []byte(`{55+ "size":2,56+ "state":"01\n10",57+ "provided":"#.\n.#",58+ "mode_title":"Test"59+ }`)60+61+ snippet, err := RenderPuzzleSnippet("Takuzu", "", data)62+ if err != nil {63+ t.Fatal(err)64+ }65+66+ if !strings.Contains(snippet, "| 1 | 0 | . |") {67+ t.Fatalf("expected first row to keep only provided value, got:\n%s", snippet)68+ }69+ if !strings.Contains(snippet, "| 2 | . | 0 |") {70+ t.Fatalf("expected second row to keep only provided value, got:\n%s", snippet)71+ }72+}73+74+func TestRenderPuzzleSnippetNonogramIntegratedHintsLayout(t *testing.T) {75+ data := []byte(`{76+ "width":3,77+ "height":2,78+ "row-hints":[[3],[1,1]],79+ "col-hints":[[1],[2],[1]]80+ }`)81+82+ snippet, err := RenderPuzzleSnippet("Nonogram", "", data)83+ if err != nil {84+ t.Fatal(err)85+ }86+87+ if !strings.Contains(snippet, "### Puzzle Grid with Integrated Hints") {88+ t.Fatalf("expected integrated nonogram heading, got:\n%s", snippet)89+ }90+ if strings.Contains(snippet, "### Row Hints") || strings.Contains(snippet, "### Column Hints") {91+ t.Fatalf("expected legacy split hint sections to be removed, got:\n%s", snippet)92+ }93+ if !strings.Contains(snippet, "| . | . | 1 | 2 | 1 |") {94+ t.Fatalf("expected column hints row aligned above puzzle grid, got:\n%s", snippet)95+ }96+ if !strings.Contains(snippet, "| . | 3 | . | . | . |") {97+ t.Fatalf("expected single row hint to be right-aligned near grid, got:\n%s", snippet)98+ }99+ if !strings.Contains(snippet, "| 1 | 1 | . | . | . |") {100+ t.Fatalf("expected multi-value row hint to render beside puzzle row, got:\n%s", snippet)101+ }102+}103+104+func TestRenderPuzzleSnippetNonogramColumnHintsBottomAligned(t *testing.T) {105+ data := []byte(`{106+ "width":2,107+ "height":1,108+ "row-hints":[[1]],109+ "col-hints":[[2,1],[3]]110+ }`)111+112+ snippet, err := RenderPuzzleSnippet("Nonogram", "", data)113+ if err != nil {114+ t.Fatal(err)115+ }116+117+ if !strings.Contains(snippet, "| . | 2 | . |") {118+ t.Fatalf("expected top column hint row to pad shorter hints, got:\n%s", snippet)119+ }120+ if !strings.Contains(snippet, "| . | 1 | 3 |") {121+ t.Fatalf("expected lower column hint row to sit closest to grid, got:\n%s", snippet)122+ }123+}124+125+func TestDetectGameType(t *testing.T) {126+ gameType, err := DetectGameType(sudoku.Model{})127+ if err != nil {128+ t.Fatal(err)129+ }130+ if gameType != "Sudoku" {131+ t.Fatalf("gameType = %q, want %q", gameType, "Sudoku")132+ }133+134+ gameType, err = DetectGameType(&wordsearch.Model{})135+ if err != nil {136+ t.Fatal(err)137+ }138+ if gameType != "Word Search" {139+ t.Fatalf("gameType = %q, want %q", gameType, "Word Search")140+ }141+142+ _, err = DetectGameType(testUnknownGamer{})143+ if err == nil {144+ t.Fatal("expected unknown gamer detection error")145+ }146+}147+148+func TestBuildDocument(t *testing.T) {149+ doc := BuildDocument(DocumentConfig{150+ Version: "v-test",151+ Category: "Sudoku",152+ ModeSelection: "mixed modes",153+ Count: 2,154+ Seed: "seed-1",155+ GeneratedAt: time.Date(2026, 2, 22, 10, 0, 0, 0, time.UTC),156+ }, []PuzzleSection{157+ {Index: 1, GameType: "Sudoku", Mode: "Easy", Body: "body-one"},158+ {Index: 2, GameType: "Sudoku", Mode: "Hard", Body: "body-two"},159+ })160+161+ if !strings.Contains(doc, "# PuzzleTea Export") {162+ t.Fatal("expected export title in markdown document")163+ }164+ if !strings.Contains(doc, "Version: v-test") {165+ t.Fatal("expected version metadata in markdown document")166+ }167+ if matched := regexp.MustCompile(`## [a-z]+-[a-z]+ - 1`).MatchString(doc); !matched {168+ t.Fatal("expected first puzzle heading in adjective-noun pattern")169+ }170+ if matched := regexp.MustCompile(`\n---\n\n## [a-z]+-[a-z]+ - 2`).MatchString(doc); !matched {171+ t.Fatal("expected puzzle separator and second heading in adjective-noun pattern")172+ }173+ if strings.Contains(doc, "## Puzzle 1 - Sudoku (Easy)") {174+ t.Fatal("expected legacy puzzle heading format to be removed")175+ }176+}177+178+type testUnknownGamer struct{}179+180+func (testUnknownGamer) GetDebugInfo() string { return "" }181+func (testUnknownGamer) GetFullHelp() [][]key.Binding { return nil }182+func (testUnknownGamer) GetSave() ([]byte, error) { return nil, nil }183+func (testUnknownGamer) IsSolved() bool { return false }184+func (testUnknownGamer) Reset() game.Gamer { return testUnknownGamer{} }185+func (testUnknownGamer) SetTitle(string) game.Gamer { return testUnknownGamer{} }186+func (testUnknownGamer) Init() tea.Cmd { return nil }187+func (testUnknownGamer) View() string { return "" }188+func (testUnknownGamer) Update(tea.Msg) (game.Gamer, tea.Cmd) { return testUnknownGamer{}, nil }
A markdownexport/support.go +26 -0
A nonogram-pack-01.md +776 -0
@@ -0,0 +1,776 @@
1+# PuzzleTea Export2+3+- Generated: 2026-02-21T20:42:05-07:004+- Version: v1.6.0-1-gd260f2e-dirty5+- Category: Nonogram6+- Mode Selection: Standard7+- Count: 318+- Seed: meow9+10+## ember-newt - 111+12+### Puzzle Grid with Integrated Hints13+14+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |15+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |16+| . | . | . | . | 6 | 4 | 2 | 1 | . | . | . | 1 | 2 | . |17+| . | . | . | . | 1 | 1 | 4 | 5 | 2 | 5 | 5 | 2 | 5 | 2 |18+| . | . | . | . | 1 | 1 | 1 | 2 | 4 | 2 | 3 | 3 | 1 | 7 |19+| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |20+| . | . | 3 | 6 | . | . | . | . | . | . | . | . | . | . |21+| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |22+| . | 2 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |23+| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |24+| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |25+| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |26+| . | . | 3 | 6 | . | . | . | . | . | . | . | . | . | . |27+| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |28+| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |29+30+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.31+32+---33+34+## ember-moss - 235+36+### Puzzle Grid with Integrated Hints37+38+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |39+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |40+| . | . | . | . | . | 1 | . | . | . | . | 2 | . | . | 2 |41+| . | . | . | . | . | 4 | . | . | . | . | 1 | . | . | 2 |42+| . | . | . | . | 1 | 1 | 3 | 6 | 3 | 3 | 1 | 2 | 1 | 1 |43+| . | . | . | . | 5 | 1 | 2 | 3 | 3 | 6 | 2 | 5 | 8 | 1 |44+| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |45+| . | 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . |46+| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |47+| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |48+| 2 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |49+| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |50+| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |51+| . | 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |52+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |53+| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |54+55+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.56+57+---58+59+## stone-viper - 360+61+### Puzzle Grid with Integrated Hints62+63+| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |64+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |65+| . | . | . | . | . | 2 | 2 | . | . | 4 | . | 1 | 1 | 2 | 2 |66+| . | . | . | . | . | 3 | 1 | 3 | 1 | 1 | 2 | 3 | 1 | 1 | 1 |67+| . | . | . | . | . | 2 | 2 | 5 | 3 | 2 | 3 | 3 | 1 | 4 | 2 |68+| . | . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |69+| 1 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |70+| . | . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |71+| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |72+| . | . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |73+| . | 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |74+| . | . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |75+| . | . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |76+| . | . | 3 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |77+| . | 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |78+79+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.80+81+---82+83+## lonely-vale - 484+85+### Puzzle Grid with Integrated Hints86+87+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |88+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |89+| . | . | . | . | . | . | 1 | . | . | . | . | . | . |90+| . | . | . | . | . | . | 4 | . | . | 3 | 2 | . | . |91+| . | . | . | 6 | 3 | 2 | 1 | . | 5 | 3 | 3 | 5 | 6 |92+| . | . | . | 3 | 6 | 4 | 1 | 9 | 3 | 1 | 1 | 1 | 2 |93+| . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |94+| 3 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |95+| . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |96+| 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |97+| . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |98+| . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |99+| . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |100+| 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |101+| 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |102+| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |103+104+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.105+106+---107+108+## arctic-basalt - 5109+110+### Puzzle Grid with Integrated Hints111+112+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |113+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |114+| . | . | . | . | . | 1 | . | . | . | . | . | . | . | . |115+| . | . | . | . | . | 1 | . | 1 | . | . | . | . | . | . |116+| . | . | . | . | 6 | 1 | 5 | 1 | 8 | 2 | 4 | 2 | 3 | 4 |117+| . | . | . | . | 1 | 4 | 3 | 2 | 1 | 3 | 4 | 5 | 2 | 2 |118+| 3 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |119+| 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |120+| 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |121+| 1 | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |122+| . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |123+| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |124+| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |125+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |126+| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |127+| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |128+129+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.130+131+---132+133+## calm-cloud - 6134+135+### Puzzle Grid with Integrated Hints136+137+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |138+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |139+| . | . | . | 5 | 3 | 5 | . | . | 2 | 3 | 4 | . | 3 |140+| . | . | . | 1 | 1 | 1 | 5 | 1 | 2 | 3 | 3 | 4 | 2 |141+| . | . | . | 2 | 2 | 1 | 3 | 8 | 4 | 1 | 1 | 5 | 1 |142+| . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |143+| . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |144+| . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |145+| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |146+| 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |147+| 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |148+| 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |149+| . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |150+| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |151+| . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |152+153+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.154+155+---156+157+## scarlet-sage - 7158+159+### Puzzle Grid with Integrated Hints160+161+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |162+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |163+| . | . | . | . | . | . | . | . | . | 1 | . | . | . | 1 |164+| . | . | . | . | . | . | 2 | . | 5 | 1 | . | 1 | 1 | 1 |165+| . | . | . | . | 8 | 4 | 1 | 4 | 1 | 1 | . | 2 | 2 | 2 |166+| . | . | . | . | 1 | 2 | 1 | 3 | 2 | 3 | 8 | 3 | 1 | 1 |167+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |168+| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |169+| . | . | . | 8 | . | . | . | . | . | . | . | . | . | . |170+| 2 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |171+| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |172+| . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |173+| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |174+| 2 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |175+| . | . | . | 6 | . | . | . | . | . | . | . | . | . | . |176+| . | 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |177+178+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.179+180+---181+182+## wandering-viper - 8183+184+### Puzzle Grid with Integrated Hints185+186+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |187+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |188+| . | . | . | . | . | . | . | . | . | . | 2 | . | 1 | . |189+| . | . | . | . | . | 5 | . | 2 | 1 | . | 2 | . | 2 | 3 |190+| . | . | . | . | 4 | 1 | 5 | 4 | 1 | 2 | 1 | 1 | 1 | 2 |191+| . | . | . | . | 4 | 1 | 3 | 2 | 4 | 6 | 2 | 6 | 2 | 1 |192+| . | . | 1 | 4 | . | . | . | . | . | . | . | . | . | . |193+| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |194+| . | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |195+| . | 4 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |196+| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |197+| 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |198+| . | . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |199+| 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |200+| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |201+| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |202+203+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.204+205+---206+207+## twilight-owl - 9208+209+### Puzzle Grid with Integrated Hints210+211+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |212+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |213+| . | . | . | . | 3 | . | . | . | 3 | 2 | . | 4 | . | . |214+| . | . | . | . | 1 | 6 | 7 | . | 1 | 1 | . | 2 | 1 | 2 |215+| . | . | . | . | 1 | 1 | 1 | 6 | 2 | 1 | 5 | 2 | 6 | 2 |216+| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |217+| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |218+| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |219+| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |220+| . | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |221+| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |222+| . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |223+| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |224+| 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |225+| . | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |226+227+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.228+229+---230+231+## silver-bison - 10232+233+### Puzzle Grid with Integrated Hints234+235+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |236+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |237+| . | . | . | . | . | . | . | . | . | . | . | . | 1 |238+| . | . | . | . | 1 | 2 | . | . | . | 2 | . | . | 2 |239+| . | . | . | 5 | 3 | 1 | . | . | . | 1 | 2 | . | 1 |240+| . | . | . | 2 | 1 | 1 | 3 | 2 | 8 | 1 | 2 | 3 | 1 |241+| . | . | . | 1 | 1 | 1 | 6 | 6 | 1 | 2 | 3 | 6 | 1 |242+| . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |243+| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |244+| 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |245+| 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |246+| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |247+| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |248+| 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |249+| 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |250+| . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |251+| . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |252+253+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.254+255+---256+257+## pearl-river - 11258+259+### Puzzle Grid with Integrated Hints260+261+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |262+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |263+| . | . | . | . | 1 | . | 1 | 1 | . | . | 3 | . | . | . |264+| . | . | . | . | 4 | 6 | 3 | 6 | 1 | 4 | 3 | 5 | 5 | 1 |265+| . | . | . | . | 3 | 2 | 3 | 1 | 2 | 1 | 2 | 1 | 2 | 4 |266+| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |267+| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |268+| . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |269+| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |270+| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |271+| . | 4 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |272+| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |273+| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |274+| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |275+| . | . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |276+277+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.278+279+---280+281+## eager-cloud - 12282+283+### Puzzle Grid with Integrated Hints284+285+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |286+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |287+| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . |288+| . | . | . | . | 1 | 1 | . | . | 2 | 2 | . | . | . | . |289+| . | . | . | . | 6 | 1 | 5 | 3 | 2 | 1 | . | 2 | . | 2 |290+| . | . | . | . | 1 | 2 | 3 | 4 | 1 | 2 | 9 | 4 | 6 | 3 |291+| . | . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |292+| . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |293+| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |294+| . | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |295+| 1 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |296+| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |297+| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |298+| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |299+| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |300+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |301+302+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.303+304+---305+306+## sandy-quartz - 13307+308+### Puzzle Grid with Integrated Hints309+310+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |311+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |312+| . | . | . | . | . | 1 | 1 | . | . | . | . | . | . | . |313+| . | . | . | . | 1 | 1 | 3 | . | . | 3 | 1 | 1 | . | 1 |314+| . | . | . | . | 1 | 1 | 2 | 2 | 2 | 1 | 4 | 6 | 4 | 2 |315+| . | . | . | . | 6 | 1 | 1 | 4 | 3 | 1 | 2 | 1 | 3 | 2 |316+| 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |317+| . | . | 2 | 1 | . | . | . | . | . | . | . | . | . | . |318+| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |319+| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |320+| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |321+| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |322+| . | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |323+| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |324+| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |325+| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |326+327+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.328+329+---330+331+## cosmic-tide - 14332+333+### Puzzle Grid with Integrated Hints334+335+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |336+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |337+| . | . | . | . | . | . | . | . | . | . | . | . | . | 1 |338+| . | . | . | . | . | . | 1 | 1 | . | . | 1 | . | 1 | 1 |339+| . | . | . | . | 6 | . | 1 | 3 | . | 2 | 3 | 4 | 4 | 2 |340+| . | . | . | . | 3 | 9 | 3 | 4 | 8 | 1 | 2 | 4 | 3 | 1 |341+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |342+| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |343+| . | 2 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |344+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |345+| 2 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |346+| . | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |347+| 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |348+| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |349+| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |350+| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |351+352+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.353+354+---355+356+## copper-river - 15357+358+### Puzzle Grid with Integrated Hints359+360+| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |361+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |362+| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . | 3 |363+| . | . | . | . | . | 4 | 2 | 1 | 2 | 2 | . | 1 | 3 | 1 | 1 |364+| . | . | . | . | . | 1 | 3 | 3 | 1 | 1 | 4 | 3 | 1 | 4 | 1 |365+| . | . | . | . | . | 1 | 1 | 4 | 1 | 2 | 3 | 3 | 1 | 2 | 1 |366+| . | . | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |367+| . | . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |368+| 1 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |369+| . | 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |370+| . | . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |371+| . | . | 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |372+| . | . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |373+| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |374+| . | . | . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |375+| . | . | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |376+377+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.378+379+---380+381+## deep-badger - 16382+383+### Puzzle Grid with Integrated Hints384+385+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |386+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |387+| . | . | . | . | 1 | . | . | . | . | . | . | . | . |388+| . | . | . | 1 | 1 | . | . | . | . | . | . | . | . |389+| . | . | . | 1 | 2 | 1 | . | . | 4 | . | 2 | . | . |390+| . | . | . | 1 | 1 | 3 | 7 | 3 | 1 | 2 | 5 | 6 | 3 |391+| . | . | . | 2 | 1 | 1 | 1 | 5 | 2 | 4 | 1 | 1 | 2 |392+| 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |393+| . | . | 8 | . | . | . | . | . | . | . | . | . | . |394+| 2 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |395+| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |396+| . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |397+| . | . | 9 | . | . | . | . | . | . | . | . | . | . |398+| 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |399+| . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |400+| . | 1 | 2 | . | . | . | . | . | . | . | . | . | . |401+| . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |402+403+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.404+405+---406+407+## mighty-trout - 17408+409+### Puzzle Grid with Integrated Hints410+411+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |412+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |413+| . | . | . | . | . | . | . | . | . | . | 1 | . | . | 1 |414+| . | . | . | . | 1 | 5 | . | 2 | . | . | 1 | 2 | . | 1 |415+| . | . | . | . | 2 | 1 | 4 | 1 | 2 | 2 | 1 | 5 | 2 | 1 |416+| . | . | . | . | 2 | 1 | 3 | 1 | 4 | 4 | 2 | 1 | 3 | 1 |417+| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |418+| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |419+| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |420+| 3 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |421+| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |422+| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |423+| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |424+| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |425+| . | . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |426+| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |427+428+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.429+430+---431+432+## blazing-trout - 18433+434+### Puzzle Grid with Integrated Hints435+436+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |437+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |438+| . | . | . | . | . | . | . | . | . | . | 2 | . | . | . |439+| . | . | . | . | 1 | 1 | 4 | . | . | . | 1 | . | 1 | . |440+| . | . | . | . | 2 | 3 | 1 | 1 | 4 | 3 | 2 | 7 | 2 | 6 |441+| . | . | . | . | 5 | 3 | 2 | 7 | 2 | 6 | 1 | 2 | 4 | 3 |442+| 4 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |443+| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |444+| . | 4 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |445+| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |446+| . | . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |447+| 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |448+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |449+| 2 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |450+| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |451+| . | 1 | 1 | 6 | . | . | . | . | . | . | . | . | . | . |452+453+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.454+455+---456+457+## gilded-ivy - 19458+459+### Puzzle Grid with Integrated Hints460+461+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |462+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |463+| . | . | . | . | . | 3 | . | . | . | . | . | . | . | . |464+| . | . | . | . | . | 2 | 4 | 2 | . | . | . | . | . | 1 |465+| . | . | . | . | 2 | 1 | 1 | 4 | 4 | 6 | 4 | 1 | 6 | 4 |466+| . | . | . | . | 3 | 1 | 1 | 1 | 2 | 2 | 1 | 7 | 3 | 2 |467+| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |468+| . | . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |469+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |470+| . | . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |471+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |472+| 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |473+| . | . | 2 | 1 | . | . | . | . | . | . | . | . | . | . |474+| . | . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |475+| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |476+| . | 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |477+478+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.479+480+---481+482+## woven-lagoon - 20483+484+### Puzzle Grid with Integrated Hints485+486+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |487+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |488+| . | . | . | . | 3 | . | . | . | 1 | . | . | 1 | . | . |489+| . | . | . | . | 1 | . | 1 | . | 1 | . | . | 2 | . | 2 |490+| . | . | . | . | 1 | . | 3 | 2 | 1 | 6 | 2 | 1 | 3 | 2 |491+| . | . | . | . | 1 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 3 | 3 |492+| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |493+| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |494+| . | 3 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |495+| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |496+| 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |497+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |498+| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |499+| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |500+| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |501+| 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |502+503+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.504+505+---506+507+## fading-aurora - 21508+509+### Puzzle Grid with Integrated Hints510+511+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |512+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |513+| . | . | . | 1 | 1 | 1 | 5 | . | 3 | . | 1 | 2 | . |514+| . | . | . | 4 | 4 | 5 | 1 | 5 | 2 | 8 | 2 | 1 | 4 |515+| . | . | . | 1 | 2 | 2 | 1 | 1 | 1 | 1 | 3 | 1 | 1 |516+| 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |517+| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |518+| 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |519+| 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |520+| 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |521+| . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |522+| . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |523+| 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |524+| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |525+| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |526+527+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.528+529+---530+531+## crimson-owl - 22532+533+### Puzzle Grid with Integrated Hints534+535+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |536+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |537+| . | . | . | . | . | . | 1 | 1 | 2 | 2 | . | 2 | . | 3 |538+| . | . | . | . | 2 | 2 | 1 | 3 | 4 | 4 | 6 | 5 | 5 | 3 |539+| . | . | . | . | 3 | 1 | 5 | 3 | 1 | 1 | 1 | 1 | 4 | 1 |540+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |541+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |542+| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |543+| . | . | . | 8 | . | . | . | . | . | . | . | . | . | . |544+| . | . | . | 6 | . | . | . | . | . | . | . | . | . | . |545+| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |546+| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |547+| . | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . |548+| 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |549+| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |550+551+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.552+553+---554+555+## timber-falcon - 23556+557+### Puzzle Grid with Integrated Hints558+559+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |560+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |561+| . | . | . | . | . | . | 2 | . | . | . | . | 2 | . | . |562+| . | . | . | . | . | . | 3 | . | . | . | 2 | 2 | 2 | . |563+| . | . | . | . | . | 5 | 1 | 6 | 6 | . | 1 | 1 | 1 | 1 |564+| . | . | . | . | 9 | 1 | 1 | 2 | 2 | 6 | 4 | 1 | 2 | 5 |565+| . | . | 2 | 2 | . | . | . | . | . | . | . | . | . | . |566+| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |567+| . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |568+| . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |569+| . | . | 8 | 1 | . | . | . | . | . | . | . | . | . | . |570+| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |571+| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |572+| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |573+| . | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . |574+| . | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |575+576+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.577+578+---579+580+## blazing-acorn - 24581+582+### Puzzle Grid with Integrated Hints583+584+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |585+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |586+| . | . | . | . | 1 | . | . | 1 | . | . | . | . | . | . |587+| . | . | . | . | 1 | 1 | 3 | 2 | 3 | . | . | 1 | . | . |588+| . | . | . | . | 1 | 2 | 3 | 1 | 1 | 2 | 5 | 2 | 6 | 2 |589+| . | . | . | . | 2 | 2 | 1 | 2 | 2 | 3 | 2 | 4 | 2 | 4 |590+| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |591+| 2 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |592+| . | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |593+| . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |594+| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |595+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |596+| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |597+| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |598+| . | . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |599+| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |600+601+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.602+603+---604+605+## onyx-pond - 25606+607+### Puzzle Grid with Integrated Hints608+609+| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |610+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |611+| . | . | . | . | . | . | . | . | 2 | . | . | . | . | . | . |612+| . | . | . | . | . | 2 | 1 | . | 1 | . | 2 | . | 3 | 2 | . |613+| . | . | . | . | . | 3 | 2 | 5 | 1 | 1 | 1 | 7 | 1 | 1 | . |614+| . | . | . | . | . | 1 | 2 | 4 | 1 | 8 | 3 | 2 | 3 | 2 | 9 |615+| . | . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |616+| . | . | 1 | 2 | 5 | . | . | . | . | . | . | . | . | . | . |617+| 1 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |618+| . | . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |619+| . | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |620+| . | . | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |621+| . | . | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |622+| . | 3 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |623+| . | . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |624+| . | 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |625+626+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.627+628+---629+630+## desert-hollow - 26631+632+### Puzzle Grid with Integrated Hints633+634+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |635+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |636+| . | . | . | . | . | . | . | . | . | . | . | 3 | . | . |637+| . | . | . | . | . | . | 1 | . | . | . | . | 1 | . | 2 |638+| . | . | . | . | 7 | 2 | 5 | 3 | 1 | 4 | 7 | 1 | 7 | 3 |639+| . | . | . | . | 1 | 5 | 1 | 3 | 5 | 4 | 2 | 1 | 2 | 2 |640+| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |641+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |642+| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |643+| . | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |644+| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |645+| 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |646+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |647+| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |648+| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |649+| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |650+651+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.652+653+---654+655+## emerald-newt - 27656+657+### Puzzle Grid with Integrated Hints658+659+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |660+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |661+| . | . | . | . | . | . | . | 1 | . | . | . | . | . | . |662+| . | . | . | . | 3 | . | . | 1 | 3 | . | . | . | . | . |663+| . | . | . | . | 2 | 6 | 4 | 3 | 1 | 6 | 6 | 7 | 2 | 2 |664+| . | . | . | . | 1 | 3 | 3 | 1 | 2 | 2 | 2 | 1 | 3 | 6 |665+| . | . | 1 | 4 | . | . | . | . | . | . | . | . | . | . |666+| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |667+| . | . | 8 | 1 | . | . | . | . | . | . | . | . | . | . |668+| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |669+| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |670+| . | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |671+| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |672+| . | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |673+| . | 2 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |674+| 2 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |675+676+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.677+678+---679+680+## velvet-viper - 28681+682+### Puzzle Grid with Integrated Hints683+684+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |685+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |686+| . | . | . | . | . | . | . | 1 | 2 | 1 | . | . | . | . |687+| . | . | . | . | 1 | 2 | . | 4 | 1 | 3 | 1 | 2 | 4 | 1 |688+| . | . | . | . | 3 | 1 | 2 | 1 | 1 | 1 | 4 | 4 | 2 | 5 |689+| . | . | . | . | 3 | 2 | 6 | 1 | 3 | 1 | 1 | 1 | 1 | 1 |690+| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |691+| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |692+| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |693+| . | 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |694+| . | 4 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |695+| . | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |696+| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |697+| . | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |698+| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |699+| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |700+701+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.702+703+---704+705+## crystal-larch - 29706+707+### Puzzle Grid with Integrated Hints708+709+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |710+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |711+| . | . | . | . | . | . | . | 3 | . | . | 1 | . | 1 | 1 |712+| . | . | . | . | . | . | . | 1 | 2 | . | 1 | 1 | 1 | 1 |713+| . | . | . | . | 1 | . | . | 1 | 3 | . | 2 | 2 | 3 | 3 |714+| . | . | . | . | 6 | 7 | 6 | 1 | 3 | 8 | 1 | 2 | 1 | 1 |715+| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |716+| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |717+| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |718+| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |719+| . | . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |720+| . | . | 7 | 2 | . | . | . | . | . | . | . | . | . | . |721+| . | . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |722+| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |723+| . | . | 1 | 3 | . | . | . | . | . | . | . | . | . | . |724+| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |725+726+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.727+728+---729+730+## aqua-robin - 30731+732+### Puzzle Grid with Integrated Hints733+734+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |735+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |736+| . | . | . | . | . | . | . | . | . | . | 1 | . | . | . |737+| . | . | . | . | . | . | . | . | 5 | . | 1 | . | . | . |738+| . | . | . | . | 5 | 2 | 3 | 3 | 1 | 6 | 2 | 5 | 6 | 2 |739+| . | . | . | . | 4 | 7 | 5 | 2 | 1 | 1 | 3 | 3 | 3 | 3 |740+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |741+| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |742+| . | . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |743+| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |744+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |745+| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |746+| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |747+| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |748+| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |749+| . | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |750+751+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.752+753+---754+755+## starry-trout - 31756+757+### Puzzle Grid with Integrated Hints758+759+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |760+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |761+| . | . | . | . | . | 1 | . | . | . | . | . | 2 | . |762+| . | . | . | . | 2 | 1 | 4 | 2 | 1 | 1 | . | 1 | . |763+| . | . | . | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 4 | 2 | 7 |764+| . | . | . | 4 | 4 | 2 | 2 | 3 | 1 | 5 | 3 | 1 | 2 |765+| . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |766+| 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |767+| 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . |768+| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |769+| 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |770+| 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |771+| 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |772+| 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |773+| 5 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |774+| 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |775+776+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.
A pack-01/issue-01.pdf +0 -0

Binary file.

A pack-01/nonogram.md +332 -0
@@ -0,0 +1,332 @@
1+# PuzzleTea Export2+3+- Generated: 2026-02-21T21:15:15-07:004+- Version: v1.6.0-1-gd260f2e-dirty5+- Category: Nonogram6+- Mode Selection: mixed modes7+- Count: 128+- Seed: none9+10+## lunar-ridge - 111+12+### Puzzle Grid with Integrated Hints13+14+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |15+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |16+| . | . | . | . | . | . | . | . | . | . | . | . | 4 | 3 | 1 | . | . | . | . | . | . | 2 | . | . | . | . | . |17+| . | . | . | . | . | . | . | . | . | 3 | . | . | 1 | 2 | 2 | 3 | 6 | 1 | 6 | . | 1 | 1 | 6 | . | . | . | . |18+| . | . | . | . | . | . | . | 9 | . | 4 | 1 | . | 4 | 3 | 2 | 1 | 2 | 2 | 3 | 1 | 4 | 2 | 4 | 2 | . | 3 | 3 |19+| . | . | . | . | . | . | . | 1 | 1 | 2 | 1 | . | 1 | 3 | 5 | 1 | 1 | 7 | 4 | 2 | 1 | 1 | 1 | 1 | 1 | 5 | 5 |20+| . | . | . | . | . | . | . | 2 | 13 | 3 | 1 | 1 | 2 | 2 | 1 | 1 | 2 | 4 | 1 | 2 | 4 | 3 | 2 | 2 | 11 | 7 | 3 |21+| . | . | . | . | . | . | . | 2 | 3 | 1 | 6 | 16 | 2 | 1 | 3 | 5 | 1 | 1 | 1 | 11 | 3 | 2 | 2 | 7 | 1 | 1 | 3 |22+| . | . | . | 2 | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |23+| . | 1 | 2 | 3 | 1 | 1 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |24+| . | . | 3 | 3 | 4 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |25+| . | . | 2 | 2 | 7 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |26+| . | 5 | 4 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |27+| . | . | 3 | 3 | 6 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |28+| . | . | . | 5 | 1 | 2 | 8 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |29+| . | . | . | 3 | 4 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |30+| . | 2 | 4 | 3 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |31+| . | . | . | . | 13 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |32+| . | . | . | 5 | 1 | 1 | 8 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |33+| . | . | 1 | 2 | 8 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |34+| . | . | . | 2 | 5 | 4 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |35+| . | . | . | 4 | 3 | 3 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |36+| . | . | 2 | 1 | 4 | 1 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |37+| . | . | . | 1 | 1 | 7 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |38+| . | . | 2 | 3 | 1 | 7 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |39+| 1 | 1 | 2 | 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |40+| . | . | 2 | 4 | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |41+| 1 | 1 | 1 | 4 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |42+43+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.44+45+---46+47+## coral-acorn - 248+49+### Puzzle Grid with Integrated Hints50+51+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |52+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |53+| . | . | . | . | 1 | . | . | . | . | . | . | . | . | . |54+| . | . | . | . | 1 | . | . | . | . | . | . | 2 | 2 | . |55+| . | . | . | . | 1 | . | . | . | . | . | . | 2 | 1 | 1 |56+| . | . | . | . | 1 | 1 | 3 | 2 | . | . | 3 | 1 | 2 | 2 |57+| . | . | . | . | 1 | 1 | 1 | 3 | 6 | 6 | 1 | 1 | 1 | 2 |58+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |59+| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |60+| . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |61+| 1 | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |62+| . | . | . | 4 | . | . | . | . | . | . | . | . | . | . |63+| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |64+| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |65+| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |66+| . | . | . | 2 | . | . | . | . | . | . | . | . | . | . |67+| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |68+69+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.70+71+---72+73+## dawn-seal - 374+75+### Puzzle Grid with Integrated Hints76+77+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |78+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |79+| . | . | . | . | . | . | . | . | . | . | . | . | 1 | . |80+| . | . | . | . | . | . | 2 | . | 1 | 5 | . | . | 1 | . |81+| . | . | . | . | 1 | 2 | 1 | . | 1 | 1 | 6 | 1 | 1 | 3 |82+| . | . | . | . | 3 | 2 | 2 | 2 | 1 | 1 | 1 | 4 | 1 | 4 |83+| . | . | 1 | 2 | . | . | . | . | . | . | . | . | . | . |84+| . | . | 1 | 2 | . | . | . | . | . | . | . | . | . | . |85+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |86+| . | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |87+| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |88+| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |89+| 1 | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |90+| . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |91+| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |92+| . | . | 2 | 2 | . | . | . | . | . | . | . | . | . | . |93+94+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.95+96+---97+98+## sage-owl - 499+100+### Puzzle Grid with Integrated Hints101+102+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 |103+| --- | --- | --- | --- | --- | --- | --- | --- |104+| . | . | . | 1 | . | 1 | . | 1 |105+| . | . | . | 1 | 3 | 2 | 2 | 3 |106+| . | 1 | 2 | . | . | . | . | . |107+| . | . | 4 | . | . | . | . | . |108+| . | 1 | 1 | . | . | . | . | . |109+| 1 | 1 | 1 | . | . | . | . | . |110+| . | 1 | 1 | . | . | . | . | . |111+112+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.113+114+---115+116+## cedar-peak - 5117+118+### Puzzle Grid with Integrated Hints119+120+| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 |121+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |122+| . | . | . | . | . | . | . | . | . | 1 | . | . | . | . | . | . | . | . | . | . |123+| . | . | . | . | . | . | . | . | . | 1 | . | 2 | . | . | . | . | . | . | . | . |124+| . | . | . | . | . | 1 | . | . | . | 1 | 1 | 1 | . | . | . | . | . | . | . | . |125+| . | . | . | . | . | 2 | 4 | 1 | . | 1 | 1 | 1 | 2 | 2 | 4 | . | . | . | . | . |126+| . | . | . | . | . | 1 | 1 | 4 | 2 | 1 | 1 | 1 | 1 | 2 | 3 | . | 2 | 1 | 1 | 2 |127+| . | . | . | . | . | 1 | 2 | 2 | 1 | 3 | 2 | 1 | 6 | 2 | 3 | 5 | 5 | 5 | 1 | 3 |128+| . | . | . | . | . | 4 | 2 | 1 | 8 | 1 | 1 | 2 | 1 | 1 | 1 | 4 | 2 | 1 | 4 | 1 |129+| . | 3 | 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |130+| 1 | 2 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |131+| . | . | 2 | 2 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |132+| . | 4 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |133+| . | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |134+| . | 1 | 2 | 5 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |135+| . | . | 1 | 2 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |136+| 1 | 1 | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |137+| . | 1 | 4 | 1 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |138+| . | 3 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |139+| . | 1 | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |140+| . | . | 1 | 6 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |141+| . | . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |142+| . | 2 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |143+| . | 2 | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |144+145+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.146+147+---148+149+## radiant-aurora - 6150+151+### Puzzle Grid with Integrated Hints152+153+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |154+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |155+| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . | . | . | . | 1 | . | 1 | . | . | . | . | . | . | . |156+| . | . | . | . | . | . | . | 2 | 2 | . | 2 | . | 5 | . | . | . | 3 | 2 | 1 | 1 | 3 | 1 | 1 | 1 | . | . | . |157+| . | . | . | . | . | . | . | 2 | 2 | . | 1 | 2 | 1 | 1 | 1 | . | 2 | 4 | 3 | 1 | 1 | 3 | 2 | 2 | . | 2 | . |158+| . | . | . | . | . | . | . | 2 | 1 | 3 | 3 | 1 | 3 | 1 | 3 | 3 | 1 | 1 | 2 | 3 | 1 | 1 | 1 | 1 | . | 4 | 2 |159+| . | . | . | . | . | . | . | 4 | 2 | 3 | 1 | 5 | 1 | 1 | 2 | 2 | 2 | 1 | 1 | 3 | 2 | 1 | 1 | 1 | 2 | 4 | 3 |160+| . | . | . | . | . | . | . | 3 | 1 | 4 | 2 | 1 | 1 | 6 | 3 | 8 | 2 | 1 | 2 | 1 | 1 | 2 | 8 | 1 | 3 | 2 | 2 |161+| . | . | . | . | . | . | . | 1 | 1 | 3 | 2 | 1 | 1 | 2 | 1 | 2 | 4 | 1 | 1 | 1 | 1 | 4 | 1 | 1 | 8 | 2 | 4 |162+| . | . | 2 | 1 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |163+| . | . | 1 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |164+| . | . | . | . | 6 | 8 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |165+| . | 3 | 1 | 3 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |166+| 1 | 2 | 1 | 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |167+| . | 1 | 3 | 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |168+| . | . | . | 6 | 5 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |169+| . | 1 | 2 | 2 | 3 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |170+| . | . | 4 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |171+| . | 1 | 1 | 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |172+| . | 2 | 2 | 2 | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |173+| . | . | . | 1 | 9 | 5 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |174+| . | . | . | 3 | 5 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |175+| . | . | . | 3 | 1 | 5 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |176+| . | . | 2 | 5 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |177+| . | . | 2 | 2 | 3 | 6 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |178+| . | 1 | 1 | 5 | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |179+| 1 | 2 | 2 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |180+| . | . | . | 8 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |181+| . | . | 2 | 1 | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |182+183+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.184+185+---186+187+## ivory-shell - 7188+189+### Puzzle Grid with Integrated Hints190+191+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |192+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |193+| . | . | . | . | 2 | 4 | 1 | 1 | . | . | . | 1 | . | 2 |194+| . | . | . | . | 1 | 2 | 1 | 4 | 5 | 3 | 1 | 1 | 5 | 1 |195+| . | . | . | . | 2 | 1 | 2 | 2 | 1 | 1 | 6 | 1 | 1 | 2 |196+| 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |197+| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |198+| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |199+| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |200+| . | . | 4 | 1 | . | . | . | . | . | . | . | . | . | . |201+| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |202+| . | . | 2 | 2 | . | . | . | . | . | . | . | . | . | . |203+| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |204+| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |205+| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |206+207+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.208+209+---210+211+## pearl-reef - 8212+213+### Puzzle Grid with Integrated Hints214+215+| R1 | R2 | R3 | R4 | R5 | R6 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |216+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |217+| . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | 2 | . | . | . |218+| . | . | . | . | . | . | . | . | . | 3 | . | . | 1 | . | . | . | . | . | . | 1 | . | . | 1 | . | . | . |219+| . | . | . | . | . | . | . | 4 | 1 | 1 | . | . | 3 | . | . | . | 3 | 1 | 3 | 2 | . | 2 | 3 | 1 | . | . |220+| . | . | . | . | . | . | 10 | 2 | 4 | 2 | 5 | . | 1 | 7 | 4 | . | 2 | 2 | 1 | 4 | 7 | 1 | 1 | 1 | 2 | 1 |221+| . | . | . | . | . | . | 2 | 2 | 3 | 2 | 7 | 11 | 2 | 2 | 1 | . | 1 | 2 | 4 | 3 | 4 | 6 | 1 | 1 | 5 | 5 |222+| . | . | . | . | . | . | 1 | 1 | 4 | 1 | 4 | 2 | 2 | 1 | 2 | 11 | 9 | 3 | 2 | 1 | 2 | 2 | 2 | 3 | 1 | 6 |223+| . | . | . | . | . | . | 1 | 1 | 2 | 2 | 1 | 1 | 6 | 7 | 3 | 7 | 1 | 3 | 2 | 1 | 4 | 3 | 2 | 2 | 9 | 3 |224+| 3 | 1 | 5 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |225+| . | 2 | 2 | 6 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |226+| . | . | 2 | 7 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |227+| . | . | 2 | 7 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |228+| . | 1 | 6 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |229+| . | . | 4 | 1 | 4 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |230+| . | 3 | 4 | 1 | 6 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |231+| . | 1 | 4 | 4 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |232+| . | . | 1 | 6 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |233+| . | . | . | 1 | 6 | 10 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |234+| . | . | . | 5 | 7 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |235+| . | 2 | 4 | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |236+| . | 1 | 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |237+| 2 | 1 | 1 | 3 | 3 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |238+| . | . | . | . | 15 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |239+| . | 1 | 1 | 1 | 8 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |240+| . | 3 | 5 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |241+| . | 1 | 1 | 2 | 7 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |242+| . | 3 | 3 | 1 | 2 | 6 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |243+| 1 | 3 | 2 | 3 | 4 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |244+245+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.246+247+---248+249+## copper-heron - 9250+251+### Puzzle Grid with Integrated Hints252+253+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |254+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |255+| . | . | . | . | . | 1 | . | . | . | . | . | . | . | . |256+| . | . | . | . | . | 1 | 2 | . | . | . | . | . | . | . |257+| . | . | . | . | . | 1 | 3 | . | 1 | 2 | 1 | 3 | 3 | . |258+| . | . | . | . | 1 | 1 | 1 | 2 | 2 | 1 | 2 | 1 | 1 | . |259+| . | . | . | . | 5 | 1 | 1 | 1 | 2 | 1 | 5 | 2 | 1 | 1 |260+| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |261+| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |262+| . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |263+| . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |264+| . | . | 4 | 1 | . | . | . | . | . | . | . | . | . | . |265+| . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |266+| . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |267+| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |268+| . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |269+| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |270+271+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.272+273+---274+275+## chilly-ivy - 10276+277+### Puzzle Grid with Integrated Hints278+279+| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |280+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |281+| . | . | . | . | . | . | . | . | . | . | . | . | 1 | . | 1 |282+| . | . | . | . | . | . | 1 | . | . | 3 | 1 | 1 | 2 | . | 1 |283+| . | . | . | . | . | . | 1 | 5 | 2 | 3 | 1 | 2 | 1 | 2 | 1 |284+| . | . | . | . | . | 5 | 1 | 3 | 3 | 1 | 4 | 3 | 1 | 2 | 1 |285+| . | . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |286+| . | . | . | . | 3 | . | . | . | . | . | . | . | . | . | . |287+| 1 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |288+| . | . | 4 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |289+| . | . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |290+| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |291+| . | 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |292+| . | . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |293+| . | . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |294+| . | . | . | 1 | 2 | . | . | . | . | . | . | . | . | . | . |295+296+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.297+298+---299+300+## aqua-lava - 11301+302+### Puzzle Grid with Integrated Hints303+304+| R1 | R2 | C1 | C2 | C3 | C4 | C5 |305+| --- | --- | --- | --- | --- | --- | --- |306+| . | . | 1 | . | . | . | . |307+| . | . | 1 | 3 | 3 | 1 | 1 |308+| . | 1 | . | . | . | . | . |309+| . | 1 | . | . | . | . | . |310+| 2 | 1 | . | . | . | . | . |311+| . | 2 | . | . | . | . | . |312+| 2 | 1 | . | . | . | . | . |313+314+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.315+316+---317+318+## polar-sparrow - 12319+320+### Puzzle Grid with Integrated Hints321+322+| R1 | R2 | C1 | C2 | C3 | C4 | C5 |323+| --- | --- | --- | --- | --- | --- | --- |324+| . | . | 1 | . | . | . | . |325+| . | . | 3 | 3 | 2 | 4 | 2 |326+| 2 | 1 | . | . | . | . | . |327+| . | 4 | . | . | . | . | . |328+| . | 4 | . | . | . | . | . |329+| 1 | 1 | . | . | . | . | . |330+| 1 | 1 | . | . | . | . | . |331+332+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.
A pack-01/shikaku.md +368 -0
@@ -0,0 +1,368 @@
1+# PuzzleTea Export2+3+- Generated: 2026-02-21T21:14:57-07:004+- Version: v1.6.0-1-gd260f2e-dirty5+- Category: Nonogram6+- Mode Selection: mixed modes7+- Count: 128+- Seed: none9+10+## meadow-bramble - 111+12+### Puzzle Grid with Integrated Hints13+14+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |15+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |16+| . | . | . | . | . | . | . | . | . | 1 | 1 | . | . | 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | 2 | . |17+| . | . | . | . | . | . | . | 1 | 2 | 3 | 1 | . | . | 1 | 2 | 3 | 2 | 1 | . | . | 1 | . | . | 3 | 1 | 5 | 1 |18+| . | . | . | . | . | . | . | 5 | 5 | 4 | 1 | 1 | 1 | 2 | 1 | 2 | 1 | 4 | 1 | 1 | 3 | . | . | 1 | 2 | 1 | 4 |19+| . | . | . | . | . | . | . | 2 | 1 | 1 | 4 | 2 | 2 | 1 | 1 | 3 | 2 | 4 | 5 | 1 | 1 | 4 | 2 | 1 | 2 | 3 | 3 |20+| . | . | . | . | . | . | . | 3 | 2 | 1 | 2 | 1 | 2 | 3 | 4 | 3 | 2 | 2 | 5 | 3 | 4 | 1 | 1 | 6 | 2 | 1 | 2 |21+| . | . | . | . | . | . | . | 1 | 2 | 2 | 1 | 8 | 7 | 1 | 2 | 1 | 3 | 1 | 3 | 7 | 2 | 4 | 10 | 1 | 1 | 1 | 2 |22+| . | . | . | 3 | 6 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |23+| . | . | . | 1 | 3 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |24+| . | . | . | . | 5 | 3 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |25+| . | . | . | 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |26+| . | . | . | . | 4 | 8 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |27+| . | . | . | . | 2 | 9 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |28+| . | . | 2 | 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |29+| . | . | . | 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |30+| . | . | 3 | 3 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |31+| 2 | 2 | 1 | 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |32+| . | . | 2 | 1 | 4 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |33+| . | . | 1 | 7 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |34+| . | . | 6 | 1 | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |35+| . | . | . | 1 | 6 | 6 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |36+| . | . | . | . | 7 | 5 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |37+| . | . | 2 | 6 | 1 | 2 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |38+| . | . | 1 | 2 | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |39+| . | . | 1 | 3 | 2 | 1 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |40+| . | 2 | 4 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |41+| 3 | 1 | 2 | 1 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |42+43+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.44+45+---46+47+## bronze-heron - 248+49+### Puzzle Grid with Integrated Hints50+51+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |52+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |53+| . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | 2 | . | . | . | . | . | . | . | . |54+| . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | 1 | 2 | . | . | . | 2 | 2 | . | . |55+| . | . | . | . | . | . | . | . | . | . | 1 | . | . | 2 | 3 | 1 | . | 1 | 3 | 2 | . | 7 | . | 2 | 4 | 4 | 2 |56+| . | . | . | . | . | . | . | 1 | . | 1 | 1 | 9 | 3 | 4 | 2 | 5 | . | 3 | 1 | 1 | . | 3 | 3 | 1 | 1 | 1 | 8 |57+| . | . | . | . | . | . | . | 3 | 9 | 1 | 6 | 1 | 3 | 7 | 2 | 1 | 7 | 2 | 1 | 1 | 6 | 3 | 3 | 2 | 3 | 1 | 2 |58+| . | . | . | . | . | . | . | 1 | 3 | 1 | 3 | 3 | 1 | 1 | 6 | 6 | 2 | 2 | 1 | 1 | 8 | 2 | 1 | 2 | 1 | 4 | 1 |59+| . | . | . | . | . | . | . | 6 | 1 | 9 | 4 | 3 | 8 | 1 | 1 | 2 | 8 | 4 | 1 | 4 | 1 | 1 | 7 | 2 | 1 | 3 | 1 |60+| . | . | . | 2 | 4 | 8 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |61+| . | . | . | 2 | 3 | 1 | 9 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |62+| . | 1 | 3 | 1 | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |63+| . | . | 1 | 1 | 5 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |64+| . | . | . | 12 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |65+| . | . | 2 | 4 | 3 | 6 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |66+| . | . | . | . | 10 | 2 | 6 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |67+| . | . | 1 | 2 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |68+| 2 | 2 | 1 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |69+| . | . | 1 | 5 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |70+| . | 1 | 2 | 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |71+| . | . | 2 | 2 | 1 | 4 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |72+| . | . | . | . | 9 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |73+| . | . | . | . | 10 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |74+| . | . | . | 3 | 8 | 4 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |75+| . | 1 | 2 | 5 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |76+| . | . | . | 1 | 4 | 9 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |77+| . | . | 1 | 6 | 2 | 4 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |78+| . | . | . | 6 | 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |79+| . | . | 1 | 1 | 5 | 6 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |80+81+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.82+83+---84+85+## cherry-hollow - 386+87+### Puzzle Grid with Integrated Hints88+89+| R1 | R2 | R3 | R4 | R5 | R6 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 |90+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |91+| . | . | . | . | . | . | . | . | . | 1 | . | . | . | . | . | . | . | . | . | . | . |92+| . | . | . | . | . | . | . | 1 | 1 | 1 | . | . | . | . | 2 | . | . | . | . | . | 1 |93+| . | . | . | . | . | . | . | 1 | 3 | 1 | 3 | 1 | 1 | 1 | 1 | 3 | . | . | 1 | 3 | 1 |94+| . | . | . | . | . | . | . | 4 | 1 | 1 | 2 | 3 | 2 | 2 | 2 | 1 | 4 | . | 2 | 1 | 1 |95+| . | . | . | . | . | . | 2 | 1 | 2 | 5 | 1 | 2 | 3 | 2 | 3 | 3 | 3 | 4 | 3 | 1 | 1 |96+| . | . | . | . | . | . | 7 | 1 | 3 | 1 | 3 | 1 | 1 | 4 | 1 | 2 | 2 | 2 | 1 | 2 | 2 |97+| . | . | . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |98+| . | . | . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |99+| . | . | 3 | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |100+| . | 1 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |101+| . | . | . | 5 | 4 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |102+| . | . | . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |103+| . | . | . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |104+| . | . | . | 2 | 5 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |105+| . | . | . | 2 | 1 | 8 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |106+| . | . | 5 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |107+| . | . | 4 | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |108+| . | . | 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |109+| . | . | 5 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |110+| 1 | 1 | 1 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |111+| . | . | . | 4 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |112+113+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.114+115+---116+117+## autumn-lynx - 4118+119+### Puzzle Grid with Integrated Hints120+121+| R1 | R2 | C1 | C2 | C3 | C4 | C5 |122+| --- | --- | --- | --- | --- | --- | --- |123+| . | . | . | 2 | 1 | . | 1 |124+| . | . | 3 | 1 | 3 | 2 | 1 |125+| 2 | 1 | . | . | . | . | . |126+| . | 2 | . | . | . | . | . |127+| 1 | 2 | . | . | . | . | . |128+| 1 | 3 | . | . | . | . | . |129+| . | 2 | . | . | . | . | . |130+131+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.132+133+---134+135+## dawn-tide - 5136+137+### Puzzle Grid with Integrated Hints138+139+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |140+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |141+| . | . | . | . | . | . | . | . | . | . | . | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |142+| . | . | . | . | . | . | . | . | . | . | . | 2 | . | . | . | . | . | . | . | . | . | . | . | . | 1 | . | . | . |143+| . | . | . | . | . | . | . | . | . | 5 | . | 1 | . | . | . | 1 | . | . | 1 | . | . | . | 2 | . | 1 | . | . | . |144+| . | . | . | . | . | . | . | . | . | 1 | 2 | 3 | . | . | 2 | 4 | 2 | . | 3 | 1 | . | . | 1 | . | 1 | 4 | . | 8 |145+| . | . | . | . | . | . | . | . | . | 2 | 3 | 1 | 2 | 1 | 2 | 1 | 3 | . | 1 | 1 | . | . | 2 | 1 | 4 | 5 | 2 | 1 |146+| . | . | . | . | . | . | . | . | 4 | 1 | 2 | 1 | 13 | 1 | 2 | 2 | 1 | 9 | 2 | 2 | 7 | 12 | 3 | 10 | 1 | 1 | 10 | 1 |147+| . | . | . | . | . | . | . | . | 7 | 4 | 2 | 1 | 1 | 7 | 6 | 2 | 8 | 2 | 1 | 2 | 8 | 4 | 2 | 4 | 1 | 2 | 2 | 2 |148+| . | . | . | . | . | . | . | . | 4 | 1 | 7 | 1 | 1 | 3 | 1 | 1 | 1 | 2 | 3 | 7 | 1 | 1 | 1 | 2 | 1 | 4 | 1 | 2 |149+| . | . | . | . | 4 | 2 | 1 | 8 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |150+| . | . | . | . | 7 | 1 | 4 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |151+| . | 2 | 1 | 1 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |152+| . | . | . | 5 | 2 | 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |153+| . | . | . | . | 11 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |154+| . | . | . | 1 | 1 | 4 | 5 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |155+| . | . | . | 2 | 3 | 1 | 1 | 8 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |156+| 1 | 1 | 3 | 2 | 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |157+| . | . | . | 8 | 1 | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |158+| . | . | . | . | 2 | 3 | 3 | 8 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |159+| . | . | . | 1 | 4 | 2 | 6 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |160+| . | . | . | . | . | 3 | 3 | 12 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |161+| . | . | 1 | 4 | 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |162+| . | . | . | 1 | 1 | 3 | 5 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |163+| . | . | . | . | . | . | 5 | 14 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |164+| . | . | 3 | 1 | 1 | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |165+| . | . | 4 | 4 | 2 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |166+| . | . | . | 3 | 2 | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |167+| . | . | 2 | 2 | 4 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |168+| . | . | 2 | 1 | 2 | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |169+170+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.171+172+---173+174+## diamond-peak - 6175+176+### Puzzle Grid with Integrated Hints177+178+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |179+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |180+| . | . | . | . | . | . | . | 2 | 1 | . | . | . | . | . | . | . | . | 3 | 1 | . | . | 2 | 1 | . | . | . | . |181+| . | . | . | . | . | . | . | 2 | 1 | 7 | . | 1 | 3 | . | . | 1 | 5 | 2 | 2 | 2 | 5 | 6 | 3 | . | . | . | . |182+| . | . | . | . | . | . | . | 5 | 1 | 3 | . | 2 | 7 | 9 | . | 1 | 1 | 5 | 1 | 2 | 4 | 1 | 4 | 2 | 4 | 5 | . |183+| . | . | . | . | . | . | . | 3 | 8 | 4 | 4 | 12 | 5 | 1 | 12 | 12 | 5 | 1 | 2 | 3 | 4 | 1 | 5 | 6 | 1 | 9 | 11 |184+| . | . | . | . | . | . | . | 2 | 2 | 1 | 10 | 1 | 2 | 4 | 3 | 2 | 2 | 1 | 4 | 5 | 1 | 5 | 1 | 9 | 10 | 1 | 4 |185+| . | . | . | 3 | 1 | 9 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |186+| 1 | 4 | 1 | 2 | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |187+| . | . | 2 | 6 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |188+| . | 1 | 3 | 2 | 1 | 5 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |189+| . | . | . | . | . | 8 | 11 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |190+| . | 1 | 4 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |191+| . | . | 2 | 6 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |192+| . | . | 1 | 5 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |193+| . | . | . | . | 3 | 8 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |194+| . | . | . | 11 | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |195+| . | . | . | . | . | 16 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |196+| . | . | . | 2 | 2 | 6 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |197+| . | . | . | . | 6 | 3 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |198+| . | . | . | . | 6 | 1 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |199+| . | . | 6 | 1 | 2 | 1 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |200+| . | . | . | . | . | 10 | 9 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |201+| . | . | . | . | . | 7 | 9 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |202+| . | . | . | 2 | 2 | 2 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |203+| 2 | 1 | 2 | 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |204+| . | . | 3 | 2 | 1 | 1 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |205+206+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.207+208+---209+210+## bold-fern - 7211+212+### Puzzle Grid with Integrated Hints213+214+| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |215+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |216+| . | . | . | . | 2 | . | . | . | . | 1 | . | . | 1 | . |217+| . | . | . | . | 1 | . | . | 1 | 2 | 1 | 1 | 1 | 1 | 2 |218+| . | . | . | . | 2 | . | 3 | 1 | 1 | 1 | 1 | 3 | 1 | 1 |219+| . | . | . | . | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 1 | 2 | 1 |220+| . | . | . | 1 | . | . | . | . | . | . | . | . | . | . |221+| . | 1 | 1 | 6 | . | . | . | . | . | . | . | . | . | . |222+| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |223+| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |224+| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |225+| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |226+| . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |227+| . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |228+| . | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |229+| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |230+231+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.232+233+---234+235+## cedar-ridge - 8236+237+### Puzzle Grid with Integrated Hints238+239+| R1 | R2 | C1 | C2 | C3 | C4 | C5 |240+| --- | --- | --- | --- | --- | --- | --- |241+| . | . | 1 | . | . | 1 | 1 |242+| . | . | 1 | 1 | 1 | 1 | 3 |243+| . | 1 | . | . | . | . | . |244+| 2 | 1 | . | . | . | . | . |245+| . | 1 | . | . | . | . | . |246+| 1 | 1 | . | . | . | . | . |247+| 1 | 2 | . | . | . | . | . |248+249+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.250+251+---252+253+## prairie-trout - 9254+255+### Puzzle Grid with Integrated Hints256+257+| R1 | R2 | C1 | C2 | C3 | C4 | C5 |258+| --- | --- | --- | --- | --- | --- | --- |259+| . | . | . | . | . | . | 1 |260+| . | . | 1 | 1 | 2 | 2 | 3 |261+| 1 | 1 | . | . | . | . | . |262+| . | 3 | . | . | . | . | . |263+| . | 2 | . | . | . | . | . |264+| . | 2 | . | . | . | . | . |265+| . | 1 | . | . | . | . | . |266+267+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.268+269+---270+271+## celestial-tide - 10272+273+### Puzzle Grid with Integrated Hints274+275+| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |276+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |277+| . | . | . | . | . | 1 | . | . | . | . | . | . | . |278+| . | . | . | 1 | . | 2 | 4 | 1 | 1 | 1 | 2 | . | . |279+| . | . | . | 1 | 2 | 1 | 1 | 3 | 2 | 3 | 4 | 2 | 5 |280+| . | . | . | 1 | 6 | 2 | 2 | 2 | 5 | 2 | 1 | 6 | 4 |281+| 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |282+| . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |283+| 2 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |284+| . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |285+| 3 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |286+| 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |287+| 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |288+| 2 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |289+| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |290+| . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |291+292+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.293+294+---295+296+## lunar-juniper - 11297+298+### Puzzle Grid with Integrated Hints299+300+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |301+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |302+| . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | 1 | . | . | 4 | 2 | . |303+| . | . | . | . | . | . | . | 3 | . | . | 1 | . | . | . | . | . | . | . | . | . | . | 1 | . | 1 | 2 | 1 | . |304+| . | . | . | . | . | . | . | 1 | . | . | 1 | . | 1 | 3 | 3 | 2 | . | . | 3 | 1 | 3 | 3 | . | 1 | 2 | 1 | 1 |305+| . | . | . | . | . | . | . | 3 | . | 1 | 4 | . | 8 | 3 | 3 | 3 | 1 | 3 | 3 | 3 | 1 | 1 | 5 | 5 | 1 | 1 | 2 |306+| . | . | . | . | . | . | . | 3 | 2 | 2 | 1 | 7 | 1 | 1 | 2 | 2 | 9 | 1 | 1 | 3 | 2 | 2 | 1 | 3 | 1 | 1 | 1 |307+| . | . | . | . | . | . | . | 1 | 3 | 2 | 1 | 2 | 1 | 2 | 6 | 1 | 1 | 1 | 1 | 3 | 6 | 1 | 1 | 3 | 2 | 1 | 3 |308+| . | . | . | . | . | . | . | 1 | 1 | 4 | 5 | 3 | 2 | 4 | 1 | 1 | 3 | 6 | 1 | 1 | 1 | 3 | 3 | 1 | 2 | 1 | 2 |309+| . | 2 | 1 | 1 | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |310+| . | . | . | 3 | 2 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |311+| . | . | 1 | 6 | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |312+| . | . | 7 | 2 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |313+| . | . | . | 3 | 10 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |314+| . | . | . | 3 | 6 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |315+| . | . | . | 1 | 8 | 1 | 5 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |316+| . | 3 | 2 | 2 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |317+| . | 1 | 1 | 3 | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |318+| 2 | 2 | 3 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |319+| . | 1 | 2 | 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |320+| . | . | . | 1 | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |321+| . | . | . | 2 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |322+| . | 1 | 1 | 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |323+| 1 | 1 | 1 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |324+| . | 1 | 3 | 5 | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |325+| . | . | 6 | 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |326+| . | . | . | 4 | 6 | 4 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |327+| . | . | . | 1 | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |328+| . | 1 | 1 | 1 | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |329+330+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.331+332+---333+334+## rustic-timber - 12335+336+### Puzzle Grid with Integrated Hints337+338+| R1 | R2 | R3 | R4 | R5 | R6 | R7 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | C15 | C16 | C17 | C18 | C19 | C20 |339+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |340+| . | . | . | . | . | . | . | . | . | . | 2 | . | . | . | 4 | . | . | . | . | . | . | . | . | . | . | . | . |341+| . | . | . | . | . | . | . | . | . | 1 | 2 | . | . | . | 2 | . | . | . | 2 | . | . | 2 | 1 | . | . | 2 | 1 |342+| . | . | . | . | . | . | . | . | 1 | 2 | 1 | 2 | . | 6 | 2 | . | 1 | 3 | 2 | 1 | . | 1 | 5 | 1 | . | 1 | 1 |343+| . | . | . | . | . | . | . | . | 1 | 4 | 1 | 1 | 1 | 1 | 1 | 5 | 1 | 2 | 5 | 1 | 4 | 4 | 1 | 1 | 6 | 3 | 1 |344+| . | . | . | . | . | . | . | 6 | 5 | 1 | 2 | 6 | 3 | 1 | 1 | 2 | 6 | 4 | 1 | 4 | 7 | 1 | 1 | 1 | 3 | 1 | 7 |345+| . | . | . | . | . | . | . | 2 | 1 | 1 | 3 | 1 | 1 | 2 | 1 | 1 | 1 | 2 | 1 | 1 | 1 | 1 | 2 | 4 | 3 | 3 | 1 |346+| . | . | . | . | . | . | . | 2 | 2 | 1 | 1 | 1 | 2 | 1 | 1 | 3 | 1 | 3 | 2 | 3 | 1 | 1 | 2 | 3 | 1 | 3 | 3 |347+| . | . | . | 5 | 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |348+| . | . | 2 | 3 | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |349+| . | 2 | 5 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |350+| . | . | 1 | 3 | 3 | 5 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |351+| . | 2 | 1 | 4 | 2 | 5 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |352+| . | . | 1 | 2 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |353+| . | 2 | 1 | 1 | 1 | 1 | 7 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |354+| . | . | 2 | 1 | 4 | 3 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |355+| . | . | . | . | . | 5 | 12 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |356+| . | . | 2 | 2 | 1 | 6 | 4 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |357+| . | . | 4 | 1 | 3 | 5 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |358+| . | . | 3 | 1 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |359+| 1 | 1 | 1 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |360+| . | . | . | . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |361+| 1 | 1 | 3 | 1 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |362+| . | . | 1 | 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |363+| . | . | . | 1 | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |364+| 3 | 1 | 1 | 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |365+| . | . | 2 | 1 | 7 | 1 | 2 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |366+| . | . | 5 | 1 | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . | . |367+368+Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.
A pack-01/takuzu.md +252 -0
@@ -0,0 +1,252 @@
1+# PuzzleTea Export2+3+- Generated: 2026-02-21T21:16:33-07:004+- Version: v1.6.0-1-gd260f2e-dirty5+- Category: Takuzu6+- Mode Selection: mixed modes7+- Count: 128+- Seed: none9+10+## scarlet-lichen - 111+12+### Given Grid13+14+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |15+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |16+| 1 | . | . | 0 | . | . | 0 | . | 0 | 0 | . | . | . | . | . |17+| 2 | . | . | . | . | . | . | 1 | . | . | 0 | . | . | 1 | . |18+| 3 | 0 | . | . | . | 1 | . | . | . | 1 | . | 0 | . | . | . |19+| 4 | 0 | . | 1 | . | . | 0 | . | . | . | . | . | . | . | 0 |20+| 5 | . | 0 | . | 1 | . | . | . | 0 | . | . | 0 | . | . | . |21+| 6 | 0 | . | . | . | 1 | . | . | . | . | . | . | 1 | . | . |22+| 7 | . | . | . | 0 | . | . | . | 0 | . | 0 | . | . | . | 0 |23+| 8 | . | . | . | . | 0 | . | 1 | . | 1 | . | . | . | 0 | . |24+| 9 | . | . | . | 1 | . | . | . | . | . | . | 1 | 1 | . | 0 |25+| 10 | . | . | 0 | . | 0 | . | 0 | . | . | 0 | . | . | . | . |26+| 11 | . | 1 | . | . | . | 0 | . | . | . | . | . | . | 0 | . |27+| 12 | . | . | . | 1 | . | . | . | . | 1 | . | 0 | . | . | 0 |28+| 13 | 1 | . | . | . | . | . | . | 0 | . | . | . | 0 | . | 0 |29+| 14 | . | 1 | . | 1 | . | 0 | . | . | 0 | . | . | . | 0 | . |30+31+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.32+33+---34+35+## fiery-flint - 236+37+### Given Grid38+39+| | 1 | 2 | 3 | 4 | 5 | 6 |40+| --- | --- | --- | --- | --- | --- | --- |41+| 1 | . | 1 | . | 0 | . | 1 |42+| 2 | 0 | . | 1 | . | 0 | . |43+| 3 | . | 1 | . | 0 | . | 0 |44+| 4 | . | 1 | 0 | . | 0 | . |45+| 5 | 1 | . | . | 1 | . | 0 |46+| 6 | 1 | . | 1 | . | 1 | . |47+48+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.49+50+---51+52+## lunar-seal - 353+54+### Given Grid55+56+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |57+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |58+| 1 | 0 | . | 0 | . | . | 1 | . | . | 0 | . |59+| 2 | . | 1 | . | . | 1 | . | 1 | . | . | 1 |60+| 3 | 1 | . | 1 | . | . | 1 | . | 1 | . | . |61+| 4 | . | . | . | . | 0 | . | 1 | . | 0 | . |62+| 5 | . | 0 | . | 0 | . | 1 | . | 1 | . | . |63+| 6 | 0 | . | . | . | 0 | . | 0 | . | . | 0 |64+| 7 | . | 0 | . | 1 | . | . | . | . | 0 | . |65+| 8 | 1 | . | 0 | . | . | . | 0 | . | . | 0 |66+| 9 | . | 0 | . | 0 | . | 1 | . | 1 | . | . |67+| 10 | . | . | 1 | . | 0 | . | 1 | . | . | 0 |68+69+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.70+71+---72+73+## sable-pebble - 474+75+### Given Grid76+77+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |78+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |79+| 1 | 0 | . | 0 | . | 1 | . | 1 | . | . | . |80+| 2 | . | . | . | 1 | . | 0 | . | 0 | . | . |81+| 3 | . | 1 | . | . | . | . | . | . | 1 | 1 |82+| 4 | 0 | . | 1 | . | 1 | . | . | . | . | . |83+| 5 | . | 1 | . | 1 | . | 0 | . | . | 0 | . |84+| 6 | . | . | . | . | 0 | . | . | 0 | . | 0 |85+| 7 | . | . | . | 0 | . | . | 0 | . | 0 | . |86+| 8 | . | . | 1 | . | 1 | . | . | 0 | . | . |87+| 9 | 1 | . | . | . | . | 1 | . | . | . | 1 |88+| 10 | . | 0 | . | . | . | . | 1 | . | 0 | . |89+90+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.91+92+---93+94+## molten-osprey - 595+96+### Given Grid97+98+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |99+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |100+| 1 | 1 | . | 1 | . | . | . | . | 1 | . | 0 |101+| 2 | . | 1 | . | 0 | . | . | 0 | . | 1 | . |102+| 3 | 1 | . | 0 | . | . | . | . | . | . | 1 |103+| 4 | . | 1 | . | 0 | . | 1 | . | 1 | . | . |104+| 5 | . | . | 1 | . | 0 | . | 1 | . | . | 0 |105+| 6 | 0 | . | . | 0 | . | 1 | . | . | 1 | . |106+| 7 | . | . | 1 | . | 0 | . | 1 | . | . | 0 |107+| 8 | . | 0 | . | 1 | . | 0 | . | 1 | . | . |108+| 9 | 1 | . | 0 | . | . | . | 1 | . | 0 | . |109+| 10 | . | 1 | . | . | 0 | . | . | 0 | . | . |110+111+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.112+113+---114+115+## echo-fjord - 6116+117+### Given Grid118+119+| | 1 | 2 | 3 | 4 | 5 | 6 |120+| --- | --- | --- | --- | --- | --- | --- |121+| 1 | . | 1 | . | 0 | . | 0 |122+| 2 | 0 | . | 1 | . | 0 | . |123+| 3 | . | 1 | . | 0 | . | 0 |124+| 4 | . | 0 | . | 1 | 0 | . |125+| 5 | 1 | . | 0 | . | 0 | . |126+| 6 | . | 0 | . | 0 | . | 1 |127+128+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.129+130+---131+132+## deep-osprey - 7133+134+### Given Grid135+136+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |137+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |138+| 1 | . | . | 0 | . | . | . | . | 0 | . | 0 | . | 0 | . | . |139+| 2 | 1 | . | . | . | . | . | 1 | . | 1 | . | 0 | . | . | . |140+| 3 | . | . | . | . | 0 | . | 1 | . | . | 1 | . | 1 | . | . |141+| 4 | . | 0 | 0 | . | . | 0 | . | . | . | . | . | . | 1 | . |142+| 5 | 0 | . | . | . | . | . | 0 | . | . | 0 | . | 0 | . | . |143+| 6 | . | 0 | . | 1 | . | 0 | . | . | 1 | . | . | . | . | . |144+| 7 | 0 | . | 0 | . | 0 | . | . | 1 | . | . | . | . | . | . |145+| 8 | . | . | . | 0 | . | . | . | . | 1 | . | . | 0 | . | 0 |146+| 9 | . | . | 1 | . | . | . | . | . | . | . | 0 | . | 1 | . |147+| 10 | . | 1 | . | 1 | . | . | . | . | . | . | . | . | . | 0 |148+| 11 | . | . | . | . | 1 | . | . | 1 | 1 | . | . | . | 1 | . |149+| 12 | . | 1 | . | 0 | . | . | 0 | . | . | 0 | . | . | . | . |150+| 13 | . | . | 1 | . | 1 | . | . | . | . | . | 1 | . | 0 | . |151+| 14 | 0 | . | . | . | . | 0 | . | 1 | . | . | . | . | . | 0 |152+153+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.154+155+---156+157+## forest-cedar - 8158+159+### Given Grid160+161+| | 1 | 2 | 3 | 4 | 5 | 6 |162+| --- | --- | --- | --- | --- | --- | --- |163+| 1 | . | 0 | 1 | . | . | 0 |164+| 2 | 0 | . | 0 | . | 1 | . |165+| 3 | . | 0 | . | 0 | . | 1 |166+| 4 | 1 | . | . | 1 | 0 | . |167+| 5 | . | 1 | 0 | . | . | 0 |168+| 6 | 0 | . | . | 1 | . | 1 |169+170+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.171+172+---173+174+## pearl-robin - 9175+176+### Given Grid177+178+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |179+| --- | --- | --- | --- | --- | --- | --- | --- | --- |180+| 1 | 0 | . | 0 | . | 0 | . | . | . |181+| 2 | . | 1 | . | 1 | . | 0 | . | 1 |182+| 3 | 1 | . | 1 | . | . | . | 1 | . |183+| 4 | . | 1 | . | 1 | . | 1 | . | . |184+| 5 | . | . | 1 | . | 0 | . | . | 0 |185+| 6 | . | 1 | . | 0 | . | . | 0 | . |186+| 7 | 1 | . | . | . | . | 1 | . | 0 |187+| 8 | . | . | 1 | . | 1 | . | 0 | . |188+189+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.190+191+---192+193+## cobalt-brook - 10194+195+### Given Grid196+197+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |198+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |199+| 1 | . | 0 | 0 | . | . | . | . | 0 | . | 1 |200+| 2 | 0 | . | . | . | 0 | . | . | . | 0 | . |201+| 3 | . | 0 | . | 0 | . | 0 | . | . | 1 | . |202+| 4 | 0 | . | 0 | . | 1 | . | 1 | . | . | . |203+| 5 | . | 0 | . | 1 | . | 1 | . | 0 | . | . |204+| 6 | . | . | 0 | 1 | . | . | . | . | . | 0 |205+| 7 | . | 1 | . | . | 0 | . | 1 | . | 0 | . |206+| 8 | 1 | . | . | 0 | . | 1 | . | . | . | 0 |207+| 9 | . | . | 0 | . | 1 | . | . | 1 | . | 0 |208+| 10 | 0 | . | . | . | . | 1 | 1 | . | 1 | . |209+210+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.211+212+---213+214+## coral-ivy - 11215+216+### Given Grid217+218+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |219+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |220+| 1 | . | . | 1 | . | 0 | . | 1 | . | 0 | . | . | 0 |221+| 2 | . | . | . | 0 | . | 0 | . | 0 | . | 0 | . | . |222+| 3 | . | . | 1 | . | 0 | . | 0 | . | . | . | 0 | . |223+| 4 | . | 1 | . | . | . | . | . | . | . | 1 | . | 0 |224+| 5 | . | . | 1 | 1 | . | 0 | . | . | 1 | . | . | . |225+| 6 | . | 1 | . | . | 0 | . | . | . | . | 0 | . | . |226+| 7 | 0 | . | . | . | . | 0 | . | 1 | . | . | . | . |227+| 8 | . | . | . | 1 | . | . | . | . | 1 | . | 0 | . |228+| 9 | 0 | . | . | . | . | . | . | . | . | . | . | 1 |229+| 10 | . | 1 | . | 1 | . | . | 0 | . | . | . | . | . |230+| 11 | 0 | . | . | . | . | 0 | . | 1 | . | 0 | . | . |231+| 12 | 0 | . | 1 | . | 0 | . | . | . | 1 | . | 1 | . |232+233+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.234+235+---236+237+## eager-lava - 12238+239+### Given Grid240+241+| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |242+| --- | --- | --- | --- | --- | --- | --- | --- | --- |243+| 1 | 1 | . | 1 | . | . | . | 0 | . |244+| 2 | . | . | . | . | 1 | 0 | . | 1 |245+| 3 | . | 1 | . | 1 | . | . | 1 | . |246+| 4 | 1 | . | 1 | . | 0 | . | . | 1 |247+| 5 | . | 1 | . | 1 | . | 0 | . | . |248+| 6 | 1 | . | . | . | 0 | . | . | 0 |249+| 7 | . | 1 | 0 | . | . | 1 | . | . |250+| 8 | 1 | . | . | 1 | . | . | 0 | . |251+252+Goal: fill with 0/1 so no three equal adjacent cells appear, each row/column has equal 0 and 1 counts, and rows/columns are unique.
A pdfexport/order.go +104 -0
@@ -0,0 +1,104 @@
1+package pdfexport2+3+import (4+ "hash/fnv"5+ "math/rand/v2"6+ "sort"7+ "strings"8+ "time"9+)10+11+func OrderPuzzlesForPrint(puzzles []Puzzle, seed string) []Puzzle {12+ ordered := append([]Puzzle(nil), puzzles...)13+ if len(ordered) <= 1 {14+ return ordered15+ }16+17+ sort.SliceStable(ordered, func(i, j int) bool {18+ if ordered[i].DifficultyScore != ordered[j].DifficultyScore {19+ return ordered[i].DifficultyScore < ordered[j].DifficultyScore20+ }21+ if c := strings.Compare(normalizeToken(ordered[i].Category), normalizeToken(ordered[j].Category)); c != 0 {22+ return c < 023+ }24+ if c := strings.Compare(normalizeToken(ordered[i].ModeSelection), normalizeToken(ordered[j].ModeSelection)); c != 0 {25+ return c < 026+ }27+ if c := strings.Compare(ordered[i].SourceFileName, ordered[j].SourceFileName); c != 0 {28+ return c < 029+ }30+ return ordered[i].Index < ordered[j].Index31+ })32+33+ rng := seededRand(seed)34+35+ const bandSize = 636+ for start := 0; start < len(ordered); start += bandSize {37+ end := min(start+bandSize, len(ordered))38+ shuffleBand(ordered[start:end], rng)39+ }40+41+ // Reduce same-category runs across band edges while preserving42+ // the overall difficulty trajectory.43+ for i := 1; i < len(ordered); i++ {44+ if !sameCategory(ordered[i-1], ordered[i]) {45+ continue46+ }47+ for j := i + 1; j < min(i+4, len(ordered)); j++ {48+ if sameCategory(ordered[i-1], ordered[j]) {49+ continue50+ }51+ ordered[i], ordered[j] = ordered[j], ordered[i]52+ break53+ }54+ }55+56+ return ordered57+}58+59+func shuffleBand(puzzles []Puzzle, rng *rand.Rand) {60+ if len(puzzles) <= 1 {61+ return62+ }63+64+ perm := rng.Perm(len(puzzles))65+ shuffled := make([]Puzzle, len(puzzles))66+ for i, idx := range perm {67+ shuffled[i] = puzzles[idx]68+ }69+ copy(puzzles, shuffled)70+71+ for i := 1; i < len(puzzles); i++ {72+ if !sameCategory(puzzles[i-1], puzzles[i]) {73+ continue74+ }75+ for j := i + 1; j < len(puzzles); j++ {76+ if sameCategory(puzzles[i-1], puzzles[j]) {77+ continue78+ }79+ puzzles[i], puzzles[j] = puzzles[j], puzzles[i]80+ break81+ }82+ }83+}84+85+func seededRand(seed string) *rand.Rand {86+ if strings.TrimSpace(seed) == "" {87+ seed = time.Now().Format(time.RFC3339Nano)88+ }89+ h := fnv.New64a()90+ h.Write([]byte(seed))91+ s := h.Sum64()92+ return rand.New(rand.NewPCG(s, ^s))93+}94+95+func sameCategory(a, b Puzzle) bool {96+ return normalizeToken(a.Category) == normalizeToken(b.Category)97+}98+99+func normalizeToken(s string) string {100+ s = strings.ToLower(strings.TrimSpace(s))101+ s = strings.ReplaceAll(s, "-", " ")102+ s = strings.ReplaceAll(s, "_", " ")103+ return strings.Join(strings.Fields(s), " ")104+}
A pdfexport/order_test.go +36 -0
A pdfexport/parse.go +496 -0
@@ -0,0 +1,496 @@
1+package pdfexport2+3+import (4+ "fmt"5+ "os"6+ "path/filepath"7+ "regexp"8+ "strconv"9+ "strings"10+ "time"11+)12+13+var (14+ puzzleHeadingPattern = regexp.MustCompile(`^##\s+(.+?)\s+-\s+(\d+)\s*$`)15+ nonogramRowHeaderRegex = regexp.MustCompile(`^R\d+$`)16+ nonogramColHeaderRegex = regexp.MustCompile(`^C\d+$`)17+ tableSepCellRegex = regexp.MustCompile(`^:?-{3,}:?$`)18+)19+20+func ParseFiles(paths []string) ([]PackDocument, error) {21+ docs := make([]PackDocument, 0, len(paths))22+ for _, path := range paths {23+ doc, err := ParseFile(path)24+ if err != nil {25+ return nil, err26+ }27+ docs = append(docs, doc)28+ }29+ return docs, nil30+}31+32+func ParseFile(path string) (PackDocument, error) {33+ data, err := os.ReadFile(path)34+ if err != nil {35+ return PackDocument{}, fmt.Errorf("read input markdown: %w", err)36+ }37+ return ParseMarkdown(path, string(data))38+}39+40+func ParseMarkdown(path, content string) (PackDocument, error) {41+ content = strings.ReplaceAll(content, "\r\n", "\n")42+ content = strings.ReplaceAll(content, "\r", "\n")43+ lines := strings.Split(content, "\n")44+45+ firstContentLine := firstNonEmptyLine(lines)46+ if firstContentLine < 0 {47+ return PackDocument{}, parseError(path, 1, "input markdown is empty")48+ }49+ if strings.TrimSpace(lines[firstContentLine]) != "# PuzzleTea Export" {50+ return PackDocument{}, parseError(path, firstContentLine+1, "expected markdown title '# PuzzleTea Export'")51+ }52+53+ headings := findHeadingLines(lines)54+ if len(headings) == 0 {55+ return PackDocument{}, parseError(path, firstContentLine+1, "expected at least one puzzle section heading")56+ }57+58+ meta, err := parseMetadata(lines[firstContentLine+1:headings[0]], path, firstContentLine+2)59+ if err != nil {60+ return PackDocument{}, err61+ }62+ meta.SourceFileName = filepath.Base(path)63+64+ puzzles := make([]Puzzle, 0, len(headings))65+ for i, start := range headings {66+ end := len(lines)67+ if i+1 < len(headings) {68+ end = headings[i+1]69+ }70+71+ puzzle, err := parsePuzzleSection(lines[start:end], path, start+1, meta)72+ if err != nil {73+ return PackDocument{}, err74+ }75+ puzzles = append(puzzles, puzzle)76+ }77+78+ if len(puzzles) == 0 {79+ return PackDocument{}, parseError(path, firstContentLine+1, "no puzzle sections were parsed")80+ }81+82+ return PackDocument{83+ SourcePath: path,84+ Metadata: meta,85+ Puzzles: puzzles,86+ }, nil87+}88+89+func parseMetadata(lines []string, path string, startLine int) (PackMetadata, error) {90+ meta := PackMetadata{}91+92+ for i, line := range lines {93+ trimmed := strings.TrimSpace(line)94+ if trimmed == "" {95+ continue96+ }97+ if !strings.HasPrefix(trimmed, "- ") {98+ continue99+ }100+101+ entry := strings.TrimSpace(strings.TrimPrefix(trimmed, "- "))102+ key, value, ok := strings.Cut(entry, ":")103+ if !ok {104+ continue105+ }106+107+ lineNo := startLine + i108+ key = strings.ToLower(strings.TrimSpace(key))109+ value = strings.TrimSpace(value)110+111+ switch key {112+ case "generated":113+ meta.GeneratedRaw = value114+ if ts, err := time.Parse(time.RFC3339, value); err == nil {115+ meta.GeneratedAt = ts116+ }117+ case "version":118+ meta.Version = value119+ case "category":120+ meta.Category = value121+ case "mode selection":122+ meta.ModeSelection = value123+ case "count":124+ count, err := strconv.Atoi(value)125+ if err != nil {126+ return PackMetadata{}, parseError(path, lineNo, "invalid Count value %q", value)127+ }128+ meta.Count = count129+ case "seed":130+ meta.Seed = value131+ case "export format":132+ meta.Format = value133+ }134+ }135+136+ if strings.TrimSpace(meta.Category) == "" {137+ return PackMetadata{}, parseError(path, startLine, "missing required metadata field: Category")138+ }139+ if strings.TrimSpace(meta.ModeSelection) == "" {140+ return PackMetadata{}, parseError(path, startLine, "missing required metadata field: Mode Selection")141+ }142+143+ return meta, nil144+}145+146+func parsePuzzleSection(section []string, path string, startLine int, meta PackMetadata) (Puzzle, error) {147+ if len(section) == 0 {148+ return Puzzle{}, parseError(path, startLine, "empty puzzle section")149+ }150+151+ heading := strings.TrimSpace(section[0])152+ matches := puzzleHeadingPattern.FindStringSubmatch(heading)153+ if len(matches) != 3 {154+ return Puzzle{}, parseError(path, startLine, "invalid puzzle heading %q", heading)155+ }156+157+ index, err := strconv.Atoi(matches[2])158+ if err != nil {159+ return Puzzle{}, parseError(path, startLine, "invalid puzzle index %q", matches[2])160+ }161+162+ bodyLines := append([]string(nil), section[1:]...)163+ trimSectionBody(&bodyLines)164+ body := strings.Join(bodyLines, "\n")165+166+ p := Puzzle{167+ SourcePath: path,168+ SourceFileName: meta.SourceFileName,169+ Category: meta.Category,170+ ModeSelection: meta.ModeSelection,171+ Name: matches[1],172+ Index: index,173+ Body: body,174+ }175+176+ if strings.EqualFold(strings.TrimSpace(meta.Category), "nonogram") {177+ nonogram, err := parseNonogramBody(bodyLines, path, startLine+1)178+ if err != nil {179+ return Puzzle{}, err180+ }181+ p.Nonogram = nonogram182+ return p, nil183+ }184+185+ table, err := parseGridTableBody(bodyLines, path, startLine+1)186+ if err != nil {187+ return Puzzle{}, err188+ }189+ p.Table = table190+191+ return p, nil192+}193+194+func parseNonogramBody(bodyLines []string, path string, bodyStartLine int) (*NonogramData, error) {195+ tableLines, tableLineNumbers := findFirstMarkdownTable(bodyLines, bodyStartLine)196+ if len(tableLines) < 3 {197+ lineNo := bodyStartLine198+ if len(tableLineNumbers) > 0 {199+ lineNo = tableLineNumbers[0]200+ }201+ return nil, parseError(path, lineNo, "expected nonogram markdown table with header, separator, and data rows")202+ }203+204+ header := parseTableRow(tableLines[0])205+ if len(header) == 0 {206+ return nil, parseError(path, tableLineNumbers[0], "nonogram header row is empty")207+ }208+209+ rowHintCols := 0210+ colCount := 0211+ for _, cell := range header {212+ switch {213+ case nonogramRowHeaderRegex.MatchString(cell):214+ rowHintCols++215+ case nonogramColHeaderRegex.MatchString(cell):216+ colCount++217+ }218+ }219+ if rowHintCols < 1 || colCount < 1 {220+ return nil, parseError(path, tableLineNumbers[0], "expected nonogram header cells like R1.. and C1..")221+ }222+223+ expectedCols := rowHintCols + colCount224+ dataRows := make([][]string, 0, len(tableLines)-2)225+ for i := 2; i < len(tableLines); i++ {226+ cells := parseTableRow(tableLines[i])227+ if len(cells) < expectedCols {228+ return nil, parseError(path, tableLineNumbers[i], "expected %d columns, found %d", expectedCols, len(cells))229+ }230+ if len(cells) > expectedCols {231+ cells = cells[:expectedCols]232+ }233+ dataRows = append(dataRows, cells)234+ }235+236+ if len(dataRows) == 0 {237+ return nil, parseError(path, tableLineNumbers[0], "nonogram table has no data rows")238+ }239+240+ colHintRows := 0241+ for _, row := range dataRows {242+ if rowHintPlaceholderRow(row[:rowHintCols]) {243+ colHintRows++244+ continue245+ }246+ break247+ }248+249+ height := len(dataRows) - colHintRows250+ if height <= 0 {251+ return nil, parseError(path, tableLineNumbers[len(tableLineNumbers)-1], "nonogram table does not contain puzzle rows")252+ }253+254+ rowHints := make([][]int, height)255+ grid := make([][]string, height)256+ for y := 0; y < height; y++ {257+ row := dataRows[colHintRows+y]258+ hints := parseHintCells(row[:rowHintCols])259+ if len(hints) == 0 {260+ hints = []int{0}261+ }262+ rowHints[y] = hints263+264+ gridRow := make([]string, colCount)265+ for x := 0; x < colCount; x++ {266+ cell := strings.TrimSpace(row[rowHintCols+x])267+ switch cell {268+ case "", ".":269+ gridRow[x] = " "270+ default:271+ gridRow[x] = cell272+ }273+ }274+ grid[y] = gridRow275+ }276+277+ colHints := make([][]int, colCount)278+ for x := 0; x < colCount; x++ {279+ hints := make([]int, 0, colHintRows)280+ for r := 0; r < colHintRows; r++ {281+ if v, ok := parseHintValue(dataRows[r][rowHintCols+x]); ok {282+ hints = append(hints, v)283+ }284+ }285+ if len(hints) == 0 {286+ hints = []int{0}287+ }288+ colHints[x] = hints289+ }290+291+ return &NonogramData{292+ Width: colCount,293+ Height: height,294+ RowHints: rowHints,295+ ColHints: colHints,296+ Grid: grid,297+ }, nil298+}299+300+func parseGridTableBody(bodyLines []string, path string, bodyStartLine int) (*GridTable, error) {301+ tableLines, tableLineNumbers := findFirstMarkdownTable(bodyLines, bodyStartLine)302+ if len(tableLines) == 0 {303+ return nil, nil304+ }305+ if len(tableLines) < 2 {306+ return nil, parseError(path, tableLineNumbers[0], "expected markdown table to include data rows")307+ }308+309+ rows := make([][]string, 0, len(tableLines))310+ for i, line := range tableLines {311+ cells := parseTableRow(line)312+ if len(cells) == 0 {313+ return nil, parseError(path, tableLineNumbers[i], "empty table row")314+ }315+ rows = append(rows, cells)316+ }317+318+ hasHeaderRow := false319+ if len(rows) > 1 && isMarkdownSeparatorRow(rows[1]) {320+ hasHeaderRow = true321+ rows = append(rows[:1], rows[2:]...)322+ }323+ if len(rows) < 2 {324+ return nil, parseError(path, tableLineNumbers[0], "table must include header and data rows")325+ }326+327+ width := 0328+ for _, row := range rows {329+ if len(row) > width {330+ width = len(row)331+ }332+ }333+ for i := range rows {334+ if len(rows[i]) >= width {335+ continue336+ }337+ padded := make([]string, width)338+ copy(padded, rows[i])339+ rows[i] = padded340+ }341+342+ return &GridTable{343+ Rows: rows,344+ HasHeaderRow: hasHeaderRow,345+ HasHeaderCol: detectHeaderColumn(rows, hasHeaderRow),346+ }, nil347+}348+349+func findFirstMarkdownTable(lines []string, startLine int) ([]string, []int) {350+ table := []string{}351+ lineNumbers := []int{}352+ started := false353+354+ for i, line := range lines {355+ trimmed := strings.TrimSpace(line)356+ if strings.HasPrefix(trimmed, "|") {357+ started = true358+ table = append(table, line)359+ lineNumbers = append(lineNumbers, startLine+i)360+ continue361+ }362+ if started {363+ break364+ }365+ }366+367+ return table, lineNumbers368+}369+370+func parseTableRow(line string) []string {371+ trimmed := strings.TrimSpace(line)372+ trimmed = strings.TrimPrefix(trimmed, "|")373+ trimmed = strings.TrimSuffix(trimmed, "|")374+ if trimmed == "" {375+ return []string{}376+ }377+378+ parts := strings.Split(trimmed, "|")379+ cells := make([]string, 0, len(parts))380+ for _, part := range parts {381+ cells = append(cells, strings.TrimSpace(part))382+ }383+ return cells384+}385+386+func isMarkdownSeparatorRow(cells []string) bool {387+ if len(cells) == 0 {388+ return false389+ }390+ for _, cell := range cells {391+ if !tableSepCellRegex.MatchString(strings.TrimSpace(cell)) {392+ return false393+ }394+ }395+ return true396+}397+398+func rowHintPlaceholderRow(cells []string) bool {399+ for _, cell := range cells {400+ trimmed := strings.TrimSpace(cell)401+ if trimmed != "" && trimmed != "." {402+ return false403+ }404+ }405+ return true406+}407+408+func parseHintCells(cells []string) []int {409+ hints := make([]int, 0, len(cells))410+ for _, cell := range cells {411+ if v, ok := parseHintValue(cell); ok {412+ hints = append(hints, v)413+ }414+ }415+ return hints416+}417+418+func detectHeaderColumn(rows [][]string, hasHeaderRow bool) bool {419+ if len(rows) < 2 {420+ return false421+ }422+423+ start := 1424+ if !hasHeaderRow {425+ start = 0426+ }427+428+ total := 0429+ numeric := 0430+ for i := start; i < len(rows); i++ {431+ if len(rows[i]) == 0 {432+ continue433+ }434+ total++435+ if _, err := strconv.Atoi(strings.TrimSpace(rows[i][0])); err == nil {436+ numeric++437+ }438+ }439+440+ return total > 0 && numeric*100/total >= 70441+}442+443+func parseHintValue(cell string) (int, bool) {444+ trimmed := strings.TrimSpace(cell)445+ if trimmed == "" || trimmed == "." {446+ return 0, false447+ }448+ v, err := strconv.Atoi(trimmed)449+ if err != nil {450+ return 0, false451+ }452+ return v, true453+}454+455+func trimSectionBody(lines *[]string) {456+ for len(*lines) > 0 {457+ trimmed := strings.TrimSpace((*lines)[0])458+ if trimmed != "" && trimmed != "---" {459+ break460+ }461+ *lines = (*lines)[1:]462+ }463+ for len(*lines) > 0 {464+ trimmed := strings.TrimSpace((*lines)[len(*lines)-1])465+ if trimmed != "" && trimmed != "---" {466+ break467+ }468+ *lines = (*lines)[:len(*lines)-1]469+ }470+}471+472+func firstNonEmptyLine(lines []string) int {473+ for i, line := range lines {474+ if strings.TrimSpace(line) != "" {475+ return i476+ }477+ }478+ return -1479+}480+481+func findHeadingLines(lines []string) []int {482+ indexes := []int{}483+ for i, line := range lines {484+ if strings.HasPrefix(strings.TrimSpace(line), "## ") {485+ indexes = append(indexes, i)486+ }487+ }488+ return indexes489+}490+491+func parseError(path string, line int, format string, args ...any) error {492+ if line < 1 {493+ line = 1494+ }495+ return fmt.Errorf("%s:%d: %s", path, line, fmt.Sprintf(format, args...))496+}
A pdfexport/parse_test.go +135 -0
@@ -0,0 +1,135 @@
1+package pdfexport2+3+import (4+ "os"5+ "path/filepath"6+ "strconv"7+ "testing"8+)9+10+func TestParseMarkdownNonogram(t *testing.T) {11+ doc, err := ParseMarkdown("sample.md", sampleNonogramDoc("Standard", 1))12+ if err != nil {13+ t.Fatal(err)14+ }15+16+ if got, want := doc.Metadata.Category, "Nonogram"; got != want {17+ t.Fatalf("category = %q, want %q", got, want)18+ }19+ if got, want := len(doc.Puzzles), 1; got != want {20+ t.Fatalf("puzzles = %d, want %d", got, want)21+ }22+23+ p := doc.Puzzles[0]24+ if p.Nonogram == nil {25+ t.Fatal("expected parsed nonogram data")26+ }27+ if p.Nonogram.Width != 2 || p.Nonogram.Height != 2 {28+ t.Fatalf("nonogram size = %dx%d, want 2x2", p.Nonogram.Width, p.Nonogram.Height)29+ }30+31+ if got, want := p.Nonogram.RowHints[0][0], 1; got != want {32+ t.Fatalf("first row first hint = %d, want %d", got, want)33+ }34+ if got, want := p.Nonogram.ColHints[0][0], 1; got != want {35+ t.Fatalf("first col first hint = %d, want %d", got, want)36+ }37+38+ if got, want := p.Nonogram.Grid[0][0], " "; got != want {39+ t.Fatalf("grid dot replacement = %q, want %q", got, want)40+ }41+}42+43+func TestParseFilesMultipleInputs(t *testing.T) {44+ temp := t.TempDir()45+46+ fileA := filepath.Join(temp, "pack-a.md")47+ if err := os.WriteFile(fileA, []byte(sampleNonogramDoc("Standard", 1)), 0o644); err != nil {48+ t.Fatal(err)49+ }50+51+ fileB := filepath.Join(temp, "pack-b.md")52+ if err := os.WriteFile(fileB, []byte(sampleNonogramDoc("Classic", 2)), 0o644); err != nil {53+ t.Fatal(err)54+ }55+56+ docs, err := ParseFiles([]string{fileA, fileB})57+ if err != nil {58+ t.Fatal(err)59+ }60+ if got, want := len(docs), 2; got != want {61+ t.Fatalf("docs = %d, want %d", got, want)62+ }63+64+ if got, want := docs[0].Metadata.SourceFileName, "pack-a.md"; got != want {65+ t.Fatalf("first source file = %q, want %q", got, want)66+ }67+ if got, want := docs[1].Puzzles[0].ModeSelection, "Classic"; got != want {68+ t.Fatalf("second mode selection = %q, want %q", got, want)69+ }70+}71+72+func TestParseMarkdownTakuzuTable(t *testing.T) {73+ doc, err := ParseMarkdown("takuzu.md", sampleTakuzuDoc())74+ if err != nil {75+ t.Fatal(err)76+ }77+78+ if got, want := doc.Metadata.Category, "Takuzu"; got != want {79+ t.Fatalf("category = %q, want %q", got, want)80+ }81+82+ p := doc.Puzzles[0]83+ if p.Nonogram != nil {84+ t.Fatal("expected nonogram data to be nil for takuzu")85+ }86+ if p.Table == nil {87+ t.Fatal("expected parsed grid table for takuzu")88+ }89+ if !p.Table.HasHeaderRow {90+ t.Fatal("expected takuzu table to detect a header row")91+ }92+ if !p.Table.HasHeaderCol {93+ t.Fatal("expected takuzu table to detect a header column")94+ }95+96+ if got, want := p.Table.Rows[1][1], "."; got != want {97+ t.Fatalf("table cell = %q, want %q", got, want)98+ }99+}100+101+func sampleNonogramDoc(mode string, idx int) string {102+ return "# PuzzleTea Export\n\n" +103+ "- Generated: 2026-02-21T20:42:05-07:00\n" +104+ "- Version: v1.6.0\n" +105+ "- Category: Nonogram\n" +106+ "- Mode Selection: " + mode + "\n" +107+ "- Count: 1\n" +108+ "- Seed: zine\n\n" +109+ "## ember-newt - " + strconv.Itoa(idx) + "\n\n" +110+ "### Puzzle Grid with Integrated Hints\n\n" +111+ "| R1 | R2 | C1 | C2 |\n" +112+ "| --- | --- | --- | --- |\n" +113+ "| . | . | 1 | 2 |\n" +114+ "| . | . | 3 | 4 |\n" +115+ "| 1 | 1 | . | . |\n" +116+ "| . | 2 | . | . |\n\n" +117+ "Row hints are right-aligned beside each row.\n"118+}119+120+func sampleTakuzuDoc() string {121+ return "# PuzzleTea Export\n\n" +122+ "- Generated: 2026-02-21T20:42:05-07:00\n" +123+ "- Version: v1.6.0\n" +124+ "- Category: Takuzu\n" +125+ "- Mode Selection: Beginner\n" +126+ "- Count: 1\n" +127+ "- Seed: zine\n\n" +128+ "## scarlet-lichen - 1\n\n" +129+ "### Given Grid\n\n" +130+ "| | 1 | 2 | 3 |\n" +131+ "| --- | --- | --- | --- |\n" +132+ "| 1 | . | 0 | . |\n" +133+ "| 2 | 1 | . | 0 |\n\n" +134+ "Goal: fill with 0/1.\n"135+}
A pdfexport/render.go +477 -0
@@ -0,0 +1,477 @@
1+package pdfexport2+3+import (4+ "fmt"5+ "math"6+ "os"7+ "path/filepath"8+ "sort"9+ "strconv"10+ "strings"11+ "time"12+13+ "github.com/go-pdf/fpdf"14+)15+16+const (17+ a5WidthMM = 148.018+ a5HeightMM = 210.019+)20+21+func WritePDF(outputPath string, docs []PackDocument, puzzles []Puzzle, cfg RenderConfig) error {22+ if strings.TrimSpace(outputPath) == "" {23+ return fmt.Errorf("output path is required")24+ }25+ if len(puzzles) == 0 {26+ return fmt.Errorf("no puzzles to render")27+ }28+29+ if cfg.GeneratedAt.IsZero() {30+ cfg.GeneratedAt = time.Now()31+ }32+ if strings.TrimSpace(cfg.Title) == "" {33+ cfg.Title = defaultTitle(docs)34+ }35+ if strings.TrimSpace(cfg.AdvertText) == "" {36+ cfg.AdvertText = "Find more puzzles at github.com/FelineStateMachine/puzzletea"37+ }38+39+ pdf := fpdf.NewCustom(&fpdf.InitType{40+ OrientationStr: "P",41+ UnitStr: "mm",42+ Size: fpdf.SizeType{43+ Wd: a5WidthMM,44+ Ht: a5HeightMM,45+ },46+ })47+ pdf.SetAutoPageBreak(false, 0)48+ pdf.SetCreator("PuzzleTea", true)49+ pdf.SetAuthor("PuzzleTea", true)50+ pdf.SetTitle(cfg.Title, true)51+52+ renderTitlePage(pdf, docs, puzzles, cfg)53+ for _, puzzle := range puzzles {54+ renderPuzzlePage(pdf, puzzle)55+ }56+57+ dir := filepath.Dir(outputPath)58+ if dir != "" && dir != "." {59+ if err := os.MkdirAll(dir, 0o755); err != nil {60+ return fmt.Errorf("create output directory: %w", err)61+ }62+ }63+64+ if err := pdf.OutputFileAndClose(outputPath); err != nil {65+ return fmt.Errorf("write pdf file: %w", err)66+ }67+ return nil68+}69+70+func renderTitlePage(pdf *fpdf.Fpdf, docs []PackDocument, puzzles []Puzzle, cfg RenderConfig) {71+ pdf.AddPage()72+ pageW, pageH := pdf.GetPageSize()73+ margin := 12.074+75+ pdf.SetTextColor(20, 20, 20)76+ pdf.SetFont("Helvetica", "B", 22)77+ pdf.SetXY(0, 24)78+ pdf.CellFormat(pageW, 10, cfg.Title, "", 0, "C", false, 0, "")79+80+ pdf.SetFont("Helvetica", "", 11)81+ pdf.SetTextColor(70, 70, 70)82+ pdf.SetXY(0, 36)83+ pdf.CellFormat(pageW, 6, "A5 Printable Puzzle Pack", "", 0, "C", false, 0, "")84+85+ metaY := 50.086+ pdf.SetTextColor(25, 25, 25)87+ pdf.SetFont("Helvetica", "", 10)88+ pdf.SetXY(margin, metaY)89+ pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Generated: %s", cfg.GeneratedAt.Format(time.RFC3339)), "", 0, "L", false, 0, "")90+ metaY += 691+92+ categories := summarizeCategories(puzzles)93+ pdf.SetXY(margin, metaY)94+ pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Puzzles: %d", len(puzzles)), "", 0, "L", false, 0, "")95+ metaY += 696+ pdf.SetXY(margin, metaY)97+ pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Categories: %s", strings.Join(categories, ", ")), "", 0, "L", false, 0, "")98+ metaY += 899+100+ pdf.SetFont("Helvetica", "B", 10)101+ pdf.SetTextColor(45, 45, 45)102+ pdf.SetXY(margin, metaY)103+ pdf.CellFormat(pageW-2*margin, 6, "Source Exports", "", 0, "L", false, 0, "")104+ metaY += 7105+106+ pdf.SetFont("Helvetica", "", 8.5)107+ pdf.SetTextColor(85, 85, 85)108+ for _, doc := range docs {109+ line := fmt.Sprintf("%s | Category: %s | Mode: %s | Count: %d | Seed: %s",110+ doc.Metadata.SourceFileName,111+ doc.Metadata.Category,112+ doc.Metadata.ModeSelection,113+ doc.Metadata.Count,114+ emptyAs(doc.Metadata.Seed, "none"),115+ )116+ wrapped := pdf.SplitLines([]byte(line), pageW-2*margin)117+ for _, raw := range wrapped {118+ if metaY > pageH-45 {119+ break120+ }121+ pdf.SetXY(margin, metaY)122+ pdf.CellFormat(pageW-2*margin, 5, string(raw), "", 0, "L", false, 0, "")123+ metaY += 5124+ }125+ if metaY > pageH-45 {126+ break127+ }128+ }129+130+ pdf.SetTextColor(50, 50, 50)131+ pdf.SetFont("Helvetica", "B", 12)132+ pdf.SetXY(margin, pageH-30)133+ pdf.CellFormat(pageW-2*margin, 7, "Made with PuzzleTea", "", 0, "C", false, 0, "")134+135+ pdf.SetFont("Helvetica", "", 10)136+ pdf.SetXY(margin, pageH-22)137+ pdf.CellFormat(pageW-2*margin, 6, cfg.AdvertText, "", 0, "C", false, 0, "")138+}139+140+func renderPuzzlePage(pdf *fpdf.Fpdf, puzzle Puzzle) {141+ pdf.AddPage()142+ pageW, pageH := pdf.GetPageSize()143+144+ pdf.SetTextColor(20, 20, 20)145+ pdf.SetFont("Helvetica", "B", 13)146+ pdf.SetXY(0, 10)147+ title := fmt.Sprintf("%s %d: %s", puzzle.Category, puzzle.Index, puzzle.Name)148+ pdf.CellFormat(pageW, 7, title, "", 0, "C", false, 0, "")149+150+ pdf.SetFont("Helvetica", "", 9)151+ pdf.SetTextColor(95, 95, 95)152+ pdf.SetXY(0, 17)153+ subtitle := fmt.Sprintf("Mode: %s | Source: %s | Difficulty score: %.2f",154+ emptyAs(puzzle.ModeSelection, "mixed modes"),155+ puzzle.SourceFileName,156+ puzzle.DifficultyScore,157+ )158+ pdf.CellFormat(pageW, 5, subtitle, "", 0, "C", false, 0, "")159+160+ if puzzle.Nonogram != nil {161+ renderNonogramPage(pdf, puzzle.Nonogram)162+ return163+ }164+ if puzzle.Table != nil {165+ renderGridTablePage(pdf, puzzle.Table)166+ return167+ }168+ renderFallbackPage(pdf, puzzle, pageH)169+}170+171+func renderNonogramPage(pdf *fpdf.Fpdf, data *NonogramData) {172+ if data == nil || data.Width <= 0 || data.Height <= 0 {173+ return174+ }175+176+ pageW, pageH := pdf.GetPageSize()177+ marginX := 10.0178+ top := 28.0179+ bottom := 12.0180+181+ rowHintCols := maxHintDepth(data.RowHints)182+ colHintRows := maxHintDepth(data.ColHints)183+ if rowHintCols < 1 {184+ rowHintCols = 1185+ }186+ if colHintRows < 1 {187+ colHintRows = 1188+ }189+190+ totalCols := rowHintCols + data.Width191+ totalRows := colHintRows + data.Height192+193+ availW := pageW - 2*marginX194+ availH := pageH - top - bottom195+ cellSize := math.Min(availW/float64(totalCols), availH/float64(totalRows))196+ if cellSize > 8.6 {197+ cellSize = 8.6198+ }199+200+ blockW := float64(totalCols) * cellSize201+ blockH := float64(totalRows) * cellSize202+ startX := (pageW - blockW) / 2203+ startY := top + (availH-blockH)/2204+205+ pdf.SetDrawColor(45, 45, 45)206+ pdf.SetLineWidth(0.12)207+ for r := 0; r < totalRows; r++ {208+ for c := 0; c < totalCols; c++ {209+ x := startX + float64(c)*cellSize210+ y := startY + float64(r)*cellSize211+ pdf.Rect(x, y, cellSize, cellSize, "D")212+213+ switch {214+ case r < colHintRows && c >= rowHintCols:215+ col := c - rowHintCols216+ if text := colHintText(data.ColHints[col], colHintRows, r); text != "" {217+ drawCellText(pdf, x, y, cellSize, cellSize, text, true)218+ }219+ case r >= colHintRows && c < rowHintCols:220+ row := r - colHintRows221+ if text := rowHintText(data.RowHints[row], rowHintCols, c); text != "" {222+ drawCellText(pdf, x, y, cellSize, cellSize, text, true)223+ }224+ case r >= colHintRows && c >= rowHintCols:225+ row := r - colHintRows226+ col := c - rowHintCols227+ if row < len(data.Grid) && col < len(data.Grid[row]) {228+ cellText := strings.TrimSpace(data.Grid[row][col])229+ if cellText != "" && cellText != "." {230+ drawCellText(pdf, x, y, cellSize, cellSize, cellText, false)231+ }232+ }233+ }234+ }235+ }236+237+ xSep := startX + float64(rowHintCols)*cellSize238+ ySep := startY + float64(colHintRows)*cellSize239+ pdf.SetLineWidth(0.4)240+ pdf.Line(xSep, startY, xSep, startY+blockH)241+ pdf.Line(startX, ySep, startX+blockW, ySep)242+243+ pdf.SetLineWidth(0.55)244+ pdf.Rect(startX, startY, blockW, blockH, "D")245+}246+247+func renderGridTablePage(pdf *fpdf.Fpdf, table *GridTable) {248+ if table == nil || len(table.Rows) == 0 {249+ return250+ }251+252+ pageW, pageH := pdf.GetPageSize()253+ marginX := 10.0254+ top := 28.0255+ bottom := 12.0256+257+ rows := len(table.Rows)258+ cols := 0259+ for _, row := range table.Rows {260+ if len(row) > cols {261+ cols = len(row)262+ }263+ }264+ if cols == 0 {265+ return266+ }267+268+ availW := pageW - 2*marginX269+ availH := pageH - top - bottom270+ cellSize := math.Min(availW/float64(cols), availH/float64(rows))271+ if cellSize > 11.2 {272+ cellSize = 11.2273+ }274+ if cellSize < 3.3 {275+ cellSize = 3.3276+ }277+278+ blockW := float64(cols) * cellSize279+ blockH := float64(rows) * cellSize280+ startX := (pageW - blockW) / 2281+ startY := top + (availH-blockH)/2282+283+ pdf.SetDrawColor(55, 55, 55)284+ pdf.SetLineWidth(0.16)285+ for r := 0; r < rows; r++ {286+ for c := 0; c < cols; c++ {287+ x := startX + float64(c)*cellSize288+ y := startY + float64(r)*cellSize289+ pdf.Rect(x, y, cellSize, cellSize, "D")290+291+ var text string292+ if c < len(table.Rows[r]) {293+ text = strings.TrimSpace(table.Rows[r][c])294+ }295+ if text == "." {296+ text = " "297+ }298+ if text == "" {299+ continue300+ }301+302+ dim := (table.HasHeaderRow && r == 0) || (table.HasHeaderCol && c == 0)303+ drawCellText(pdf, x, y, cellSize, cellSize, text, dim)304+ }305+ }306+307+ if table.HasHeaderRow {308+ ySep := startY + cellSize309+ pdf.SetLineWidth(0.42)310+ pdf.Line(startX, ySep, startX+blockW, ySep)311+ }312+ if table.HasHeaderCol {313+ xSep := startX + cellSize314+ pdf.SetLineWidth(0.42)315+ pdf.Line(xSep, startY, xSep, startY+blockH)316+ }317+318+ pdf.SetLineWidth(0.6)319+ pdf.Rect(startX, startY, blockW, blockH, "D")320+}321+322+func renderFallbackPage(pdf *fpdf.Fpdf, puzzle Puzzle, pageH float64) {323+ pageW, _ := pdf.GetPageSize()324+ marginX := 10.0325+ top := 28.0326+ bottom := 12.0327+ availW := pageW - 2*marginX328+ availH := pageH - top - bottom329+330+ lines := sanitizeBody(puzzle.Body)331+ fontSize := 9.2332+ lineHeight := 4.8333+334+ pdf.SetFont("Courier", "", fontSize)335+ wrapped := make([]string, 0, len(lines))336+ for _, line := range lines {337+ chunks := pdf.SplitLines([]byte(line), availW)338+ if len(chunks) == 0 {339+ wrapped = append(wrapped, "")340+ continue341+ }342+ for _, raw := range chunks {343+ wrapped = append(wrapped, string(raw))344+ }345+ }346+347+ if total := float64(len(wrapped)) * lineHeight; total > availH && len(wrapped) > 0 {348+ maxLines := int(availH / lineHeight)349+ if maxLines < len(wrapped) {350+ wrapped = append(wrapped[:max(0, maxLines-1)], "...")351+ }352+ }353+354+ blockH := float64(len(wrapped)) * lineHeight355+ startY := top + (availH-blockH)/2356+357+ pdf.SetTextColor(50, 50, 50)358+ y := startY359+ for _, line := range wrapped {360+ w := pdf.GetStringWidth(line)361+ x := (pageW - w) / 2362+ if x < marginX {363+ x = marginX364+ }365+ pdf.SetXY(x, y)366+ pdf.CellFormat(availW, lineHeight, line, "", 0, "L", false, 0, "")367+ y += lineHeight368+ }369+}370+371+func sanitizeBody(body string) []string {372+ lines := strings.Split(body, "\n")373+ cleaned := make([]string, 0, len(lines))374+ for _, line := range lines {375+ trimmed := strings.TrimSpace(line)376+ if trimmed == "" || trimmed == "---" {377+ cleaned = append(cleaned, "")378+ continue379+ }380+ if strings.HasPrefix(trimmed, "### ") {381+ cleaned = append(cleaned, strings.TrimSpace(strings.TrimPrefix(trimmed, "### ")))382+ continue383+ }384+ if strings.HasPrefix(trimmed, "|") {385+ line = strings.ReplaceAll(line, ".", " ")386+ }387+ cleaned = append(cleaned, line)388+ }389+ return cleaned390+}391+392+func drawCellText(pdf *fpdf.Fpdf, x, y, w, h float64, text string, dim bool) {393+ if strings.TrimSpace(text) == "" {394+ return395+ }396+ if dim {397+ pdf.SetTextColor(130, 130, 130)398+ } else {399+ pdf.SetTextColor(25, 25, 25)400+ }401+402+ fontSize := math.Max(3.1, math.Min(6.3, h*0.63))403+ pdf.SetFont("Helvetica", "", fontSize)404+ lineH := fontSize * 0.9405+ pdf.SetXY(x, y+(h-lineH)/2)406+ pdf.CellFormat(w, lineH, text, "", 0, "C", false, 0, "")407+}408+409+func colHintText(hints []int, depth, row int) string {410+ if len(hints) == 0 {411+ return ""412+ }413+ start := depth - len(hints)414+ if row < start {415+ return ""416+ }417+ return strconv.Itoa(hints[row-start])418+}419+420+func rowHintText(hints []int, depth, col int) string {421+ if len(hints) == 0 {422+ return ""423+ }424+ start := depth - len(hints)425+ if col < start {426+ return ""427+ }428+ return strconv.Itoa(hints[col-start])429+}430+431+func maxHintDepth(hints [][]int) int {432+ maxDepth := 0433+ for _, h := range hints {434+ if len(h) > maxDepth {435+ maxDepth = len(h)436+ }437+ }438+ return maxDepth439+}440+441+func summarizeCategories(puzzles []Puzzle) []string {442+ set := map[string]struct{}{}443+ for _, p := range puzzles {444+ category := strings.TrimSpace(p.Category)445+ if category == "" {446+ continue447+ }448+ set[category] = struct{}{}449+ }450+451+ categories := make([]string, 0, len(set))452+ for category := range set {453+ categories = append(categories, category)454+ }455+ sort.Strings(categories)456+ if len(categories) == 0 {457+ return []string{"Unknown"}458+ }459+ return categories460+}461+462+func defaultTitle(docs []PackDocument) string {463+ if len(docs) == 1 {464+ category := strings.TrimSpace(docs[0].Metadata.Category)465+ if category != "" {466+ return fmt.Sprintf("%s Puzzle Pack", category)467+ }468+ }469+ return "PuzzleTea Mixed Puzzle Pack"470+}471+472+func emptyAs(v, fallback string) string {473+ if strings.TrimSpace(v) == "" {474+ return fallback475+ }476+ return v477+}
A pdfexport/types.go +64 -0