clone

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

main
puzzletea / games / wordsearch / print_adapter.go
print_adapter.go 6.9K
1package wordsearch23import (4 "strings"56 "codeberg.org/go-pdf/fpdf"7 "github.com/FelineStateMachine/puzzletea/export/pdf"8)910type printAdapter struct{}1112var PDFPrintAdapter = printAdapter{}1314const (15 wordBankHeaderHeight = 9.016 wordBankListTopGap = 2.217)1819func (printAdapter) CanonicalGameType() string { return "Word Search" }20func (printAdapter) Aliases() []string {21 return []string{"word search", "wordsearch"}22}2324func (printAdapter) BuildPDFPayload(save []byte) (any, error) {25 return pdfexport.ParseWordSearchPrintData(save)26}2728func (printAdapter) RenderPDFBody(pdf *fpdf.Fpdf, payload any) error {29 switch data := payload.(type) {30 case *pdfexport.WordSearchData:31 renderWordSearchPage(pdf, data)32 }33 return nil34}3536func renderWordSearchPage(pdf *fpdf.Fpdf, data *pdfexport.WordSearchData) {37 if data == nil || data.Width <= 0 || data.Height <= 0 {38 return39 }4041 pageW, pageH := pdf.GetPageSize()42 pageNo := pdf.PageNo()43 body := pdfexport.PuzzleBodyRect(pageW, pageH, pageNo)44 availW := body.W45 availH := body.H4647 columnCount := wordSearchColumnCount(data.Width, len(data.Words))48 wordFontSize := pdfexport.PuzzleWordBankFontSize49 wordLineHeight := 4.250 gridListGap := pdfexport.WordSearchGridGapMM5152 estimatedWordLines := estimateWordBankLineCount(pdf, data.Words, columnCount, availW, wordFontSize)53 wordBankHeight := wordBankHeaderHeight + wordBankListTopGap + float64(estimatedWordLines)*wordLineHeight54 maxWordBankHeight := availH * 0.4255 if wordBankHeight > maxWordBankHeight {56 wordBankHeight = maxWordBankHeight57 }58 if wordBankHeight < 16 {59 wordBankHeight = 1660 }6162 gridAreaH := availH - wordBankHeight - gridListGap63 if gridAreaH < availH*0.5 {64 gridAreaH = availH * 0.565 }6667 gridArea := pdfexport.Rect{X: body.X, Y: body.Y, W: availW, H: gridAreaH}68 cellSize := pdfexport.FitWordSearchCellSize(data.Width, data.Height, gridArea)69 if cellSize <= 0 {70 return71 }7273 gridW := float64(data.Width) * cellSize74 gridH := float64(data.Height) * cellSize75 gridX := body.X + (gridArea.W-gridW)/276 gridY := body.Y + (gridArea.H-gridH)/27778 drawWordSearchGrid(pdf, data, gridX, gridY, cellSize)79 drawWordBank(80 pdf,81 data.Words,82 body.X,83 gridY+gridH+gridListGap,84 availW,85 pageH-pdfexport.PuzzleBottomInsetMM-(gridY+gridH+gridListGap),86 columnCount,87 )88}8990func drawWordSearchGrid(pdf *fpdf.Fpdf, data *pdfexport.WordSearchData, startX, startY, cellSize float64) {91 pdf.SetDrawColor(45, 45, 45)92 pdf.SetLineWidth(pdfexport.ThinGridLineMM)93 for y := range data.Height {94 for x := range data.Width {95 cellX := startX + float64(x)*cellSize96 cellY := startY + float64(y)*cellSize97 pdf.Rect(cellX, cellY, cellSize, cellSize, "D")9899 cellText := " "100 if y < len(data.Grid) && x < len(data.Grid[y]) {101 cellText = strings.TrimSpace(strings.ToUpper(data.Grid[y][x]))102 }103 if cellText == "" || cellText == "." {104 continue105 }106107 fontSize := pdfexport.StandardCellFontSize(cellSize, 0.74)108 lineH := fontSize * 0.86109 pdf.SetFont(pdfexport.SansFontFamily, "B", fontSize)110 pdf.SetTextColor(pdfexport.PrimaryTextGray, pdfexport.PrimaryTextGray, pdfexport.PrimaryTextGray)111 pdf.SetXY(cellX, cellY+(cellSize-lineH)/2)112 pdf.CellFormat(cellSize, lineH, cellText, "", 0, "C", false, 0, "")113 }114 }115116 pdf.SetLineWidth(pdfexport.OuterBorderLineMM)117 pdf.Rect(startX, startY, float64(data.Width)*cellSize, float64(data.Height)*cellSize, "D")118}119120func drawWordBank(pdf *fpdf.Fpdf, words []string, x, y, width, height float64, columns int) {121 if height <= 0 {122 return123 }124125 pdf.SetTextColor(40, 40, 40)126 pdf.SetFont(pdfexport.SansFontFamily, "B", pdfexport.PuzzleWordBankHeadSize)127 pdf.SetXY(x, y)128 pdf.CellFormat(width, 4.8, "Word Bank", "", 0, "L", false, 0, "")129130 pdf.SetFont(pdfexport.SansFontFamily, "", pdfexport.PuzzleWordBankFontSize)131 pdf.SetTextColor(pdfexport.RuleTextGray, pdfexport.RuleTextGray, pdfexport.RuleTextGray)132 pdf.SetXY(x, y+4.8)133 pdf.CellFormat(width, 4.2, "Words may run in all 8 directions", "", 0, "L", false, 0, "")134135 listY := y + wordBankHeaderHeight + wordBankListTopGap136 if len(words) == 0 {137 pdf.SetFont(pdfexport.SansFontFamily, "", pdfexport.PuzzleWordBankHeadSize)138 pdf.SetTextColor(pdfexport.SecondaryTextGray, pdfexport.SecondaryTextGray, pdfexport.SecondaryTextGray)139 pdf.SetXY(x, listY)140 pdf.CellFormat(width, 4.6, "(word list unavailable)", "", 0, "L", false, 0, "")141 return142 }143144 columnGap := 4.0145 if columns < 1 {146 columns = 1147 }148 colWidth := (width - float64(columns-1)*columnGap) / float64(columns)149 if colWidth <= 0 {150 colWidth = width151 columns = 1152 }153154 colLines := layoutWordBankColumns(pdf, words, columns, colWidth)155 lineHeight := 4.1156 maxLines := int((height - (listY - y)) / lineHeight)157 if maxLines <= 0 {158 return159 }160161 pdf.SetTextColor(pdfexport.PrimaryTextGray, pdfexport.PrimaryTextGray, pdfexport.PrimaryTextGray)162 pdf.SetFont(pdfexport.SansFontFamily, "B", pdfexport.PuzzleWordBankHeadSize)163 for c := range columns {164 colX := x + float64(c)*(colWidth+columnGap)165 curY := listY166 lines := colLines[c]167 for _, line := range lines {168 if int((curY-listY)/lineHeight) >= maxLines {169 break170 }171 pdf.SetXY(colX, curY)172 pdf.CellFormat(colWidth, lineHeight, line, "", 0, "L", false, 0, "")173 curY += lineHeight174 }175 }176}177178func wordSearchColumnCount(width, wordCount int) int {179 if width >= 20 || wordCount > 18 {180 return 3181 }182 if wordCount <= 0 {183 return 1184 }185 return 2186}187188func estimateWordBankLineCount(pdf *fpdf.Fpdf, words []string, columns int, availW, fontSize float64) int {189 if len(words) == 0 {190 return 1191 }192 if columns < 1 {193 columns = 1194 }195196 colWidth := (availW - float64(columns-1)*4.0) / float64(columns)197 if colWidth <= 0 {198 colWidth = availW199 }200201 pdf.SetFont(pdfexport.SansFontFamily, "", fontSize)202 lineCounts := make([]int, columns)203 for _, word := range words {204 text := strings.ToUpper(strings.TrimSpace(word))205 if text == "" {206 continue207 }208 lines := pdf.SplitLines([]byte(text), colWidth)209 if len(lines) == 0 {210 lines = [][]byte{[]byte(text)}211 }212 idx := minLineCountColumn(lineCounts)213 lineCounts[idx] += len(lines)214 }215216 maxLines := 0217 for _, lineCount := range lineCounts {218 if lineCount > maxLines {219 maxLines = lineCount220 }221 }222 if maxLines == 0 {223 return 1224 }225 return maxLines226}227228func layoutWordBankColumns(pdf *fpdf.Fpdf, words []string, columns int, colWidth float64) [][]string {229 colLines := make([][]string, columns)230 if len(words) == 0 || columns <= 0 {231 return colLines232 }233234 lineCounts := make([]int, columns)235 for _, word := range words {236 text := strings.ToUpper(strings.TrimSpace(word))237 if text == "" {238 continue239 }240 wrapped := pdf.SplitLines([]byte(text), colWidth)241 if len(wrapped) == 0 {242 wrapped = [][]byte{[]byte(text)}243 }244245 idx := minLineCountColumn(lineCounts)246 for _, line := range wrapped {247 colLines[idx] = append(colLines[idx], string(line))248 }249 lineCounts[idx] += len(wrapped)250 }251252 return colLines253}254255func minLineCountColumn(lineCounts []int) int {256 idx := 0257 for i := 1; i < len(lineCounts); i++ {258 if lineCounts[i] < lineCounts[idx] {259 idx = i260 }261 }262 return idx263}