clone

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

main
puzzletea / export / pack / render.go
render.go 4.4K
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 if puzzles[i].ActualDifficultyElo != nil || puzzles[i].TargetDifficultyElo != nil {56 continue57 }5859 mode := normalizeDifficultyToken(puzzles[i].ModeSelection)60 if mode == "" || strings.Contains(mode, "mixed modes") {61 puzzles[i].DifficultyScore = 0.562 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium63 puzzles[i].DifficultySource = "mixed-mode fallback"64 continue65 }6667 category := normalizeDifficultyToken(puzzles[i].Category)68 modes, ok := lookup[category]69 if !ok {70 puzzles[i].DifficultyScore = 0.571 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium72 puzzles[i].DifficultySource = "category lookup fallback"73 continue74 }7576 score, ok := modes[mode]77 if !ok {78 puzzles[i].DifficultyScore = 0.579 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceMedium80 puzzles[i].DifficultySource = "mode lookup fallback"81 continue82 }8384 puzzles[i].DifficultyScore = score85 puzzles[i].DifficultyConfidence = pdfexport.DifficultyConfidenceHigh86 puzzles[i].DifficultySource = "mode-order"87 }88}8990func parseSheetLayout(raw string) (pdfexport.SheetLayout, error) {91 switch strings.ToLower(strings.TrimSpace(raw)) {92 case "", "half-letter":93 return pdfexport.SheetLayoutHalfLetter, nil94 case "duplex-booklet":95 return pdfexport.SheetLayoutDuplexBooklet, nil96 default:97 return pdfexport.SheetLayoutHalfLetter, fmt.Errorf("--sheet-layout must be half-letter or duplex-booklet")98 }99}100101func buildModeDifficultyLookup(definitions []puzzle.Definition) map[string]map[string]float64 {102 lookup := make(map[string]map[string]float64, len(definitions))103 for _, def := range definitions {104 scores := make(map[string]float64)105 variantTitles := []string{}106 for _, variant := range def.Variants {107 title := strings.TrimSpace(variant.Title)108 if title == "" {109 continue110 }111 variantTitles = append(variantTitles, title)112 }113 addModeOrderScores(scores, variantTitles)114 addLegacyModeOrderScores(scores, def)115 if len(scores) == 0 {116 continue117 }118 lookup[normalizeDifficultyToken(def.Name)] = scores119 }120 return lookup121}122123func addLegacyModeOrderScores(scores map[string]float64, def puzzle.Definition) {124 if len(def.LegacyModes) == 0 {125 titles := make([]string, 0, len(def.Modes))126 for _, mode := range def.Modes {127 title := strings.TrimSpace(mode.Title)128 if title != "" {129 titles = append(titles, title)130 }131 }132 addModeOrderScores(scores, titles)133 return134 }135136 modeIndex := make(map[puzzle.ModeID]int, len(def.Modes))137 for i, mode := range def.Modes {138 modeIndex[mode.ID] = i139 }140 for _, alias := range def.LegacyModes {141 title := strings.TrimSpace(alias.Title)142 if title == "" {143 continue144 }145 i, ok := modeIndex[alias.ID]146 if !ok || len(def.Modes) <= 1 {147 scores[normalizeDifficultyToken(title)] = 0.5148 continue149 }150 scores[normalizeDifficultyToken(title)] = float64(i) / float64(len(def.Modes)-1)151 }152}153154func addModeOrderScores(scores map[string]float64, titles []string) {155 switch len(titles) {156 case 0:157 return158 case 1:159 scores[normalizeDifficultyToken(titles[0])] = 0.5160 default:161 for i, title := range titles {162 scores[normalizeDifficultyToken(title)] = float64(i) / float64(len(titles)-1)163 }164 }165}