1package spellpuzzle23import (4 "encoding/json"5 "fmt"6 "strings"78 "codeberg.org/go-pdf/fpdf"9 "github.com/FelineStateMachine/puzzletea/export/pdf"10)1112type printAdapter struct{}1314var PDFPrintAdapter = printAdapter{}1516type printPayload struct {17 Board board18 Placements []WordPlacement19 Bank []rune20}2122type printLayout struct {23 cellSize float6424 boardX float6425 boardY float6426 tileSize float6427 tileGap float6428 bankX float6429 bankY float6430}3132func (printAdapter) CanonicalGameType() string { return "Spell Puzzle" }33func (printAdapter) Aliases() []string {34 return []string{"spell", "spellpuzzle"}35}3637func (printAdapter) BuildPDFPayload(save []byte) (any, error) {38 if len(strings.TrimSpace(string(save))) == 0 {39 return nil, nil40 }4142 var exported Save43 if err := json.Unmarshal(save, &exported); err != nil {44 return nil, fmt.Errorf("decode spell puzzle save: %w", err)45 }4647 placements := append([]WordPlacement(nil), exported.Placements...)48 for i := range placements {49 placements[i].Found = false50 }51 placements = normalizePlacements(placements)52 if len(placements) == 0 {53 return nil, nil54 }5556 board := buildBoard(placements)57 if board.Width <= 0 || board.Height <= 0 {58 return nil, nil59 }6061 return &printPayload{62 Board: board,63 Placements: placements,64 Bank: []rune(exported.Bank),65 }, nil66}6768func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {69 switch data := payload.(type) {70 case *printPayload:71 renderSpellPuzzlePage(pdf, data)72 }73 return nil74}7576func renderSpellPuzzlePage(pdf *fpdf.Fpdf, data *printPayload) {77 if data == nil || data.Board.Width <= 0 || data.Board.Height <= 0 {78 return79 }8081 pageW, pageH := pdf.GetPageSize()82 pageNo := pdf.PageNo()83 body := pdfexport.PuzzleBodyRect(pageW, pageH, pageNo)84 rules := []string{"Form words from the letter bank to fill the crossword"}85 ruleLines := pdfexport.InstructionLineCount(pdf, body.W, rules)86 area := pdfexport.PuzzleBoardRect(pageW, pageH, pageNo, ruleLines)87 layout, ok := computePrintLayout(area, data)88 if !ok {89 return90 }9192 drawSpellPuzzleBoard(pdf, data.Board, layout)93 drawSpellPuzzleBank(pdf, data.Bank, layout)9495 contentBottom := layout.bankY + layout.tileSize96 ruleY := pdfexport.InstructionY(contentBottom, pageH, ruleLines)97 pdfexport.RenderInstructions(pdf, body.X, ruleY, body.W, rules)98}99100func computePrintLayout(area pdfexport.Rect, data *printPayload) (printLayout, bool) {101 if data == nil || data.Board.Width <= 0 || data.Board.Height <= 0 {102 return printLayout{}, false103 }104105 letterCount := max(len(data.Bank), 1)106 tileGap := 2.5107 tileSize := min(9.5, (area.W-float64(letterCount-1)*tileGap)/float64(letterCount))108 if tileSize < 5.2 {109 tileGap = 1.4110 tileSize = (area.W - float64(letterCount-1)*tileGap) / float64(letterCount)111 }112 if tileSize <= 0 {113 return printLayout{}, false114 }115116 bankSectionHeight := tileSize + 6.0117 boardArea := pdfexport.Rect{118 X: area.X,119 Y: area.Y,120 W: area.W,121 H: area.H - bankSectionHeight - 4.0,122 }123 if boardArea.H <= 0 {124 return printLayout{}, false125 }126127 cellSize := pdfexport.FitCompactCellSize(data.Board.Width, data.Board.Height, boardArea)128 if cellSize <= 0 {129 return printLayout{}, false130 }131132 boardX, boardY := pdfexport.CenteredOrigin(boardArea, data.Board.Width, data.Board.Height, cellSize)133 bankWidth := float64(len(data.Bank))*tileSize + float64(max(len(data.Bank)-1, 0))*tileGap134 bankX := area.X + (area.W-bankWidth)/2135 bankY := boardArea.Y + boardArea.H + 4.0 + (bankSectionHeight-tileSize)/2136137 return printLayout{138 cellSize: cellSize,139 boardX: boardX,140 boardY: boardY,141 tileSize: tileSize,142 tileGap: tileGap,143 bankX: bankX,144 bankY: bankY,145 }, true146}147148func drawSpellPuzzleBoard(pdf *fpdf.Fpdf, b board, layout printLayout) {149 pdf.SetFillColor(255, 255, 255)150 for y := range b.Height {151 for x := range b.Width {152 cell := b.Cells[y][x]153 if !cell.Occupied {154 continue155 }156 cellX := layout.boardX + float64(x)*layout.cellSize157 cellY := layout.boardY + float64(y)*layout.cellSize158 pdf.Rect(cellX, cellY, layout.cellSize, layout.cellSize, "F")159 }160 }161162 pdf.SetDrawColor(150, 150, 150)163 pdf.SetLineWidth(pdfexport.ThinGridLineMM)164 for y := range b.Height {165 for x := range b.Width {166 cell := b.Cells[y][x]167 if !cell.Occupied {168 continue169 }170 cellX := layout.boardX + float64(x)*layout.cellSize171 cellY := layout.boardY + float64(y)*layout.cellSize172 pdf.Rect(cellX, cellY, layout.cellSize, layout.cellSize, "D")173 }174 }175176 pdf.SetDrawColor(25, 25, 25)177 pdf.SetLineWidth(pdfexport.MajorGridLineMM)178 for y := range b.Height {179 for x := range b.Width {180 cell := b.Cells[y][x]181 if !cell.Occupied {182 continue183 }184185 cellX := layout.boardX + float64(x)*layout.cellSize186 cellY := layout.boardY + float64(y)*layout.cellSize187 if b.hasHorizontalEdge(x, y) {188 pdf.Line(cellX, cellY, cellX+layout.cellSize, cellY)189 }190 if b.hasVerticalEdge(x, y) {191 pdf.Line(cellX, cellY, cellX, cellY+layout.cellSize)192 }193 if b.hasHorizontalEdge(x, y+1) {194 pdf.Line(cellX, cellY+layout.cellSize, cellX+layout.cellSize, cellY+layout.cellSize)195 }196 if b.hasVerticalEdge(x+1, y) {197 pdf.Line(cellX+layout.cellSize, cellY, cellX+layout.cellSize, cellY+layout.cellSize)198 }199 }200 }201}202203func drawSpellPuzzleBank(pdf *fpdf.Fpdf, bank []rune, layout printLayout) {204 if len(bank) == 0 {205 return206 }207208 fontSize := pdfexport.ClampStandardCellFontSize(pdfexport.StandardCellFontSize(layout.tileSize, 0.56))209 lineH := fontSize * 0.9210 pdf.SetFont(pdfexport.SansFontFamily, "B", fontSize)211 pdf.SetTextColor(pdfexport.PrimaryTextGray, pdfexport.PrimaryTextGray, pdfexport.PrimaryTextGray)212 pdf.SetDrawColor(45, 45, 45)213 pdf.SetLineWidth(pdfexport.ThinGridLineMM)214215 for i, letter := range bank {216 x := layout.bankX + float64(i)*(layout.tileSize+layout.tileGap)217 pdf.Rect(x, layout.bankY, layout.tileSize, layout.tileSize, "D")218 pdf.SetXY(x, layout.bankY+(layout.tileSize-lineH)/2)219 pdf.CellFormat(layout.tileSize, lineH, strings.ToUpper(string(letter)), "", 0, "C", false, 0, "")220 }221}