diff --git a/go.mod b/go.mod index 11953b5..3f5b9cf 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,39 @@ module git.makyo.dev/makyo/gogogogogram -go 1.24.6 +go 1.25.0 require ( - github.com/charmbracelet/bubbletea v1.3.6 + charm.land/lipgloss/v2 v2.0.2 + github.com/charmbracelet/bubbletea v1.3.10 github.com/smartystreets/goconvey v1.8.1 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.9.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/mod v0.9.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.3.8 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index d815c63..091a462 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,49 @@ +charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= +charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= -github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= -github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502 h1:hzWNs3UQRSUTS6YCbLaQnwqKBFXT5Yh1OOw6+26apqg= +github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502/go.mod h1:mkUCcxn9w9j89JJp3pOza5tmDQZPgIB75UfmQlFYvas= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= @@ -41,17 +52,13 @@ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sS github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= diff --git a/main.go b/main.go index 22fa5aa..d89810b 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,12 @@ import ( "fmt" "os" + "git.makyo.dev/makyo/gogogogogram/ui" tea "github.com/charmbracelet/bubbletea" ) func main() { - p := tea.NewProgram(newModel(4, 4)) + p := tea.NewProgram(ui.NewModel(4, 4)) if _, err := p.Run(); err != nil { fmt.Printf("Ah drat — %v\n", err) os.Exit(1) diff --git a/state/history.go b/state/history.go index 57fdde9..51e52d7 100644 --- a/state/history.go +++ b/state/history.go @@ -1,20 +1,27 @@ package state -import "strconv" +import ( + "fmt" + "strconv" +) // History returns the gameplay history for replays func (s *State) History() string { return s.history } -func UnmarshalAll(history string) *State { +func UnmarshalAll(history string) (*State, error) { s := Unmarshal(history) for { - if !s.Step() { + res, err := s.Step() + if err != nil { + return nil, err + } + if !res { break } } - return s + return s, nil } func Unmarshal(history string) *State { @@ -25,16 +32,33 @@ func Unmarshal(history string) *State { } } -func (s *State) Step() bool { +func (s *State) Step() (bool, error) { if s.historyIndex >= len(s.history) { - return false + return false, nil + } + + switch s.history[s.historyIndex] { + case 'i', 'm', 'f', 'c', 'r', 'l', 'u', 'd', 'R', 'L', 'U', 'D': + err := s.beforeAct() + if err != nil { + return false, err + } } switch s.history[s.historyIndex] { case 'g': - s.historyStart() + if s.initialized { + return false, fmt.Errorf("initialization step in invalid location (index %d)", s.historyIndex) + } + err := s.historyStart() + if err != nil { + return false, err + } case 'i': - s.historyInitSection() + err := s.historyInitSection() + if err != nil { + return false, err + } case 'm': s.mark(*s.cursor) case 'f': @@ -57,15 +81,43 @@ func (s *State) Step() bool { s.cursorSectionUp() case 'D': s.cursorSectionDown() + case 't': // TODO for now, this is just sugar. Will be a timestamp for events completed. + for { + s.historyIndex++ + if s.historyIndex >= len(s.history) || s.history[s.historyIndex] == ')' { + break + } + } + case ' ', '\n', '\t': + break + case '#': + for { + s.historyIndex++ + if s.historyIndex >= len(s.history) || s.history[s.historyIndex] == '\n' { + break + } + } + default: + return false, fmt.Errorf("invalid step in history: %s (index %d)", string(s.history[s.historyIndex]), s.historyIndex) } s.historyIndex++ - return true + return true, nil } -func (s *State) historyStart() { +func (s *State) beforeAct() error { + if !s.initialized { + return fmt.Errorf("tried to act on an uninitialized state (index %d)", s.historyIndex) + } + return nil +} + +func (s *State) historyStart() error { peek := s.historyIndex + 1 - p, peek := s.historyPoint(peek) + p, peek, err := s.historyPoint(peek) + if err != nil { + return err + } s.sectionSize = p.X s.cellsPerSection = p.Y s.cells = newField(s.size()) @@ -75,11 +127,17 @@ func (s *State) historyStart() { s.colHeaders = make([]header, s.size()) s.historyIndex = peek s.score.Blackout = make([]bool, s.size()) + s.initialized = true + return nil } -func (s *State) historyInitSection() { +func (s *State) historyInitSection() error { peek := s.historyIndex + 1 - p, peek := s.historyPoint(peek) + p, peek, err := s.historyPoint(peek) + if err != nil { + return err + } + segment := []byte(s.history[peek : peek+s.cellsPerSection*s.cellsPerSection]) for i, c := range segment { curr := Point{ @@ -98,36 +156,49 @@ func (s *State) historyInitSection() { peek++ } s.historyIndex = peek + return nil } -func (s *State) historyPoint(index int) (Point, int) { +func (s *State) historyPoint(index int) (Point, int, error) { var x, y string - // Advance past paren + // Advance past opening paren index++ for { + if index >= len(s.history) { + return Point{}, 0, fmt.Errorf("point.X never ended? (index %d)", index) + } switch s.history[index] { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': x += string(s.history[index]) index++ continue + case ',': + index++ + default: + return Point{}, 0, fmt.Errorf("invalid character in point.X: %s (index %d)", string(s.history[index]), index) } break } - // Advance past comma - index++ for { + if index >= len(s.history) { + return Point{}, 0, fmt.Errorf("point.Y never ended? (index %d)", index) + } switch s.history[index] { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': y += string(s.history[index]) index++ continue + case ')': + break + default: + return Point{}, 0, fmt.Errorf("invalid character in point.Y: %s (index %d)", string(s.history[index]), index) } break } pX, _ := strconv.Atoi(x) pY, _ := strconv.Atoi(y) - return Point{pX, pY}, index + return Point{pX, pY}, index, nil } diff --git a/state/history_test.go b/state/history_test.go index 5d5b2c3..b5a8378 100644 --- a/state/history_test.go +++ b/state/history_test.go @@ -1,6 +1,7 @@ package state import ( + "errors" "regexp" "testing" @@ -31,8 +32,9 @@ func TestHistory(t *testing.T) { }) Convey("You can load a game from its saved history", func() { - history := "g(2,2)i(0,0)oxoxi(1,0)xoxoi(0,1)ooooi(1,1)xxxxfRmDcLUrldu" - s := UnmarshalAll(history) + history := "g(2,2)i(0,0)oxoxi(1,0)xoxoi(0,1)ooooi(1,1)xxxx\n# Here we goooo~\nfRmDcLUrldut(1773881959)" + s, err := UnmarshalAll(history) + So(err, ShouldBeNil) Convey("It sets the history", func() { So(s.History(), ShouldEqual, history) @@ -53,5 +55,44 @@ func TestHistory(t *testing.T) { So(s.String(), ShouldEqual, "X. \n \no .\n....\n") }) }) + + Convey("Errors are handled during loading", func() { + Convey("It raises errors if acting on an uninitialized state", func() { + s, err := UnmarshalAll("m") + So(err, ShouldResemble, errors.New("tried to act on an uninitialized state (index 0)")) + So(s, ShouldBeNil) + }) + + Convey("It raises errors if trying to initialize an initialized state", func() { + s, err := UnmarshalAll("g(1,1)g(1,1)") + So(err, ShouldResemble, errors.New("initialization step in invalid location (index 6)")) + So(s, ShouldBeNil) + }) + + Convey("It raises errors in reading points", func() { + s, err := UnmarshalAll("g(1") + So(err, ShouldResemble, errors.New("point.X never ended? (index 3)")) + So(s, ShouldBeNil) + + s, err = UnmarshalAll("g(a,1)") + So(err, ShouldResemble, errors.New("invalid character in point.X: a (index 2)")) + So(s, ShouldBeNil) + + s, err = UnmarshalAll("g(1,1)i(0,1") + So(err, ShouldResemble, errors.New("point.Y never ended? (index 11)")) + So(s, ShouldBeNil) + + s, err = UnmarshalAll("g(1,a)") + So(err, ShouldResemble, errors.New("invalid character in point.Y: a (index 4)")) + So(s, ShouldBeNil) + }) + + Convey("It only accepts valid steps", func() { + history := "g(2,2)i(0,0)oxoxi(1,0)xoxoi(0,1)ooooi(1,1)xxxxz" + s, err := UnmarshalAll(history) + So(err, ShouldResemble, errors.New("invalid step in history: z (index 46)")) + So(s, ShouldBeNil) + }) + }) }) } diff --git a/state/state.go b/state/state.go index dbf91b3..a0ba458 100644 --- a/state/state.go +++ b/state/state.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "math/rand" + "time" ) type Point struct { @@ -25,6 +26,8 @@ type header struct { } type State struct { + initialized bool + cursor *Point score score @@ -56,6 +59,7 @@ func New(sectionSize, cellsPerSection int) *State { } } // TODO reveal 1 row per section, 1 col per sections/2 + s.initialized = true return s } @@ -259,6 +263,7 @@ func (s *State) update(p Point) { s.sections.setCorrect(sectionPoint, true) s.updateCompletedSections() s.scoreValidCompletedSections() + s.history += fmt.Sprintf("t(%d)", time.Now().Unix()) return } diff --git a/state/state_test.go b/state/state_test.go index a8a34d3..4e91bd4 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -1,7 +1,9 @@ package state import ( + "fmt" "testing" + "time" . "github.com/smartystreets/goconvey/convey" ) @@ -62,10 +64,13 @@ func TestState(t *testing.T) { s.cells.vivify(Point{2, 2}) Convey("It marks sections as correct when they are correctly guessed", func() { - s.mark(Point{0, 0}) + s.Mark() So(s.cells.correct(Point{0, 0}), ShouldBeTrue) So(s.sections.correct(Point{0, 0}), ShouldBeTrue) So(s.String(), ShouldEqual, "O . \n \n. . \n \n") + Convey("It adds a timestamp to the history when a section is completed", func() { + So(s.history, ShouldEndWith, fmt.Sprintf("mt(%d)", time.Now().Unix())) + }) }) Convey("It marks sections as complete if they meet the criteria", func() { @@ -84,7 +89,6 @@ func TestState(t *testing.T) { s.mark(Point{2, 0}) s.mark(Point{0, 2}) s.mark(Point{2, 2}) - //So(res, ShouldResemble, []bool{true, true, true, true}) }) }) }) diff --git a/state/view.go b/state/view.go new file mode 100644 index 0000000..70cdddc --- /dev/null +++ b/state/view.go @@ -0,0 +1,95 @@ +package state + +import ( + "fmt" + + "charm.land/lipgloss/v2" +) + +var ( + complete = []string{"██ ", "██ "} + flagged = []string{"╲╱ ", "╱╲ "} + marked = []string{"╭╮ ", "╰╯ "} + blank = []string{".- ", " "} + + cursorStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("#005566")) + + gridStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Black) +) + +func (c *cell) View() []string { + if c.complete() { + return complete + } else if c.flagged() { + return flagged + } else if c.marked() { + return marked + } else { + return blank + } +} + +func (f *field) View(p *Point, sectionSize, cellsPerSection int) string { + res := gridStyle.Render("┌") + for c := 0; c < cellsPerSection*sectionSize*3; c++ { + res += gridStyle.Render("─") + if c == cellsPerSection*sectionSize*3-1 { + res += gridStyle.Render("┐") + "\n" + break + } + if c%(cellsPerSection*3) == cellsPerSection*3-1 { + res += gridStyle.Render("┬") + } + } + resA := gridStyle.Render("│") + resB := gridStyle.Render("│") + for i, c := range f.cells { + cellRes := c.View() + if i == f.i(*p) { + resA += cursorStyle.Render(cellRes[0]) + resB += cursorStyle.Render(cellRes[1]) + } else { + resA += cellRes[0] + resB += cellRes[1] + } + if i%cellsPerSection == cellsPerSection-1 { + resA += gridStyle.Render("│") + resB += gridStyle.Render("│") + } + if i%f.size == f.size-1 { + res += fmt.Sprintf("%s\n%s\n", resA, resB) + resA = gridStyle.Render("│") + resB = gridStyle.Render("│") + } + if i != 0 && i%(cellsPerSection*cellsPerSection*sectionSize) == 0 { + res += gridStyle.Render("├") + for section := 0; section < sectionSize; section++ { + for sectionCell := 0; sectionCell < cellsPerSection; sectionCell++ { + res += gridStyle.Render("───") + } + if section < sectionSize-1 { + res += gridStyle.Render("┼") + } + } + res += gridStyle.Render("┤") + "\n" + } + } + res += gridStyle.Render("└") + for c := 0; c < cellsPerSection*sectionSize*3; c++ { + res += gridStyle.Render("─") + if c == cellsPerSection*sectionSize*3-1 { + res += gridStyle.Render("┘\n") + break + } + if c%(cellsPerSection*3) == cellsPerSection*3-1 { + res += gridStyle.Render("┴") + } + } + return res +} + +func (s *State) View() string { + return s.cells.View(s.cursor, s.sectionSize, s.cellsPerSection) +} diff --git a/model.go b/ui/model.go similarity index 71% rename from model.go rename to ui/model.go index 888b4f0..fc99ac1 100644 --- a/model.go +++ b/ui/model.go @@ -1,19 +1,26 @@ -package main +package ui -import "git.makyo.dev/makyo/gogogogogram/state" +import ( + "os" + + "git.makyo.dev/makyo/gogogogogram/state" +) type model struct { fieldSize, sectionSize, cellsPerSection int state *state.State + filename string + file *os.File + clears, score, factor, track int columnStates, rowStates [][]int columnsCorrect, rowsCorrect []bool } -func newModel(sectionSize, cellsPerSection int) model { +func NewModel(sectionSize, cellsPerSection int) model { m := model{ fieldSize: sectionSize * cellsPerSection, sectionSize: sectionSize, diff --git a/tea.go b/ui/tea.go similarity index 74% rename from tea.go rename to ui/tea.go index 0948f56..5010f35 100644 --- a/tea.go +++ b/ui/tea.go @@ -1,4 +1,4 @@ -package main +package ui import tea "github.com/charmbracelet/bubbletea" @@ -17,6 +17,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "ctrl+c": return m, tea.Quit + // Saving + case "ctrl+s": + return m, nil + // Movement by cell case "up", "w": m.state.CursorCellUp() @@ -31,17 +35,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.state.CursorCellLeft() // Movement by section - case "ctrl+up", "ctrl+w", "shift+up", "shift+w": + case "shift+up", "W": m.state.CursorSectionUp() - case "ctrl+down", "ctrl+s", "shift+down", "shift+s": + case "shift+down", "S": m.state.CursorSectionDown() - case "ctrl+right", "ctrl+d", "shift+right", "shift+d": + case "shift+right", "D": m.state.CursorSectionRight() - case "ctrl+left", "ctrl+a", "shift+left", "shift+a": - m.state.CursorSectionRight() + case "shift+left", "A": + m.state.CursorSectionLeft() // Marking/flagging case " ", "enter": @@ -52,6 +56,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "delete", "backspace": m.state.Clear() + } } @@ -59,5 +64,5 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m model) View() string { - return "" + return m.state.View() }