Files
game-of-life/main.go
Natercio Moniz ea5b5c4e75 Initial commit
implemented the game of life variation with hexagonal grid
2025-12-16 11:33:00 +00:00

170 lines
3.6 KiB
Go

package main
import (
"image/color"
"log"
"time"
"github.com/nmoniz/game-of-life/grid"
"github.com/nmoniz/game-of-life/math"
"github.com/nmoniz/game-of-life/ui"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/input"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
type Game struct {
ui *ebitenui.UI
Grid grid.HexLayout
Alive map[grid.Hex]struct{}
Dead map[grid.Hex]struct{}
Ticker Ticker
Speed int
Paused bool
UnderPopThreshold int
OverPopThreshold int
ReproductionTarget int
}
func NewGame() *Game {
g := &Game{
ui: &ebitenui.UI{Container: ui.Root()},
Grid: grid.HexLayout{
Orientation: grid.PointyTopLayout(),
Origin: math.Pair[int]{X: 4, Y: 4},
Radius: math.Pair[int]{X: 8, Y: 8},
},
Alive: make(map[grid.Hex]struct{}),
Dead: make(map[grid.Hex]struct{}),
Ticker: *NewTicker(1, time.Second),
Speed: 0,
OverPopThreshold: 5,
UnderPopThreshold: 2,
ReproductionTarget: 2,
}
_ = ui.EventBus().Listen(ui.SpeedSliderEvent, func(e ui.Event) {
args, ok := e.Data().(*widget.SliderChangedEventArgs)
if !ok {
return
}
g.Speed = args.Current
if g.Speed > 0 {
g.Ticker.Reset(g.Speed, time.Second)
}
})
_ = ui.EventBus().Listen(ui.OverPopulationSliderEvent, func(e ui.Event) {
args, ok := e.Data().(*widget.SliderChangedEventArgs)
if !ok {
return
}
g.OverPopThreshold = args.Current
})
_ = ui.EventBus().Listen(ui.UnderPopulationSliderEvent, func(e ui.Event) {
args, ok := e.Data().(*widget.SliderChangedEventArgs)
if !ok {
return
}
g.UnderPopThreshold = args.Current
})
_ = ui.EventBus().Listen(ui.ReproductionTargetEvent, func(e ui.Event) {
args, ok := e.Data().(*widget.SliderChangedEventArgs)
if !ok {
return
}
g.ReproductionTarget = args.Current
})
_ = ui.EventBus().Listen(ui.ResetButtonEvent, func(ui.Event) {
g.Alive = make(map[grid.Hex]struct{})
g.Dead = make(map[grid.Hex]struct{})
})
return g
}
func (g *Game) Update() error {
g.ui.Update()
if !input.UIHovered && input.MouseButtonJustPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
hexPos := g.Grid.PixToHex(math.NewPair(x, y))
if _, ok := g.Alive[hexPos]; ok {
delete(g.Alive, hexPos)
} else {
g.Alive[hexPos] = struct{}{}
}
}
if g.Speed == 0 {
return nil
}
if len(g.Alive) > 0 && g.Ticker.HasTicked() {
g.Dead = make(map[grid.Hex]struct{})
neighbourhood := make(map[grid.Hex]int)
for hex := range g.Alive {
neighbourhood[hex] = 0
for _, neighbourHex := range hex.AllNeighbors() {
neighbourhood[neighbourHex]++
}
}
for hex, count := range neighbourhood {
if _, ok := g.Alive[hex]; ok {
if count < g.UnderPopThreshold || count > g.OverPopThreshold {
g.Dead[hex] = struct{}{}
delete(g.Alive, hex)
}
} else if count == g.ReproductionTarget {
g.Alive[hex] = struct{}{}
}
}
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
for k := range g.Dead {
pixPosition := g.Grid.HexToPix(k)
vector.FillCircle(screen, float32(pixPosition.X), float32(pixPosition.Y), 5, color.RGBA{128, 128, 128, 0}, true)
}
for k := range g.Alive {
pixPosition := g.Grid.HexToPix(k)
vector.FillCircle(screen, float32(pixPosition.X), float32(pixPosition.Y), 5, color.RGBA{0, 255, 0, 0}, true)
}
g.ui.Draw(screen)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 800, 600
}
func main() {
ebiten.SetWindowSize(800, 600)
ebiten.SetWindowTitle("Game of Life with hexagons")
if err := ebiten.RunGame(NewGame()); err != nil {
log.Fatal(err)
}
}