commit 627ca8873205148e6cddb5536a87a42ff2c41d2e Author: Ben Vezzani Date: Sun Aug 17 22:02:40 2025 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/common/colors/hex.go b/common/colors/hex.go new file mode 100644 index 0000000..26af3ad --- /dev/null +++ b/common/colors/hex.go @@ -0,0 +1,14 @@ +package colors + +import ( + "image/color" +) + +func FromInt(in int32) color.Color { + return color.RGBA{ + uint8(in >> 16), + uint8(in >> 8), + uint8(in), + 0xff, + } +} diff --git a/common/colors/hex_test.go b/common/colors/hex_test.go new file mode 100644 index 0000000..984b0dd --- /dev/null +++ b/common/colors/hex_test.go @@ -0,0 +1,62 @@ +package colors + +import ( + "image/color" + "reflect" + "testing" +) + +func TestFromInt(t *testing.T) { + type args struct { + in int32 + } + tests := []struct { + name string + args args + want color.Color + }{ + { + name: "black", + args: args{ + in: 0x000000, + }, + want: color.RGBA{ + 0, 0, 0, 0xff, + }, + }, + { + name: "red", + args: args{ + in: 0xff0000, + }, + want: color.RGBA{ + 0xff, 0, 0, 0xff, + }, + }, + { + name: "green", + args: args{ + in: 0x00ff00, + }, + want: color.RGBA{ + 0, 0xff, 0, 0xff, + }, + }, + { + name: "blue", + args: args{ + in: 0x0000ff, + }, + want: color.RGBA{ + 0, 0, 0xff, 0xff, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FromInt(tt.args.in); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FromInt() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/games/minesweeper/game/game.go b/games/minesweeper/game/game.go new file mode 100644 index 0000000..5d2f842 --- /dev/null +++ b/games/minesweeper/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/main.go b/games/minesweeper/main.go new file mode 100644 index 0000000..b23f639 --- /dev/null +++ b/games/minesweeper/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "log" + + "git.vezzani.net/ben/games/games/minesweeper/game" + "github.com/hajimehoshi/ebiten/v2" +) + +func main() { + g := game.New(10, 10, 50) + w, h := g.Layout(0, 0) + ebiten.SetWindowSize(w, h) + ebiten.SetWindowTitle("Minesweeper") + if err := ebiten.RunGame(g); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da0c2bb --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.vezzani.net/ben/games + +go 1.24 + +require ( + github.com/hajimehoshi/ebiten/v2 v2.8.8 + golang.org/x/image v0.20.0 +) + +require ( + github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/purego v0.8.0 // indirect + github.com/jezek/xgb v1.1.1 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..58e3580 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= +github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= +github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= +github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= +github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/hajimehoshi/bitmapfont/v3 v3.2.0 h1:0DISQM/rseKIJhdF29AkhvdzIULqNIIlXAGWit4ez1Q= +github.com/hajimehoshi/bitmapfont/v3 v3.2.0/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= +github.com/hajimehoshi/ebiten/v2 v2.8.8 h1:xyMxOAn52T1tQ+j3vdieZ7auDBOXmvjUprSrxaIbsi8= +github.com/hajimehoshi/ebiten/v2 v2.8.8/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=