1package packexport23import (4 "fmt"5 "strings"6 "time"78 "github.com/FelineStateMachine/puzzletea/export/pdf"9 "github.com/FelineStateMachine/puzzletea/puzzle"10)1112type RenderOptions struct {13 Title string14 Header string15 Advert string16 Volume int17 SheetLayout string18 ShuffleSeed string19 GeneratedAt time.Time20}2122func BuildRenderConfig(options RenderOptions, _ []pdfexport.PackDocument) (pdfexport.RenderConfig, error) {23 if options.Volume < 1 {24 return pdfexport.RenderConfig{}, fmt.Errorf("volume must be >= 1")25 }2627 layout, err := parseSheetLayout(options.SheetLayout)28 if err != nil {29 return pdfexport.RenderConfig{}, err30 }3132 subtitle := strings.TrimSpace(options.Title)33 if subtitle == "" {34 subtitle = "PuzzleTea Mixed Puzzle Pack"35 }3637 cfg := pdfexport.RenderConfig{38 CoverSubtitle: subtitle,39 HeaderText: strings.TrimSpace(options.Header),40 VolumeNumber: options.Volume,41 AdvertText: strings.TrimSpace(options.Advert),42 GeneratedAt: options.GeneratedAt,43 ShuffleSeed: strings.TrimSpace(options.ShuffleSeed),44 SheetLayout: layout,45 }46 if cfg.AdvertText == "" {47 cfg.AdvertText = DefaultAdvert48 }49 return cfg, nil50}5152func AnnotatePuzzlesForPrint(puzzles []pdfexport.Puzzle, definitions []puzzle.Definition) {53 lookup := buildModeDifficultyLookup(definitions)54 for i := range puzzles {55 mode := normalizeDifficultyToken(puzzles[i].ModeSelection)56 if mode == "" || strings.Contains(mode, "mixed modes") {57 puzzles[i].DifficultyScore = 0.558 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium59 puzzles[i].DifficultySource = "mixed-mode fallback"60 continue61 }6263 category := normalizeDifficultyToken(puzzles[i].Category)64 modes, ok := lookup[category]65 if !ok {66 puzzles[i].DifficultyScore = 0.567 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium68 puzzles[i].DifficultySource = "category lookup fallback"69 continue70 }7172 score, ok := modes[mode]73 if !ok {74 puzzles[i].DifficultyScore = 0.575 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium76 puzzles[i].DifficultySource = "mode lookup fallback"77 continue78 }7980 puzzles[i].DifficultyScore = score81 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceHigh82 puzzles[i].DifficultySource = "mode-order"83 }84}8586func parseSheetLayout(raw string) (pdfexport.SheetLayout, error) {87 switch strings.ToLower(strings.TrimSpace(raw)) {88 case "", "half-letter":89 return pdfexport.SheetLayoutHalfLetter, nil90 case "duplex-booklet":91 return pdfexport.SheetLayoutDuplexBooklet, nil92 default:93 return pdfexport.SheetLayoutHalfLetter, fmt.Errorf("--sheet-layout must be half-letter or duplex-booklet")94 }95}9697func buildModeDifficultyLookup(definitions []puzzle.Definition) map[string]map[string]float64 {98 lookup := make(map[string]map[string]float64, len(definitions))99 for _, def := range definitions {100 titles := []string{}101 for _, mode := range def.Modes {102 title := strings.TrimSpace(mode.Title)103 if title == "" {104 continue105 }106 titles = append(titles, title)107 }108 if len(titles) == 0 {109 continue110 }111112 scores := make(map[string]float64, len(titles))113 if len(titles) == 1 {114 scores[normalizeDifficultyToken(titles[0])] = 0.5115 } else {116 for i, title := range titles {117 scores[normalizeDifficultyToken(title)] = float64(i) / float64(len(titles)-1)118 }119 }120 lookup[normalizeDifficultyToken(def.Name)] = scores121 }122 return lookup123}