clone

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

chore/regenerate-vhs-gifs
puzzletea / resolve / resolve.go
resolve.go 4.2K
1package resolve23import (4 "errors"5 "fmt"6 "hash/fnv"7 "math/rand/v2"8 "strings"910 "github.com/FelineStateMachine/puzzletea/game"11 "github.com/FelineStateMachine/puzzletea/puzzle"12 "github.com/FelineStateMachine/puzzletea/registry"13)1415// Normalize lowercases and replaces hyphens/underscores with spaces for16// fuzzy matching of CLI arguments to mode names.17func Normalize(s string) string {18 return puzzle.NormalizeName(s)19}2021// Mode finds a mode within a game entry by name. If name is empty,22// returns the first (default) mode.23func Mode(entry registry.Entry, name string) (game.Spawner, string, error) {24 if len(entry.Modes) == 0 {25 return nil, "", fmt.Errorf("game %q has no available modes", entry.Definition.Name)26 }2728 if name == "" {29 mode := entry.Modes[0]30 return mode.Spawner, mode.Definition.Title, nil31 }3233 norm := Normalize(name)34 for _, mode := range entry.Modes {35 if Normalize(mode.Definition.Title) == norm {36 return mode.Spawner, mode.Definition.Title, nil37 }38 }3940 return nil, "", fmt.Errorf("unknown mode %q for %s\n\nAvailable modes:\n %s",41 name, entry.Definition.Name, strings.Join(ModeNames(entry), "\n "))42}4344// ModeNames returns the display names of all modes in an entry.45func ModeNames(entry registry.Entry) []string {46 names := make([]string, 0, len(entry.Modes))47 for _, mode := range entry.Modes {48 names = append(names, mode.Definition.Title)49 }50 return names51}5253// RNGFromString creates a deterministic RNG seeded from an arbitrary string.54// The string is hashed with FNV-64a to produce a uint64 seed.55func RNGFromString(seed string) *rand.Rand {56 h := fnv.New64a()57 h.Write([]byte(seed))58 s := h.Sum64()59 return rand.New(rand.NewPCG(s, ^s))60}6162// seededEntry pairs a SeededSpawner with its category and mode metadata.63type seededEntry struct {64 spawner game.SeededSpawner65 gameType string66 mode string67}6869func seededModeForDefinition(seed string, entry registry.Entry) (seededEntry, bool) {70 var best seededEntry71 var bestHash uint6472 found := false7374 for _, mode := range entry.Modes {75 if mode.Seeded == nil {76 continue77 }78 h := fnv.New64a()79 h.Write([]byte(seed))80 h.Write([]byte{0})81 h.Write([]byte(entry.Definition.Name))82 h.Write([]byte{0})83 h.Write([]byte(mode.Definition.Title))84 score := h.Sum64()85 if !found || score > bestHash {86 bestHash = score87 best = seededEntry{88 spawner: mode.Seeded,89 gameType: entry.Definition.Name,90 mode: mode.Definition.Title,91 }92 found = true93 }94 }9596 return best, found97}9899// SeededMode selects a game type and mode from all available modes across100// all categories using rendezvous hashing (highest random weight). Each101// eligible mode is scored by hashing the seed string together with its102// (gameType, modeTitle) pair; the highest score wins.103//104// This is resilient to changes in the category/mode lists: adding or105// removing a mode only affects seeds where the changed entry would have106// been the winner.107func SeededMode(seed string, entries []registry.Entry) (game.SeededSpawner, string, string, error) {108 var best seededEntry109 var bestHash uint64110 found := false111 for _, entry := range entries {112 selected, ok := seededModeForDefinition(seed, entry)113 if !ok {114 continue115 }116 h := fnv.New64a()117 h.Write([]byte(seed))118 h.Write([]byte{0})119 h.Write([]byte(selected.gameType))120 h.Write([]byte{0})121 h.Write([]byte(selected.mode))122 score := h.Sum64()123 if !found || score > bestHash {124 bestHash = score125 best = selected126 found = true127 }128 }129 if !found {130 return nil, "", "", errors.New("no seeded modes available")131 }132 return best.spawner, best.gameType, best.mode, nil133}134135// SeededModeForGame deterministically selects a seeded mode within a single136// game definition, so the same seed and game name always produce the same137// puzzle for that game.138func SeededModeForGame(seed, gameType string, entries []registry.Entry) (game.SeededSpawner, string, string, error) {139 norm := Normalize(gameType)140 for _, entry := range entries {141 if Normalize(entry.Definition.Name) != norm {142 continue143 }144145 selected, ok := seededModeForDefinition(seed, entry)146 if !ok {147 return nil, "", "", fmt.Errorf("game %q has no seeded modes", entry.Definition.Name)148 }149 return selected.spawner, selected.gameType, selected.mode, nil150 }151152 return nil, "", "", fmt.Errorf("unknown game %q", gameType)153}