clone

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

refactor/root-tui-model
puzzletea / weekly / weekly.go
weekly.go 3.5K
1package weekly23import (4 "fmt"5 "hash/fnv"6 "math/rand/v2"7 "regexp"8 "strconv"9 "time"1011 "github.com/FelineStateMachine/puzzletea/game"12 "github.com/FelineStateMachine/puzzletea/registry"13 "github.com/FelineStateMachine/puzzletea/schedule"14)1516type Entry = schedule.Entry1718// Info identifies a single weekly gauntlet puzzle.19type Info struct {20 Year int21 Week int22 Index int23}2425var (26 eligibleModes = buildEligibleModes()27 weeklyNamePattern = regexp.MustCompile(`^Week (\d{2})-(\d{4}) - #(\d{2})$`)28 currentWeekEntries = 9929)3031func buildEligibleModes() []Entry {32 return schedule.BuildEligibleModes(registry.DailyEntries())33}3435// Name returns the canonical persisted name for a weekly puzzle.36func Name(year, week, index int) string {37 return fmt.Sprintf("Week %02d-%04d - #%02d", week, year, index)38}3940// Prefix returns the exact query prefix for all rows in a weekly gauntlet.41func Prefix(year, week int) string {42 return fmt.Sprintf("Week %02d-%04d - #", week, year)43}4445// NameForDate returns the weekly puzzle name for the date's ISO week.46func NameForDate(date time.Time, index int) string {47 year, week := date.ISOWeek()48 return Name(year, week, index)49}5051// Current returns the current ISO week identity for the provided date.52func Current(date time.Time) Info {53 year, week := date.ISOWeek()54 return Info{Year: year, Week: week}55}5657// StartOfWeek returns the Monday for the given ISO week.58func StartOfWeek(year, week int, loc *time.Location) time.Time {59 if loc == nil {60 loc = time.Local61 }62 jan4 := time.Date(year, time.January, 4, 0, 0, 0, 0, loc)63 weekdayOffset := (int(jan4.Weekday()) + 6) % 764 weekOneMonday := jan4.AddDate(0, 0, -weekdayOffset)65 return weekOneMonday.AddDate(0, 0, (week-1)*7)66}6768// AddWeeks moves to the next or previous ISO week.69func AddWeeks(weekStart time.Time, delta int) time.Time {70 return weekStart.AddDate(0, 0, delta*7)71}7273// ParseName parses a canonical weekly name.74func ParseName(name string) (Info, bool) {75 matches := weeklyNamePattern.FindStringSubmatch(name)76 if len(matches) != 4 {77 return Info{}, false78 }7980 week, err := strconv.Atoi(matches[1])81 if err != nil {82 return Info{}, false83 }84 year, err := strconv.Atoi(matches[2])85 if err != nil {86 return Info{}, false87 }88 index, err := strconv.Atoi(matches[3])89 if err != nil {90 return Info{}, false91 }92 if week < 1 || week > 53 || index < 1 || index > currentWeekEntries {93 return Info{}, false94 }9596 info := Info{Year: year, Week: week, Index: index}97 if Name(info.Year, info.Week, info.Index) != name {98 return Info{}, false99 }100 return info, true101}102103// Seed returns a deterministic uint64 seed derived from the weekly identity.104func Seed(year, week, index int) uint64 {105 h := fnv.New64a()106 h.Write([]byte(Name(year, week, index)))107 return h.Sum64()108}109110// RNG creates a deterministic RNG seeded from the weekly identity.111func RNG(year, week, index int) *rand.Rand {112 seed := Seed(year, week, index)113 return rand.New(rand.NewPCG(seed, ^seed))114}115116// Mode selects the weekly mode from the daily-eligible pool using rendezvous117// hashing on the canonical weekly name.118func Mode(year, week, index int) (game.SeededSpawner, string, string) {119 if len(eligibleModes) == 0 {120 return nil, "", ""121 }122123 seedName := Name(year, week, index)124 best, found := schedule.SelectBySeed(seedName, eligibleModes)125 if !found {126 return nil, "", ""127 }128 return best.Spawner, best.GameType, best.Mode129}130131// BonusXP returns the slot-based bonus XP for a completed weekly puzzle.132func BonusXP(index int) int {133 if index < 10 {134 return 0135 }136 return index / 10137}