From 8e7db331a763c0f2b5c56b45493d3ea492da1705 Mon Sep 17 00:00:00 2001 From: Ben Vezzani Date: Fri, 5 Sep 2025 22:18:36 -0400 Subject: [PATCH] some refinement --- common/elements/v1/blocks/block.go | 24 ++- common/elements/v1/blocks/options.go | 8 +- common/elements/v1/buttons/button.go | 3 +- common/elements/v1/buttons/options.go | 8 +- common/elements/v1/config.go | 15 -- common/elements/v1/core/config.go | 17 ++ common/elements/v1/core/core.go | 32 ++++ common/elements/v1/element.go | 55 +------ common/elements/v1/mouse/mouse.go | 5 +- common/elements/v1/stacks/options.go | 10 +- common/elements/v1/stacks/stack.go | 6 +- common/geo/bounds.go | 10 ++ common/geo/point.go | 19 +++ games/minesweeper/v2/game/game.go | 229 ++++++++++++++++++++++++++ games/minesweeper/v2/main.go | 1 + tools/component_test/editor.go | 48 +++--- 16 files changed, 371 insertions(+), 119 deletions(-) delete mode 100644 common/elements/v1/config.go create mode 100644 common/elements/v1/core/config.go create mode 100644 common/elements/v1/core/core.go create mode 100644 common/geo/bounds.go create mode 100644 common/geo/point.go create mode 100644 games/minesweeper/v2/game/game.go create mode 100644 games/minesweeper/v2/main.go diff --git a/common/elements/v1/blocks/block.go b/common/elements/v1/blocks/block.go index cbfd734..4524c6d 100644 --- a/common/elements/v1/blocks/block.go +++ b/common/elements/v1/blocks/block.go @@ -3,32 +3,30 @@ package blocks import ( "image/color" - "git.vezzani.net/ben/games/common/elements/v1" - "git.vezzani.net/ben/games/common/elements/v1/mouse" - + "git.vezzani.net/ben/games/common/elements/v1/core" + "git.vezzani.net/ben/games/common/geo" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) -func New(ops ...OptFunc) elements.Element { - b := Block{} +func New(ops ...OptFunc) *Block { + b := &Block{} for i := range ops { - ops[i](&b) + ops[i](b) } - return &b + return b } type Block struct { - elements.Core + core.Element backgroundColor *color.Color - mouse.NopHandler - width, height int - name string + width, height int + name string } -func (b *Block) Bounds() elements.Bounds { - return elements.Bounds{ +func (b *Block) Bounds() geo.Bounds { + return geo.Bounds{ Min: b.Anchor(), Width: b.width, Height: b.height, diff --git a/common/elements/v1/blocks/options.go b/common/elements/v1/blocks/options.go index 9644f5f..99e6c9c 100644 --- a/common/elements/v1/blocks/options.go +++ b/common/elements/v1/blocks/options.go @@ -3,14 +3,16 @@ package blocks import ( "image/color" - "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/core" ) type OptFunc func(*Block) -func Core(f elements.OptFunc) OptFunc { +func Core(fs ...core.OptFunc) OptFunc { return func(b *Block) { - f(&b.Core) + for _, f := range fs { + f(&b.Element) + } } } diff --git a/common/elements/v1/buttons/button.go b/common/elements/v1/buttons/button.go index 54c459d..b70d2a0 100644 --- a/common/elements/v1/buttons/button.go +++ b/common/elements/v1/buttons/button.go @@ -3,7 +3,6 @@ package buttons import ( "image/color" - "git.vezzani.net/ben/games/common/elements/v1" "git.vezzani.net/ben/games/common/elements/v1/blocks" "git.vezzani.net/ben/games/common/elements/v1/mouse" "git.vezzani.net/ben/games/common/ux/v1" @@ -13,7 +12,7 @@ import ( "golang.org/x/image/font" ) -func New(ops ...OptFunc) elements.Element { +func New(ops ...OptFunc) *Button { b := Button{ Block: blocks.Block{}, font: ux.FontFace, diff --git a/common/elements/v1/buttons/options.go b/common/elements/v1/buttons/options.go index afcde1c..857be7a 100644 --- a/common/elements/v1/buttons/options.go +++ b/common/elements/v1/buttons/options.go @@ -3,16 +3,18 @@ package buttons import ( "image/color" - "git.vezzani.net/ben/games/common/elements/v1" "git.vezzani.net/ben/games/common/elements/v1/blocks" + "git.vezzani.net/ben/games/common/elements/v1/core" "git.vezzani.net/ben/games/common/elements/v1/mouse" ) type OptFunc func(button *Button) -func Core(f elements.OptFunc) OptFunc { +func Core(fs ...core.OptFunc) OptFunc { return func(b *Button) { - f(&b.Core) + for _, f := range fs { + f(&b.Element) + } } } diff --git a/common/elements/v1/config.go b/common/elements/v1/config.go deleted file mode 100644 index 193596e..0000000 --- a/common/elements/v1/config.go +++ /dev/null @@ -1,15 +0,0 @@ -package elements - -type OptFunc func(*Core) - -func Children(children ...Element) OptFunc { - return func(c *Core) { - c.children = children - } -} - -func Name(name string) OptFunc { - return func(s *Core) { - s.name = name - } -} diff --git a/common/elements/v1/core/config.go b/common/elements/v1/core/config.go new file mode 100644 index 0000000..7424b90 --- /dev/null +++ b/common/elements/v1/core/config.go @@ -0,0 +1,17 @@ +package core + +import "git.vezzani.net/ben/games/common/elements/v1" + +type OptFunc func(*Element) + +func Children(children ...elements.Element) OptFunc { + return func(c *Element) { + c.children = children + } +} + +func Name(name string) OptFunc { + return func(s *Element) { + s.name = name + } +} diff --git a/common/elements/v1/core/core.go b/common/elements/v1/core/core.go new file mode 100644 index 0000000..8196cff --- /dev/null +++ b/common/elements/v1/core/core.go @@ -0,0 +1,32 @@ +package core + +import ( + "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/mouse" + "git.vezzani.net/ben/games/common/geo" +) + +type Element struct { + mouse.NopHandler + + anchor geo.Point + children []elements.Element + + name string // For debugging +} + +func (c *Element) Anchor() geo.Point { + return c.anchor +} + +func (c *Element) SetAnchor(a geo.Point) { + d := c.Anchor().Delta(a) + for _, ch := range c.Children() { + ch.SetAnchor(ch.Anchor().Add(d)) + } + c.anchor = a +} + +func (c *Element) Children() []elements.Element { + return c.children +} diff --git a/common/elements/v1/element.go b/common/elements/v1/element.go index 3f33463..5baf06c 100644 --- a/common/elements/v1/element.go +++ b/common/elements/v1/element.go @@ -1,63 +1,14 @@ package elements import ( + "git.vezzani.net/ben/games/common/geo" "github.com/hajimehoshi/ebiten/v2" ) type Element interface { Draw(*ebiten.Image) - SetAnchor(Point) - Anchor() Point + SetAnchor(geo.Point) + Anchor() geo.Point Size() (w, h int) Children() []Element } - -type Point struct { - X, Y int -} - -func (p Point) Delta(p2 Point) Point { - return Point{ - X: p2.X - p.X, - Y: p2.Y - p.Y, - } -} - -func (p Point) Add(p2 Point) Point { - return Point{ - X: p.X + p2.X, - Y: p.Y + p2.Y, - } -} - -type Bounds struct { - Min Point - Width, Height int -} - -func (b *Bounds) Contains(p Point) bool { - return p.X >= b.Min.X && p.X <= b.Min.X+b.Width && p.Y >= b.Min.Y && p.Y <= b.Min.Y+b.Height -} - -type Core struct { - anchor Point - children []Element - - name string // For debugging -} - -func (c *Core) Anchor() Point { - return c.anchor -} - -func (c *Core) SetAnchor(a Point) { - d := c.Anchor().Delta(a) - for _, ch := range c.Children() { - ch.SetAnchor(ch.Anchor().Add(d)) - } - c.anchor = a -} - -func (c *Core) Children() []Element { - return c.children -} diff --git a/common/elements/v1/mouse/mouse.go b/common/elements/v1/mouse/mouse.go index cd4443d..c20a2ed 100644 --- a/common/elements/v1/mouse/mouse.go +++ b/common/elements/v1/mouse/mouse.go @@ -2,6 +2,7 @@ package mouse import ( "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/geo" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) @@ -24,8 +25,8 @@ type State struct { Clicked bool } -func (s *State) Point() elements.Point { - return elements.Point{ +func (s *State) Point() geo.Point { + return geo.Point{ X: s.X, Y: s.Y, } diff --git a/common/elements/v1/stacks/options.go b/common/elements/v1/stacks/options.go index cefb6c9..701ee81 100644 --- a/common/elements/v1/stacks/options.go +++ b/common/elements/v1/stacks/options.go @@ -1,12 +1,16 @@ package stacks -import "git.vezzani.net/ben/games/common/elements/v1" +import ( + "git.vezzani.net/ben/games/common/elements/v1/core" +) type OptFunc func(*Stack) -func Core(f elements.OptFunc) OptFunc { +func Core(fs ...core.OptFunc) OptFunc { return func(s *Stack) { - f(&s.Core) + for _, f := range fs { + f(&s.Element) + } } } diff --git a/common/elements/v1/stacks/stack.go b/common/elements/v1/stacks/stack.go index d4792af..6ea4975 100644 --- a/common/elements/v1/stacks/stack.go +++ b/common/elements/v1/stacks/stack.go @@ -1,13 +1,13 @@ package stacks import ( - "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/core" "git.vezzani.net/ben/games/common/elements/v1/mouse" "github.com/hajimehoshi/ebiten/v2" ) -func New(ops ...OptFunc) elements.Element { +func New(ops ...OptFunc) *Stack { s := Stack{} for op := range ops { ops[op](&s) @@ -29,7 +29,7 @@ func New(ops ...OptFunc) elements.Element { } type Stack struct { - elements.Core + core.Element mouse.NopHandler horizontal bool } diff --git a/common/geo/bounds.go b/common/geo/bounds.go new file mode 100644 index 0000000..162d822 --- /dev/null +++ b/common/geo/bounds.go @@ -0,0 +1,10 @@ +package geo + +type Bounds struct { + Min Point + Width, Height int +} + +func (b *Bounds) Contains(p Point) bool { + return p.X >= b.Min.X && p.X <= b.Min.X+b.Width && p.Y >= b.Min.Y && p.Y <= b.Min.Y+b.Height +} diff --git a/common/geo/point.go b/common/geo/point.go new file mode 100644 index 0000000..158c497 --- /dev/null +++ b/common/geo/point.go @@ -0,0 +1,19 @@ +package geo + +type Point struct { + X, Y int +} + +func (p Point) Delta(p2 Point) Point { + return Point{ + X: p2.X - p.X, + Y: p2.Y - p.Y, + } +} + +func (p Point) Add(p2 Point) Point { + return Point{ + X: p.X + p2.X, + Y: p.Y + p2.Y, + } +} diff --git a/games/minesweeper/v2/game/game.go b/games/minesweeper/v2/game/game.go new file mode 100644 index 0000000..5d2f842 --- /dev/null +++ b/games/minesweeper/v2/game/game.go @@ -0,0 +1,229 @@ +package game + +import ( + "image/color" + "math/rand" + "strconv" + + "git.vezzani.net/ben/games/common/colors" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/hajimehoshi/ebiten/v2/vector" + "golang.org/x/image/font/basicfont" +) + +type mouseButton int + +const ( + cellSize = 32 + borderThickness = 1 + + sideMargin = cellSize + topMargin = cellSize * 2 + bottomMargin = cellSize + + left mouseButton = iota + right +) + +var ( + backgroundColor = colors.FromInt(0xffffff) + borderColor = colors.FromInt(0x999999) + revealedColor = colors.FromInt(0xdddddd) + downColor = colors.FromInt(0xaaaaaa) + + dangerColors = [8]color.Color{ + colors.FromInt(0x000000), + colors.FromInt(0x440000), + colors.FromInt(0x880000), + colors.FromInt(0xaa0000), + colors.FromInt(0xcc0000), + colors.FromInt(0xdd0000), + colors.FromInt(0xee0000), + colors.FromInt(0xff0000), + } + + cols, rows int32 + + board []square + + leftDown, rightDown bool + leftClicked, rightClicked bool + + mouseX, mouseY int + + over bool +) + +type square struct { + mined bool + revealed bool + danger uint8 +} + +func New(colCount, rowCount, mineCount int32) *Core { + cols = colCount + rows = rowCount + board = make([]square, cols*rows) + for range mineCount { + addr := rand.Int31n(cols * rows) + for board[addr].mined { + addr = rand.Int31n(cols * rows) + } + board[addr].mined = true + } + + initDanger() + + return &Core{} +} + +type Core struct{} + +func (c Core) Update() error { + mouseX, mouseY = ebiten.CursorPosition() + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + leftDown = true + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + rightDown = true + } + + if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { + leftDown = false + if !rightDown { + handleClick(left) + } + } + if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) { + rightDown = false + if !leftDown { + handleClick(right) + } + } + + return nil +} + +func (c Core) Draw(screen *ebiten.Image) { + maxX, maxY := c.Layout(0, 0) + vector.DrawFilledRect(screen, 0, 0, float32(maxX), float32(maxY), backgroundColor, false) + + drawGrid(screen) +} + +func (c Core) Layout(_, _ int) (screenWidth, screenHeight int) { + return int(cellSize*cols + sideMargin*2), int(cellSize*rows + topMargin + bottomMargin) +} + +func drawGrid(screen *ebiten.Image) { + vector.StrokeRect(screen, sideMargin, topMargin, float32(cellSize*cols), float32(cellSize*rows), borderThickness, borderColor, false) + + mouseSquare := int32(-1) + + if leftDown || rightDown { + mouseSquare = mouseOverSquare() + } + + for i := range rows * cols { + clr := backgroundColor + if board[i].revealed { + clr = revealedColor + } else { + if mouseSquare == i { + clr = downColor + } + } + x, y, w, h := getCellSquare(i) + vector.DrawFilledRect(screen, x, y, w, h, clr, false) + vector.StrokeRect(screen, x, y, w, h, borderThickness, borderColor, false) + if board[i].revealed && board[i].danger > 0 { + txt := strconv.Itoa(int(board[i].danger)) + + text.Draw(screen, txt, basicfont.Face7x13, int(x)+12, int(y)+20, dangerColors[board[i].danger]) + } + } +} + +func mouseOverSquare() int32 { + if int32(mouseX) < sideMargin || + int32(mouseY) < topMargin || + int32(mouseX) > sideMargin+cellSize*cols || + int32(mouseX) > topMargin+cellSize*rows { + return -1 + } + + baseMouseX := (int32(mouseX) - sideMargin) / cellSize + baseMouseY := (int32(mouseY) - topMargin) / cellSize + + return baseMouseY*rows + baseMouseX +} + +func getCellSquare(addr int32) (float32, float32, float32, float32) { + c, r := float32(addr%cols), float32(addr/rows) + return sideMargin + c*cellSize, topMargin + r*cellSize, cellSize, cellSize +} + +func handleClick(side mouseButton) { + sq := mouseOverSquare() + if sq == -1 { + return + } + + if side == left { + if board[sq].mined { + + } else { + reveal(sq) + } + } +} + +func reveal(addr int32) { + if board[addr].revealed { + return + } + board[addr].revealed = true + for _, nAddr := range getNeighbors(addr) { + if board[nAddr].revealed || board[nAddr].mined { + continue + } + reveal(nAddr) + } +} + +func getNeighbors(addr int32) []int32 { + ns := make([]int32, 0) + sides := make([]int32, 0) + c, r := addr%cols, addr/rows + if c > 0 { + sides = append(sides, addr-1) + } + if c < cols-1 { + sides = append(sides, addr+1) + } + if r > 0 { + ns = append(ns, addr-cols) + for _, s := range sides { + ns = append(ns, s-cols) + } + } + if r < rows-1 { + ns = append(ns, addr+cols) + for _, s := range sides { + ns = append(ns, s+cols) + } + } + return append(ns, sides...) +} + +func initDanger() { + for i := range board { + for _, n := range getNeighbors(int32(i)) { + if board[n].mined { + board[i].danger++ + } + } + } +} diff --git a/games/minesweeper/v2/main.go b/games/minesweeper/v2/main.go new file mode 100644 index 0000000..5ec3cc8 --- /dev/null +++ b/games/minesweeper/v2/main.go @@ -0,0 +1 @@ +package v2 diff --git a/tools/component_test/editor.go b/tools/component_test/editor.go index 35faa44..0a08ffc 100644 --- a/tools/component_test/editor.go +++ b/tools/component_test/editor.go @@ -1,11 +1,12 @@ package main import ( - "git.vezzani.net/ben/games/common/elements/v1" "git.vezzani.net/ben/games/common/elements/v1/blocks" "git.vezzani.net/ben/games/common/elements/v1/buttons" + "git.vezzani.net/ben/games/common/elements/v1/core" "git.vezzani.net/ben/games/common/elements/v1/mouse" "git.vezzani.net/ben/games/common/elements/v1/stacks" + "git.vezzani.net/ben/games/common/geo" "git.vezzani.net/ben/games/common/sprites/v1" "github.com/hajimehoshi/ebiten/v2" @@ -14,29 +15,30 @@ import ( var root = stacks.New( stacks.Horizontal(), - //stacks.Core(blocks.BackgroundColor(color.White)), - stacks.Core(elements.Name("parent")), - stacks.Core(elements.Children( + stacks.Core(core.Name("parent")), + stacks.Core(core.Children( stacks.New( - stacks.Core(elements.Name("left")), - stacks.Core(elements.Children( - buttons.New( - buttons.BlockOpt(blocks.Size(100, 100)), - buttons.Label("hello"), - buttons.BlockOpt(blocks.BackgroundColor(colornames.Green)), - buttons.OnClick(func(ms mouse.State) { - println("green") - }), + stacks.Core( + core.Name("left"), + core.Children( + buttons.New( + buttons.BlockOpt(blocks.Size(100, 100)), + buttons.Label("hello"), + buttons.BlockOpt(blocks.BackgroundColor(colornames.Green)), + buttons.OnClick(func(ms mouse.State) { + println("green") + }), + ), + blocks.New( + blocks.Size(100, 100), + blocks.BackgroundColor(colornames.Yellow), + ), ), - blocks.New( - blocks.Size(100, 100), - blocks.BackgroundColor(colornames.Yellow), - ), - )), + ), ), stacks.New( - stacks.Core(elements.Name("right")), - stacks.Core(elements.Children( + stacks.Core(core.Name("right")), + stacks.Core(core.Children( blocks.New( blocks.Size(100, 100), blocks.BackgroundColor(colornames.Blue), @@ -55,7 +57,7 @@ func newEditor() *editor { } type editor struct { - bounds elements.Bounds + bounds geo.Bounds handleMouse func() bool } @@ -71,8 +73,8 @@ func (e *editor) Draw(screen *ebiten.Image) { func (e *editor) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { if e.bounds.Width != outsideWidth || e.bounds.Height != outsideHeight { - e.bounds = elements.Bounds{ - Min: elements.Point{}, + e.bounds = geo.Bounds{ + Min: geo.Point{}, Width: outsideWidth, Height: outsideHeight, }