24 Commits

Author SHA1 Message Date
e8ed283233 next try to enhance performance: now using uint9 again and no pointers 2024-07-15 14:21:21 +02:00
89d25db9e7 added profiling target, makes it easier 2024-07-15 14:21:03 +02:00
89903fdcec better comments, simplified check 2024-07-13 20:46:11 +02:00
05d56568e4 fix #7: use only left mouse button to draw, it toggles cell state 2024-07-13 19:31:25 +02:00
Thomas von Dein
50b630791a bumpversion 2024-07-12 23:00:28 +02:00
957db29a37 added C test 2024-07-12 23:00:28 +02:00
eb95c72538 fixed grid lines performance problem 2024-07-12 23:00:28 +02:00
604cbea127 use optimized rule check if B3/S23 is active 2024-07-12 23:00:28 +02:00
f3e7428775 draw the grid explicitly thus leading to full cells w/o the grid 2024-07-12 23:00:28 +02:00
6685207fde revert last revert and fixed history slow down bug 2024-07-12 23:00:28 +02:00
41da9b8536 revert history=>struct, but evolution doesn't work anymore anyway 2024-07-12 23:00:28 +02:00
6dec8c74ef bool => uint8 2024-07-12 23:00:28 +02:00
861ba86b0c using go-routines (one per row), makes it faster 2024-07-12 23:00:28 +02:00
7b0a74fb93 use cells instead of only bools, use pointer list to all neighbors 2024-07-12 23:00:28 +02:00
e516b218fd tuning fail 2024-06-14 19:58:03 +02:00
6544052bb7 tried more variants: writepixel+pointer+int, which is not much better 2024-06-14 19:56:00 +02:00
45e5fc7e3b tried arche ecs: utter fail, needs 4.3 the time 2024-06-12 20:01:30 +02:00
39da34cb5c more tests 2024-06-11 23:59:47 +02:00
T.v.Dein
1623277c85 Update TODO.md: add pointer idea, try ecs 2024-06-11 23:08:49 +02:00
15bce3cb3a add grid idea 2024-06-11 19:47:01 +02:00
3cff41c991 fixed colors of standard theme 2024-06-11 19:43:30 +02:00
d66fb489fe using switch in input checks 2024-06-11 19:39:46 +02:00
927e47dc92 remove old debug prints, update TODO 2024-06-11 19:22:29 +02:00
f14f4ff21a removed unneeded code, fixed game exit from menu 2024-06-11 19:15:40 +02:00
37 changed files with 1587 additions and 231 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ rect*
*prof *prof
*lif *lif
*rle *rle
svgicons

View File

@@ -1,5 +1,8 @@
.PHONY: all .PHONY all:
all: all: build
.PHONY: build
build:
make -C src make -C src
mv src/golsky . mv src/golsky .
@@ -7,3 +10,8 @@ all:
clean: clean:
make -C src clean make -C src clean
rm -f dump* rect* rm -f dump* rect*
.PHONY: profile
profile: build
./golsky -W 1500 -H 1500 -d --profile-file cpu.profile
go tool pprof --http localhost:8888 golsky cpu.profile

View File

@@ -82,17 +82,16 @@ Usage of ./golsky:
While it runs, there are a couple of commands you can use: While it runs, there are a couple of commands you can use:
* left mouse click: set a cell to alife (also pauses the game)
* right mouse click: set a cell to dead
* space: pause or resume the game * space: pause or resume the game
* while game is paused: press n to forward one step * while game is paused: press n to forward one step
* page up: speed up * page up: speed up
* page down: slow down * page down: slow down
* Mouse wheel: zoom in or out * Mouse wheel: zoom in or out
* move mouse while left mouse button pressed: move canvas * move mouse while left mouse button pressed: move canvas
* i: enter "insert" (draw) mode: use left mouse to set cells alife and right * i: enter "insert" (draw) mode: use left mouse to toggle cells alife state.
button to dead. Leave with "space". While in insert mode, use middle mouse Leave with insert mode "space". While in insert mode, use middle mouse
button to drag grid. button to drag the grid.
* r: reset to 1:1 zoom * r: reset to 1:1 zoom
* escape: open menu * escape: open menu
* s: save game state to file (can be loaded with -l) * s: save game state to file (can be loaded with -l)

18
TODO.md
View File

