- commit
- fe4a67e29168999121abb22fee5ca11b9f5ab661
- author
- felinestatemachine <dami.etoile@gmail.com>
| A | .gitignore | 32 | ++++++ |
| A | LICENSE | 21 | ++++ |
| A | core.go | 23 | +++++ |
| A | game/ | 23 | +++++ |
| A | go.mod | 46 | +++++++++ |
| A | go.sum | 88 | +++++++++++++++++ |
| A | main.go | 14 | +++ |
| A | model.go | 152 | ++++++++++++++++++++++++++++++ |
| A | nonogram/ | 70 | ++++++++++++++ |
| A | nonogram/ | 26 | +++++ |
| A | nonogram/ | 139 | +++++++++++++++++++++++++++ |
| A | nonogram/ | 63 | ++++++++++++ |
| A | nonogram/ | 83 | ++++++++++++++++ |
| A | nonogram/ | 110 | ++++++++++++++++++++++ |
| A | nonogram/ | 102 | ++++++++++++++++++++ |
| A | sudoku/ | 6 | + |
| A | sudoku/ | 25 | +++++ |
| A | sudoku/ | 122 | ++++++++++++++++++++++++ |
| A | sudoku/ | 48 | +++++++++ |
| A | sudoku/ | 72 | ++++++++++++++ |
@@ -0,0 +1,32 @@
1+# If you prefer the allow list template instead of the deny list, see community template:2+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore3+#4+# Binaries for programs and plugins5+*.exe6+*.exe~7+*.dll8+*.so9+*.dylib10+11+# Test binary, built with `go test -c`12+*.test13+14+# Code coverage profiles and other test artifacts15+*.out16+coverage.*17+*.coverprofile18+profile.cov19+20+# Dependency directories (remove the comment below to include it)21+# vendor/22+23+# Go workspace file24+go.work25+go.work.sum26+27+# env file28+.env29+30+# Editor/IDE31+# .idea/32+# .vscode/@@ -0,0 +1,21 @@
1+MIT License2+3+Copyright (c) 2025 D∆MI4+5+Permission is hereby granted, free of charge, to any person obtaining a copy6+of this software and associated documentation files (the "Software"), to deal7+in the Software without restriction, including without limitation the rights8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell9+copies of the Software, and to permit persons to whom the Software is10+furnished to do so, subject to the following conditions:11+12+The above copyright notice and this permission notice shall be included in all13+copies or substantial portions of the Software.14+15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE21+SOFTWARE.@@ -0,0 +1,23 @@
1+package main2+3+import (4+ "fmt"5+6+ "github.com/FelineStateMachine/puzzletea/game"7+ "github.com/FelineStateMachine/puzzletea/sudoku"8+9+ // Implementations of the Gamer interface should interface here.10+ nonogram "github.com/FelineStateMachine/puzzletea/nonogram"11+)12+13+func (m model) SpawnGame(mode game.Mode) (game.Gamer, error) {14+ switch mode := mode.(type) {15+ case nonogram.NonogramMode:16+ hints := nonogram.GenerateRandomTomography(mode)17+ return nonogram.New(mode, hints)18+ case sudoku.SudokuMode:19+ return sudoku.New(mode, sudoku.GenerateProvidedCells(mode))20+ default:21+ return nil, fmt.Errorf("Faled to create game.\nUnimplemented game type `%v`.", mode)22+ }23+}@@ -0,0 +1,23 @@
1+package game2+3+import (4+ "github.com/charmbracelet/bubbles/key"5+ tea "github.com/charmbracelet/bubbletea"6+)7+8+type Gamer interface {9+ GetDebugInfo() string10+ GetFullHelp() [][]key.Binding11+12+ GetSave() ([]byte, error)13+14+ Init() tea.Cmd15+ View() string16+ Update(msg tea.Msg) (Gamer, tea.Cmd)17+}18+19+type Mode interface {20+ Title() string21+ Description() string22+ FilterValue() string23+}@@ -0,0 +1,46 @@
1+module github.com/FelineStateMachine/puzzletea2+3+go 1.24.54+5+require github.com/charmbracelet/bubbletea v1.3.66+7+require (8+ github.com/alecthomas/chroma/v2 v2.14.0 // indirect9+ github.com/atotto/clipboard v0.1.4 // indirect10+ github.com/aymerick/douceur v0.2.0 // indirect11+ github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect12+ github.com/dlclark/regexp2 v1.11.0 // indirect13+ github.com/gorilla/css v1.0.1 // indirect14+ github.com/microcosm-cc/bluemonday v1.0.27 // indirect15+ github.com/muesli/reflow v0.3.0 // indirect16+ github.com/sahilm/fuzzy v0.1.1 // indirect17+ github.com/yuin/goldmark v1.7.8 // indirect18+ github.com/yuin/goldmark-emoji v1.0.5 // indirect19+ golang.org/x/net v0.33.0 // indirect20+ golang.org/x/term v0.31.0 // indirect21+)22+23+require (24+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect25+ github.com/charmbracelet/bubbles v0.21.026+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect27+ github.com/charmbracelet/glamour v0.10.028+ github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c66083429+ github.com/charmbracelet/x/ansi v0.9.3 // indirect30+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect31+ github.com/charmbracelet/x/term v0.2.1 // indirect32+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect33+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect34+ github.com/mattn/go-isatty v0.0.20 // indirect35+ github.com/mattn/go-localereader v0.0.1 // indirect36+ github.com/mattn/go-runewidth v0.0.16 // indirect37+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect38+ github.com/muesli/cancelreader v0.2.2 // indirect39+ github.com/muesli/termenv v0.16.0 // indirect40+ github.com/rivo/uniseg v0.4.7 // indirect41+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect42+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect43+ golang.org/x/sync v0.15.0 // indirect44+ golang.org/x/sys v0.33.0 // indirect45+ golang.org/x/text v0.24.0 // indirect46+)@@ -0,0 +1,88 @@
1+github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=2+github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=3+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=4+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=5+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=6+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=7+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=8+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=9+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=10+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=11+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=12+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=13+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=14+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=15+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=16+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=17+github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=18+github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=19+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=20+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=21+github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=22+github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=23+github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=24+github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=25+github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=26+github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=27+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=28+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=29+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=30+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=31+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=32+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=33+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=34+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=35+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=36+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=37+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=38+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=39+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=40+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=41+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=42+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=43+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=44+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=45+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=46+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=47+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=48+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=49+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=50+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=51+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=52+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=53+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=54+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=55+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=56+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=57+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=58+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=59+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=60+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=61+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=62+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=63+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=64+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=65+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=66+github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=67+github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=68+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=69+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=70+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=71+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=72+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=73+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=74+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=75+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=76+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=77+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=78+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=79+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=80+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=81+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=82+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=83+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=84+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=85+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=86+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=87+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=88+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=@@ -0,0 +1,152 @@
1+package main2+3+import (4+ "fmt"5+6+ "github.com/FelineStateMachine/puzzletea/game"7+ "github.com/FelineStateMachine/puzzletea/nonogram"8+ "github.com/FelineStateMachine/puzzletea/sudoku"9+ "github.com/charmbracelet/bubbles/list"10+ tea "github.com/charmbracelet/bubbletea"11+ "github.com/charmbracelet/glamour"12+ "github.com/charmbracelet/lipgloss"13+)14+15+var (16+ rootStyle = lipgloss.NewStyle().Margin(1, 2)17+ debugStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).BorderForeground(lipgloss.Color("124"))18+19+ GameModes = []list.Item{20+ nonogram.NewMode("Easy - 5x5", "A random nonogram on a five by five board.", 5, 5),21+ nonogram.NewMode("Medium - 10x10", "A random nonogram on a ten by ten board.", 10, 10),22+ nonogram.NewMode("Hard - 15x15", "A random nonogram on a fifteen by fifteen board.", 15, 15),23+ nonogram.NewMode("Extra - 5x10", "A random nonogram on a five by ten board.", 5, 10),24+25+ sudoku.NewMode("Easy - 38 Provided Cells", "A random sudoku with at least 38 cells provided to start.", 38),26+ sudoku.NewMode("Hard - 26 Provided Cells", "A random sudoku with at least 26 cells provided to start.", 26)}27+)28+29+const (30+ menuView = iota31+ gameView32+)33+34+type model struct {35+ state int36+37+ gamemodesList list.Model38+39+ mode game.Mode40+ game game.Gamer41+42+ debug bool43+ debugRenderer *glamour.TermRenderer44+ debuginfo string45+}46+47+func initialModel() model {48+ r := initDebugRenderer()49+ l := initGameModeList()50+ return model{51+ debugRenderer: r,52+ gamemodesList: l,53+ }54+}55+56+func (m model) Init() tea.Cmd {57+ return nil58+}59+60+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {61+ var cmd tea.Cmd62+63+ switch msg := msg.(type) {64+ case tea.WindowSizeMsg:65+ h, v := rootStyle.GetFrameSize()66+ m.gamemodesList.SetSize(msg.Width-h, msg.Height-v)67+68+ case tea.KeyMsg:69+ switch msg.Type {70+ case tea.KeyCtrlN:71+ m.state = menuView72+ m.debug = false73+ case tea.KeyEnter:74+ if m.state == menuView {75+ m.mode, _ = m.gamemodesList.SelectedItem().(game.Mode)76+ m.game, _ = m.SpawnGame(m.mode)77+ m.state = gameView78+ return m, nil79+ }80+ case tea.KeyCtrlC:81+ return m, tea.Quit82+ case tea.KeyCtrlE:83+ m.debug = !m.debug84+ }85+ }86+87+ switch m.state {88+ case gameView:89+ m.game, cmd = m.game.Update(msg)90+ if m.debug {91+ m.debuginfo = m.renderDebugInfo()92+ }93+ case menuView:94+ m.gamemodesList, cmd = m.gamemodesList.Update(msg)95+ }96+97+ return m, cmd98+}99+100+func (m model) View() string {101+ var s string102+103+ switch m.state {104+ case menuView:105+ s = rootStyle.Render(m.gamemodesList.View())106+ return s107+ case gameView:108+ var debugInfo string109+ if m.debug {110+ debugInfo = debugStyle.Render(m.debuginfo)111+ }112+ s = lipgloss.JoinVertical(lipgloss.Left,113+ m.game.View(),114+ debugInfo,115+ )116+ default:117+ s = fmt.Sprintf("unknown state: %d", m.state)118+ panic(s)119+ }120+121+ return rootStyle.Render(s)122+}123+124+func initGameModeList() list.Model {125+ gamemodes := list.New(GameModes, list.NewDefaultDelegate(), 64, 64)126+ gamemodes.Title = "Select Gamemode"127+ gamemodes.DisableQuitKeybindings()128+ gamemodes.SetFilteringEnabled(false)129+ gamemodes.SetShowHelp(false)130+ return gamemodes131+}132+133+func initDebugRenderer() *glamour.TermRenderer {134+ renderer, err := glamour.NewTermRenderer(135+ glamour.WithAutoStyle(),136+ glamour.WithPreservedNewLines(),137+ glamour.WithChromaFormatter("terminal16m"),138+ )139+ if err != nil {140+ panic(err)141+ }142+ return renderer143+}144+145+func (m model) renderDebugInfo() string {146+ s, err := m.debugRenderer.Render(m.game.GetDebugInfo())147+ if err != nil {148+ panic(err)149+ }150+151+ return s152+}@@ -0,0 +1,70 @@
1+package nonogram2+3+import (4+ "encoding/json"5+ "fmt"6+ "math"7+ "math/rand"8+ "strings"9+)10+11+type Save struct {12+ Solved bool `json:"solved"`13+ State string `json:"state"`14+ Width int `json:"width"`15+ Height int `json:"height"`16+ RowHints TomographyDefinition `json:"row-hints"`17+ ColHints TomographyDefinition `json:"col-hints"`18+}19+20+func (m Model) GetSave() ([]byte, error) {21+ curr := generateTomography(m.grid)22+ solved := curr.rows.equal(m.rowHints) && curr.cols.equal(m.colHints)23+ save := Save{24+ RowHints: m.rowHints,25+ ColHints: m.colHints,26+ Solved: solved,27+ State: m.grid.String(),28+ Width: m.width,29+ Height: m.height,30+ }31+ jsonData, err := json.Marshal(save)32+ if err != nil {33+ return nil, fmt.Errorf("Unable to marshal save data:\n%v", save)34+ }35+ return jsonData, nil36+}37+38+func (m Model) GetTomography() Hints {39+ return generateTomography(m.grid)40+}41+42+func GenerateRandomTomography(mode NonogramMode) Hints {43+ return generateTomography(newGrid(generateRandomState(mode.Height, mode.Width)))44+}45+46+func generateRandomState(h, w int) state {47+ if h <= 0 || w <= 0 {48+ return ""49+ }50+51+ var b strings.Builder52+ b.Grow((w + 1) * h)53+54+ for i := range h {55+ rowWeight := float32(math.Pow(rand.Float64(), 3))56+ for range w {57+ filled := rand.Float32() > rowWeight // Percentage chance to fill.58+ if filled {59+ b.WriteRune(filledTile)60+ } else {61+ b.WriteRune(emptyTile)62+ }63+ }64+ if i < h-1 {65+ b.WriteRune('\n')66+ }67+ }68+69+ return state(b.String())70+}@@ -0,0 +1,26 @@
1+package nonogram2+3+import (4+ "github.com/FelineStateMachine/puzzletea/game"5+)6+7+type NonogramMode struct {8+ title string9+ description string10+ Height, Width int11+}12+13+var _ game.Mode = NonogramMode{} // Verify that T implements I.14+15+func (n NonogramMode) Title() string { return "nonogram\t" + n.title }16+func (n NonogramMode) Description() string { return n.description }17+func (n NonogramMode) FilterValue() string { return "nonogram " + n.title + " " + n.description }18+19+func NewMode(title, description string, Height, Width int) NonogramMode {20+ return NonogramMode{21+ title: title,22+ description: description,23+ Height: Height,24+ Width: Width,25+ }26+}@@ -0,0 +1,139 @@
1+package nonogram2+3+import (4+ "errors"5+ "fmt"6+ "github.com/FelineStateMachine/puzzletea/game"7+ "github.com/charmbracelet/bubbles/key"8+ tea "github.com/charmbracelet/bubbletea"9+ "github.com/charmbracelet/lipgloss"10+)11+12+const (13+ filledTile = '.'14+ markedTile = '-'15+ emptyTile = ' '16+)17+18+type cursor struct {19+ x, y int20+}21+22+type Model struct {23+ width, height int24+ rowHints TomographyDefinition25+ colHints TomographyDefinition26+ cursor cursor27+ grid grid28+ keys KeyMap29+}30+31+func New(mode NonogramMode, hints Hints, save ...string) (game.Gamer, error) {32+ h, w := mode.Height, mode.Width33+ r, c := hints.rows, hints.cols34+ if w < r.RequiredLen() {35+ return Model{}, errors.New("Puzzle width does not support row tomography definition.")36+ }37+ if h < c.RequiredLen() {38+ return Model{}, errors.New("Puzzle height does not support column tomography definition.")39+ }40+ s := loadSave(createEmptyState(h, w), save...)41+ m := Model{42+ width: w,43+ height: h,44+ rowHints: r,45+ colHints: c,46+ grid: newGrid(s),47+ keys: DefaultKeyMap,48+ }49+50+ return m, nil51+}52+53+func (m Model) Init() tea.Cmd {54+ return nil55+}56+57+func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {58+ switch msg := msg.(type) {59+ case tea.KeyMsg:60+ switch {61+ case key.Matches(msg, m.keys.FillTile):62+ m.updateTile(filledTile)63+ case key.Matches(msg, m.keys.MarkTile):64+ m.updateTile(markedTile)65+ case key.Matches(msg, m.keys.ClearTile):66+ m.updateTile(emptyTile)67+ case key.Matches(msg, m.keys.Up):68+ if m.cursor.y > 0 {69+ m.cursor.y--70+ }71+ case key.Matches(msg, m.keys.Down):72+ if m.cursor.y < m.height-1 {73+ m.cursor.y++74+ }75+ case key.Matches(msg, m.keys.Left):76+ if m.cursor.x > 0 {77+ m.cursor.x--78+ }79+ case key.Matches(msg, m.keys.Right):80+ if m.cursor.x < m.width-1 {81+ m.cursor.x++82+ }83+ }84+85+ }86+ m.updateKeyBindinds()87+ return m, nil88+}89+90+func (m Model) View() string {91+ maxWidth, maxHeight := m.rowHints.RequiredLen()*cellWidth, m.colHints.RequiredLen()92+ g := gridView(m.grid, m.cursor)93+ r := rowHintView(m.rowHints, maxWidth)94+ c := colHintView(m.colHints, maxHeight)95+ spacer := baseStyle.Width(maxWidth).Height(maxHeight).Render("")96+97+ s1 := lipgloss.JoinHorizontal(lipgloss.Bottom, spacer, c)98+ s2 := lipgloss.JoinHorizontal(lipgloss.Top, r, g)99+100+ s := lipgloss.JoinVertical(lipgloss.Left, s1, s2)101+102+ return s103+}104+105+func (m Model) GetDebugInfo() string {106+ curr := generateTomography(m.grid)107+ solved := curr.rows.equal(m.rowHints) && curr.cols.equal(m.colHints)108+ s := fmt.Sprintf(109+ "# Nonogram - Dev Info\n"+110+ "| Property | Value |\n"+111+ "| :----- | :---- |\n"+112+ "| Solved | %v |\n"+113+ "| Cursor | (%d,%d) |\n"+114+ "| Dimensions | [%dx%d] |\n"+115+ "| Hint Render Width | %dr %dc |\n"+116+ "\n"+117+ "Row Tomography\n```%v```\n"+118+ "Row Hints\n```%v```\n\n"+119+ "Col Tomography\n```%v```\n"+120+ "Col Hints\n```%v```\n",121+ solved,122+ m.cursor.x,123+ m.cursor.y,124+ m.width,125+ m.height,126+ m.rowHints.RequiredLen()*cellWidth,127+ m.colHints.RequiredLen(),128+ curr.rows,129+ m.rowHints,130+ curr.cols,131+ m.colHints,132+ )133+134+ return s135+}136+137+func (m *Model) updateTile(r rune) {138+ m.grid[m.cursor.y][m.cursor.x] = r139+}@@ -0,0 +1,63 @@
1+package nonogram2+3+import (4+ "strings"5+)6+7+type grid [][]rune8+type state string9+10+func newGrid(s state) grid {11+ rows := strings.Split(string(s), "\n")12+13+ grid := make([][]rune, len(rows))14+15+ for i, col := range rows {16+ grid[i] = []rune(col)17+ }18+19+ return grid20+}21+22+func (g grid) String() string {23+ rows := make([]string, len(g))24+25+ for i, row := range g {26+ rows[i] = string(row)27+ }28+29+ grid := strings.Join(rows, "\n")30+ return grid31+}32+33+func createEmptyState(h, w int) state {34+ if h <= 0 || w <= 0 {35+ return ""36+ }37+38+ var b strings.Builder39+ b.Grow((w + 1) * h)40+41+ for i := range h {42+ for range w {43+ b.WriteRune(emptyTile)44+ }45+ if i < h-1 {46+ b.WriteRune('\n')47+ }48+ }49+50+ return state(b.String())51+}52+53+// loadSave - Parses optional param save. If none, uses default.54+func loadSave(def state, optionalSave ...string) state {55+ var s state56+ //TODO: use default state as state and layer save runes onto it. this would allow save strings to be malformed a bit.57+ if len(optionalSave) > 0 {58+ s = state(optionalSave[0])59+ } else {60+ s = def61+ }62+ return s63+}@@ -0,0 +1,83 @@
1+package nonogram2+3+import (4+ "github.com/charmbracelet/bubbles/key"5+)6+7+type CursorKeyMap struct {8+ Up key.Binding9+ Down key.Binding10+ Left key.Binding11+ Right key.Binding12+}13+14+type KeyMap struct {15+ CursorKeyMap16+ FillTile key.Binding17+ MarkTile key.Binding18+ ClearTile key.Binding19+}20+21+var DefaultKeyMap = KeyMap{22+ CursorKeyMap: CursorKeyMap{23+ Up: key.NewBinding(24+ key.WithKeys("k", "up", "w"),25+ key.WithHelp("↑/w/k", "Up"),26+ ),27+ Down: key.NewBinding(28+ key.WithKeys("j", "down", "s"),29+ key.WithHelp("↓/s/j", "Down"),30+ ),31+ Left: key.NewBinding(32+ key.WithKeys("h", "left", "a"),33+ key.WithHelp("←/a/h", "Left"),34+ ),35+ Right: key.NewBinding(36+ key.WithKeys("l", "right", "d"),37+ key.WithHelp("→/d/l", "Right"),38+ ),39+ },40+41+ FillTile: key.NewBinding(42+ key.WithKeys("z"),43+ key.WithHelp("z", "Fill"),44+ ),45+ MarkTile: key.NewBinding(46+ key.WithKeys("x"),47+ key.WithHelp("x", "Mark"),48+ ),49+ ClearTile: key.NewBinding(50+ key.WithKeys("backspace"),51+ key.WithHelp("⌫", "Clear"),52+ ),53+}54+55+func (m *Model) updateKeyBindinds() {56+ m.keys.Up.SetEnabled(m.cursor.y > 0)57+ m.keys.Down.SetEnabled(m.cursor.y < m.height-1)58+ m.keys.Left.SetEnabled(m.cursor.x > 0)59+ m.keys.Right.SetEnabled(m.cursor.x < m.width-1)60+61+ cursorVal := m.grid[m.cursor.y][m.cursor.x]62+ switch cursorVal {63+ case filledTile:64+ m.keys.FillTile.SetEnabled(false)65+ m.keys.MarkTile.SetEnabled(true)66+ m.keys.ClearTile.SetEnabled(true)67+ case emptyTile:68+ m.keys.FillTile.SetEnabled(true)69+ m.keys.MarkTile.SetEnabled(true)70+ m.keys.ClearTile.SetEnabled(false)71+ case markedTile:72+ m.keys.FillTile.SetEnabled(true)73+ m.keys.MarkTile.SetEnabled(false)74+ m.keys.ClearTile.SetEnabled(true)75+ }76+}77+78+func (m Model) GetFullHelp() [][]key.Binding {79+ return [][]key.Binding{80+ {m.keys.Up, m.keys.Down, m.keys.Left, m.keys.Right},81+ {m.keys.FillTile, m.keys.MarkTile, m.keys.ClearTile},82+ }83+}@@ -0,0 +1,110 @@
1+package nonogram2+3+import (4+ "fmt"5+ "strings"6+7+ "github.com/charmbracelet/lipgloss"8+)9+10+var (11+ baseStyle = lipgloss.NewStyle()12+13+ filledStyle = baseStyle.14+ Background(lipgloss.Color("255")).15+ Foreground(lipgloss.Color("255"))16+17+ markedStyle = baseStyle.18+ Foreground(lipgloss.Color("245"))19+20+ emptyStyle = baseStyle.21+ Foreground(lipgloss.Color("250"))22+23+ highlightStyle = baseStyle.24+ Background(lipgloss.Color("205")).25+ Foreground(lipgloss.Color("255"))26+27+ cellWidth = 328+29+ hintCellStyle = baseStyle.Width(cellWidth)30+31+ renderStyleMap = map[rune]lipgloss.Style{32+ filledTile: filledStyle,33+ markedTile: markedStyle,34+ emptyTile: emptyStyle,35+ }36+37+ renderRuneMap = map[rune]string{38+ filledTile: "⬤",39+ markedTile: "⊗",40+ emptyTile: "◯",41+ }42+)43+44+func colHintView(c TomographyDefinition, height int) string {45+ var renderedCols []string46+ for _, hints := range c {47+ var colHints []string48+ for range height - len(hints) {49+ spacer := hintCellStyle.Render(" ")50+ colHints = append(colHints, spacer)51+ }52+ for _, hint := range hints {53+ hintCell := hintCellStyle.54+ Align(lipgloss.Center).55+ Render(fmt.Sprintf("%d", hint))56+ colHints = append(colHints, hintCell)57+ }58+ renderedCol := lipgloss.JoinVertical(lipgloss.Left, colHints...)59+ renderedCols = append(renderedCols, renderedCol)60+ }61+ return lipgloss.JoinHorizontal(lipgloss.Top, renderedCols...)62+}63+func rowHintView(r TomographyDefinition, width int) string {64+ var renderedRows []string65+ for _, hints := range r {66+ var rowHints []string67+ for _, hint := range hints {68+ hintCell := fmt.Sprintf("%2d", hint)69+ rowHints = append(rowHints, hintCell)70+ }71+ renderedRow := baseStyle.Width(width).72+ Align(lipgloss.Right).73+ Render(strings.Join(rowHints, " "))74+ renderedRows = append(renderedRows, renderedRow)75+ }76+ s := lipgloss.JoinVertical(lipgloss.Left, renderedRows...)77+ return s78+}79+80+func gridView(g grid, c cursor) string {81+ var rows []string82+ for y, row := range g {83+ var rowBuider []string84+ for x, cell := range row {85+ highlighted := x == c.x && y == c.y86+ cell := tileView(cell, highlighted)87+ rowBuider = append(rowBuider, cell)88+89+ }90+ row := lipgloss.JoinHorizontal(lipgloss.Top, rowBuider...)91+ rows = append(rows, row)92+ }93+ grid := lipgloss.JoinVertical(lipgloss.Left, rows...)94+ return grid95+}96+97+func tileView(val rune, h bool) string {98+ s, ok := renderStyleMap[val]99+ if !ok {100+ s = renderStyleMap[emptyTile]101+ }102+ if h {103+ s = highlightStyle104+ }105+ r, ok := renderRuneMap[val]106+ if !ok {107+ r = renderRuneMap[emptyTile]108+ }109+ return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)110+}@@ -0,0 +1,102 @@
1+package nonogram2+3+type TomographyDefinition [][]int4+5+type Hints struct {6+ rows, cols TomographyDefinition7+}8+9+func generateTomography(g grid) Hints {10+ height := len(g)11+ width := 012+ if height > 0 {13+ for _, row := range g {14+ if len(row) > width {15+ width = len(row)16+ }17+ }18+ }19+20+ rowHints := make([][]int, height)21+ for y, row := range g {22+ count := 023+ var rowHint []int24+ for _, tile := range row {25+ if tile == filledTile {26+ count++27+ } else {28+ if count > 0 {29+ rowHint = append(rowHint, count)30+ }31+ count = 032+ }33+ }34+ if count > 0 {35+ rowHint = append(rowHint, count)36+ }37+ if len(rowHint) == 0 {38+ rowHint = append(rowHint, 0)39+ }40+ rowHints[y] = rowHint41+ }42+43+ colHints := make([][]int, width)44+ for x := range width {45+ count := 046+ var colHint []int47+ for y := range height {48+ r := emptyTile49+ if x < len(g[y]) {50+ r = g[y][x]51+ }52+53+ if r == filledTile {54+ count++55+ } else {56+ if count > 0 {57+ colHint = append(colHint, count)58+ }59+ count = 060+ }61+ }62+ if count > 0 {63+ colHint = append(colHint, count)64+ }65+ if len(colHint) == 0 {66+ colHint = append(colHint, 0)67+ }68+ colHints[x] = colHint69+ }70+71+ return Hints{72+ rowHints,73+ colHints,74+ }75+}76+77+func (t TomographyDefinition) RequiredLen() int {78+ var maxLen int79+ for _, r := range t {80+ if len(r) > maxLen {81+ maxLen = len(r)82+ }83+ }84+ return maxLen85+}86+87+func (t TomographyDefinition) equal(o TomographyDefinition) bool {88+ if len(t) != len(o) {89+ return false90+ }91+ for i, r := range t {92+ if len(r) != len(o[i]) {93+ return false94+ }95+ for j, v := range r {96+ if v != o[i][j] {97+ return false98+ }99+ }100+ }101+ return true102+}@@ -0,0 +1,25 @@
1+package sudoku2+3+import (4+ "github.com/FelineStateMachine/puzzletea/game"5+)6+7+type SudokuMode struct {8+ title string9+ description string10+ ProvidedCount int11+}12+13+var _ game.Mode = SudokuMode{} // Verify that T implements I.14+15+func (n SudokuMode) Title() string { return "sudoku\t" + n.title }16+func (n SudokuMode) Description() string { return n.description }17+func (n SudokuMode) FilterValue() string { return "sudoku " + n.title + " " + n.description }18+19+func NewMode(title, description string, providedCount int) SudokuMode {20+ return SudokuMode{21+ title: title,22+ description: description,23+ ProvidedCount: providedCount,24+ }25+}@@ -0,0 +1,122 @@
1+package sudoku2+3+import (4+ "fmt"5+ "slices"6+ "strconv"7+8+ "github.com/FelineStateMachine/puzzletea/game"9+ "github.com/charmbracelet/bubbles/key"10+ tea "github.com/charmbracelet/bubbletea"11+ "github.com/charmbracelet/lipgloss"12+)13+14+const GRIDSIZE = 915+16+const (17+ cellBGColor = lipgloss.Color("249")18+ providedFGColor = lipgloss.Color("229")19+ cursorBGColor = lipgloss.Color("10")20+)21+22+type cursor struct {23+ x, y int24+}25+26+type Model struct {27+ cursor cursor28+ grid grid29+ provided []cell30+ keys KeyMap31+}32+33+// New creates a new sudoku game using the provided cell values.34+func New(mode SudokuMode, provided []cell, save ...string) (game.Gamer, error) {35+ g := loadSave(newGrid(provided), save...)36+ m := Model{37+ grid: g,38+ provided: provided,39+ keys: DefaultKeyMap,40+ }41+ return m, nil42+}43+44+// Init implements game.Gamer.45+func (m Model) Init() tea.Cmd {46+ return nil47+}48+49+// Update implements game.Gamer.50+func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {51+ switch msg := msg.(type) {52+ case tea.KeyMsg:53+ switch {54+ case key.Matches(msg, m.keys.FillValue):55+ val, _ := strconv.Atoi(msg.String())56+ m.updateCell(val)57+ case key.Matches(msg, m.keys.Up):58+ if m.cursor.y > 0 {59+ m.cursor.y--60+ }61+ case key.Matches(msg, m.keys.Down):62+ if m.cursor.y < GRIDSIZE-1 {63+ m.cursor.y++64+ }65+ case key.Matches(msg, m.keys.Left):66+ if m.cursor.x > 0 {67+ m.cursor.x--68+ }69+ case key.Matches(msg, m.keys.Right):70+ if m.cursor.x < GRIDSIZE-1 {71+ m.cursor.x++72+ }73+ }74+75+ }76+ m.updateKeyBindinds()77+ return m, nil78+}79+80+func (m *Model) updateCell(v int) {81+ c := &m.grid[m.cursor.y][m.cursor.x]82+ if slices.Contains(m.provided, *c) {83+84+ }85+ c.v = v86+}87+88+// View implements game.Gamer.89+func (m Model) View() string {90+ var rows []string91+ for _, row := range m.grid {92+ var cells []string93+ for _, cell := range row {94+95+ cellStyle := lipgloss.NewStyle().Background(cellBGColor)96+ if slices.Contains(m.provided, cell) {97+ cellStyle = cellStyle.Foreground(providedFGColor)98+ }99+ if m.cursor.x == cell.x && m.cursor.y == cell.y {100+ cellStyle = cellStyle.Background(cursorBGColor)101+ }102+103+ c := cellStyle.Render(fmt.Sprintf("%d", cell.v))104+ cells = append(cells, c)105+ }106+ r := lipgloss.JoinHorizontal(lipgloss.Right, cells...)107+ rows = append(rows, r)108+ }109+ return lipgloss.JoinVertical(lipgloss.Left, rows...)110+}111+112+// GetDebugInfo implements game.Gamer.113+func (m Model) GetDebugInfo() string {114+ return fmt.Sprintf(`115+# Sudoko116+Cursor (%dx%d)117+Provided Cells: '%v'118+`,119+ m.cursor.x, m.cursor.y,120+ m.provided,121+ )122+}@@ -0,0 +1,48 @@
1+package sudoku2+3+type cell struct {4+ x, y int5+ v int6+}7+8+type grid = [9][9]cell9+10+func GenerateProvidedCells(m SudokuMode) []cell {11+ // TODO: implement12+ cells := []cell{13+ {1, 2, 3},14+ {3, 4, 5},15+ }16+ //create solved board17+ // iteratively remove cells and ensure only 1 solution18+ // backtrack if not unique19+ // return known values20+ return cells21+}22+23+func newGrid(provided []cell) grid {24+ var g grid25+ for i := range GRIDSIZE {26+ g[i] = [GRIDSIZE]cell{}27+28+ for j := range GRIDSIZE {29+ g[i][j].x = j30+ g[i][j].y = i31+ }32+ }33+ for _, hint := range provided {34+ g[hint.y][hint.x].v = hint.v35+ }36+ return g37+}38+39+// loadSave - Parses optional param save. If none, uses default.40+func loadSave(def grid, optionalSave ...string) grid {41+ var g grid42+ if len(optionalSave) > 0 {43+ // TODO load saved cell values.44+ } else {45+ g = def46+ }47+ return g48+}@@ -0,0 +1,72 @@
1+package sudoku2+3+import (4+ "github.com/charmbracelet/bubbles/key"5+)6+7+type CursorKeyMap struct {8+ Up key.Binding9+ Down key.Binding10+ Left key.Binding11+ Right key.Binding12+}13+14+type KeyMap struct {15+ CursorKeyMap16+ FillValue key.Binding17+ ClearCell key.Binding18+}19+20+var DefaultKeyMap = KeyMap{21+ CursorKeyMap: CursorKeyMap{22+ Up: key.NewBinding(23+ key.WithKeys("k", "up", "w"),24+ key.WithHelp("↑/w/k", "Up"),25+ ),26+ Down: key.NewBinding(27+ key.WithKeys("j", "down", "s"),28+ key.WithHelp("↓/s/j", "Down"),29+ ),30+ Left: key.NewBinding(31+ key.WithKeys("h", "left", "a"),32+ key.WithHelp("←/a/h", "Left"),33+ ),34+ Right: key.NewBinding(35+ key.WithKeys("l", "right", "d"),36+ key.WithHelp("→/d/l", "Right"),37+ ),38+ },39+ FillValue: key.NewBinding(40+ key.WithKeys("1", "2", "3", "4", "5", "6", "7", "8", "9"),41+ key.WithHelp("[1-9]", "Fill Cell"),42+ ),43+ ClearCell: key.NewBinding(44+ key.WithKeys("backspace"),45+ key.WithHelp("bkspc", "Clear Cell Contents"),46+ ),47+}48+49+func (m *Model) updateKeyBindinds() {50+ m.keys.Up.SetEnabled(m.cursor.y > 0)51+ m.keys.Down.SetEnabled(m.cursor.y < 8)52+ m.keys.Left.SetEnabled(m.cursor.x > 0)53+ m.keys.Right.SetEnabled(m.cursor.x < 8)54+ allowInteraction := !m.IsProvidedCell()55+ m.keys.FillValue.SetEnabled(allowInteraction)56+ m.keys.ClearCell.SetEnabled(allowInteraction)57+}58+59+func (m Model) IsProvidedCell() bool {60+ v := false61+ for _, hint := range m.provided {62+ if m.cursor.x == hint.x && m.cursor.y == hint.y {63+ v = true64+ }65+ }66+ return v67+}68+69+// GetFullHelp implements game.Gamer.70+func (m Model) GetFullHelp() [][]key.Binding {71+ panic("unimplemented")72+}