package main import ( "fmt" "image/color" "log" "math/rand" "os" "strconv" "strings" "github.com/alecthomas/repr" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/vector" "github.com/spf13/pflag" ) const ( VERSION = "v0.0.1" Alive = 1 Dead = 0 ) type Grid struct { Data [][]int } type Images struct { Black, White, Beige *ebiten.Image } type Game struct { Grids []*Grid // 2 grids: one current, one next History *Grid Index int // points to current grid Width, Height, Cellsize, Density int ScreenWidth, ScreenHeight int Generations int Black, White, Grey, Beige color.RGBA Speed int Debug, Paused, Empty, Invert bool ShowEvolution, NoGrid, RunOneStep bool Rule *Rule Tiles Images } func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return game.ScreenWidth, game.ScreenHeight } func (game *Game) CheckRule(state, neighbors int) int { var nextstate int // The standard Game of Life is symbolized in rule-string notation // as B3/S23 (23/3 here). A cell is born if it has exactly three // neighbors, survives if it has two or three living neighbors, // and dies otherwise. The first number, or list of numbers, is // what is required for a dead cell to be born. if state == 0 && Contains(game.Rule.Birth, neighbors) { nextstate = 1 } else if state == 1 && Contains(game.Rule.Death, neighbors) { nextstate = 1 } else { nextstate = 0 } return nextstate } // find an item in a list, generic variant func Contains[E comparable](s []E, v E) bool { for _, vs := range s { if v == vs { return true } } return false } func (game *Game) UpdateCells() { // compute cells next := game.Index ^ 1 // next grid index, we just xor 0|1 to 1|0 for y := 0; y < game.Height; y++ { for x := 0; x < game.Width; x++ { state := game.Grids[game.Index].Data[y][x] // 0|1 == dead or alive neighbors := CountNeighbors(game, x, y) // alive neighbor count // actually apply the current rules nextstate := game.CheckRule(state, neighbors) // change state of current cell in next grid game.Grids[next].Data[y][x] = nextstate if state == 1 { game.History.Data[y][x] = 1 } } } // switch grid for rendering game.Index ^= 1 // global counter game.Generations++ if game.RunOneStep { game.RunOneStep = false } } // a GOL rule type Rule struct { Birth []int Death []int } // parse one part of a GOL rule into rule slice func NumbersToList(numbers string) []int { list := []int{} items := strings.Split(numbers, "") for _, item := range items { num, err := strconv.Atoi(item) if err != nil { log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err) } list = append(list, num) } return list } // parse GOL rule, used in CheckRule() func ParseGameRule(rule string) *Rule { parts := strings.Split(rule, "/") if len(parts) < 2 { log.Fatalf("Invalid game rule <%s>", rule) } golrule := &Rule{} for _, part := range parts { if part[0] == 'B' { golrule.Birth = NumbersToList(part[1:]) } else { golrule.Death = NumbersToList(part[1:]) } } return golrule } // check user input func (game *Game) CheckInput() { if inpututil.IsKeyJustPressed(ebiten.KeyQ) { os.Exit(0) } if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) { game.Paused = !game.Paused } if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { ToggleCell(game, Alive) game.Paused = true // drawing while running makes no sense } if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { ToggleCell(game, Dead) game.Paused = true // drawing while running makes no sense } if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) { if game.Speed > 1 { game.Speed-- ebiten.SetTPS(game.Speed) } } if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) { if game.Speed < 120 { game.Speed++ ebiten.SetTPS(game.Speed) } } if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) { switch { case game.Speed > 5: game.Speed -= 5 case game.Speed <= 5: game.Speed = 1 } ebiten.SetTPS(game.Speed) } if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) { if game.Speed <= 115 { game.Speed += 5 ebiten.SetTPS(game.Speed) } } if game.Paused { if inpututil.IsKeyJustPressed(ebiten.KeyN) { game.RunOneStep = true } } } func (game *Game) Update() error { game.CheckInput() if !game.Paused || game.RunOneStep { game.UpdateCells() } return nil } // set a cell to alive or dead func ToggleCell(game *Game, alive int) { xPX, yPX := ebiten.CursorPosition() x := xPX / game.Cellsize y := yPX / game.Cellsize //fmt.Printf("cell at %d,%d\n", x, y) game.Grids[game.Index].Data[y][x] = alive game.History.Data[y][x] = 1 } // draw the new grid state func (game *Game) Draw(screen *ebiten.Image) { // we fill the whole screen with a background color, the cells // themselfes will be 1px smaller as their nominal size, producing // a nice grey grid with grid lines op := &ebiten.DrawImageOptions{} if game.NoGrid { screen.Fill(game.White) } else { screen.Fill(game.Grey) } for y := 0; y < game.Height; y++ { for x := 0; x < game.Width; x++ { op.GeoM.Reset() op.GeoM.Translate(float64(x*game.Cellsize), float64(y*game.Cellsize)) switch game.Grids[game.Index].Data[y][x] { case 1: screen.DrawImage(game.Tiles.Black, op) case 0: if game.History.Data[y][x] == 1 && game.ShowEvolution { screen.DrawImage(game.Tiles.Beige, op) } else { screen.DrawImage(game.Tiles.White, op) } } } } if game.Debug { paused := "" if game.Paused { paused = "-- paused --" } ebitenutil.DebugPrint( screen, fmt.Sprintf("FPS: %d, Generations: %d %s", game.Speed, game.Generations, paused), ) } } func (game *Game) InitGrid() { grid := &Grid{Data: make([][]int, game.Height)} gridb := &Grid{Data: make([][]int, game.Height)} history := &Grid{Data: make([][]int, game.Height)} for y := 0; y < game.Height; y++ { grid.Data[y] = make([]int, game.Width) gridb.Data[y] = make([]int, game.Width) history.Data[y] = make([]int, game.Width) if !game.Empty { for x := 0; x < game.Width; x++ { if rand.Intn(game.Density) == 1 { history.Data[y][x] = 1 grid.Data[y][x] = 1 } } } } game.Grids = []*Grid{ grid, gridb, } game.History = history } // fill a cell with the given color func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA) { vector.DrawFilledRect( tile, float32(1), float32(1), float32(cellsize-1), float32(cellsize-1), col, false, ) } // prepare tile images func (game *Game) InitTiles() { game.Black = color.RGBA{0, 0, 0, 0xff} game.White = color.RGBA{200, 200, 200, 0xff} game.Grey = color.RGBA{128, 128, 128, 0xff} game.Beige = color.RGBA{0xff, 0xf8, 0xdc, 0xff} if game.Invert { game.White = color.RGBA{0, 0, 0, 0xff} game.Black = color.RGBA{200, 200, 200, 0xff} //game.Beige = color.RGBA{0x8b, 0x1a, 0x1a, 0xff} game.Beige = color.RGBA{0x30, 0x1c, 0x11, 0xff} } game.Tiles.Beige = ebiten.NewImage(game.Cellsize, game.Cellsize) game.Tiles.Black = ebiten.NewImage(game.Cellsize, game.Cellsize) game.Tiles.White = ebiten.NewImage(game.Cellsize, game.Cellsize) cellsize := game.ScreenWidth / game.Cellsize FillCell(game.Tiles.Beige, cellsize, game.Beige) FillCell(game.Tiles.Black, cellsize, game.Black) FillCell(game.Tiles.White, cellsize, game.White) } func (game *Game) Init() { // setup the game game.ScreenWidth = game.Cellsize * game.Width game.ScreenHeight = game.Cellsize * game.Height game.InitGrid() game.InitTiles() game.Index = 0 } // count the living neighbors of a cell func CountNeighbors(game *Game, x, y int) int { sum := 0 // so we look ad all 8 neighbors surrounding us. In case we are on // an edge, then we'll look at the neighbor on the other side of // the grid, thus wrapping lookahead around. for i := -1; i < 2; i++ { for j := -1; j < 2; j++ { col := (x + i + game.Width) % game.Width row := (y + j + game.Height) % game.Height sum += game.Grids[game.Index].Data[row][col] } } // don't count ourselfes though sum -= game.Grids[game.Index].Data[y][x] return sum } func main() { game := &Game{} showversion := false var rule string pflag.IntVarP(&game.Width, "width", "W", 40, "grid width in cells") pflag.IntVarP(&game.Height, "height", "H", 40, "grid height in cells") pflag.IntVarP(&game.Cellsize, "cellsize", "c", 8, "cell size in pixels") pflag.IntVarP(&game.Speed, "tps", "t", 60, "game speed in ticks per second") pflag.IntVarP(&game.Density, "density", "D", 10, "density of random cells") pflag.StringVarP(&rule, "rule", "r", "B3/S23", "game rule") pflag.BoolVarP(&showversion, "version", "v", false, "show version") pflag.BoolVarP(&game.Paused, "paused", "p", false, "do not start simulation (use space to start)") pflag.BoolVarP(&game.Debug, "debug", "d", false, "show debug info") pflag.BoolVarP(&game.NoGrid, "nogrid", "n", false, "do not draw grid lines") pflag.BoolVarP(&game.Empty, "empty", "e", false, "start with an empty screen") pflag.BoolVarP(&game.Invert, "invert", "i", false, "invert colors (dead cell: black)") pflag.BoolVarP(&game.ShowEvolution, "show-evolution", "s", false, "show evolution tracks") pflag.Parse() if showversion { fmt.Printf("This is gameoflife version %s\n", VERSION) os.Exit(0) } game.Rule = ParseGameRule(rule) repr.Print(game.Rule.Birth) repr.Print(game.Rule.Death) game.Init() ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight) ebiten.SetWindowTitle("Game of life") ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) ebiten.SetTPS(game.Speed) if err := ebiten.RunGame(game); err != nil { log.Fatal(err) } }