@@ -1,23 +1,29 @@
- add all other options like size etc - add all other options like size etc
- add gif export - add gif export
- add toolbar - add toolbar (not working yet, see branch trackui)
- turn input ifs to switch
- only draw visible part of the world - only draw visible part of the world
- use themes instead of the current weird color lists
- print current mode to the bottom like pause, insert and mark - print current mode to the bottom like pause, insert and mark
- add https://www.ibiblio.org/lifepatterns/october1970.html - add https://www.ibiblio.org/lifepatterns/october1970.html
- history: dont count age but do calc to get index to age tile based on cell age - history: dont count age but do calc to get index to age tile based on cell age
- maybe pre calc neighbors as 8 slice of pointers to neighboring cells to faster do the count - maybe pre calc neighbors as 8 slice of pointers to neighboring cells to faster do the count
see various-tests/perf-2dim-pointers/: it's NOT faster :(
- use an array of 8 pointers to neighbors. on edge just add either fake dead neighbors or the wrap around neighbors.
- try arche ecs variant with either a component of the cells neighbors or using relations.
- https://mattnakama.com/blog/go-branchless-coding/ - https://mattnakama.com/blog/go-branchless-coding/
- add performance measurements, see: - add performance measurements, see:
DrawTriangles: https://github.com/TLINDEN/testgol DrawTriangles: https://github.com/TLINDEN/testgol
WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels
https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm
- pre-draw the grid separately to a cache grid image, then during
- show gridlines menu has no effect of grid was enabled with -g rendering, first draw the dead background, then the life cells, and
lastly the grid - if enabled. If disabled, there's be no gap between
the cells anymore.
- Speed - Speed
https://conwaylife.com/forums/viewtopic.php?f=7&t=3237 https://conwaylife.com/forums/viewtopic.php?f=7&t=3237
Look at try-pointers-and-cells branch, we're using pre-calculated
neighbor list of pointers to cells, but it's only a liiiiitle bit
better :(
- Patterns: - Patterns:

1
go.mod
View File

@@ -16,6 +16,7 @@ require (
github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21 // indirect github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jezek/xgb v1.1.1 // indirect github.com/jezek/xgb v1.1.1 // indirect
github.com/mlange-42/arche v0.13.0 // indirect
github.com/tinne26/etxt v0.0.8 // indirect github.com/tinne26/etxt v0.0.8 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect

2
go.sum
View File

@@ -16,6 +16,8 @@ github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2X
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY= github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/mlange-42/arche v0.13.0 h1:ef0fu9qC2KIr8wIlVs+CgeQ5CSUJ8A1Hut6nXYdf+xk=
github.com/mlange-42/arche v0.13.0/go.mod h1:bFktKnvGDj2kP01xar79z0hKwGHdnoaEZR8HWmJkIyU=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc= github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc=

View File

@@ -120,7 +120,7 @@ func removeWhitespace(input string) string {
} }
// Store a grid to an RLE file // Store a grid to an RLE file
func StoreGridToRLE(grid [][]bool, filename, rule string, width, height int) error { func StoreGridToRLE(grid [][]uint8, filename, rule string, width, height int) error {
fd, err := os.Create(filename) fd, err := os.Create(filename)
if err != nil { if err != nil {
return err return err
@@ -132,7 +132,7 @@ func StoreGridToRLE(grid [][]bool, filename, rule string, width, height int) err
line := "" line := ""
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
char := "b" char := "b"
if grid[y][x] { if grid[y][x] == 1 {
char = "o" char = "o"
} }

View File

@@ -43,16 +43,16 @@ type Config struct {
} }
const ( const (
VERSION = "v0.0.8" VERSION = "v0.0.9"
Alive = true Alive = 1
Dead = false Dead = 0
DEFAULT_GRID_WIDTH = 600 DEFAULT_GRID_WIDTH = 600
DEFAULT_GRID_HEIGHT = 400 DEFAULT_GRID_HEIGHT = 400
DEFAULT_CELLSIZE = 4 DEFAULT_CELLSIZE = 4
DEFAULT_ZOOMFACTOR = 400 DEFAULT_ZOOMFACTOR = 400
DEFAULT_GEOM = "640x384" DEFAULT_GEOM = "640x384"
DEFAULT_THEME = "standard" // "light" // inverse => "dark" DEFAULT_THEME = "standard"
) )
const KEYBINDINGS string = ` const KEYBINDINGS string = `
@@ -62,9 +62,9 @@ const KEYBINDINGS string = `
- PAGE DOWN: slow down - PAGE DOWN: slow down
- MOUSE WHEEL: zoom in or out - MOUSE WHEEL: zoom in or out
- LEFT MOUSE BUTTON: use to drag canvas, keep clicked and move mouse - LEFT MOUSE BUTTON: use to drag canvas, keep clicked and move mouse
- I: enter "insert" (draw) mode: use left mouse to set cells alife and right - I: enter "insert" (draw) mode: use left mouse to toggle a cells alife state.
button to dead. Leave with "space". While in insert mode, use middle mouse Leave with insert mode with "space". While in insert mode, use middle mouse
button to drag grid. button to drag the grid.
- R: reset to 1:1 zoom - R: reset to 1:1 zoom
- ESCAPE: open menu, o: open options menu - ESCAPE: open menu, o: open options menu
- S: save game state to file (can be loaded with -l) - S: save game state to file (can be loaded with -l)
@@ -278,10 +278,8 @@ func (config *Config) SwitchTheme(theme string) {
} }
func (config *Config) ToggleGridlines() { func (config *Config) ToggleGridlines() {
fmt.Printf("toggle grid lines, current: %t\n", config.ShowGrid)
config.ShowGrid = !config.ShowGrid config.ShowGrid = !config.ShowGrid
config.RestartCache = true config.RestartCache = true
fmt.Printf("toggle grid lines, new: %t\n", config.ShowGrid)
} }
func (config *Config) ToggleEvolution() { func (config *Config) ToggleEvolution() {

View File

@@ -10,7 +10,6 @@ type Game struct {
CurrentScene SceneName CurrentScene SceneName
Config *Config Config *Config
Scale float32 Scale float32
Screen *ebiten.Image
} }
func NewGame(config *Config, startscene SceneName) *Game { func NewGame(config *Config, startscene SceneName) *Game {
@@ -34,7 +33,6 @@ func NewGame(config *Config, startscene SceneName) *Game {
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
ebiten.SetScreenClearedEveryFrame(true) ebiten.SetScreenClearedEveryFrame(true)
game.Screen = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
return game return game
} }

View File

@@ -12,35 +12,115 @@ import (
"github.com/tlinden/golsky/rle" "github.com/tlinden/golsky/rle"
) )
// func (cell *Cell) Count() uint8 {
// var count uint8
// for idx := 0; idx < cell.NeighborCount; idx++ {
// count += cell.Neighbors[idx].State
// }
// return count
// }
type Neighbor struct {
X, Y int
}
type Grid struct { type Grid struct {
Data [][]bool Data [][]uint8
Width, Height, Density int NeighborCount [][]int
Neighbors [][][]Neighbor
Empty bool Empty bool
Config *Config
} }
// Create new empty grid and allocate Data according to provided dimensions // Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int, empty bool) *Grid { func NewGrid(config *Config) *Grid {
grid := &Grid{ grid := &Grid{
Height: height, Data: make([][]uint8, config.Height),
Width: width, NeighborCount: make([][]int, config.Height),
Density: density, Neighbors: make([][][]Neighbor, config.Height),
Data: make([][]bool, height), Empty: config.Empty,
Empty: empty, Config: config,
} }
for y := 0; y < height; y++ { // first setup the cells
grid.Data[y] = make([]bool, width) for y := 0; y < config.Height; y++ {
grid.Data[y] = make([]uint8, config.Width)
grid.Neighbors[y] = make([][]Neighbor, config.Width)
grid.NeighborCount[y] = make([]int, config.Width)
for x := 0; x < config.Width; x++ {
grid.Data[y][x] = 0
}
}
// in a second pass, collect positions to the neighbors of each cell
for y := 0; y < config.Height; y++ {
for x := 0; x < config.Width; x++ {
grid.SetupNeighbors(x, y)
}
} }
return grid return grid
} }
func (grid *Grid) SetupNeighbors(x, y int) {
idx := 0
var neighbors []Neighbor
for nbgY := -1; nbgY < 2; nbgY++ {
for nbgX := -1; nbgX < 2; nbgX++ {
var col, row int
if grid.Config.Wrap {
// In wrap mode we look at all the 8 neighbors surrounding us.
// In case we are on an edge we'll look at the neighbor on the
// other side of the grid, thus wrapping lookahead around
// using the mod() function.
col = (x + nbgX + grid.Config.Width) % grid.Config.Width
row = (y + nbgY + grid.Config.Height) % grid.Config.Height
} else {
// In traditional grid mode the edges are deadly
if x+nbgX < 0 || x+nbgX >= grid.Config.Width || y+nbgY < 0 || y+nbgY >= grid.Config.Height {
continue
}
col = x + nbgX
row = y + nbgY
}
if col == x && row == y {
continue
}
neighbors = append(neighbors, Neighbor{X: col, Y: row})
grid.NeighborCount[y][x]++
idx++
}
}
grid.Neighbors[y][x] = neighbors
}
// count the living neighbors of a cell
func (grid *Grid) CountNeighbors(x, y int) uint8 {
var count uint8
for idx := 0; idx < grid.NeighborCount[y][x]; idx++ {
count += grid.Data[grid.Neighbors[y][x][idx].Y][grid.Neighbors[y][x][idx].X]
}
return count
}
// Create a new 1:1 instance // Create a new 1:1 instance
func (grid *Grid) Clone() *Grid { func (grid *Grid) Clone() *Grid {
newgrid := &Grid{} newgrid := &Grid{}
newgrid.Width = grid.Width newgrid.Config = grid.Config
newgrid.Height = grid.Height
newgrid.Data = grid.Data newgrid.Data = grid.Data
return newgrid return newgrid
@@ -59,7 +139,7 @@ func (grid *Grid) Copy(other *Grid) {
func (grid *Grid) Clear() { func (grid *Grid) Clear() {
for y := range grid.Data { for y := range grid.Data {
for x := range grid.Data[y] { for x := range grid.Data[y] {
grid.Data[y][x] = false grid.Data[y][x] = 0
} }
} }
} }
@@ -69,8 +149,8 @@ func (grid *Grid) FillRandom() {
if !grid.Empty { if !grid.Empty {
for y := range grid.Data { for y := range grid.Data {
for x := range grid.Data[y] { for x := range grid.Data[y] {
if rand.Intn(grid.Density) == 1 { if rand.Intn(grid.Config.Density) == 1 {
grid.Data[y][x] = true grid.Data[y][x] = 1
} }
} }
} }
@@ -78,9 +158,9 @@ func (grid *Grid) FillRandom() {
} }
func (grid *Grid) Dump() { func (grid *Grid) Dump() {
for y := 0; y < grid.Height; y++ { for y := 0; y < grid.Config.Height; y++ {
for x := 0; x < grid.Width; x++ { for x := 0; x < grid.Config.Width; x++ {
if grid.Data[y][x] { if grid.Data[y][x] == 1 {
fmt.Print("XX") fmt.Print("XX")
} else { } else {
fmt.Print(" ") fmt.Print(" ")
@@ -93,8 +173,8 @@ func (grid *Grid) Dump() {
// initialize using a given RLE pattern // initialize using a given RLE pattern
func (grid *Grid) LoadRLE(pattern *rle.RLE) { func (grid *Grid) LoadRLE(pattern *rle.RLE) {
if pattern != nil { if pattern != nil {
startX := (grid.Width / 2) - (pattern.Width / 2) startX := (grid.Config.Width / 2) - (pattern.Width / 2)
startY := (grid.Height / 2) - (pattern.Height / 2) startY := (grid.Config.Height / 2) - (pattern.Height / 2)
var y, x int var y, x int
for rowIndex, patternRow := range pattern.Pattern { for rowIndex, patternRow := range pattern.Pattern {
@@ -103,7 +183,7 @@ func (grid *Grid) LoadRLE(pattern *rle.RLE) {
x = colIndex + startX x = colIndex + startX
y = rowIndex + startY y = rowIndex + startY
grid.Data[y][x] = true grid.Data[y][x] = 1
} }
} }
} }
@@ -214,7 +294,7 @@ func (grid *Grid) SaveState(filename, rule string) error {
for y := range grid.Data { for y := range grid.Data {
for _, cell := range grid.Data[y] { for _, cell := range grid.Data[y] {
row := "." row := "."
if cell { if cell == 1 {
row = "o" row = "o"
} }

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"image/color" "image/color"
"os"
"github.com/ebitenui/ebitenui" "github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget" "github.com/ebitenui/ebitenui/widget"
@@ -19,6 +18,7 @@ type SceneMenu struct {
Ui *ebitenui.UI Ui *ebitenui.UI
FontColor color.RGBA FontColor color.RGBA
First bool First bool
Exit bool
} }
func NewMenuScene(game *Game, config *Config) Scene { func NewMenuScene(game *Game, config *Config) Scene {
@@ -54,6 +54,10 @@ func (scene *SceneMenu) SetNext(next SceneName) {
func (scene *SceneMenu) Update() error { func (scene *SceneMenu) Update() error {
scene.Ui.Update() scene.Ui.Update()
if scene.Exit {
return ebiten.Termination
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.Config.DelayedStart = false scene.Config.DelayedStart = false
scene.Leave() scene.Leave()
@@ -120,7 +124,7 @@ func (scene *SceneMenu) Init() {
quit := NewMenuButton("Exit Golsky", quit := NewMenuButton("Exit Golsky",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
os.Exit(0) scene.Exit = true
}) })
rowContainer.AddChild(empty) rowContainer.AddChild(empty)

View File

@@ -1,10 +1,8 @@
package main package main
import ( import (
"fmt"
"image/color" "image/color"
"github.com/alecthomas/repr"
"github.com/ebitenui/ebitenui" "github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget" "github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@@ -96,8 +94,6 @@ func (scene *SceneOptions) Init() {
gridlines := NewCheckbox("Show grid lines", gridlines := NewCheckbox("Show grid lines",
scene.Config.ShowGrid, scene.Config.ShowGrid,
func(args *widget.CheckboxChangedEventArgs) { func(args *widget.CheckboxChangedEventArgs) {
fmt.Println("CHECKBOX CALLED")
repr.Println(args.State)
scene.Config.ToggleGridlines() scene.Config.ToggleGridlines()
}) })

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"image" "image"
"log" "log"
"sync"
"unsafe" "unsafe"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@@ -21,6 +22,21 @@ const (
DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s" DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s"
) )
type History struct {
Age [][]int64
}
func NewHistory(height, width int) History {
hist := History{}
hist.Age = make([][]int64, height)
for y := 0; y < height; y++ {
hist.Age[y] = make([]int64, width)
}
return hist
}
type ScenePlay struct { type ScenePlay struct {
Game *Game Game *Game
Config *Config Config *Config
@@ -31,7 +47,7 @@ type ScenePlay struct {
Clear bool Clear bool
Grids []*Grid // 2 grids: one current, one next Grids []*Grid // 2 grids: one current, one next
History [][]int64 // holds state of past dead cells for evolution traces History History // holds state of past dead cells for evolution traces
Index int // points to current grid Index int // points to current grid
Generations int64 // Stats Generations int64 // Stats
TicksElapsed int // tick counter for game speed TicksElapsed int // tick counter for game speed
@@ -46,6 +62,7 @@ type ScenePlay struct {
RunOneStep bool // mutable flags from config RunOneStep bool // mutable flags from config
TPG int // current game speed (ticks per game) TPG int // current game speed (ticks per game)
Theme Theme Theme Theme
RuleCheckFunc func(uint8, uint8) uint8
} }
func NewPlayScene(game *Game, config *Config) Scene { func NewPlayScene(game *Game, config *Config) Scene {
@@ -83,18 +100,38 @@ func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *ScenePlay) CheckRule(state bool, neighbors int) bool { /* The standard Scene of Life is symbolized in rule-string notation
var nextstate bool * 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.
* we abbreviate the calculation: if state is 0 and 3 neighbors
* are a life, check will be just 3. If the cell is alive, 9 will
* be added to the life neighbors (to avoid a collision with the
* result 3), which will be 11|12 in case of 2|3 life neighbors.
*/
func (scene *ScenePlay) CheckRuleB3S23(state uint8, neighbors uint8) uint8 {
switch (9 * state) + neighbors {
case 11:
fallthrough
case 12:
fallthrough
case 3:
return Alive
}
// The standard Scene of Life is symbolized in rule-string notation return Dead
// 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 && Contains(scene.Config.Rule.Birth, neighbors) { /*
* The generic rule checker is able to calculate cell state for any
* GOL rul, including B3/S23.
*/
func (scene *ScenePlay) CheckRuleGeneric(state uint8, neighbors uint8) uint8 {
var nextstate uint8
if state != 1 && Contains(scene.Config.Rule.Birth, neighbors) {
nextstate = Alive nextstate = Alive
} else if state && Contains(scene.Config.Rule.Death, neighbors) { } else if state == 1 && Contains(scene.Config.Rule.Death, neighbors) {
nextstate = Alive nextstate = Alive
} else { } else {
nextstate = Dead nextstate = Dead
@@ -116,14 +153,21 @@ func (scene *ScenePlay) UpdateCells() {
// next grid index, we just xor 0|1 to 1|0 // next grid index, we just xor 0|1 to 1|0
next := scene.Index ^ 1 next := scene.Index ^ 1
var wg sync.WaitGroup
wg.Add(scene.Config.Height)
// compute life status of cells // compute life status of cells
for y := 0; y < scene.Config.Height; y++ { for y := 0; y < scene.Config.Height; y++ {
go func() {
defer wg.Done()
for x := 0; x < scene.Config.Width; x++ { for x := 0; x < scene.Config.Width; x++ {
state := scene.Grids[scene.Index].Data[y][x] // 0|1 == dead or alive state := scene.Grids[scene.Index].Data[y][x] // 0|1 == dead or alive
neighbors := scene.CountNeighbors(x, y) // alive neighbor count neighbors := scene.Grids[scene.Index].CountNeighbors(x, y)
// actually apply the current rules // actually apply the current rules
nextstate := scene.CheckRule(state, neighbors) nextstate := scene.RuleCheckFunc(state, neighbors)
// change state of current cell in next grid // change state of current cell in next grid
scene.Grids[next].Data[y][x] = nextstate scene.Grids[next].Data[y][x] = nextstate
@@ -134,14 +178,14 @@ func (scene *ScenePlay) UpdateCells() {
// deduce the color to use if evolution tracing is enabled // deduce the color to use if evolution tracing is enabled
// 60FPS: // 60FPS:
if state != nextstate { if state != nextstate {
scene.History[y][x] = scene.Generations scene.History.Age[y][x] = scene.Generations
}
}
}
}()
} }
// 10FPS: wg.Wait()
//scene.History.Data[y][x] = (state ^ (1 ^ nextstate)) * (scene.Generations - scene.History.Data[y][x])
}
}
}
// switch grid for rendering // switch grid for rendering
scene.Index ^= 1 scene.Index ^= 1
@@ -174,50 +218,40 @@ func (scene *ScenePlay) CheckExit() error {
} }
func (scene *ScenePlay) CheckInput() { func (scene *ScenePlay) CheckInput() {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { // primary functions, always available
switch {
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.SetNext(Menu) scene.SetNext(Menu)
} case inpututil.IsKeyJustPressed(ebiten.KeyO):
if inpututil.IsKeyJustPressed(ebiten.KeyO) {
scene.SetNext(Options) scene.SetNext(Options)
} case inpututil.IsKeyJustPressed(ebiten.KeyC):
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
scene.Config.Markmode = true scene.Config.Markmode = true
scene.Config.Drawmode = false scene.Config.Drawmode = false
scene.Config.Paused = true scene.Config.Paused = true
} case inpututil.IsKeyJustPressed(ebiten.KeyI):
if inpututil.IsKeyJustPressed(ebiten.KeyI) {
scene.Config.Drawmode = true scene.Config.Drawmode = true
scene.Config.Paused = true scene.Config.Paused = true
} }
if scene.Config.Markmode { if scene.Config.Markmode {
// no need to check any more input in mark mode
return return
} }
if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) { switch {
case inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter):
scene.Config.TogglePaused() scene.Config.TogglePaused()
} case inpututil.IsKeyJustPressed(ebiten.KeyPageDown):
if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
if scene.TPG < 120 { if scene.TPG < 120 {
scene.TPG++ scene.TPG++
} }
} case inpututil.IsKeyJustPressed(ebiten.KeyPageUp):
if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
if scene.TPG >= 1 { if scene.TPG >= 1 {
scene.TPG-- scene.TPG--
} }
} case inpututil.IsKeyJustPressed(ebiten.KeyS):
if inpututil.IsKeyJustPressed(ebiten.KeyS) {
scene.SaveState() scene.SaveState()
} case inpututil.IsKeyJustPressed(ebiten.KeyD):
if inpututil.IsKeyJustPressed(ebiten.KeyD) {
scene.Config.Debug = !scene.Config.Debug scene.Config.Debug = !scene.Config.Debug
} }
@@ -230,15 +264,10 @@ func (scene *ScenePlay) CheckInput() {
func (scene *ScenePlay) CheckDrawingInput() { func (scene *ScenePlay) CheckDrawingInput() {
if scene.Config.Drawmode { if scene.Config.Drawmode {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { switch {
scene.ToggleCellOnCursorPos(Alive) case inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft):
} scene.ToggleCellOnCursorPos()
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
scene.ToggleCellOnCursorPos(Dead)
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.Config.Drawmode = false scene.Config.Drawmode = false
} }
} }
@@ -282,16 +311,14 @@ func (scene *ScenePlay) CheckDraggingInput() {
} }
// also support the arrow keys to move the canvas // also support the arrow keys to move the canvas
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { switch {
case ebiten.IsKeyPressed(ebiten.KeyArrowLeft):
scene.Camera.Position[0] -= 1 scene.Camera.Position[0] -= 1
} case ebiten.IsKeyPressed(ebiten.KeyArrowRight):
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
scene.Camera.Position[0] += 1 scene.Camera.Position[0] += 1
} case ebiten.IsKeyPressed(ebiten.KeyArrowUp):
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
scene.Camera.Position[1] -= 1 scene.Camera.Position[1] -= 1
} case ebiten.IsKeyPressed(ebiten.KeyArrowDown):
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
scene.Camera.Position[1] += 1 scene.Camera.Position[1] += 1
} }
@@ -385,10 +412,10 @@ func (scene *ScenePlay) SaveRectRLE() {
height = scene.Mark.Y - scene.Point.Y height = scene.Mark.Y - scene.Point.Y
} }
grid := make([][]bool, height) grid := make([][]uint8, height)
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
grid[y] = make([]bool, width) grid[y] = make([]uint8, width)
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx] grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx]
@@ -437,15 +464,15 @@ func (scene *ScenePlay) Update() error {
} }
// set a cell to alive or dead // set a cell to alive or dead
func (scene *ScenePlay) ToggleCellOnCursorPos(alive bool) { func (scene *ScenePlay) ToggleCellOnCursorPos() {
// use cursor pos relative to the world // use cursor pos relative to the world
worldX, worldY := scene.Camera.ScreenToWorld(ebiten.CursorPosition()) worldX, worldY := scene.Camera.ScreenToWorld(ebiten.CursorPosition())
x := int(worldX) / scene.Config.Cellsize x := int(worldX) / scene.Config.Cellsize
y := int(worldY) / scene.Config.Cellsize y := int(worldY) / scene.Config.Cellsize
if x > -1 && y > -1 && x < scene.Config.Width && y < scene.Config.Height { if x > -1 && y > -1 && x < scene.Config.Width && y < scene.Config.Height {
scene.Grids[scene.Index].Data[y][x] = alive scene.Grids[scene.Index].Data[y][x] ^= 1
scene.History[y][x] = 1 scene.History.Age[y][x] = 1
} }
} }
@@ -470,7 +497,7 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
if scene.Config.ShowEvolution { if scene.Config.ShowEvolution {
scene.DrawEvolution(screen, x, y, op) scene.DrawEvolution(screen, x, y, op)
} else { } else {
if scene.Grids[scene.Index].Data[y][x] { if scene.Grids[scene.Index].Data[y][x] == 1 {
scene.World.DrawImage(scene.Theme.Tile(ColLife), op) scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
} }
} }
@@ -482,15 +509,10 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
scene.Camera.Render(scene.World, screen) scene.Camera.Render(scene.World, screen)
scene.DrawDebug(screen) scene.DrawDebug(screen)
op.GeoM.Reset()
op.GeoM.Translate(0, 0)
scene.Game.Screen.DrawImage(screen, op)
} }
func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten.DrawImageOptions) { func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten.DrawImageOptions) {
age := scene.Generations - scene.History[y][x] age := scene.Generations - scene.History.Age[y][x]
switch scene.Grids[scene.Index].Data[y][x] { switch scene.Grids[scene.Index].Data[y][x] {
case Alive: case Alive:
@@ -501,7 +523,7 @@ func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten
} }
case Dead: case Dead:
// only draw dead cells in case evolution trace is enabled // only draw dead cells in case evolution trace is enabled
if scene.History[y][x] > 1 && scene.Config.ShowEvolution { if scene.History.Age[y][x] > 1 && scene.Config.ShowEvolution {
switch { switch {
case age < 10: case age < 10:
scene.World.DrawImage(scene.Theme.Tile(ColAge1), op) scene.World.DrawImage(scene.Theme.Tile(ColAge1), op)
@@ -573,17 +595,24 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
// load a pre-computed pattern from RLE file // load a pre-computed pattern from RLE file
func (scene *ScenePlay) InitPattern() { func (scene *ScenePlay) InitPattern() {
scene.Grids[0].LoadRLE(scene.Config.RLE) scene.Grids[0].LoadRLE(scene.Config.RLE)
// rule might have changed
scene.InitRuleCheckFunc()
} }
// pre-render offscreen cache image // pre-render offscreen cache image
func (scene *ScenePlay) InitCache() { func (scene *ScenePlay) InitCache() {
// setup theme
scene.Theme.SetGrid(scene.Config.ShowGrid)
if !scene.Config.ShowGrid {
scene.Cache.Fill(scene.Theme.Color(ColDead))
return
}
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
if scene.Config.ShowGrid {
scene.Cache.Fill(scene.Theme.Color(ColGrid)) scene.Cache.Fill(scene.Theme.Color(ColGrid))
} else {
scene.Cache.Fill(scene.Theme.Color(ColDead))
}
for y := 0; y < scene.Config.Height; y++ { for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ { for x := 0; x < scene.Config.Width; x++ {
@@ -600,8 +629,8 @@ func (scene *ScenePlay) InitCache() {
// initialize grid[s], either using pre-computed from state or rle file, or random // initialize grid[s], either using pre-computed from state or rle file, or random
func (scene *ScenePlay) InitGrid() { func (scene *ScenePlay) InitGrid() {
grida := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) grida := NewGrid(scene.Config)
gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) gridb := NewGrid(scene.Config)
// startup is delayed until user has selected options // startup is delayed until user has selected options
grida.FillRandom() grida.FillRandom()
@@ -611,10 +640,8 @@ func (scene *ScenePlay) InitGrid() {
gridb, gridb,
} }
scene.History = make([][]int64, scene.Config.Height) scene.History = NewHistory(scene.Config.Height, scene.Config.Width)
for y := 0; y < scene.Config.Height; y++ {
scene.History[y] = make([]int64, scene.Config.Width)
}
} }
func (scene *ScenePlay) Init() { func (scene *ScenePlay) Init() {
@@ -646,6 +673,8 @@ func (scene *ScenePlay) Init() {
scene.InitCache() scene.InitCache()
if scene.Config.DelayedStart && !scene.Config.Empty { if scene.Config.DelayedStart && !scene.Config.Empty {
// do not fill the grid when the main menu comes up first, the
// user decides interactively what to do
scene.Config.Empty = true scene.Config.Empty = true
scene.InitGrid() scene.InitGrid()
scene.Config.Empty = false scene.Config.Empty = false
@@ -671,38 +700,10 @@ func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b))) return int(*(*byte)(unsafe.Pointer(&b)))
} }
// count the living neighbors of a cell func (scene *ScenePlay) InitRuleCheckFunc() {
func (scene *ScenePlay) CountNeighbors(x, y int) int { if scene.Config.Rule.Definition == "B3/S23" {
var sum int scene.RuleCheckFunc = scene.CheckRuleB3S23
grid := scene.Grids[scene.Index].Data
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if scene.Config.Wrap {
// In wrap mode we look at all the 8 neighbors surrounding us.
// In case we are on an edge we'll look at the neighbor on the
// other side of the grid, thus wrapping lookahead around
// using the mod() function.
col = (x + nbgX + scene.Config.Width) % scene.Config.Width
row = (y + nbgY + scene.Config.Height) % scene.Config.Height
} else { } else {
// In traditional grid mode the edges are deadly scene.RuleCheckFunc = scene.CheckRuleGeneric
if x+nbgX < 0 || x+nbgX >= scene.Config.Width || y+nbgY < 0 || y+nbgY >= scene.Config.Height {
continue
} }
col = x + nbgX
row = y + nbgY
}
sum += bool2int(grid[row][col])
}
}
// don't count ourselfes though
sum -= bool2int(grid[y][x])
return sum
} }

View File

@@ -9,13 +9,13 @@ import (
// a GOL rule // a GOL rule
type Rule struct { type Rule struct {
Definition string Definition string
Birth []int Birth []uint8
Death []int Death []uint8
} }
// parse one part of a GOL rule into rule slice // parse one part of a GOL rule into rule slice
func NumbersToList(numbers string) []int { func NumbersToList(numbers string) []uint8 {
list := []int{} list := []uint8{}
items := strings.Split(numbers, "") items := strings.Split(numbers, "")
for _, item := range items { for _, item := range items {
@@ -24,7 +24,7 @@ func NumbersToList(numbers string) []int {
log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err) log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err)
} }
list = append(list, num) list = append(list, uint8(num))
} }
return list return list

View File

@@ -26,8 +26,10 @@ const (
// readily available from play.go // readily available from play.go
type Theme struct { type Theme struct {
Tiles map[int]*ebiten.Image Tiles map[int]*ebiten.Image
GridTiles map[int]*ebiten.Image
Colors map[int]color.RGBA Colors map[int]color.RGBA
Name string Name string
ShowGrid bool
} }
type ThemeDef struct { type ThemeDef struct {
@@ -38,32 +40,32 @@ var THEMES = map[string]ThemeDef{
"standard": { "standard": {
life: "e15f0b", life: "e15f0b",
dead: "5a5a5a", dead: "5a5a5a",
old: "ff1e1e",
grid: "808080", grid: "808080",
old: "7b5e4b",
age1: "735f52", age1: "735f52",
age2: "6c6059", age2: "6c6059",
age3: "635d59", age3: "635d59",
age4: "808080", age4: "7b5e4b",
}, },
"dark": { "dark": {
life: "c8c8c8", life: "c8c8c8",
dead: "000000", dead: "000000",
old: "ff1e1e", old: "ff1e1e",
grid: "808080",
age1: "522600", age1: "522600",
age2: "422300", age2: "422300",
age3: "2b1b00", age3: "2b1b00",
age4: "191100", age4: "191100",
grid: "808080",
}, },
"light": { "light": {
life: "000000", life: "000000",
dead: "c8c8c8", dead: "c8c8c8",
old: "ff1e1e", old: "ff1e1e",
grid: "808080",
age1: "ffc361", age1: "ffc361",
age2: "ffd38c", age2: "ffd38c",
age3: "ffe3b5", age3: "ffe3b5",
age4: "fff0e0", age4: "fff0e0",
grid: "808080",
}, },
} }
@@ -84,10 +86,14 @@ func NewTheme(def ThemeDef, cellsize int, name string) Theme {
} }
theme.Tiles = make(map[int]*ebiten.Image, 6) theme.Tiles = make(map[int]*ebiten.Image, 6)
theme.GridTiles = make(map[int]*ebiten.Image, 6)
for cid, col := range theme.Colors { for cid, col := range theme.Colors {
theme.Tiles[cid] = ebiten.NewImage(cellsize, cellsize) theme.Tiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.Tiles[cid], cellsize, col) FillCell(theme.Tiles[cid], cellsize, col, 0)
theme.GridTiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.GridTiles[cid], cellsize, col, 1)
} }
return theme return theme
@@ -97,6 +103,10 @@ func NewTheme(def ThemeDef, cellsize int, name string) Theme {
// unknown type is being used, which is ok, since the code is the only // unknown type is being used, which is ok, since the code is the only
// user anyway // user anyway
func (theme *Theme) Tile(col int) *ebiten.Image { func (theme *Theme) Tile(col int) *ebiten.Image {
if theme.ShowGrid {
return theme.GridTiles[col]
}
return theme.Tiles[col] return theme.Tiles[col]
} }
@@ -104,6 +114,10 @@ func (theme *Theme) Color(col int) color.RGBA {
return theme.Colors[col] return theme.Colors[col]
} }
func (theme *Theme) SetGrid(showgrid bool) {
theme.ShowGrid = showgrid
}
type ThemeManager struct { type ThemeManager struct {
Theme string Theme string
Themes map[string]Theme Themes map[string]Theme
@@ -152,11 +166,11 @@ func (manager *ThemeManager) SetCurrentTheme(theme string) {
// //
// So we don't draw a grid, we just left a grid behind, which saves us // So we don't draw a grid, we just left a grid behind, which saves us
// from a lot of drawing operations. // from a lot of drawing operations.
func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA) { func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA, x int) {
vector.DrawFilledRect( vector.DrawFilledRect(
tile, tile,
float32(1), float32(x),
float32(1), float32(x),
float32(cellsize), float32(cellsize),
float32(cellsize), float32(cellsize),
col, false, col, false,

View File

@@ -3,7 +3,10 @@
Running with 1500x1500 grid 5k times Running with 1500x1500 grid 5k times
| Variation | Description | Duration | | Variation | Description | Duration |
|--------------------|-----------------------------------------------------------------------------|----------| |------------------------------|-----------------------------------------------------------------------------|-------------------|
| perf-2dim | uses 2d grid of bools, no tuning | 00:03:14 | | perf-2dim | uses 2d grid of bools, no tuning | 00:03:14 |
| perf-2dim-pointers | use 2d grid of `Cell{Neighbors,NeighborCount}`s using pointers to neighbors | 00:03:35 | | perf-2dim-pointers | use 2d grid of `Cell{Neighbors,NeighborCount}`s using pointers to neighbors | 00:03:35/00:04:75 |
| perf-2dim-pointers-array | same as above but array of neighbors instead of slice | 00:02:40 |
| perf-2dim-pointers-all-array | use arrays for everything, static 1500x1500 | infinite, aborted |
| perf-1dim | use 1d grid of bools, access using y*x, no further tuning | 00:03:24 | | perf-1dim | use 1d grid of bools, access using y*x, no further tuning | 00:03:24 |
| perf-ecs | use arche ecs, unusable | 00:14:51 |

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,141 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"time"
"unsafe"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
type Cell struct {
State bool
Neighbors [8]*Cell
NeighborCount int
}
type Grid [1500][1500]Cell
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) {
cell.NeighborCount = 0
for _, neighbor := range cell.Neighbors {
cell.NeighborCount += bool2int(neighbor.State)
}
}
func SetNeighbors(grid Grid, x, y int) {
cells := []*Cell{}
deadcell := &Cell{}
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= max || y+nbgY < 0 || y+nbgY >= max {
cells = append(cells, deadcell)
} else {
col = x + nbgX
row = y + nbgY
if col == x && row == y {
// do not add self
continue
}
cells = append(cells, &grid[row][col])
}
}
}
for idx, cell := range cells {
grid[y][x].Neighbors[idx] = cell
}
}
func Init() Grid {
grid := Grid{}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
if rand.Intn(density) == 1 {
grid[y][x].State = true
}
}
}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
SetNeighbors(grid, x, y)
}
}
return grid
}
func Loop(grid Grid) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
cell := &grid[y][x]
state := cell.State
cell.Count(x, y)
if state && cell.NeighborCount > 1 {
if debug {
fmt.Printf(
"Loop %d - cell at %d,%d is %t and has %d living neighbors\n",
i, x, y, state, cell.NeighborCount)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
loopstart := time.Now()
Loop(grid)
loopend := time.Now()
diff := loopstart.Sub(loopend)
fmt.Printf("Loop took %.04f\n", diff.Seconds())
}

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,139 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"time"
"unsafe"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
type Cell struct {
State bool
Neighbors [8]*Cell
NeighborCount int
}
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) {
cell.NeighborCount = 0
for _, neighbor := range cell.Neighbors {
cell.NeighborCount += bool2int(neighbor.State)
}
}
func SetNeighbors(grid [][]Cell, x, y int) {
cells := []*Cell{}
deadcell := &Cell{}
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= max || y+nbgY < 0 || y+nbgY >= max {
cells = append(cells, deadcell)
} else {
col = x + nbgX
row = y + nbgY
if col == x && row == y {
// do not add self
continue
}
cells = append(cells, &grid[row][col])
}
}
}
for idx, cell := range cells {
grid[y][x].Neighbors[idx] = cell
}
}
func Init() [][]Cell {
grid := make([][]Cell, max)
for y := 0; y < max; y++ {
grid[y] = make([]Cell, max)
for x := 0; x < max; x++ {
if rand.Intn(density) == 1 {
grid[y][x].State = true
}
}
}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
SetNeighbors(grid, x, y)
}
}
return grid
}
func Loop(grid [][]Cell) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
cell := &grid[y][x]
state := cell.State
cell.Count(x, y)
if state && cell.NeighborCount > 1 {
if debug {
fmt.Printf(
"Loop %d - cell at %d,%d is %t and has %d living neighbors\n",
i, x, y, state, cell.NeighborCount)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
loopstart := time.Now()
Loop(grid)
loopend := time.Now()
diff := loopstart.Sub(loopend)
fmt.Printf("Loop took %.04f\n", diff.Seconds())
}

View File

@@ -0,0 +1,5 @@
module perf
go 1.22
require github.com/mlange-42/arche v0.13.0 // indirect

View File

@@ -0,0 +1,2 @@
github.com/mlange-42/arche v0.13.0 h1:ef0fu9qC2KIr8wIlVs+CgeQ5CSUJ8A1Hut6nXYdf+xk=
github.com/mlange-42/arche v0.13.0/go.mod h1:bFktKnvGDj2kP01xar79z0hKwGHdnoaEZR8HWmJkIyU=

View File

@@ -0,0 +1,145 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"github.com/mlange-42/arche/ecs"
"github.com/mlange-42/arche/generic"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
// components
type Pos struct {
X, Y, GridX, GridY int
}
type Cell struct {
State bool
Neighbors [8]ecs.Entity
}
type ECS struct {
World *ecs.World
Filter *generic.Filter2[Pos, Cell]
Map *generic.Map2[Pos, Cell]
}
func (cell *Cell) NeighborCount(ECS *ECS) int {
sum := 0
for _, neighbor := range cell.Neighbors {
if ECS.World.Alive(neighbor) {
_, cel := ECS.Map.Get(neighbor)
if cel.State {
sum++
}
}
}
return sum
}
func Loop(ECS *ECS) {
c := 0
for i := 0; i < loops; i++ {
query := ECS.Filter.Query(ECS.World)
for query.Next() {
_, cel := query.Get()
if cel.State && cel.NeighborCount(ECS) > 1 {
c = 1
}
}
}
if c > 1 {
c = 0
}
}
func SetupWorld() *ECS {
world := ecs.NewWorld()
builder := generic.NewMap2[Pos, Cell](&world)
// we need a temporary grid in order to find out neighbors
grid := [max][max]ecs.Entity{}
// setup entities
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
e := builder.New()
pos, cell := builder.Get(e)
pos.X = x
pos.Y = y // pos.GridX = x*cellsize
cell.State = false
if rand.Intn(density) == 1 {
cell.State = true
}
// store to tmp grid
grid[y][x] = e
}
}
// global filter
filter := generic.NewFilter2[Pos, Cell]()
query := filter.Query(&world)
for query.Next() {
pos, cel := query.Get()
n := 0
for x := -1; x < 2; x++ {
for y := -1; y < 2; y++ {
XX := pos.X + x
YY := pos.Y + y
if XX < 0 || XX >= max || YY < 0 || YY >= max {
continue
}
if pos.X != XX || pos.Y != YY {
cel.Neighbors[n] = grid[XX][YY]
n++
}
}
}
}
return &ECS{World: &world, Filter: filter, Map: &builder}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
fmt.Print("Setup ... ")
ECS := SetupWorld()
fmt.Println("done")
fmt.Println(ECS.World.Stats())
// main loop
Loop(ECS)
}

2
various-tests/raygol/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
golsky
*.o

View File

@@ -0,0 +1,33 @@
CFLAGS = -Wall -Wextra -Werror -O2 -g
LDFLAGS= -L/usr/local/lib -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 -g
CC = clang
OBJS = main.o game.o grid.o
DST = golsky
PREFIX = /usr/local
UID = root
GID = 0
MAN = udpxd.1
.PHONY: all
all: $(DST)
$(DST): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $(DST)
%.o: %.c
$(CC) -c $(CFLAGS) $*.c -o $*.o
.PHONY: clean
clean:
rm -f *.o $(DST)
.PHONY: install
install: $(DST)
install -d -o $(UID) -g $(GID) $(PREFIX)/sbin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(DST) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(MAN) $(PREFIX)/man/man1/
.PHONY: run
run:
LD_LIBRARY_PATH=/usr/local/lib ./golsky

View File

@@ -0,0 +1,48 @@
#include "game.h"
#include <stdio.h>
Game *Init(int width, int height, int gridwidth, int gridheight, int density) {
struct Game *game = malloc(sizeof(struct Game));
game->ScreenWidth = width;
game->ScreenHeight = height;
game->Cellsize = width / gridwidth;
game->Width = gridwidth;
game->Height = gridheight;
InitWindow(width, height, "golsky");
SetTargetFPS(60);
game->Grid = NewGrid(gridwidth, gridheight, density);
return game;
}
void Update(Game *game) {
if (IsKeyDown(KEY_Q)) {
game->Done = true;
exit(0);
}
}
void Draw(Game *game) {
BeginDrawing();
ClearBackground(RAYWHITE);
for (int y = 0; y < game->Width; y++) {
for (int x = 0; x < game->Height; x++) {
if (game->Grid->Data[y][x] == 1) {
DrawRectangle(x * game->Cellsize, y * game->Cellsize, game->Cellsize,
game->Cellsize, GREEN);
} else {
DrawRectangle(x * game->Cellsize, y * game->Cellsize, game->Cellsize,
game->Cellsize, RAYWHITE);
}
}
}
DrawText("TEST", game->ScreenWidth / 2, 10, 20, RED);
EndDrawing();
}

View File

@@ -0,0 +1,25 @@
#ifndef _HAVE_GAME_H
#define _HAVE_GAME_H
#include "grid.h"
#include "raylib.h"
#include <stdlib.h>
typedef struct Game {
// Camera2D Camera;
int ScreenWidth;
int ScreenHeight;
int Cellsize;
// Grid dimensions
int Width;
int Height;
bool Done;
Grid *Grid;
} Game;
Game *Init(int width, int height, int gridwidth, int gridheight, int density);
void Update(Game *game);
void Draw(Game *game);
#endif

View File

@@ -0,0 +1,28 @@
#include "grid.h"
Grid *NewGrid(int width, int height, int density) {
Grid *grid = malloc(sizeof(struct Grid));
grid->Width = width;
grid->Height = height;
grid->Density = density;
grid->Data = malloc(height * sizeof(int *));
for (int y = 0; y < grid->Height; y++) {
grid->Data[y] = malloc(width * sizeof(int *));
}
FillRandom(grid);
return grid;
}
void FillRandom(Grid *grid) {
int r;
for (int y = 0; y < grid->Width; y++) {
for (int x = 0; x < grid->Height; x++) {
r = GetRandomValue(0, grid->Density);
if (r == 1)
grid->Data[y][x] = r;
}
}
}

View File

@@ -0,0 +1,18 @@
#ifndef _HAVE_GRID_H
#define _HAVE_GRID_H
#include "raylib.h"
#include <stdio.h>
#include <stdlib.h>
typedef struct Grid {
int Width;
int Height;
int Density;
int **Data;
} Grid;
Grid *NewGrid(int width, int height, int density);
void FillRandom(Grid *grid);
#endif

View File

@@ -0,0 +1,15 @@
#include "game.h"
#include "raylib.h"
int main(void) {
Game *game = Init(800, 800, 10, 10, 8);
while (!WindowShouldClose()) {
Update(game);
Draw(game);
}
CloseWindow();
free(game);
return 0;
}

View File

@@ -0,0 +1,13 @@
module testgol
go 1.22
require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect
github.com/hajimehoshi/ebiten/v2 v2.7.4 // indirect
github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)

View File

@@ -0,0 +1,14 @@
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU=
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M=
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.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,306 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"runtime/pprof"
"unsafe"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Images struct {
Black, White *ebiten.Image
}
type Cell struct {
State uint8
Neighbors [8]*Cell
NeighborCount int
}
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) uint8 {
var sum uint8
for idx := 0; idx < cell.NeighborCount; idx++ {
sum += cell.Neighbors[idx].State
}
return sum
}
func SetNeighbors(grid [][]*Cell, x, y, width, height int) {
idx := 0
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= width || y+nbgY < 0 || y+nbgY >= height {
continue
}
col = x + nbgX
row = y + nbgY
if col == x && row == y {
continue
}
grid[y][x].Neighbors[idx] = grid[row][col]
grid[y][x].NeighborCount++
idx++
}
}
}
type Grid struct {
Data [][]*Cell
Width, Height, Density int
}
// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int) *Grid {
grid := &Grid{
Height: height,
Width: width,
Density: density,
Data: make([][]*Cell, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]*Cell, width)
for x := 0; x < width; x++ {
grid.Data[y][x] = &Cell{}
if rand.Intn(density) == 1 {
grid.Data[y][x].State = 1
}
}
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
SetNeighbors(grid.Data, x, y, width, height)
}
}
return grid
}
type Game struct {
Width, Height, Cellsize, Density int
ScreenWidth, ScreenHeight int
Grids []*Grid
Index int
Elapsed int64
TPG int64 // adjust game speed independently of TPS
Pause, Debug, Profile, Gridlines bool
Pixels []byte
OffScreen *ebiten.Image
}
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return game.ScreenWidth, game.ScreenHeight
}
// live console output of the grid
func (game *Game) DebugDump() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
if game.Debug {
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if game.Grids[game.Index].Data[y][x].State == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
fmt.Printf("FPS: %0.2f\n", ebiten.ActualTPS())
}
func (game *Game) Init() {
// setup two grids, one for display, one for next state
grida := NewGrid(game.Width, game.Height, game.Density)
gridb := NewGrid(game.Width, game.Height, game.Density)
game.Grids = []*Grid{
grida,
gridb,
}
game.Pixels = make([]byte, game.ScreenWidth*game.ScreenHeight*4)
game.OffScreen = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
}
// count the living neighbors of a cell
func (game *Game) CountNeighbors(x, y int) uint8 {
return game.Grids[game.Index].Data[y][x].Count(x, y)
}
// the heart of the game
func (game *Game) CheckRule(state uint8, neighbors uint8) uint8 {
var nextstate uint8
if state == 1 && neighbors == 3 {
nextstate = 1
} else if state == 1 && (neighbors == 2 || neighbors == 3) {
nextstate = 1
} else {
nextstate = 0
}
return nextstate
}
// we only update the cells if we are not in pause state or if the
// game timer (TPG) is elapsed.
func (game *Game) UpdateCells() {
if game.Pause {
return
}
if game.Elapsed < game.TPG {
game.Elapsed++
return
}
// next grid index. we only have to, so we just xor it
next := game.Index ^ 1
// calculate cell life state, this is the actual game of life
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
state := game.Grids[game.Index].Data[y][x].State
neighbors := game.CountNeighbors(x, y)
// actually apply the current rules
nextstate := game.CheckRule(state, neighbors)
// change state of current cell in next grid
game.Grids[next].Data[y][x].State = nextstate
}
}
// switch grid for rendering
game.Index ^= 1
game.Elapsed = 0
game.UpdatePixels()
}
func (game *Game) Update() error {
game.UpdateCells()
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
game.Pause = !game.Pause
}
return nil
}
/*
*
r, g, b := color(it)
78 p := 4 * (i + j*screenWidth)
79 gm.offscreenPix[p] = r
80 gm.offscreenPix[p+1] = g
81 gm.offscreenPix[p+2] = b
82 gm.offscreenPix[p+3] = 0xff
*/
func (game *Game) UpdatePixels() {
var col byte
gridx := 0
gridy := 0
idx := 0
for y := 0; y < game.ScreenHeight; y++ {
for x := 0; x < game.ScreenWidth; x++ {
gridx = x / game.Cellsize
gridy = y / game.Cellsize
col = 0xff
if game.Grids[game.Index].Data[gridy][gridx].State == 1 {
col = 0x0
}
if game.Gridlines {
if x%game.Cellsize == 0 || y%game.Cellsize == 0 {
col = 128
}
}
idx = 4 * (x + y*game.ScreenWidth)
game.Pixels[idx] = col
game.Pixels[idx+1] = col
game.Pixels[idx+2] = col
game.Pixels[idx+3] = 0xff
idx++
}
}
game.OffScreen.WritePixels(game.Pixels)
}
func (game *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(game.OffScreen, nil)
game.DebugDump()
}
func main() {
size := 1500
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 8,
TPG: 10,
Debug: false,
Profile: true,
Gridlines: false,
}
game.ScreenWidth = game.Width * game.Cellsize
game.ScreenHeight = game.Height * game.Cellsize
game.Init()
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
ebiten.SetWindowTitle("triangle conway's game of life")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
if game.Profile {
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
}
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,13 @@
module testgol
go 1.22
require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect
github.com/hajimehoshi/ebiten/v2 v2.7.4 // indirect
github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)

View File

@@ -0,0 +1,14 @@
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU=
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M=
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.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,306 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"runtime/pprof"
"unsafe"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Images struct {
Black, White *ebiten.Image
}
type Cell struct {
State bool
Neighbors [8]*Cell
NeighborCount int
}
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) int {
sum := 0
for idx := 0; idx < cell.NeighborCount; idx++ {
sum += bool2int(cell.Neighbors[idx].State)
}
return sum
}
func SetNeighbors(grid [][]*Cell, x, y, width, height int) {
idx := 0
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= width || y+nbgY < 0 || y+nbgY >= height {
continue
}
col = x + nbgX
row = y + nbgY
if col == x && row == y {
continue
}
grid[y][x].Neighbors[idx] = grid[row][col]
grid[y][x].NeighborCount++
idx++
}
}
}
type Grid struct {
Data [][]*Cell
Width, Height, Density int
}
// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int) *Grid {
grid := &Grid{
Height: height,
Width: width,
Density: density,
Data: make([][]*Cell, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]*Cell, width)
for x := 0; x < width; x++ {
grid.Data[y][x] = &Cell{}
if rand.Intn(density) == 1 {
grid.Data[y][x].State = true
}
}
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
SetNeighbors(grid.Data, x, y, width, height)
}
}
return grid
}
type Game struct {
Width, Height, Cellsize, Density int
ScreenWidth, ScreenHeight int
Grids []*Grid
Index int
Elapsed int64
TPG int64 // adjust game speed independently of TPS
Pause, Debug, Profile, Gridlines bool
Pixels []byte
OffScreen *ebiten.Image
}
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return game.ScreenWidth, game.ScreenHeight
}
// live console output of the grid
func (game *Game) DebugDump() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
if game.Debug {
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if game.Grids[game.Index].Data[y][x].State {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
fmt.Printf("FPS: %0.2f\n", ebiten.ActualTPS())
}
func (game *Game) Init() {
// setup two grids, one for display, one for next state
grida := NewGrid(game.Width, game.Height, game.Density)
gridb := NewGrid(game.Width, game.Height, game.Density)
game.Grids = []*Grid{
grida,
gridb,
}
game.Pixels = make([]byte, game.ScreenWidth*game.ScreenHeight*4)
game.OffScreen = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
}
// count the living neighbors of a cell
func (game *Game) CountNeighbors(x, y int) int {
return game.Grids[game.Index].Data[y][x].Count(x, y)
}
// the heart of the game
func (game *Game) CheckRule(state bool, neighbors int) bool {
var nextstate bool
if state && neighbors == 3 {
nextstate = true
} else if state && (neighbors == 2 || neighbors == 3) {
nextstate = true
} else {
nextstate = false
}
return nextstate
}
// we only update the cells if we are not in pause state or if the
// game timer (TPG) is elapsed.
func (game *Game) UpdateCells() {
if game.Pause {
return
}
if game.Elapsed < game.TPG {
game.Elapsed++
return
}
// next grid index. we only have to, so we just xor it
next := game.Index ^ 1
// calculate cell life state, this is the actual game of life
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
state := game.Grids[game.Index].Data[y][x].State
neighbors := game.CountNeighbors(x, y)
// actually apply the current rules
nextstate := game.CheckRule(state, neighbors)
// change state of current cell in next grid
game.Grids[next].Data[y][x].State = nextstate
}
}
// switch grid for rendering
game.Index ^= 1
game.Elapsed = 0
game.UpdatePixels()
}
func (game *Game) Update() error {
game.UpdateCells()
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
game.Pause = !game.Pause
}
return nil
}
/*
*
r, g, b := color(it)
78 p := 4 * (i + j*screenWidth)
79 gm.offscreenPix[p] = r
80 gm.offscreenPix[p+1] = g
81 gm.offscreenPix[p+2] = b
82 gm.offscreenPix[p+3] = 0xff
*/
func (game *Game) UpdatePixels() {
var col byte
gridx := 0
gridy := 0
idx := 0
for y := 0; y < game.ScreenHeight; y++ {
for x := 0; x < game.ScreenWidth; x++ {
gridx = x / game.Cellsize
gridy = y / game.Cellsize
col = 0xff
if game.Grids[game.Index].Data[gridy][gridx].State {
col = 0x0
}
if game.Gridlines {
if x%game.Cellsize == 0 || y%game.Cellsize == 0 {
col = 128
}
}
idx = 4 * (x + y*game.ScreenWidth)
game.Pixels[idx] = col
game.Pixels[idx+1] = col
game.Pixels[idx+2] = col
game.Pixels[idx+3] = 0xff
idx++
}
}
game.OffScreen.WritePixels(game.Pixels)
}
func (game *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(game.OffScreen, nil)
game.DebugDump()
}
func main() {
size := 1500
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 8,
TPG: 10,
Debug: false,
Profile: true,
Gridlines: false,
}
game.ScreenWidth = game.Width * game.Cellsize
game.ScreenHeight = game.Height * game.Cellsize
game.Init()
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
ebiten.SetWindowTitle("triangle conway's game of life")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
if game.Profile {
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
}
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}

View File

@@ -238,35 +238,7 @@ func (game *Game) Draw(screen *ebiten.Image) {
} }
func main() { func main() {
//x := 1 size := 1500
//y := 0
col := 1 >> 0xff
fmt.Printf("col: %d\n", col)
x := 1
y := 2
c := 4
xm := x & (c - 1)
ym := y & (c - 1)
fmt.Println(xm & ym)
a := 1
b := 1
//gen := 100
hist := 0
for gen := 0; gen < 50; gen++ {
fmt.Println((a ^ (1 ^ b)) * (gen - hist))
if gen == 25 {
a = 0
}
}
}
func _main() {
size := 800
game := &Game{ game := &Game{
Width: size, Width: size,
@@ -275,7 +247,7 @@ func _main() {
Density: 8, Density: 8,
TPG: 10, TPG: 10,
Debug: false, Debug: false,
Profile: false, Profile: true,
Gridlines: false, Gridlines: false,
} }