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