1package session23import (4 "fmt"5 "strconv"6 "strings"78 "github.com/FelineStateMachine/puzzletea/difficulty"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/namegen"11 "github.com/FelineStateMachine/puzzletea/puzzle"12 "github.com/FelineStateMachine/puzzletea/registry"13 "github.com/FelineStateMachine/puzzletea/resolve"14 "github.com/FelineStateMachine/puzzletea/store"15)1617type DifficultyMetadata struct {18 TargetElo *difficulty.Elo19 ActualElo *difficulty.Elo20 Confidence difficulty.Confidence21}2223// NormalizeSeed keeps seeded names distinct from real dailies.24func NormalizeSeed(seed string) string {25 seed = strings.TrimSpace(seed)26 for _, reserved := range []string{"daily", "week"} {27 if strings.HasPrefix(strings.ToLower(seed), reserved) {28 return strings.ToLower(seed[:len(reserved)]) + seed[len(reserved):]29 }30 }31 return seed32}3334// SeededName derives the deterministic display name for a seeded puzzle.35func SeededName(seed string) string {36 nameRNG := resolve.RNGFromString("name:" + seed)37 return seed + " - " + namegen.GenerateSeeded(nameRNG)38}3940// SeededNameForGame derives the deterministic display name for a seeded41// puzzle locked to a specific game.42func SeededNameForGame(seed, gameType string) string {43 if strings.TrimSpace(gameType) == "" {44 return SeededName(seed)45 }4647 scope := game.NormalizeName(gameType)48 nameRNG := resolve.RNGFromString("name:" + seed + ":" + scope)49 return fmt.Sprintf("%s [%s] - %s",50 seed,51 gameType,52 namegen.GenerateSeeded(nameRNG),53 )54}5556// SeededNameForCreateLeaf derives the deterministic display name for a seeded57// puzzle locked to one Create leaf and target Elo.58func SeededNameForCreateLeaf(seed, gameType, leafID string, elo difficulty.Elo) string {59 if strings.TrimSpace(leafID) == "" {60 return SeededNameForGame(seed, gameType)61 }6263 scope := strings.Join([]string{64 game.NormalizeName(gameType),65 puzzle.NormalizeName(leafID),66 strconv.Itoa(int(elo)),67 }, ":")68 nameRNG := resolve.RNGFromString("name:" + seed + ":" + scope)69 return fmt.Sprintf("%s [%s %d] - %s",70 seed,71 gameType,72 elo,73 namegen.GenerateSeeded(nameRNG),74 )75}7677// ImportRecord reconstructs a saved game and reapplies the record title.78func ImportRecord(rec *store.GameRecord) (game.Gamer, error) {79 if rec == nil {80 return nil, fmt.Errorf("nil game record")81 }8283 g, err := registry.Import(rec.GameType, []byte(rec.SaveState))84 if err != nil {85 return nil, err86 }87 return g.SetTitle(rec.Name), nil88}8990// CreateRecord persists the initial state for a newly created game.91func CreateRecord(92 s *store.Store,93 g game.Gamer,94 name string,95 gameType string,96 modeTitle string,97 run store.RunMetadata,98) (*store.GameRecord, error) {99 return CreateRecordWithDifficulty(s, g, name, gameType, modeTitle, run, DifficultyMetadata{})100}101102func CreateRecordWithDifficulty(103 s *store.Store,104 g game.Gamer,105 name string,106 gameType string,107 modeTitle string,108 run store.RunMetadata,109 difficultyMeta DifficultyMetadata,110) (*store.GameRecord, error) {111 initialState, err := g.GetSave()112 if err != nil {113 return nil, fmt.Errorf("failed to get initial save: %w", err)114 }115116 rec := &store.GameRecord{117 Name: name,118 GameID: string(puzzle.CanonicalGameID(gameType)),119 GameType: gameType,120 VariantID: string(puzzle.CanonicalVariantID(modeTitle)),121 Variant: modeTitle,122 ModeID: string(puzzle.CanonicalModeID(modeTitle)),123 Mode: modeTitle,124 InitialState: string(initialState),125 SaveState: string(initialState),126 Status: store.StatusNew,127 RunKind: run.Kind,128 RunDate: run.Date,129 WeekYear: run.WeekYear,130 WeekNumber: run.WeekNumber,131 WeekIndex: run.WeekIndex,132 SeedText: run.SeedText,133 }134 if difficultyMeta.TargetElo != nil {135 v := int(*difficultyMeta.TargetElo)136 rec.TargetDifficultyElo = &v137 }138 if difficultyMeta.ActualElo != nil {139 v := int(*difficultyMeta.ActualElo)140 rec.ActualDifficultyElo = &v141 }142 rec.DifficultyConfidence = string(difficultyMeta.Confidence)143 if err := s.CreateGame(rec); err != nil {144 return nil, fmt.Errorf("failed to create game record: %w", err)145 }146 return rec, nil147}148149// ResumeAbandonedDeterministicRecord marks an abandoned deterministic game150// as in-progress before resuming it.151func ResumeAbandonedDeterministicRecord(s *store.Store, rec *store.GameRecord) error {152 if rec == nil || rec.Status != store.StatusAbandoned {153 return nil154 }155 if err := s.UpdateStatus(rec.ID, store.StatusInProgress); err != nil {156 return fmt.Errorf("failed to mark deterministic game in progress: %w", err)157 }158 return nil159}160161// GenerateUniqueName generates a unique adjective-noun name, retrying if needed.162func GenerateUniqueName(s *store.Store) string {163 for range 100 {164 name := namegen.Generate()165 exists, err := s.NameExists(name)166 if err != nil || !exists {167 return name168 }169 }170 base := namegen.Generate()171 for i := 1; ; i++ {172 name := base + "-" + strconv.Itoa(i)173 exists, err := s.NameExists(name)174 if err != nil || !exists {175 return name176 }177 }178}