mostly works but somehow pressing R to restart doesnt

This commit is contained in:
bdnugget 2025-03-16 22:30:48 +01:00
parent d99d0690e2
commit 4f3fd8e038
4 changed files with 474 additions and 217 deletions

View File

@ -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
}

13
go.mod
View File

@ -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
)

89
go.sum Normal file
View File

@ -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=

398
main.go
View File

@ -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)
}
}