clone

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

refactoring

Dami committed c81e5f4
commit
c81e5f467a56c016f951e7588dc3d5e26abc83e5
parent
514cb079e95ad8265685b49838290e9adf64285e
author
Dami <dami.etoile@gmail.com>
14 files changed, +44 -780
Mgame/print_adapter.go3+-
Mgame/print_adapter_test.go11+-
Dgame/print_markdown.go192------------------------------
Mhashiwokakero/PrintAdapter.go52+--------
Mhitori/PrintAdapter.go73+-----------
Mnonogram/PrintAdapter.go63+----------
Mnurikabe/PrintAdapter.go50+--------
Mpdfexport/jsonl.go31+----
Mpdfexport/jsonl_test.go107++---------------
Mpdfexport/render_error_test.go9+-
Mshikaku/PrintAdapter.go51+--------
Msudoku/PrintAdapter.go50+--------
Mtakuzu/PrintAdapter.go59+---------
Mwordsearch/PrintAdapter.go73+-----------
M game/print_adapter.go +1 -2
M game/print_adapter_test.go +4 -7
D game/print_markdown.go +0 -192
@@ -1,192 +0,0 @@
1-package game2-3-import (4- "fmt"5- "strconv"6- "strings"7-)8-9-func SplitLines(s string) []string {10- if strings.TrimSpace(s) == "" {11- return []string{}12- }13- return strings.Split(s, "\n")14-}15-16-func EscapeMarkdownCell(s string) string {17- return strings.ReplaceAll(s, "|", "\\|")18-}19-20-func MakeStringGrid(width, height int, fill string) [][]string {21- if width <= 0 || height <= 0 {22- return [][]string{}23- }24-25- grid := make([][]string, height)26- for y := range height {27- grid[y] = make([]string, width)28- for x := range width {29- grid[y][x] = fill30- }31- }32- return grid33-}34-35-func RenderGridTable(cells [][]string) string {36- if len(cells) == 0 {37- return "_(empty grid)_"38- }39-40- width := 041- for _, row := range cells {42- if len(row) > width {43- width = len(row)44- }45- }46- if width <= 0 {47- return "_(empty grid)_"48- }49-50- var b strings.Builder51- b.WriteString("| |")52- for x := range width {53- fmt.Fprintf(&b, " %d |", x+1)54- }55- b.WriteString("\n| --- |")56- for range width {57- b.WriteString(" --- |")58- }59- b.WriteString("\n")60-61- for y := range len(cells) {62- fmt.Fprintf(&b, "| %d |", y+1)63- for x := range width {64- cell := "."65- if x < len(cells[y]) && strings.TrimSpace(cells[y][x]) != "" {66- cell = cells[y][x]67- }68- fmt.Fprintf(&b, " %s |", EscapeMarkdownCell(cell))69- }70- b.WriteString("\n")71- }72-73- return strings.TrimRight(b.String(), "\n")74-}75-76-func ParseCSVIntGrid(raw string, width, height int) [][]int {77- if width <= 0 || height <= 0 {78- return [][]int{}79- }80-81- clues := make([][]int, height)82- for y := range height {83- clues[y] = make([]int, width)84- }85-86- rows := SplitLines(raw)87- for y := range min(len(rows), len(clues)) {88- parts := strings.Split(rows[y], ",")89- for x := range min(len(parts), len(clues[y])) {90- val, err := strconv.Atoi(strings.TrimSpace(parts[x]))91- if err != nil {92- continue93- }94- clues[y][x] = val95- }96- }97- return clues98-}99-100-func NormalizeNonogramHints(src [][]int, size int) [][]int {101- if size <= 0 {102- return [][]int{}103- }104-105- hints := make([][]int, size)106- for i := range len(hints) {107- if i >= len(src) || len(src[i]) == 0 {108- hints[i] = []int{0}109- continue110- }111- hints[i] = append([]int(nil), src[i]...)112- }113- return hints114-}115-116-func MaxNonogramHintLen(hints [][]int) int {117- maxLen := 0118- for _, hint := range hints {119- if len(hint) > maxLen {120- maxLen = len(hint)121- }122- }123- return maxLen124-}125-126-func RenderNonogramTable(127- rowHints, colHints [][]int,128- width, height, rowHintCols, colHintRows int,129-) string {130- var b strings.Builder131-132- b.WriteString("|")133- for i := range rowHintCols {134- fmt.Fprintf(&b, " R%d |", i+1)135- }136- for x := range width {137- fmt.Fprintf(&b, " C%d |", x+1)138- }139- b.WriteString("\n|")140- for range rowHintCols + width {141- b.WriteString(" --- |")142- }143- b.WriteString("\n")144-145- for hintRow := range colHintRows {146- b.WriteString("|")147- for range rowHintCols {148- b.WriteString(" . |")149- }150- for x := range width {151- b.WriteString(" ")152- b.WriteString(renderColumnHintCell(colHints[x], colHintRows, hintRow))153- b.WriteString(" |")154- }155- b.WriteString("\n")156- }157-158- for y := range height {159- rowHint := rowHints[y]160- hintStart := rowHintCols - len(rowHint)161-162- b.WriteString("|")163- for hintCol := range rowHintCols {164- if hintCol < hintStart {165- b.WriteString(" . |")166- continue167- }168- fmt.Fprintf(&b, " %d |", rowHint[hintCol-hintStart])169- }170- for range width {171- b.WriteString(" . |")172- }173- b.WriteString("\n")174- }175-176- return strings.TrimRight(b.String(), "\n")177-}178-179-func renderColumnHintCell(hint []int, depth, row int) string {180- start := depth - len(hint)181- if row < start {182- return "."183- }184- return strconv.Itoa(hint[row-start])185-}186-187-func min(a, b int) int {188- if a < b {189- return a190- }191- return b192-}
M hashiwokakero/PrintAdapter.go +2 -50
@@ -1,11 +1,6 @@
11 package hashiwokakero22 33 import (4- "encoding/json"5- "fmt"6- "strconv"7- "strings"8-94 "github.com/FelineStateMachine/puzzletea/game"105 "github.com/FelineStateMachine/puzzletea/pdfexport"116 "github.com/go-pdf/fpdf"
@@ -18,57 +13,14 @@ func (printAdapter) Aliases() []string {
1813 return []string{"hashi", "hashiwokakero", "hashi wokakero"}1914 }2015 21-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {22- var data Save23- if err := json.Unmarshal(save, &data); err != nil {24- return "", fmt.Errorf("decode hashiwokakero save: %w", err)25- }26-27- cells := game.MakeStringGrid(data.Width, data.Height, ".")28- for _, island := range data.Islands {29- if island.Y < 0 || island.Y >= len(cells) {30- continue31- }32- if island.X < 0 || island.X >= len(cells[island.Y]) {33- continue34- }35- cells[island.Y][island.X] = strconv.Itoa(island.Required)36- }37-38- var b strings.Builder39- b.WriteString("### Grid\n\n")40- b.WriteString(game.RenderGridTable(cells))41- b.WriteString("\n\n")42- b.WriteString("Rules: connect numbered islands with horizontal/vertical bridges. ")43- b.WriteString("Use up to two bridges per connection and never cross bridges.")44- return b.String(), nil45-}46-47-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {48- payload, err := pdfexport.ParseHashiPrintData(save)49- if err != nil {50- return nil, err51- }52- if !game.IsNilPrintPayload(payload) {53- return payload, nil54- }55- if strings.TrimSpace(snippet) == "" {56- return nil, nil57- }58-59- _, table, err := pdfexport.ParsePrintableFromSnippet("Hashiwokakero", snippet)60- if err != nil {61- return nil, err62- }63- return table, nil16+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {17+ return pdfexport.ParseHashiPrintData(save)6418 }6519 6620 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {6721 switch data := payload.(type) {6822 case *pdfexport.HashiData:6923 pdfexport.RenderHashiPage(pdf, data)70- case *pdfexport.GridTable:71- pdfexport.RenderGridTablePage(pdf, data)7224 }7325 return nil7426 }
M hitori/PrintAdapter.go +2 -71
@@ -1,10 +1,6 @@
11 package hitori22 33 import (4- "encoding/json"5- "fmt"6- "strings"7-84 "github.com/FelineStateMachine/puzzletea/game"95 "github.com/FelineStateMachine/puzzletea/pdfexport"106 "github.com/go-pdf/fpdf"
@@ -15,79 +11,14 @@ type printAdapter struct{}
1511 func (printAdapter) CanonicalGameType() string { return "Hitori" }1612 func (printAdapter) Aliases() []string { return []string{"hitori"} }1713 18-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {19- var data Save20- if err := json.Unmarshal(save, &data); err != nil {21- return "", fmt.Errorf("decode hitori save: %w", err)22- }23-24- rows := game.SplitLines(data.Numbers)25- targetHeight := data.Size26- if targetHeight < len(rows) {27- targetHeight = len(rows)28- }29- if targetHeight < 1 {30- targetHeight = 131- }32-33- cells := make([][]string, 0, targetHeight)34- for y := range targetHeight {35- row := []rune{}36- if y < len(rows) {37- row = []rune(rows[y])38- }39- targetWidth := data.Size40- if targetWidth < len(row) {41- targetWidth = len(row)42- }43- if targetWidth < 1 {44- targetWidth = 145- }46-47- cellsRow := make([]string, targetWidth)48- for x := range len(cellsRow) {49- cellsRow[x] = "."50- if x < len(row) && row[x] != ' ' {51- cellsRow[x] = string(row[x])52- }53- }54- cells = append(cells, cellsRow)55- }56-57- var b strings.Builder58- b.WriteString("### Grid\n\n")59- b.WriteString(game.RenderGridTable(cells))60- b.WriteString("\n\n")61- b.WriteString("Goal: shade cells so no row or column has duplicate unshaded values, ")62- b.WriteString("shaded cells do not touch orthogonally, and all unshaded cells stay connected.")63- return b.String(), nil64-}65-66-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {67- payload, err := pdfexport.ParseHitoriPrintData(save)68- if err != nil {69- return nil, err70- }71- if !game.IsNilPrintPayload(payload) {72- return payload, nil73- }74- if strings.TrimSpace(snippet) == "" {75- return nil, nil76- }77-78- _, table, err := pdfexport.ParsePrintableFromSnippet("Hitori", snippet)79- if err != nil {80- return nil, err81- }82- return table, nil14+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {15+ return pdfexport.ParseHitoriPrintData(save)8316 }8417 8518 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {8619 switch data := payload.(type) {8720 case *pdfexport.HitoriData:8821 pdfexport.RenderHitoriPage(pdf, data)89- case *pdfexport.GridTable:90- pdfexport.RenderGridTablePage(pdf, data)9122 }9223 return nil9324 }
M nonogram/PrintAdapter.go +2 -61
@@ -1,10 +1,6 @@
11 package nonogram22 33 import (4- "encoding/json"5- "fmt"6- "strings"7-84 "github.com/FelineStateMachine/puzzletea/game"95 "github.com/FelineStateMachine/puzzletea/pdfexport"106 "github.com/go-pdf/fpdf"
@@ -15,69 +11,14 @@ type printAdapter struct{}
1511 func (printAdapter) CanonicalGameType() string { return "Nonogram" }1612 func (printAdapter) Aliases() []string { return []string{"nonogram"} }1713 18-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {19- var data Save20- if err := json.Unmarshal(save, &data); err != nil {21- return "", fmt.Errorf("decode nonogram save: %w", err)22- }23-24- width := data.Width25- height := data.Height26- if width <= 0 {27- width = len(data.ColHints)28- }29- if height <= 0 {30- height = len(data.RowHints)31- }32- if width <= 0 || height <= 0 {33- return "### Puzzle Grid with Integrated Hints\n\n_(empty grid)_", nil34- }35-36- rowHints := game.NormalizeNonogramHints(data.RowHints, height)37- colHints := game.NormalizeNonogramHints(data.ColHints, width)38- rowHintCols := game.MaxNonogramHintLen(rowHints)39- colHintRows := game.MaxNonogramHintLen(colHints)40- if rowHintCols < 1 {41- rowHintCols = 142- }43- if colHintRows < 1 {44- colHintRows = 145- }46-47- var b strings.Builder48- b.WriteString("### Puzzle Grid with Integrated Hints\n\n")49- b.WriteString(game.RenderNonogramTable(rowHints, colHints, width, height, rowHintCols, colHintRows))50- b.WriteString("\n\n")51- b.WriteString("Row hints are right-aligned beside each row. ")52- b.WriteString("Column hints are stacked above each column and bottom-aligned to the grid.")53- return b.String(), nil54-}55-56-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {57- payload, err := pdfexport.ParseNonogramPrintData(save)58- if err != nil {59- return nil, err60- }61- if !game.IsNilPrintPayload(payload) {62- return payload, nil63- }64- if strings.TrimSpace(snippet) == "" {65- return nil, nil66- }67-68- nonogram, _, err := pdfexport.ParsePrintableFromSnippet("Nonogram", snippet)69- if err != nil {70- return nil, err71- }72- return nonogram, nil14+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {15+ return pdfexport.ParseNonogramPrintData(save)7316 }7417 7518 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {7619 switch data := payload.(type) {7720 case *pdfexport.NonogramData:7821 pdfexport.RenderNonogramPage(pdf, data)79- case *pdfexport.GridTable:80- pdfexport.RenderGridTablePage(pdf, data)8122 }8223 return nil8324 }
M nurikabe/PrintAdapter.go +2 -48
@@ -1,11 +1,6 @@
11 package nurikabe22 33 import (4- "encoding/json"5- "fmt"6- "strconv"7- "strings"8-94 "github.com/FelineStateMachine/puzzletea/game"105 "github.com/FelineStateMachine/puzzletea/pdfexport"116 "github.com/go-pdf/fpdf"
@@ -16,55 +11,14 @@ type printAdapter struct{}
1611 func (printAdapter) CanonicalGameType() string { return "Nurikabe" }1712 func (printAdapter) Aliases() []string { return []string{"nurikabe"} }1813 19-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {20- var data Save21- if err := json.Unmarshal(save, &data); err != nil {22- return "", fmt.Errorf("decode nurikabe save: %w", err)23- }24-25- clues := game.ParseCSVIntGrid(data.Clues, data.Width, data.Height)26- cells := game.MakeStringGrid(data.Width, data.Height, ".")27- for y := range len(cells) {28- for x := range len(cells[y]) {29- if y < len(clues) && x < len(clues[y]) && clues[y][x] > 0 {30- cells[y][x] = strconv.Itoa(clues[y][x])31- }32- }33- }34-35- var b strings.Builder36- b.WriteString("### Clue Grid\n\n")37- b.WriteString(game.RenderGridTable(cells))38- b.WriteString("\n\n")39- b.WriteString("Goal: build one connected sea while each numbered island has the exact size of its clue.")40- return b.String(), nil41-}42-43-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {44- payload, err := pdfexport.ParseNurikabePrintData(save)45- if err != nil {46- return nil, err47- }48- if !game.IsNilPrintPayload(payload) {49- return payload, nil50- }51- if strings.TrimSpace(snippet) == "" {52- return nil, nil53- }54-55- _, table, err := pdfexport.ParsePrintableFromSnippet("Nurikabe", snippet)56- if err != nil {57- return nil, err58- }59- return table, nil14+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {15+ return pdfexport.ParseNurikabePrintData(save)6016 }6117 6218 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {6319 switch data := payload.(type) {6420 case *pdfexport.NurikabeData:6521 pdfexport.RenderNurikabePage(pdf, data)66- case *pdfexport.GridTable:67- pdfexport.RenderGridTablePage(pdf, data)6822 }6923 return nil7024 }
M pdfexport/jsonl.go +6 -25
@@ -29,12 +29,11 @@ type JSONLPackMeta struct {
2929 }3030 3131 type JSONLPuzzle struct {32- Index int `json:"index"`33- Name string `json:"name"`34- Game string `json:"game"`35- Mode string `json:"mode"`36- Save json.RawMessage `json:"save"`37- Snippet string `json:"snippet,omitempty"`32+ Index int `json:"index"`33+ Name string `json:"name"`34+ Game string `json:"game"`35+ Mode string `json:"mode"`36+ Save json.RawMessage `json:"save"`3837 }3938 4039 func ParseJSONLFiles(paths []string) ([]PackDocument, error) {
@@ -111,7 +110,6 @@ func ParseJSONLFile(path string) (PackDocument, error) {
111110 ModeSelection: mode,112111 Name: record.Puzzle.Name,113112 Index: record.Puzzle.Index,114- Body: record.Puzzle.Snippet,115113 SaveData: append([]byte(nil), record.Puzzle.Save...),116114 }117115
@@ -119,7 +117,7 @@ func ParseJSONLFile(path string) (PackDocument, error) {
119117 if !ok {120118 continue121119 }122- payload, err := adapter.BuildPDFPayload(p.SaveData, record.Puzzle.Snippet)120+ payload, err := adapter.BuildPDFPayload(p.SaveData)123121 if err != nil {124122 return PackDocument{}, fmt.Errorf("%s:%d: build print payload: %w", path, lineNo, err)125123 }
@@ -143,20 +141,3 @@ func ParseJSONLFile(path string) (PackDocument, error) {
143141 doc.Puzzles = puzzles144142 return doc, nil145143 }146-147-func ParsePrintableFromSnippet(category, snippet string) (*NonogramData, *GridTable, error) {148- lines := strings.Split(strings.ReplaceAll(strings.ReplaceAll(snippet, "\r\n", "\n"), "\r", "\n"), "\n")149- if strings.EqualFold(strings.TrimSpace(category), "nonogram") {150- nonogram, err := parseNonogramBody(lines, "snippet", 1)151- if err != nil {152- return nil, nil, err153- }154- return nonogram, nil, nil155- }156-157- table, err := parseGridTableBody(lines, "snippet", 1)158- if err != nil {159- return nil, nil, err160- }161- return nil, table, nil162-}
M pdfexport/jsonl_test.go +14 -93
@@ -4,7 +4,6 @@ import (
44 "encoding/json"55 "os"66 "path/filepath"7- "reflect"87 "strings"98 "sync"109 "testing"
@@ -30,12 +29,7 @@ func TestParseJSONLFile(t *testing.T) {
3029 Name: "ember-newt",3130 Game: "Nonogram",3231 Mode: "Mini",33- Save: json.RawMessage(`{"width":2}`),34- Snippet: "### Puzzle Grid with Integrated Hints\n\n" +35- "| R1 | C1 | C2 |\n" +36- "| --- | --- | --- |\n" +37- "| . | 1 | 2 |\n" +38- "| 1 | . | . |\n",32+ Save: json.RawMessage(`{"width":2,"height":2,"row-hints":[[1],[1]],"col-hints":[[1],[1]],"state":" \n "}`),3933 },4034 }4135 writeSingleJSONLRecord(t, path, record)
@@ -49,7 +43,7 @@ func TestParseJSONLFile(t *testing.T) {
4943 }5044 payload, ok := doc.Puzzles[0].PrintPayload.(*NonogramData)5145 if !ok || payload == nil {52- t.Fatal("expected nonogram print payload from snippet fallback")46+ t.Fatal("expected nonogram print payload from save hydration")5347 }5448 }5549
@@ -328,43 +322,8 @@ func TestParseJSONLFileHydratesHashiFromSave(t *testing.T) {
328322 }329323 }330324 331-func TestParseJSONLFileIgnoresMalformedSnippetWhenSaveHydrated(t *testing.T) {332- path := filepath.Join(t.TempDir(), "sudoku-malformed-snippet.jsonl")333- record := JSONLRecord{334- Schema: ExportSchemaV1,335- Pack: JSONLPackMeta{336- Generated: "2026-02-22T10:00:00Z",337- Version: "v-test",338- Category: "Sudoku",339- ModeSelection: "Easy",340- Count: 1,341- },342- Puzzle: JSONLPuzzle{343- Index: 1,344- Name: "sage-briar",345- Game: "Sudoku",346- Mode: "Easy",347- Save: json.RawMessage(`{"provided":[{"x":0,"y":0,"v":5}]}`),348- Snippet: "| bad |\n",349- },350- }351- writeSingleJSONLRecord(t, path, record)352-353- doc, err := ParseJSONLFile(path)354- if err != nil {355- t.Fatalf("expected lenient parse when save hydration succeeds, got: %v", err)356- }357- if got, want := len(doc.Puzzles), 1; got != want {358- t.Fatalf("puzzles = %d, want %d", got, want)359- }360- payload, ok := doc.Puzzles[0].PrintPayload.(*SudokuData)361- if !ok || payload == nil {362- t.Fatal("expected sudoku print payload from save hydration")363- }364-}365-366325 func TestParseJSONLFileSilentlySkipsUnsupportedGame(t *testing.T) {367- path := filepath.Join(t.TempDir(), "lights-malformed-snippet.jsonl")326+ path := filepath.Join(t.TempDir(), "lights.jsonl")368327 record := JSONLRecord{369328 Schema: ExportSchemaV1,370329 Pack: JSONLPackMeta{
@@ -375,12 +334,11 @@ func TestParseJSONLFileSilentlySkipsUnsupportedGame(t *testing.T) {
375334 Count: 1,376335 },377336 Puzzle: JSONLPuzzle{378- Index: 1,379- Name: "glow-shore",380- Game: "Lights Out",381- Mode: "Standard",382- Save: json.RawMessage(`{"size":5}`),383- Snippet: "| bad |\n",337+ Index: 1,338+ Name: "glow-shore",339+ Game: "Lights Out",340+ Mode: "Standard",341+ Save: json.RawMessage(`{"size":5}`),384342 },385343 }386344 writeSingleJSONLRecord(t, path, record)
@@ -413,7 +371,7 @@ func init() {
413371 414372 func ensureJSONLTestAdapters() {415373 registerJSONLAdaptersOnce.Do(func() {416- register := func(category string, build func(save []byte, snippet string) (any, error), aliases ...string) {374+ register := func(category string, build func(save []byte) (any, error), aliases ...string) {417375 game.RegisterPrintAdapter(jsonlTestAdapter{418376 category: category,419377 aliases: aliases,
@@ -445,56 +403,19 @@ func ensureJSONLTestAdapters() {
445403 })446404 }447405 448-func buildPayloadAdapter(category string, parse func(save []byte) (any, error)) func([]byte, string) (any, error) {449- return func(save []byte, snippet string) (any, error) {450- payload, err := parse(save)451- if err != nil {452- return nil, err453- }454- if !isNilAny(payload) {455- return payload, nil456- }457- if strings.TrimSpace(snippet) == "" {458- return nil, nil459- }460-461- nonogram, table, err := ParsePrintableFromSnippet(category, snippet)462- if err != nil {463- return nil, err464- }465- if nonogram != nil {466- return nonogram, nil467- }468- return table, nil469- }470-}471-472-func isNilAny(v any) bool {473- if v == nil {474- return true475- }476- rv := reflect.ValueOf(v)477- switch rv.Kind() {478- case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:479- return rv.IsNil()480- default:481- return false482- }406+func buildPayloadAdapter(_ string, parse func(save []byte) (any, error)) func([]byte) (any, error) {407+ return parse483408 }484409 485410 type jsonlTestAdapter struct {486411 category string487412 aliases []string488- build func(save []byte, snippet string) (any, error)413+ build func(save []byte) (any, error)489414 }490415 491416 func (a jsonlTestAdapter) CanonicalGameType() string { return a.category }492417 func (a jsonlTestAdapter) Aliases() []string { return a.aliases }493-func (a jsonlTestAdapter) RenderMarkdownSnippet([]byte) (string, error) {494- return "", nil495-}496-497-func (a jsonlTestAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {498- return a.build(save, snippet)418+func (a jsonlTestAdapter) BuildPDFPayload(save []byte) (any, error) {419+ return a.build(save)499420 }500421 func (a jsonlTestAdapter) RenderPDFBody(*fpdf.Fpdf, any) error { return nil }
M pdfexport/render_error_test.go +3 -6
M shikaku/PrintAdapter.go +2 -49
@@ -1,11 +1,6 @@
11 package shikaku22 33 import (4- "encoding/json"5- "fmt"6- "strconv"7- "strings"8-94 "github.com/FelineStateMachine/puzzletea/game"105 "github.com/FelineStateMachine/puzzletea/pdfexport"116 "github.com/go-pdf/fpdf"
@@ -16,56 +11,14 @@ type printAdapter struct{}
1611 func (printAdapter) CanonicalGameType() string { return "Shikaku" }1712 func (printAdapter) Aliases() []string { return []string{"shikaku"} }1813 19-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {20- var data Save21- if err := json.Unmarshal(save, &data); err != nil {22- return "", fmt.Errorf("decode shikaku save: %w", err)23- }24-25- cells := game.MakeStringGrid(data.Width, data.Height, ".")26- for _, clue := range data.Clues {27- if clue.Y < 0 || clue.Y >= len(cells) {28- continue29- }30- if clue.X < 0 || clue.X >= len(cells[clue.Y]) {31- continue32- }33- cells[clue.Y][clue.X] = strconv.Itoa(clue.Value)34- }35-36- var b strings.Builder37- b.WriteString("### Clue Grid\n\n")38- b.WriteString(game.RenderGridTable(cells))39- b.WriteString("\n\n")40- b.WriteString("Goal: partition the grid into rectangles so each rectangle contains one clue and its area matches that clue.")41- return b.String(), nil42-}43-44-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {45- payload, err := pdfexport.ParseShikakuPrintData(save)46- if err != nil {47- return nil, err48- }49- if !game.IsNilPrintPayload(payload) {50- return payload, nil51- }52- if strings.TrimSpace(snippet) == "" {53- return nil, nil54- }55-56- _, table, err := pdfexport.ParsePrintableFromSnippet("Shikaku", snippet)57- if err != nil {58- return nil, err59- }60- return table, nil14+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {15+ return pdfexport.ParseShikakuPrintData(save)6116 }6217 6318 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {6419 switch data := payload.(type) {6520 case *pdfexport.ShikakuData:6621 pdfexport.RenderShikakuPage(pdf, data)67- case *pdfexport.GridTable:68- pdfexport.RenderGridTablePage(pdf, data)6922 }7023 return nil7124 }
M sudoku/PrintAdapter.go +2 -48
@@ -1,10 +1,6 @@
11 package sudoku22 33 import (4- "encoding/json"5- "fmt"6- "strings"7-84 "github.com/FelineStateMachine/puzzletea/game"95 "github.com/FelineStateMachine/puzzletea/pdfexport"106 "github.com/go-pdf/fpdf"
@@ -15,56 +11,14 @@ type printAdapter struct{}
1511 func (printAdapter) CanonicalGameType() string { return "Sudoku" }1612 func (printAdapter) Aliases() []string { return []string{"sudoku"} }1713 18-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {19- var data Save20- if err := json.Unmarshal(save, &data); err != nil {21- return "", fmt.Errorf("decode sudoku save: %w", err)22- }23-24- cells := game.MakeStringGrid(9, 9, ".")25- for _, provided := range data.Provided {26- if provided.Y < 0 || provided.Y >= len(cells) {27- continue28- }29- if provided.X < 0 || provided.X >= len(cells[provided.Y]) {30- continue31- }32- cells[provided.Y][provided.X] = fmt.Sprintf("%d", provided.V)33- }34-35- var b strings.Builder36- b.WriteString("### Given Grid\n\n")37- b.WriteString(game.RenderGridTable(cells))38- b.WriteString("\n\n")39- b.WriteString("Goal: fill each row, column, and 3x3 box with digits 1-9 exactly once.")40- return b.String(), nil41-}42-43-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {44- payload, err := pdfexport.ParseSudokuPrintData(save)45- if err != nil {46- return nil, err47- }48- if !game.IsNilPrintPayload(payload) {49- return payload, nil50- }51- if strings.TrimSpace(snippet) == "" {52- return nil, nil53- }54-55- _, table, err := pdfexport.ParsePrintableFromSnippet("Sudoku", snippet)56- if err != nil {57- return nil, err58- }59- return table, nil14+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {15+ return pdfexport.ParseSudokuPrintData(save)6016 }6117 6218 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {6319 switch data := payload.(type) {6420 case *pdfexport.SudokuData:6521 pdfexport.RenderSudokuPage(pdf, data)66- case *pdfexport.GridTable:67- pdfexport.RenderGridTablePage(pdf, data)6822 }6923 return nil7024 }
M takuzu/PrintAdapter.go +2 -57
@@ -1,10 +1,6 @@
11 package takuzu22 33 import (4- "encoding/json"5- "fmt"6- "strings"7-84 "github.com/FelineStateMachine/puzzletea/game"95 "github.com/FelineStateMachine/puzzletea/pdfexport"106 "github.com/go-pdf/fpdf"
@@ -15,65 +11,14 @@ type printAdapter struct{}
1511 func (printAdapter) CanonicalGameType() string { return "Takuzu" }1612 func (printAdapter) Aliases() []string { return []string{"takuzu"} }1713 18-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {19- var data Save20- if err := json.Unmarshal(save, &data); err != nil {21- return "", fmt.Errorf("decode takuzu save: %w", err)22- }23-24- cells := game.MakeStringGrid(data.Size, data.Size, ".")25- stateRows := game.SplitLines(data.State)26- providedRows := game.SplitLines(data.Provided)27- for y := range len(cells) {28- for x := range len(cells[y]) {29- provided := y < len(providedRows) && x < len(providedRows[y]) && providedRows[y][x] == '#'30- if !provided {31- continue32- }33- if y >= len(stateRows) || x >= len(stateRows[y]) {34- continue35- }36- switch stateRows[y][x] {37- case '0', '1':38- cells[y][x] = string(stateRows[y][x])39- }40- }41- }42-43- var b strings.Builder44- b.WriteString("### Given Grid\n\n")45- b.WriteString(game.RenderGridTable(cells))46- b.WriteString("\n\n")47- b.WriteString("Goal: fill with 0/1 so no three equal adjacent cells appear, ")48- b.WriteString("each row/column has equal 0 and 1 counts, and rows/columns are unique.")49- return b.String(), nil50-}51-52-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {53- payload, err := pdfexport.ParseTakuzuPrintData(save)54- if err != nil {55- return nil, err56- }57- if !game.IsNilPrintPayload(payload) {58- return payload, nil59- }60- if strings.TrimSpace(snippet) == "" {61- return nil, nil62- }63-64- _, table, err := pdfexport.ParsePrintableFromSnippet("Takuzu", snippet)65- if err != nil {66- return nil, err67- }68- return table, nil14+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {15+ return pdfexport.ParseTakuzuPrintData(save)6916 }7017 7118 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {7219 switch data := payload.(type) {7320 case *pdfexport.TakuzuData:7421 pdfexport.RenderTakuzuPage(pdf, data)75- case *pdfexport.GridTable:76- pdfexport.RenderGridTablePage(pdf, data)7722 }7823 return nil7924 }
M wordsearch/PrintAdapter.go +2 -71
@@ -1,10 +1,6 @@
11 package wordsearch22 33 import (4- "encoding/json"5- "fmt"6- "strings"7-84 "github.com/FelineStateMachine/puzzletea/game"95 "github.com/FelineStateMachine/puzzletea/pdfexport"106 "github.com/go-pdf/fpdf"
@@ -17,79 +13,14 @@ func (printAdapter) Aliases() []string {
1713 return []string{"word search", "wordsearch"}1814 }1915 20-func (printAdapter) RenderMarkdownSnippet(save []byte) (string, error) {21- var data Save22- if err := json.Unmarshal(save, &data); err != nil {23- return "", fmt.Errorf("decode word search save: %w", err)24- }25-26- rows := game.SplitLines(data.Grid)27- height := data.Height28- if height < len(rows) {29- height = len(rows)30- }31- width := data.Width32- for _, row := range rows {33- if n := len([]rune(row)); n > width {34- width = n35- }36- }37-38- cells := game.MakeStringGrid(width, height, ".")39- for y := range len(cells) {40- if y >= len(rows) {41- continue42- }43- rowRunes := []rune(rows[y])44- for x := range len(cells[y]) {45- if x < len(rowRunes) {46- cells[y][x] = string(rowRunes[x])47- }48- }49- }50-51- var b strings.Builder52- b.WriteString("### Grid\n\n")53- b.WriteString(game.RenderGridTable(cells))54- b.WriteString("\n\n")55- b.WriteString("### Word List\n\n")56- b.WriteString("| # | Word |\n")57- b.WriteString("| --- | --- |\n")58- for i, word := range data.Words {59- fmt.Fprintf(&b, "| %d | %s |\n", i+1, game.EscapeMarkdownCell(word.Text))60- }61- if len(data.Words) == 0 {62- b.WriteString("| 1 | (none) |\n")63- }64- b.WriteString("\nGoal: find all listed words in the grid.")65- return b.String(), nil66-}67-68-func (printAdapter) BuildPDFPayload(save []byte, snippet string) (any, error) {69- payload, err := pdfexport.ParseWordSearchPrintData(save)70- if err != nil {71- return nil, err72- }73- if !game.IsNilPrintPayload(payload) {74- return payload, nil75- }76- if strings.TrimSpace(snippet) == "" {77- return nil, nil78- }79-80- _, table, err := pdfexport.ParsePrintableFromSnippet("Word Search", snippet)81- if err != nil {82- return nil, err83- }84- return table, nil16+func (printAdapter) BuildPDFPayload(save []byte) (any, error) {17+ return pdfexport.ParseWordSearchPrintData(save)8518 }8619 8720 func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {8821 switch data := payload.(type) {8922 case *pdfexport.WordSearchData:9023 pdfexport.RenderWordSearchPage(pdf, data)91- case *pdfexport.GridTable:92- pdfexport.RenderGridTablePage(pdf, data)9324 }9425 return nil9526 }