From 4f3fd8e0389db52697b503679d167c1afb10d38c Mon Sep 17 00:00:00 2001 From: bdnugget Date: Sun, 16 Mar 2025 22:30:48 +0100 Subject: [PATCH] mostly works but somehow pressing R to restart doesnt --- _examplemouse.go | 191 ----------------------- go.mod | 13 ++ go.sum | 89 +++++++++++ main.go | 398 +++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 474 insertions(+), 217 deletions(-) delete mode 100644 _examplemouse.go create mode 100644 go.sum diff --git a/_examplemouse.go b/_examplemouse.go deleted file mode 100644 index 762f8eb..0000000 --- a/_examplemouse.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2014 The gocui Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "errors" - "fmt" - "log" - - "github.com/awesome-gocui/gocui" -) - -func main() { - g, err := gocui.NewGui(gocui.OutputNormal, true) - if err != nil { - log.Panicln(err) - } - defer g.Close() - - g.Cursor = false - g.Mouse = true - - g.SetManagerFunc(layout) - - if err := keybindings(g); err != nil { - log.Panicln(err) - } - - if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) { - log.Panicln(err) - } -} - -var initialMouseX, initialMouseY, xOffset, yOffset int -var globalMouseDown, msgMouseDown, movingMsg bool - -func layout(g *gocui.Gui) error { - maxX, maxY := g.Size() - if _, err := g.View("msg"); msgMouseDown && err == nil { - moveMsg(g) - } - if v, err := g.SetView("global", -1, -1, maxX, maxY, 0); err != nil { - if !errors.Is(err, gocui.ErrUnknownView) { - return err - } - v.Frame = false - } - if v, err := g.SetView("but1", 2, 2, 22, 7, 0); err != nil { - if !errors.Is(err, gocui.ErrUnknownView) { - return err - } - v.SelBgColor = gocui.ColorGreen - v.SelFgColor = gocui.ColorBlack - fmt.Fprintln(v, "Button 1 - line 1") - fmt.Fprintln(v, "Button 1 - line 2") - fmt.Fprintln(v, "Button 1 - line 3") - fmt.Fprintln(v, "Button 1 - line 4") - if _, err := g.SetCurrentView("but1"); err != nil { - return err - } - } - if v, err := g.SetView("but2", 24, 2, 44, 4, 0); err != nil { - if !errors.Is(err, gocui.ErrUnknownView) { - return err - } - v.SelBgColor = gocui.ColorGreen - v.SelFgColor = gocui.ColorBlack - fmt.Fprintln(v, "Button 2 - line 1") - } - updateHighlightedView(g) - return nil -} - -func keybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - return err - } - for _, n := range []string{"but1", "but2"} { - if err := g.SetKeybinding(n, gocui.MouseLeft, gocui.ModNone, showMsg); err != nil { - return err - } - } - if err := g.SetKeybinding("", gocui.MouseRelease, gocui.ModNone, mouseUp); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.MouseLeft, gocui.ModNone, globalDown); err != nil { - return err - } - if err := g.SetKeybinding("msg", gocui.MouseLeft, gocui.ModNone, msgDown); err != nil { - return err - } - return nil -} - -func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit -} - -func showMsg(g *gocui.Gui, v *gocui.View) error { - var l string - var err error - - if _, err := g.SetCurrentView(v.Name()); err != nil { - return err - } - - _, cy := v.Cursor() - if l, err = v.Line(cy); err != nil { - l = "" - } - - maxX, maxY := g.Size() - if v, err := g.SetView("msg", maxX/2-10, maxY/2, maxX/2+10, maxY/2+2, 0); err == nil || errors.Is(err, gocui.ErrUnknownView) { - v.Clear() - v.SelBgColor = gocui.ColorCyan - v.SelFgColor = gocui.ColorBlack - fmt.Fprintln(v, l) - } - return nil -} - -func updateHighlightedView(g *gocui.Gui) { - mx, my := g.MousePosition() - for _, view := range g.Views() { - view.Highlight = false - } - if v, err := g.ViewByPosition(mx, my); err == nil { - v.Highlight = true - } -} - -func moveMsg(g *gocui.Gui) { - mx, my := g.MousePosition() - if !movingMsg && (mx != initialMouseX || my != initialMouseY) { - movingMsg = true - } - g.SetView("msg", mx-xOffset, my-yOffset, mx-xOffset+20, my-yOffset+2, 0) -} - -func msgDown(g *gocui.Gui, v *gocui.View) error { - initialMouseX, initialMouseY = g.MousePosition() - if vx, vy, _, _, err := g.ViewPosition("msg"); err == nil { - xOffset = initialMouseX - vx - yOffset = initialMouseY - vy - msgMouseDown = true - } - return nil -} - -func globalDown(g *gocui.Gui, v *gocui.View) error { - mx, my := g.MousePosition() - if vx0, vy0, vx1, vy1, err := g.ViewPosition("msg"); err == nil { - if mx >= vx0 && mx <= vx1 && my >= vy0 && my <= vy1 { - return msgDown(g, v) - } - } - globalMouseDown = true - maxX, _ := g.Size() - msg := fmt.Sprintf("Mouse down at: %d,%d", mx, my) - x := mx - len(msg)/2 - if x < 0 { - x = 0 - } else if x+len(msg)+1 > maxX-1 { - x = maxX - 1 - len(msg) - 1 - } - if v, err := g.SetView("globalDown", x, my-1, x+len(msg)+1, my+1, 0); err != nil { - if !errors.Is(err, gocui.ErrUnknownView) { - return err - } - v.WriteString(msg) - } - return nil -} - -func mouseUp(g *gocui.Gui, v *gocui.View) error { - if msgMouseDown { - msgMouseDown = false - if movingMsg { - movingMsg = false - return nil - } else { - g.DeleteView("msg") - } - } else if globalMouseDown { - globalMouseDown = false - g.DeleteView("globalDown") - } - return nil -} diff --git a/go.mod b/go.mod index b0d7e64..33fc71d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,16 @@ module gitea.boner.be/bdnugget/minesweeper go 1.24.1 + +require github.com/awesome-gocui/gocui v1.1.0 + +require ( + github.com/gdamore/encoding v1.0.1 // indirect + github.com/gdamore/tcell/v2 v2.8.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..440497c --- /dev/null +++ b/go.sum @@ -0,0 +1,89 @@ +github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII= +github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= +github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= +github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +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/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 00c15ef..e88a5be 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,14 @@ package main import ( + "errors" "fmt" - "time" + "log" "math/rand/v2" + "strings" + "time" + + "github.com/awesome-gocui/gocui" ) type gameState struct { @@ -13,6 +18,9 @@ type gameState struct { numMines int field [][]point gameStartedAt time.Time + firstMove bool + cursorX int + cursorY int } type point struct { @@ -34,47 +42,385 @@ func (gs *gameState) applyToNeighbours(x, y int, do func(x, y int)) { } } -func (gs *gameState) printField() { - for _, row := range gs.field { - for _, cell := range row { - if cell.isMine { - fmt.Print("*") - } else { - fmt.Print(cell.numNeighbouringMines) - } - } - fmt.Println() - } +// Colors using gocui's attributes +var numberColors = []gocui.Attribute{ + gocui.ColorBlue, // 1 + gocui.ColorGreen, // 2 + gocui.ColorRed, // 3 + gocui.ColorMagenta, // 4 + gocui.ColorYellow, // 5 + gocui.ColorCyan, // 6 + gocui.ColorWhite, // 7 + gocui.ColorDefault, // 8 } -func (gs *gameState) initField() { +// Restore emoji cell representations +var ( + emojiUnopened = "🟦 " // Note the trailing space for consistent width + emojiMarked = "🚩 " // Note the trailing space + emojiMine = "💣 " // Note the trailing space + emojiEmpty = " " // Three spaces +) + +func (gs *gameState) printField(v *gocui.View) { + v.Clear() + + // Build the entire board string first to avoid partial rendering issues + var board strings.Builder + + for i := range gs.field { + for j := range gs.field[i] { + cell := gs.field[i][j] + + if !cell.isOpen { + if cell.isMarked { + board.WriteString(emojiMarked) // Fixed-width emoji + space + } else { + board.WriteString(emojiUnopened) // Fixed-width emoji + space + } + } else { + if cell.isMine { + board.WriteString(emojiMine) // Fixed-width emoji + space + } else if cell.numNeighbouringMines > 0 { + num := cell.numNeighbouringMines + // Format numbers to take exactly the same width as emojis (3 chars) + board.WriteString(fmt.Sprintf(" %d ", num)) + } else { + board.WriteString(emojiEmpty) // Three spaces + } + } + } + board.WriteString("\n") // End of row + } + + // Write the entire board at once + fmt.Fprint(v, board.String()) +} + +func (gs *gameState) initField(safeX, safeY int) { gs.field = make([][]point, gs.rows) for i := range gs.field { gs.field[i] = make([]point, gs.cols) } - for i := range gs.numMines { - ranX := rand.IntN(gs.rows -1) - ranY := rand.IntN(gs.cols-1) - if !gs.field[ranX][ranY].isMine { - gs.field[ranX][ranY].isMine = true - gs.applyToNeighbours(ranX, ranY, func(x, y int) { - gs.field[x][y].numNeighbouringMines++ - }) - } else { - i-- + // Create a list of all possible mine positions except the first clicked cell and its neighbors + possiblePositions := make([]struct{ x, y int }, 0, gs.rows*gs.cols) + for i := 0; i < gs.rows; i++ { + for j := 0; j < gs.cols; j++ { + // Skip the safe cell and its neighbors + if (i == safeX && j == safeY) || isCellNeighbor(i, j, safeX, safeY) { + continue + } + possiblePositions = append(possiblePositions, struct{ x, y int }{i, j}) + } + } + + // Shuffle the positions + for i := range possiblePositions { + j := rand.IntN(len(possiblePositions)) + possiblePositions[i], possiblePositions[j] = possiblePositions[j], possiblePositions[i] + } + + // Place mines using the first numMines positions + minesPlaced := 0 + for i := 0; i < len(possiblePositions) && minesPlaced < gs.numMines; i++ { + pos := possiblePositions[i] + gs.field[pos.x][pos.y].isMine = true + gs.applyToNeighbours(pos.x, pos.y, func(x, y int) { + gs.field[x][y].numNeighbouringMines++ + }) + minesPlaced++ + } +} + +func (gs *gameState) layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + // Calculate game view dimensions based on the grid size + // Each cell is exactly 3 characters wide (emoji + space) + gameWidth := gs.cols*3 + 2 // +2 for the frame + gameHeight := gs.rows + 2 // +2 for the frame (reduced padding) + + // Center the game view precisely + gameX := (maxX - gameWidth) / 2 + gameY := (maxY - gameHeight - 3) / 2 // Adjust position to center vertically, leaving room for restart button + + // Game view - centered with proper spacing + if v, err := g.SetView("game", gameX, gameY, gameX+gameWidth, gameY+gameHeight, 0); err != nil { + if !errors.Is(err, gocui.ErrUnknownView) { + return err + } + v.Title = "Minesweeper" + v.Clear() + // Remove the blank line padding that was causing extra space + gs.printField(v) + if _, err := g.SetCurrentView("game"); err != nil { + return err + } + v.Editor = gocui.EditorFunc(gs.gameEditor) + v.Editable = true + } + + // Restart button - positioned below the game view + buttonWidth := 14 + buttonY := gameY + gameHeight + 1 // Position right below the game view + if v, err := g.SetView("restart", maxX/2-buttonWidth/2, buttonY, maxX/2+buttonWidth/2, buttonY+2, 0); err != nil { + if !errors.Is(err, gocui.ErrUnknownView) { + return err + } + v.Title = "Restart" + fmt.Fprintln(v, " [R] ") + } + + return nil +} + +func (gs *gameState) keybindings(g *gocui.Gui) error { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, gs.quit); err != nil { + return err + } + + // Left click to open cells + if err := g.SetKeybinding("game", gocui.MouseLeft, gocui.ModNone, gs.handleMouseClick); err != nil { + return err + } + + // Right click to mark cells + if err := g.SetKeybinding("game", gocui.MouseRight, gocui.ModNone, gs.handleRightClick); err != nil { + return err + } + + // 'R' key to restart the game - making it lowercase and uppercase + if err := g.SetKeybinding("", 'r', gocui.ModNone, gs.restart); err != nil { + return err + } + if err := g.SetKeybinding("", 'R', gocui.ModNone, gs.restart); err != nil { + return err + } + + // Mouse binding for restart button + if err := g.SetKeybinding("restart", gocui.MouseLeft, gocui.ModNone, gs.restart); err != nil { + return err + } + return nil +} + +func (gs *gameState) quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +// Use the MousePosition method from gocui instead of view cursor +func (gs *gameState) handleMouseClick(g *gocui.Gui, v *gocui.View) error { + // Get global mouse position + mx, my := g.MousePosition() + + // Get view's position + vx0, vy0, _, _, err := g.ViewPosition("game") + if err != nil { + return err + } + + // Calculate local position within the view (accounting for frame) + localX := mx - vx0 - 1 // -1 for left frame + localY := my - vy0 - 1 // -1 for top frame + + // Calculate cell coordinates + cellX := localY + cellY := localX / 3 + + // Proceed with the game logic + if cellX >= 0 && cellX < gs.rows && cellY >= 0 && cellY < gs.cols { + // If it's the first move, initialize the field after the click + if gs.firstMove { + gs.initField(cellX, cellY) // Pass click coordinates to initField + gs.firstMove = false + gs.gameStartedAt = time.Now() // Reset the start time to when the game actually begins + } + + if !gs.field[cellX][cellY].isOpen && !gs.field[cellX][cellY].isMarked { + if gs.field[cellX][cellY].isMine { + gs.revealAllMines() + gs.isGameOver = true + } else { + gs.openCell(cellX, cellY) + } + gs.printField(v) + + gs.updateGameState(g) + } + } + return nil +} + +// Handle keyboard input for the game +func (gs *gameState) gameEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { + // Handle keyboard input for the game + if key == gocui.KeyArrowUp { + // Move cursor up + } else if key == gocui.KeyArrowDown { + // Move cursor down + } + // etc. +} + +func (gs *gameState) openCell(x, y int) { + if x < 0 || y < 0 || x >= gs.rows || y >= gs.cols || gs.field[x][y].isOpen { + return // Bounds check and already opened check + } + + gs.field[x][y].isOpen = true + + if gs.field[x][y].numNeighbouringMines == 0 { + gs.applyToNeighbours(x, y, gs.openCell) // Recursively open neighbours if no adjacent mines + } +} + +func (gs *gameState) revealAllMines() { + for i := 0; i < gs.rows; i++ { + for j := 0; j < gs.cols; j++ { + if gs.field[i][j].isMine { + gs.field[i][j].isOpen = true // Reveal all mines + } } } } +func (gs *gameState) isGameWon() bool { + openedCells := 0 + for i := 0; i < gs.rows; i++ { + for j := 0; j < gs.cols; j++ { + if gs.field[i][j].isOpen && !gs.field[i][j].isMine { + openedCells++ + } + } + } + return openedCells == (gs.rows*gs.cols - gs.numMines) // Check if all non-mine cells are opened +} + +// Helper function to check if a cell is a neighbor of another cell +func isCellNeighbor(x1, y1, x2, y2 int) bool { + return abs(x1-x2) <= 1 && abs(y1-y2) <= 1 +} + +// Helper function for absolute value +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +func (gs *gameState) restart(g *gocui.Gui, v *gocui.View) error { + // Reset game state + gs.isGameOver = false + gs.firstMove = true + gs.gameStartedAt = time.Now() + gs.field = make([][]point, gs.rows) // Clear field + for i := range gs.field { + gs.field[i] = make([]point, gs.cols) + } + + // Delete any message view + g.DeleteView("message") + + // Find the game view and refresh it + if gameView, err := g.View("game"); err == nil { + gs.printField(gameView) + } + + return nil +} + +// Add a new function to handle right-clicks for marking cells +func (gs *gameState) handleRightClick(g *gocui.Gui, v *gocui.View) error { + // Get global mouse position + mx, my := g.MousePosition() + + // Get view's position + vx0, vy0, _, _, err := g.ViewPosition("game") + if err != nil { + return err + } + + // Calculate local position within the view (accounting for frame) + localX := mx - vx0 - 1 // -1 for left frame + localY := my - vy0 - 1 // -1 for top frame + + // Calculate cell coordinates + cellX := localY + cellY := localX / 3 + + // Proceed with the game logic + if cellX >= 0 && cellX < gs.rows && cellY >= 0 && cellY < gs.cols { + // Can only mark cells that aren't already open + if !gs.field[cellX][cellY].isOpen { + // Toggle the marked state + gs.field[cellX][cellY].isMarked = !gs.field[cellX][cellY].isMarked + gs.printField(v) + } + } + return nil +} + +// Add a dedicated function for game state changes +func (gs *gameState) updateGameState(g *gocui.Gui) { + if gs.isGameWon() { + gs.revealAllMines() + gs.isGameOver = true + gs.showMessage(g, "YEET DAB!", "Congratulation! You're winRAR!\nR to restart or Ctrl+C to ragequit") + } else if gs.isGameOver { + gs.showMessage(g, "OH SHIET!", "You dun goofed!\nR to restart or Ctrl+C to ragequit") + } +} + +// Helper for message display +func (gs *gameState) showMessage(g *gocui.Gui, title, message string) error { + maxX, maxY := g.Size() + width, height := 40, 5 // Slightly larger for better appearance + + msgView, err := g.SetView("message", maxX/2-width/2, maxY/2-height/2, + maxX/2+width/2, maxY/2+height/2, 0) + if err != nil && !errors.Is(err, gocui.ErrUnknownView) { + return err + } + + msgView.Title = title + msgView.Clear() + + // Add padding for better appearance + fmt.Fprintln(msgView, "") + fmt.Fprintln(msgView, " "+message) + + return nil +} + func main() { + g, err := gocui.NewGui(gocui.OutputNormal, true) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.Cursor = false + g.Mouse = true + gs := &gameState{ rows: 10, cols: 10, - numMines: 5, + numMines: 10, gameStartedAt: time.Now(), + firstMove: true, } - gs.initField() - gs.printField() + gs.initField(0, 0) + + g.SetManagerFunc(gs.layout) + + if err := gs.keybindings(g); err != nil { + log.Panicln(err) + } + + if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) { + log.Panicln(err) + } }