clone

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

chore/app-model-decomp
puzzletea / app / create_state.go
create_state.go 7.7K
1package app23import (4 "fmt"5 "sort"6 "strconv"7 "strings"89 "github.com/FelineStateMachine/puzzletea/config"10 "github.com/FelineStateMachine/puzzletea/difficulty"11 "github.com/FelineStateMachine/puzzletea/game"12 "github.com/FelineStateMachine/puzzletea/puzzle"13 "github.com/FelineStateMachine/puzzletea/registry"1415 "charm.land/bubbles/v2/textinput"16)1718const defaultCreateElo = 12001920type createState struct {21 tree []createTreeNode22 checked map[string]bool23 expanded map[string]bool24 cursor int25 eloInput textinput.Model26 seedInput textinput.Model27 focus createFocus28}2930type createFocus int3132const (33 createFocusTree createFocus = iota34 createFocusElo35 createFocusSeed36 createFocusGenerate37)3839type createTreeNode struct {40 id string41 title string42 leaf *createLeaf43 children []createTreeNode44}4546type createLeaf struct {47 id string48 title string49 gameType string50 variant registry.VariantEntry51}5253type createVisibleNode struct {54 node createTreeNode55 depth int56}5758func newCreateState(cfg config.CreateConfig, width int) createState {59 elo := cfg.Elo60 if err := difficulty.ValidateElo(difficulty.Elo(elo)); err != nil {61 elo = defaultCreateElo62 }6364 eloInput := textinput.New()65 eloInput.Prompt = ""66 eloInput.CharLimit = 467 eloInput.SetWidth(8)68 eloInput.SetValue(strconv.Itoa(elo))6970 seedInput := textinput.New()71 seedInput.Prompt = ""72 seedInput.Placeholder = "optional seed"73 seedInput.CharLimit = 6474 seedInput.SetWidth(min(width, 48))7576 tree := buildCreateTree(registry.Entries())77 validLeafIDs := createLeafIDSet(tree)78 state := createState{79 tree: tree,80 checked: make(map[string]bool),81 expanded: make(map[string]bool),82 eloInput: eloInput,83 seedInput: seedInput,84 focus: createFocusTree,85 }86 for _, id := range cfg.SelectedLeafIDs {87 if !validLeafIDs[id] {88 continue89 }90 state.checked[id] = true91 state.expandAncestors(id)92 }93 if len(state.checked) == 0 {94 state.expandTopLevel()95 }96 return state97}9899func buildCreateTree(entries []registry.Entry) []createTreeNode {100 builders := make(map[string]*createNodeBuilder)101 order := make([]string, 0, len(entries))102 for _, entry := range entries {103 topTitle, branchTitle := createGrouping(entry.Definition.Name)104 topID := "group:" + puzzle.NormalizeName(topTitle)105 builder, ok := builders[topID]106 if !ok {107 builder = &createNodeBuilder{id: topID, title: topTitle}108 builders[topID] = builder109 order = append(order, topID)110 }111 builder.addBranch(entry, branchTitle)112 }113114 nodes := make([]createTreeNode, 0, len(order))115 for _, id := range order {116 nodes = append(nodes, builders[id].node())117 }118 return nodes119}120121type createNodeBuilder struct {122 id string123 title string124 branches []createTreeNode125}126127func (b *createNodeBuilder) addBranch(entry registry.Entry, branchTitle string) {128 leaves := createLeavesForEntry(entry, branchTitle)129 if branchTitle == "" {130 b.branches = append(b.branches, leaves...)131 return132 }133134 branchID := b.id + "/branch:" + puzzle.NormalizeName(branchTitle)135 if len(leaves) == 1 {136 b.branches = append(b.branches, leaves[0])137 return138 }139 b.branches = append(b.branches, createTreeNode{140 id: branchID,141 title: branchTitle,142 children: leaves,143 })144}145146func (b *createNodeBuilder) node() createTreeNode {147 if len(b.branches) == 1 && b.branches[0].leaf != nil {148 return b.branches[0]149 }150 return createTreeNode{id: b.id, title: b.title, children: b.branches}151}152153func createGrouping(gameType string) (topTitle, branchTitle string) {154 switch gameType {155 case "Takuzu", "Takuzu+":156 return "Takuzu", gameType157 case "Sudoku", "Sudoku RGB":158 return "Sudoku", gameType159 default:160 return gameType, ""161 }162}163164func createLeavesForEntry(entry registry.Entry, branchTitle string) []createTreeNode {165 nodes := make([]createTreeNode, 0, len(entry.Variants))166 for _, variant := range entry.Variants {167 if variant.Elo == nil {168 continue169 }170 title := variant.Definition.Title171 id := createLeafID(entry.Definition.ID, branchTitle, title)172 nodes = append(nodes, createTreeNode{173 id: id,174 title: title,175 leaf: &createLeaf{176 id: id,177 title: title,178 gameType: entry.Definition.Name,179 variant: variant,180 },181 })182 }183 return nodes184}185186func createLeafID(gameID puzzle.GameID, branchTitle, title string) string {187 parts := []string{string(gameID)}188 if branchTitle != "" {189 parts = append(parts, puzzle.NormalizeName(branchTitle))190 }191 parts = append(parts, puzzle.NormalizeName(title))192 return strings.Join(parts, "/")193}194195func (s createState) selectedLeaves() []createLeaf {196 leaves := make([]createLeaf, 0, len(s.checked))197 var walk func([]createTreeNode)198 walk = func(nodes []createTreeNode) {199 for _, node := range nodes {200 if node.leaf != nil && s.checked[node.leaf.id] {201 leaves = append(leaves, *node.leaf)202 }203 walk(node.children)204 }205 }206 walk(s.tree)207 return leaves208}209210func (s createState) selectedLeafIDs() []string {211 leaves := s.selectedLeaves()212 ids := make([]string, 0, len(leaves))213 for _, leaf := range leaves {214 ids = append(ids, leaf.id)215 }216 sort.Strings(ids)217 return ids218}219220func (s createState) selectedCount() int {221 return len(s.selectedLeaves())222}223224func (s createState) descendantLeafCounts(node createTreeNode) (selected, total int) {225 var walk func(createTreeNode)226 walk = func(node createTreeNode) {227 if node.leaf != nil {228 total++229 if s.checked[node.leaf.id] {230 selected++231 }232 return233 }234 for _, child := range node.children {235 walk(child)236 }237 }238 walk(node)239 return selected, total240}241242func (s *createState) toggleDescendantLeaves(node createTreeNode) {243 selected, total := s.descendantLeafCounts(node)244 if total == 0 {245 return246 }247 checked := selected != total248 var walk func(createTreeNode)249 walk = func(node createTreeNode) {250 if node.leaf != nil {251 s.checked[node.leaf.id] = checked252 return253 }254 for _, child := range node.children {255 walk(child)256 }257 }258 walk(node)259}260261func (s createState) seedEnabled() bool {262 return s.selectedCount() == 1263}264265func (s createState) elo() (difficulty.Elo, error) {266 value, err := strconv.Atoi(strings.TrimSpace(s.eloInput.Value()))267 if err != nil {268 return 0, fmt.Errorf("Elo must be a number")269 }270 elo := difficulty.Elo(value)271 if err := difficulty.ValidateElo(elo); err != nil {272 return 0, err273 }274 return elo, nil275}276277func (s createState) visibleNodes() []createVisibleNode {278 visible := make([]createVisibleNode, 0)279 var walk func([]createTreeNode, int)280 walk = func(nodes []createTreeNode, depth int) {281 for _, node := range nodes {282 visible = append(visible, createVisibleNode{node: node, depth: depth})283 if node.leaf == nil && s.expanded[node.id] {284 walk(node.children, depth+1)285 }286 }287 }288 walk(s.tree, 0)289 return visible290}291292func (s *createState) expandTopLevel() {293 for _, node := range s.tree {294 s.expanded[node.id] = false295 }296}297298func (s *createState) expandAncestors(leafID string) {299 var walk func([]createTreeNode, []string) bool300 walk = func(nodes []createTreeNode, ancestors []string) bool {301 for _, node := range nodes {302 if node.id == leafID {303 for _, id := range ancestors {304 s.expanded[id] = true305 }306 return true307 }308 if walk(node.children, append(ancestors, node.id)) {309 return true310 }311 }312 return false313 }314 walk(s.tree, nil)315}316317func (s createState) resolveLeafVariant(leaf createLeaf, elo difficulty.Elo) (game.EloSpawner, string, error) {318 if leaf.variant.Elo == nil {319 return nil, "", fmt.Errorf("%s %s does not support Elo generation", leaf.gameType, leaf.title)320 }321 return leaf.variant.Elo, leaf.variant.Definition.Title, nil322}323324func createLeafIDSet(nodes []createTreeNode) map[string]bool {325 ids := make(map[string]bool)326 var walk func([]createTreeNode)327 walk = func(nodes []createTreeNode) {328 for _, node := range nodes {329 if node.leaf != nil {330 ids[node.leaf.id] = true331 continue332 }333 walk(node.children)334 }335 }336 walk(nodes)337 return ids338}