17 Commits

Author SHA1 Message Date
df0f43a607 attemt to speed up using writepixels 2024-06-05 17:22:15 +02:00
162d141b34 reorganized source, added test cases for tuning 2024-06-05 16:33:35 +02:00
T.v.Dein
50fab6e1a5 Update TODO.md 2024-06-05 08:28:52 +02:00
8e361a04bd added tuning hint 2024-06-04 19:27:55 +02:00
2febea3264 added info about patterns 2024-06-04 19:25:54 +02:00
c4a00dcee2 added wrap option to option popup 2024-06-04 18:50:29 +02:00
f877cf5cb0 fixed initial zoom for smaller grids, turned grid to 1px, fix generation count 2024-06-04 18:46:59 +02:00
126de458b1 normalized pattern file loading and saving, only one option for loading: -f 2024-06-04 14:09:40 +02:00
c1a9a0f2c4 switched to lif.105 format for state files 2024-06-04 13:49:06 +02:00
443b5a2bcf fixed exit function with q, added evolution trace to options fixed inverse 2024-06-03 18:38:18 +02:00
03e1101248 lots changes:
- renamed scene files
- fixed options back using scene.Prev
- fixed initial zooming (finally)
- fixed reset zoom (key r)
- fixed initial size, now works with state loading as well
2024-06-03 17:44:17 +02:00
6527dba219 more zoom/center fixes. RLEs now load centered and visible. 2024-06-02 20:15:23 +02:00
3785799f4e fixed centering of squares, but not rectangles yet. 2024-06-02 20:15:23 +02:00
47f3693f77 fixed drawing bug: cells outside default canvas were not deleted 2024-06-02 20:15:23 +02:00
cb87815e4f fixed grid lines 2024-06-02 20:15:23 +02:00
e536f91790 fixed initial cam pos, it's now always centered 2024-06-02 20:15:23 +02:00
689b7be08b fixed clear screen problem, menus are now shown correctly. lots new
bugs though
2024-06-02 20:15:23 +02:00
35 changed files with 1321 additions and 343 deletions

3
.gitignore vendored
View File

@@ -3,3 +3,6 @@ bak
dump* dump*
rect* rect*
*profile *profile
*prof
*lif
*rle

101
Makefile
View File

@@ -1,97 +1,4 @@
# Copyright © 2024 Thomas von Dein .PHONY: all
all:
# This program is free software: you can redistribute it and/or modify make -C src
# it under the terms of the GNU General Public License as published by mv src/golsky .
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# no need to modify anything below
tool = golsky
VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2)
archs = darwin freebsd linux windows
PREFIX = /usr/local
UID = root
GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null)
#TAGS = -tags=ebitenginedebug
all: buildlocal
buildlocal:
go build $(TAGS) -o $(tool)
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) coverage.out testdata t/out
test: clean
mkdir -p t/out
go test ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest
testfuzzy: clean
go test -fuzz ./... $(ARGS)
singletest:
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
goupdate:
go get -t -u=patch ./...
buildall:
./mkrel.sh $(tool) $(VERSION)
release: buildall
gh release create $(VERSION) --generate-notes releases/*
show-versions: buildlocal
@echo "### golsky version:"
@./golsky -V
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
# lint:
# golangci-lint run -p bugs -p unused
buildwasm:
env GOOS=js GOARCH=wasm go build -o $(tool).wasm $(LDFLAGS) .
zipwasm:
zip -r openquell-$(SHORTVERSION).zip index.html $(tool).wasm wasm_exec.js
wasm: buildwasm zipwasm
@ls -l $(tool)-$(SHORTVERSION).zip

52
TODO.md
View File

@@ -1,16 +1,46 @@
- add all other options like size etc - add all other options like size etc
- do not process history if turned off
- add gif export
- add toolbar
- turn input ifs to switch
- add insert mode for edit like vi
- use left mouse to drag grid unless insert mode is active
- keep supporting middle mouse to drag so that dragging is possible in insert mode
- print current mode to the bottom like pause, insert and mark
- add https://www.ibiblio.org/lifepatterns/october1970.html
- use uint8 or bool, history needs not be larger than 256 anyway
- 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
- https://mattnakama.com/blog/go-branchless-coding/
- add performance measurements, see:
DrawTriangles: https://github.com/TLINDEN/testgol
WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels
https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm
- Clear screen problem: - Speed
- it works when hitting the K key, immediately https://conwaylife.com/forums/viewtopic.php?f=7&t=3237
- its being turned off correctly when entering menu and on when leaving it
- but regardless of the setting, after turning it off, the engine - Patterns:
seems to run a couple of ticks with the old setting before switching
scenes A Catagolue textcensus of, say, period-2 oscillators from
- looks like a race condition non-symmetrical soups can be found at
- obviously with K there are more loops before actually switching
scenes, which doesn't happen with ESC https://catagolue.hatsya.com/textcensus/b3s23/C1/xp2
The URL is made by just adding the prefix "text" to the word "census",
in any URL linked to from a Catagolue census page such as this one:
https://catagolue.hatsya.com/census/b3s23/C1
Format:
https://conwaylife.com/wiki/Apgcode
- if grid lines is disabled, they appear anyway in the first frame (then disappear) Collections:
- changing options mid-game has no effect in most cases, even after a restart https://conwaylife.com/wiki/Pattern_of_the_Year
https://www.ibiblio.org/lifepatterns/
https://entropymine.com/jason/life/
https://github.com/Matthias-Merzenich/jslife-moving
https://conwaylife.com/ref/mniemiec/lifepage.htm
https://conwaylife.com/wiki/Spaceship ff.

97
src/Makefile Normal file
View File

@@ -0,0 +1,97 @@
# Copyright © 2024 Thomas von Dein
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# no need to modify anything below
tool = golsky
VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2)
archs = darwin freebsd linux windows
PREFIX = /usr/local
UID = root
GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null)
#TAGS = -tags=ebitenginedebug
all: buildlocal
buildlocal:
go build $(TAGS) -o $(tool)
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) coverage.out testdata t/out
test: clean
mkdir -p t/out
go test ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest
testfuzzy: clean
go test -fuzz ./... $(ARGS)
singletest:
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
goupdate:
go get -t -u=patch ./...
buildall:
./mkrel.sh $(tool) $(VERSION)
release: buildall
gh release create $(VERSION) --generate-notes releases/*
show-versions: buildlocal
@echo "### golsky version:"
@./golsky -V
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
# lint:
# golangci-lint run -p bugs -p unused
buildwasm:
env GOOS=js GOARCH=wasm go build -o $(tool).wasm $(LDFLAGS) .
zipwasm:
zip -r openquell-$(SHORTVERSION).zip index.html $(tool).wasm wasm_exec.js
wasm: buildwasm zipwasm
@ls -l $(tool)-$(SHORTVERSION).zip

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 289 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -1,3 +1,5 @@
// this comes from the camera example but I enhanced it a little bit
package main package main
import ( import (
@@ -9,9 +11,12 @@ import (
) )
type Camera struct { type Camera struct {
ViewPort f64.Vec2 ViewPort f64.Vec2
Position f64.Vec2 Position f64.Vec2
ZoomFactor int ZoomFactor int
InitialZoomFactor int
InitialPosition f64.Vec2
ZoomOutFactor int
} }
func (c *Camera) String() string { func (c *Camera) String() string {
@@ -32,15 +37,17 @@ func (c *Camera) worldMatrix() ebiten.GeoM {
m := ebiten.GeoM{} m := ebiten.GeoM{}
m.Translate(-c.Position[0], -c.Position[1]) m.Translate(-c.Position[0], -c.Position[1])
viewportCenter := c.viewportCenter()
// We want to scale and rotate around center of image / screen // We want to scale and rotate around center of image / screen
m.Translate(-c.viewportCenter()[0], -c.viewportCenter()[1]) m.Translate(-viewportCenter[0], -viewportCenter[1])
m.Scale( m.Scale(
math.Pow(1.01, float64(c.ZoomFactor)), math.Pow(1.01, float64(c.ZoomFactor)),
math.Pow(1.01, float64(c.ZoomFactor)), math.Pow(1.01, float64(c.ZoomFactor)),
) )
m.Translate(c.viewportCenter()[0], c.viewportCenter()[1]) m.Translate(viewportCenter[0], viewportCenter[1])
return m return m
} }
@@ -61,8 +68,14 @@ func (c *Camera) ScreenToWorld(posX, posY int) (float64, float64) {
} }
} }
func (c *Camera) Reset() { func (c *Camera) Setup() {
c.Position[0] = 0 c.Position[0] = c.InitialPosition[0]
c.Position[1] = 0 c.Position[1] = c.InitialPosition[1]
c.ZoomFactor = 0 c.ZoomFactor = c.InitialZoomFactor
}
func (c *Camera) Reset() {
c.Position[0] = c.InitialPosition[0]
c.Position[1] = c.InitialPosition[1]
c.ZoomFactor = c.ZoomOutFactor
} }

View File

@@ -3,12 +3,12 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"os" "os"
"runtime/pprof" "runtime/pprof"
"strconv" "strconv"
"strings" "strings"
"github.com/alecthomas/repr"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/tlinden/golsky/rle" "github.com/tlinden/golsky/rle"
) )
@@ -30,6 +30,9 @@ type Config struct {
Restart, RestartGrid, RestartCache bool Restart, RestartGrid, RestartCache bool
StartWithMenu bool StartWithMenu bool
Zoomfactor int Zoomfactor int
ZoomOutFactor int
InitialCamPos []float64
DelayedStart bool // if true game, we wait. like pause but program induced
// for internal profiling // for internal profiling
ProfileFile string ProfileFile string
@@ -38,26 +41,53 @@ type Config struct {
} }
const ( const (
VERSION = "v0.0.7" VERSION = "v0.0.8"
Alive = 1 Alive = 1
Dead = 0 Dead = 0
DEFAULT_WIDTH = 600 DEFAULT_GRID_WIDTH = 600
DEFAULT_HEIGHT = 400 DEFAULT_GRID_HEIGHT = 400
DEFAULT_CELLSIZE = 4 DEFAULT_CELLSIZE = 4
DEFAULT_ZOOMFACTOR = 150 // FIXME, doesn't work? DEFAULT_ZOOMFACTOR = 150
DEFAULT_GEOM = "640x384" DEFAULT_GEOM = "640x384"
) )
func (config *Config) SetupCamera() {
config.Zoomfactor = DEFAULT_ZOOMFACTOR
// calculate the initial cam pos. It is negative if the total grid
// size is smaller than the screen in a centered position, but
// it's zero if it's equal or larger than the screen.
config.InitialCamPos = make([]float64, 2)
config.InitialCamPos[0] = float64(((config.ScreenWidth - (config.Width * config.Cellsize)) / 2) * -1)
if config.Width*config.Cellsize >= config.ScreenWidth {
// must be positive if world wider than screen
config.InitialCamPos[0] = math.Abs(config.InitialCamPos[0])
}
// same for Y
config.InitialCamPos[1] = float64(((config.ScreenHeight - (config.Height * config.Cellsize)) / 2) * -1)
if config.Height*config.Cellsize > config.ScreenHeight {
config.InitialCamPos[1] = math.Abs(config.InitialCamPos[1])
}
// Calculate zoom out factor, which shows 100% of the world. We
// need to reverse math.Pow(1.01, $zoomfactor) to get the correct
// percentage of the world to show. I.e: with a ScreenHeight of
// 384px and a world of 800px the factor to show 100% of the world
// is -75: math.Log(384/800) / math.Log(1.01). The 1.01 constant
// is being used in camera.go:worldMatrix().
// FIXME: determine if the diff is larger on width, then calc with
// width instead of height
config.ZoomOutFactor = int(
math.Log(float64(config.ScreenHeight)/(float64(config.Height)*float64(config.Cellsize))) /
math.Log(1.01))
}
// parse given window geometry and adjust game settings according to it // parse given window geometry and adjust game settings according to it
func (config *Config) ParseGeom(geom string) error { func (config *Config) ParseGeom(geom string) error {
if geom == "" {
config.ScreenWidth = config.Cellsize * config.Width
config.ScreenHeight = config.Cellsize * config.Height
config.Zoomfactor = 0
return nil
}
// force a geom // force a geom
geometry := strings.Split(geom, "x") geometry := strings.Split(geom, "x")
if len(geometry) != 2 { if len(geometry) != 2 {
@@ -74,42 +104,41 @@ func (config *Config) ParseGeom(geom string) error {
return errors.New("failed to parse height, expecting integer") return errors.New("failed to parse height, expecting integer")
} }
/*
// adjust dimensions, account for grid width+height so that cells
// fit into window
config.ScreenWidth = width - (width % config.Width)
config.ScreenHeight = height - (height % config.Height)
if config.ScreenWidth == 0 || config.ScreenHeight == 0 {
return errors.New("the number of requested cells don't fit into the requested window size")
}
*/
config.ScreenWidth = width config.ScreenWidth = width
config.ScreenHeight = height config.ScreenHeight = height
//config.Cellsize = config.ScreenWidth / config.Width
config.Cellsize = DEFAULT_CELLSIZE config.Cellsize = DEFAULT_CELLSIZE
config.Zoomfactor = DEFAULT_ZOOMFACTOR
repr.Println(config)
return nil return nil
} }
// check if we have been given an RLE file to load, then load it and // check if we have been given an RLE or LIF file to load, then load
// adjust game settings accordingly // it and adjust game settings accordingly
func (config *Config) ParseRLE(rlefile string) error { func (config *Config) ParseRLE(rlefile string) error {
if rlefile == "" { if rlefile == "" {
return nil return nil
} }
rleobj, err := rle.GetRLE(rlefile) var rleobj *rle.RLE
if err != nil {
return err if strings.HasSuffix(rlefile, ".lif") {
lifobj, err := LoadLIF(rlefile)
if err != nil {
return err
}
rleobj = lifobj
} else {
rleobject, err := rle.GetRLE(rlefile)
if err != nil {
return err
}
rleobj = rleobject
} }
if rleobj == nil { if rleobj == nil {
return errors.New("failed to load RLE file (uncatched module error)") return errors.New("failed to load pattern file (uncatched module error)")
} }
config.RLE = rleobj config.RLE = rleobj
@@ -121,6 +150,9 @@ func (config *Config) ParseRLE(rlefile string) error {
config.Cellsize = config.ScreenWidth / config.Width config.Cellsize = config.ScreenWidth / config.Width
} }
fmt.Printf("width: %d, screenwidth: %d, rlewidth: %d, cellsize: %d\n",
config.Width, config.ScreenWidth, config.RLE.Width, config.Cellsize)
// RLE needs an empty grid // RLE needs an empty grid
config.Empty = true config.Empty = true
@@ -132,25 +164,6 @@ func (config *Config) ParseRLE(rlefile string) error {
return nil return nil
} }
// parse a state file, if given, and adjust game settings accordingly
func (config *Config) ParseStatefile(statefile string) error {
if config.Statefile == "" {
return nil
}
grid, err := LoadState(config.Statefile)
if err != nil {
return fmt.Errorf("failed to load game state: %s", err)
}
config.Width = grid.Width
config.Height = grid.Height
config.Cellsize = config.ScreenWidth / config.Width
config.StateGrid = grid
return nil
}
func (config *Config) EnableCPUProfiling(filename string) error { func (config *Config) EnableCPUProfiling(filename string) error {
if filename == "" { if filename == "" {
return nil return nil
@@ -175,8 +188,8 @@ func ParseCommandline() (*Config, error) {
) )
// commandline params, most configure directly config flags // commandline params, most configure directly config flags
pflag.IntVarP(&config.Width, "width", "W", DEFAULT_WIDTH, "grid width in cells") pflag.IntVarP(&config.Width, "width", "W", DEFAULT_GRID_WIDTH, "grid width in cells")
pflag.IntVarP(&config.Height, "height", "H", DEFAULT_HEIGHT, "grid height in cells") pflag.IntVarP(&config.Height, "height", "H", DEFAULT_GRID_HEIGHT, "grid height in cells")
pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels") pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels")
pflag.StringVarP(&geom, "geom", "G", DEFAULT_GEOM, "window geometry in WxH in pixels, overturns -c") pflag.StringVarP(&geom, "geom", "G", DEFAULT_GEOM, "window geometry in WxH in pixels, overturns -c")
@@ -185,13 +198,12 @@ func ParseCommandline() (*Config, error) {
"game speed: the higher the slower (default: 10)") "game speed: the higher the slower (default: 10)")
pflag.StringVarP(&rule, "rule", "r", "B3/S23", "game rule") pflag.StringVarP(&rule, "rule", "r", "B3/S23", "game rule")
pflag.StringVarP(&rlefile, "rle-file", "f", "", "RLE pattern file") pflag.StringVarP(&rlefile, "pattern-file", "f", "", "RLE or LIF pattern file")
pflag.StringVarP(&config.Statefile, "load-state-file", "l", "", "game state file")
pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version") pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version")
pflag.BoolVarP(&config.Paused, "paused", "p", false, "do not start simulation (use space to start)") pflag.BoolVarP(&config.Paused, "paused", "p", false, "do not start simulation (use space to start)")
pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info") pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info")
pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", true, "draw grid lines") pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", false, "draw grid lines")
pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen") pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen")
pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)") pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)")
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution traces") pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution traces")
@@ -220,6 +232,9 @@ func ParseCommandline() (*Config, error) {
config.Rule = ParseGameRule(rule) config.Rule = ParseGameRule(rule)
} }
config.SetupCamera()
//repr.Println(config)
return &config, nil return &config, nil
} }
@@ -228,7 +243,6 @@ func (config *Config) TogglePaused() {
} }
func (config *Config) ToggleDebugging() { func (config *Config) ToggleDebugging() {
fmt.Println("DEBUG TOGGLED")
config.Debug = !config.Debug config.Debug = !config.Debug
} }
@@ -241,3 +255,11 @@ func (config *Config) ToggleGridlines() {
config.ShowGrid = !config.ShowGrid config.ShowGrid = !config.ShowGrid
config.RestartCache = true config.RestartCache = true
} }
func (config *Config) ToggleEvolution() {
config.ShowEvolution = !config.ShowEvolution
}
func (config *Config) ToggleWrap() {
config.Wrap = !config.Wrap
}

View File

@@ -1,8 +1,6 @@
package main package main
import ( import (
"fmt"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@@ -51,28 +49,35 @@ func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
func (game *Game) Update() error { func (game *Game) Update() error {
scene := game.GetCurrentScene() scene := game.GetCurrentScene()
scene.Update()
fmt.Printf("Clear Screen: %t\n", ebiten.IsScreenClearedEveryFrame()) if quit := scene.Update(); quit != nil {
return quit
}
next := scene.GetNext()
if next != game.CurrentScene {
game.Scenes[next].SetPrevious(game.CurrentScene)
scene.ResetNext()
game.CurrentScene = next
}
return nil return nil
} }
func (game *Game) Draw(screen *ebiten.Image) { func (game *Game) Draw(screen *ebiten.Image) {
var nextscene Scene // first draw primary scene[s], although there are only 1
for current, scene := range game.Scenes {
if scene.IsPrimary() {
// primary scenes always draw
scene.Draw(screen)
if current == game.CurrentScene {
// avoid to redraw it in the next step
return
}
}
}
scene := game.GetCurrentScene() scene := game.GetCurrentScene()
next := scene.GetNext()
if next != game.CurrentScene {
scene.ResetNext()
game.CurrentScene = next
nextscene = game.GetCurrentScene()
ebiten.SetScreenClearedEveryFrame(nextscene.Clearscreen())
}
scene.Draw(screen) scene.Draw(screen)
if nextscene != nil {
nextscene.Draw(screen)
}
} }

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@@ -78,6 +77,19 @@ func (grid *Grid) FillRandom() {
} }
} }
func (grid *Grid) Dump() {
for y := 0; y < grid.Height; y++ {
for x := 0; x < grid.Width; x++ {
if grid.Data[y][x] == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
// 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 {
@@ -95,33 +107,13 @@ func (grid *Grid) LoadRLE(pattern *rle.RLE) {
} }
} }
} }
//grid.Dump()
} }
} }
// save the contents of the whole grid as a simple mcell alike // load a lif file parameters like R and P are not supported yet
// file. One line per row, 0 for dead and 1 for life cell. func LoadLIF(filename string) (*rle.RLE, error) {
func (grid *Grid) SaveState(filename string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open state file: %w", err)
}
defer file.Close()
for y := range grid.Data {
for _, cell := range grid.Data[y] {
_, err := file.WriteString(strconv.FormatInt(cell, 10))
if err != nil {
return fmt.Errorf("failed to write to state file: %w", err)
}
}
file.WriteString("\n")
}
return nil
}
// the reverse of the above, load a mcell file
func LoadState(filename string) (*Grid, error) {
fd, err := os.Open(filename) fd, err := os.Open(filename)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -131,33 +123,60 @@ func LoadState(filename string) (*Grid, error) {
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
grid := &Grid{} gothead := false
grid := &rle.RLE{}
for scanner.Scan() { for scanner.Scan() {
items := strings.Split(scanner.Text(), "") line := scanner.Text()
row := make([]int64, len(items)) items := strings.Split(line, "")
for idx, item := range items { if len(items) < 0 {
num, err := strconv.ParseInt(item, 10, 64) continue
if err != nil {
return nil, err
}
if num > 1 {
return nil, errors.New("cells must be 0 or 1")
}
row[idx] = num
} }
grid.Data = append(grid.Data, row) if strings.Contains(line, "# r") {
parts := strings.Split(line, " ")
if len(parts) == 2 {
grid.Rule = parts[1]
}
continue
}
if items[0] == "#" {
if gothead {
break
}
continue
}
gothead = true
row := make([]int, len(items))
for idx, item := range items {
switch item {
case ".":
row[idx] = 0
case "o":
fallthrough
case "*":
row[idx] = 1
default:
return nil, errors.New("cells must be . or o")
}
}
grid.Pattern = append(grid.Pattern, row)
} }
// sanity check the grid // sanity check the grid
explen := 0 explen := 0
rows := 0 rows := 0
first := true first := true
for _, row := range grid.Data { for _, row := range grid.Pattern {
length := len(row) length := len(row)
if first { if first {
@@ -180,10 +199,43 @@ func LoadState(filename string) (*Grid, error) {
return grid, nil return grid, nil
} }
// save the contents of the whole grid as a simple lif alike
// file. One line per row, 0 for dead and 1 for life cell.
// file format: https://conwaylife.com/wiki/Life_1.05
func (grid *Grid) SaveState(filename, rule string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open state file: %w", err)
}
defer file.Close()
fmt.Fprintf(file, "#Life 1.05\n#R %s\n#D golsky state file\n#P -1 -1\n", rule)
for y := range grid.Data {
for _, cell := range grid.Data[y] {
row := ""
switch cell {
case 1:
row += "o"
case 0:
row += "."
}
_, err := file.WriteString(row)
if err != nil {
return fmt.Errorf("failed to write to state file: %w", err)
}
}
file.WriteString("\n")
}
return nil
}
// generate filenames for dumps // generate filenames for dumps
func GetFilename(generations int64) string { func GetFilename(generations int64) string {
now := time.Now() now := time.Now()
return fmt.Sprintf("dump-%s-%d.gol", now.Format("20060102150405"), generations) return fmt.Sprintf("dump-%s-%d.lif", now.Format("20060102150405"), generations)
} }
func GetFilenameRLE(generations int64) string { func GetFilenameRLE(generations int64) string {

View File

@@ -12,11 +12,12 @@ import (
) )
func main() { func main() {
dau := true var directstart bool
if len(os.Args) > 1 { if len(os.Args) > 1 {
dau = false directstart = true
} }
config, err := ParseCommandline() config, err := ParseCommandline()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -28,8 +29,9 @@ func main() {
} }
start := Play start := Play
if dau { if !directstart {
start = Menu start = Menu
config.DelayedStart = true
} }
game := NewGame(config, SceneName(start)) game := NewGame(config, SceneName(start))

View File

@@ -14,6 +14,7 @@ type SceneMenu struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Prev SceneName
Whoami SceneName Whoami SceneName
Ui *ebitenui.UI Ui *ebitenui.UI
FontColor color.RGBA FontColor color.RGBA
@@ -38,6 +39,10 @@ func (scene *SceneMenu) GetNext() SceneName {
return scene.Next return scene.Next
} }
func (scene *SceneMenu) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneMenu) ResetNext() { func (scene *SceneMenu) ResetNext() {
scene.Next = scene.Whoami scene.Next = scene.Whoami
} }
@@ -46,14 +51,11 @@ func (scene *SceneMenu) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *SceneMenu) Clearscreen() bool {
return false
}
func (scene *SceneMenu) Update() error { func (scene *SceneMenu) Update() error {
scene.Ui.Update() scene.Ui.Update()
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.Config.DelayedStart = false
scene.Leave() scene.Leave()
} }
@@ -61,6 +63,10 @@ func (scene *SceneMenu) Update() error {
} }
func (scene *SceneMenu) IsPrimary() bool {
return false
}
func (scene *SceneMenu) Draw(screen *ebiten.Image) { func (scene *SceneMenu) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen) scene.Ui.Draw(screen)
} }
@@ -98,11 +104,11 @@ func (scene *SceneMenu) Init() {
scene.SetNext(Options) scene.SetNext(Options)
}) })
separator1 := NewSeparator() separator1 := NewSeparator(3)
separator2 := NewSeparator() separator2 := NewSeparator(3)
separator3 := NewSeparator() separator3 := NewSeparator(10)
cancel := NewMenuButton("Close Window", cancel := NewMenuButton("Back",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
scene.Leave() scene.Leave()
}) })

View File

@@ -13,6 +13,7 @@ type SceneOptions struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Prev SceneName
Whoami SceneName Whoami SceneName
Ui *ebitenui.UI Ui *ebitenui.UI
FontColor color.RGBA FontColor color.RGBA
@@ -36,6 +37,10 @@ func (scene *SceneOptions) GetNext() SceneName {
return scene.Next return scene.Next
} }
func (scene *SceneOptions) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneOptions) ResetNext() { func (scene *SceneOptions) ResetNext() {
scene.Next = scene.Whoami scene.Next = scene.Whoami
} }
@@ -44,7 +49,7 @@ func (scene *SceneOptions) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *SceneOptions) Clearscreen() bool { func (scene *SceneOptions) IsPrimary() bool {
return false return false
} }
@@ -90,7 +95,7 @@ func (scene *SceneOptions) Init() {
invert := NewCheckbox("Invert", invert := NewCheckbox("Invert",
func(args *widget.CheckboxChangedEventArgs) { func(args *widget.CheckboxChangedEventArgs) {
scene.Config.Invert = true scene.Config.ToggleInvert()
}) })
scene.SetInitialValue(invert, scene.Config.Invert) scene.SetInitialValue(invert, scene.Config.Invert)
@@ -100,17 +105,31 @@ func (scene *SceneOptions) Init() {
}) })
scene.SetInitialValue(gridlines, scene.Config.ShowGrid) scene.SetInitialValue(gridlines, scene.Config.ShowGrid)
separator := NewSeparator() evolution := NewCheckbox("Show evolution traces",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleEvolution()
})
scene.SetInitialValue(evolution, scene.Config.ShowEvolution)
wrap := NewCheckbox("Wrap around edges",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleWrap()
})
scene.SetInitialValue(wrap, scene.Config.Wrap)
separator := NewSeparator(3)
cancel := NewMenuButton("Close", cancel := NewMenuButton("Close",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Menu) scene.SetNext(scene.Prev)
}) })
rowContainer.AddChild(pause) rowContainer.AddChild(pause)
rowContainer.AddChild(debugging) rowContainer.AddChild(debugging)
rowContainer.AddChild(invert) rowContainer.AddChild(invert)
rowContainer.AddChild(gridlines) rowContainer.AddChild(gridlines)
rowContainer.AddChild(evolution)
rowContainer.AddChild(wrap)
rowContainer.AddChild(separator) rowContainer.AddChild(separator)
rowContainer.AddChild(cancel) rowContainer.AddChild(cancel)

View File

@@ -5,7 +5,6 @@ import (
"image" "image"
"image/color" "image/color"
"log" "log"
"os"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
@@ -18,10 +17,15 @@ type Images struct {
Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image
} }
const (
DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s"
)
type ScenePlay struct { type ScenePlay struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Prev SceneName
Whoami SceneName Whoami SceneName
Clear bool Clear bool
@@ -44,6 +48,8 @@ type ScenePlay struct {
Mark, Point image.Point // area to marks+save Mark, Point image.Point // area to marks+save
RunOneStep bool // mutable flags from config RunOneStep bool // mutable flags from config
TPG int TPG int
Pixels []byte
PixelColor []byte
} }
func NewPlayScene(game *Game, config *Config) Scene { func NewPlayScene(game *Game, config *Config) Scene {
@@ -61,10 +67,18 @@ func NewPlayScene(game *Game, config *Config) Scene {
return scene return scene
} }
func (scene *ScenePlay) IsPrimary() bool {
return true
}
func (scene *ScenePlay) GetNext() SceneName { func (scene *ScenePlay) GetNext() SceneName {
return scene.Next return scene.Next
} }
func (scene *ScenePlay) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *ScenePlay) ResetNext() { func (scene *ScenePlay) ResetNext() {
scene.Next = scene.Whoami scene.Next = scene.Whoami
} }
@@ -73,10 +87,6 @@ func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *ScenePlay) Clearscreen() bool {
return true
}
func (scene *ScenePlay) CheckRule(state int64, neighbors int64) int64 { func (scene *ScenePlay) CheckRule(state int64, neighbors int64) int64 {
var nextstate int64 var nextstate int64
@@ -122,11 +132,13 @@ func (scene *ScenePlay) UpdateCells() {
// 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
// set history to current generation so we can infer the if scene.Config.ShowEvolution {
// age of the cell's state during rendering and use it to // set history to current generation so we can infer the
// deduce the color to use if evolution tracing is enabled // age of the cell's state during rendering and use it to
if state != nextstate { // deduce the color to use if evolution tracing is enabled
scene.History.Data[y][x] = scene.Generations if state != nextstate {
scene.History.Data[y][x] = scene.Generations
}
} }
} }
} }
@@ -146,24 +158,100 @@ func (scene *ScenePlay) UpdateCells() {
scene.TicksElapsed = 0 scene.TicksElapsed = 0
} }
// 2: works but still too slow
func (scene *ScenePlay) UpdatePixels() {
var col byte
width := scene.World.Bounds().Dx()
idx := 0
for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
col = scene.PixelColor[scene.Grids[scene.Index].Data[y][x]]
ypx := y * scene.Config.Cellsize
for Y := ypx; Y < ypx+scene.Config.Cellsize; Y++ {
xpx := x * scene.Config.Cellsize
for X := xpx; X < xpx+scene.Config.Cellsize; X++ {
idx = 4 * (X + Y*width)
scene.Pixels[idx] = col
scene.Pixels[idx+1] = col
scene.Pixels[idx+2] = col
scene.Pixels[idx+3] = 0xff
idx++
}
}
}
}
scene.Cache.WritePixels(scene.Pixels)
}
// 1: works, but a little slower than 2
func (scene *ScenePlay) _UpdatePixels() {
var col byte
gridx := 0
gridy := 0
idx := 0
width := scene.World.Bounds().Dx()
height := scene.World.Bounds().Dy()
for y := 0; y < height; y++ {
gridy = y / scene.Config.Cellsize // compute once per row
for x := 0; x < width; x++ {
gridx = x / scene.Config.Cellsize
col = scene.PixelColor[scene.Grids[scene.Index].Data[gridy][gridx]]
if scene.Config.ShowGrid {
if x%scene.Config.Cellsize == 0 || y%scene.Config.Cellsize == 0 {
col = scene.PixelColor[2]
}
}
idx = 4 * (x + y*width)
scene.Pixels[idx] = col
scene.Pixels[idx+1] = col
scene.Pixels[idx+2] = col
scene.Pixels[idx+3] = 0xff
idx++
}
}
scene.Cache.WritePixels(scene.Pixels)
}
func (scene *ScenePlay) Reset() { func (scene *ScenePlay) Reset() {
scene.Config.Paused = true scene.Config.Paused = true
scene.InitGrid(nil) scene.InitGrid()
scene.Config.Paused = false scene.Config.Paused = false
} }
// check user input // check user input
func (scene *ScenePlay) CheckInput() { func (scene *ScenePlay) CheckExit() error {
if inpututil.IsKeyJustPressed(ebiten.KeyQ) { if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
os.Exit(0) return ebiten.Termination
} }
return nil
}
func (scene *ScenePlay) CheckInput() {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.SetNext(Menu) scene.SetNext(Menu)
} }
if inpututil.IsKeyJustPressed(ebiten.KeyO) {
scene.SetNext(Options)
}
if inpututil.IsKeyJustPressed(ebiten.KeyC) { if inpututil.IsKeyJustPressed(ebiten.KeyC) {
fmt.Println("mark mode on")
scene.Config.Markmode = true scene.Config.Markmode = true
scene.Config.Paused = true scene.Config.Paused = true
} }
@@ -284,6 +372,10 @@ func (scene *ScenePlay) CheckMarkInput() {
return return
} }
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.Config.Markmode = false
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButton0) { if ebiten.IsMouseButtonPressed(ebiten.MouseButton0) {
if !scene.MarkTaken { if !scene.MarkTaken {
scene.Mark = scene.GetWorldCursorPos() scene.Mark = scene.GetWorldCursorPos()
@@ -304,7 +396,7 @@ func (scene *ScenePlay) CheckMarkInput() {
func (scene *ScenePlay) SaveState() { func (scene *ScenePlay) SaveState() {
filename := GetFilename(scene.Generations) filename := GetFilename(scene.Generations)
err := scene.Grids[scene.Index].SaveState(filename) err := scene.Grids[scene.Index].SaveState(filename, scene.Config.Rule.Definition)
if err != nil { if err != nil {
log.Printf("failed to save game state to %s: %s", filename, err) log.Printf("failed to save game state to %s: %s", filename, err)
} }
@@ -366,7 +458,9 @@ func (scene *ScenePlay) SaveRectRLE() {
func (scene *ScenePlay) Update() error { func (scene *ScenePlay) Update() error {
if scene.Config.Restart { if scene.Config.Restart {
scene.Config.Restart = false scene.Config.Restart = false
scene.Init() scene.Generations = 0
scene.InitGrid()
scene.InitCache()
return nil return nil
} }
@@ -377,6 +471,10 @@ func (scene *ScenePlay) Update() error {
return nil return nil
} }
if quit := scene.CheckExit(); quit != nil {
return quit
}
scene.CheckInput() scene.CheckInput()
scene.CheckDraggingInput() scene.CheckDraggingInput()
scene.CheckMarkInput() scene.CheckMarkInput()
@@ -395,7 +493,7 @@ func (scene *ScenePlay) ToggleCellOnCursorPos(alive int64) {
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 { 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] = alive
scene.History.Data[y][x] = 1 scene.History.Data[y][x] = 1
} }
@@ -408,47 +506,50 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
// a nice grey grid with grid lines // a nice grey grid with grid lines
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
scene.UpdatePixels()
op.GeoM.Translate(0, 0) op.GeoM.Translate(0, 0)
scene.World.DrawImage(scene.Cache, op) scene.World.DrawImage(scene.Cache, op)
var age int64 /*
var age int64
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++ {
op.GeoM.Reset() op.GeoM.Reset()
op.GeoM.Translate( op.GeoM.Translate(
float64(x*scene.Config.Cellsize), float64(x*scene.Config.Cellsize),
float64(y*scene.Config.Cellsize), float64(y*scene.Config.Cellsize),
) )
age = scene.Generations - scene.History.Data[y][x] age = scene.Generations - scene.History.Data[y][x]
switch scene.Grids[scene.Index].Data[y][x] { switch scene.Grids[scene.Index].Data[y][x] {
case Alive: case Alive:
if age > 50 && scene.Config.ShowEvolution { if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Tiles.Old, op) scene.World.DrawImage(scene.Tiles.Old, op)
} else { } else {
scene.World.DrawImage(scene.Tiles.Black, op) scene.World.DrawImage(scene.Tiles.Black, op)
} }
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.Data[y][x] > 1 && scene.Config.ShowEvolution { if scene.History.Data[y][x] > 1 && scene.Config.ShowEvolution {
switch { switch {
case age < 10: case age < 10:
scene.World.DrawImage(scene.Tiles.Age1, op) scene.World.DrawImage(scene.Tiles.Age1, op)
case age < 20: case age < 20:
scene.World.DrawImage(scene.Tiles.Age2, op) scene.World.DrawImage(scene.Tiles.Age2, op)
case age < 30: case age < 30:
scene.World.DrawImage(scene.Tiles.Age3, op) scene.World.DrawImage(scene.Tiles.Age3, op)
default: default:
scene.World.DrawImage(scene.Tiles.Age4, op) scene.World.DrawImage(scene.Tiles.Age4, op)
}
} }
} }
} }
} }
} */
scene.DrawMark(scene.World) scene.DrawMark(scene.World)
scene.Camera.Render(scene.World, screen) scene.Camera.Render(scene.World, screen)
@@ -484,10 +585,18 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
paused = "-- paused --" paused = "-- paused --"
} }
if scene.Config.Markmode {
paused = "-- mark --"
}
x, y := ebiten.CursorPosition()
debug := fmt.Sprintf( debug := fmt.Sprintf(
"FPS: %0.2f, TPG: %d, Mem: %0.2fMB, Gen: %d, Scale: %.02f, Z: %d, Clear: %t %s", DEBUG_FORMAT,
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations,
scene.Game.Scale, scene.Camera.ZoomFactor, ebiten.IsScreenClearedEveryFrame(), paused) scene.Game.Scale, scene.Camera.ZoomFactor,
scene.Camera.Position[0], scene.Camera.Position[1],
x, y,
paused)
FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10)) FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10))
FontRenderer.Renderer.SetTarget(screen) FontRenderer.Renderer.SetTarget(screen)
@@ -500,6 +609,7 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
fmt.Println(debug) fmt.Println(debug)
} }
} }
// load a pre-computed pattern from RLE file // load a pre-computed pattern from RLE file
@@ -532,23 +642,12 @@ 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(grid *Grid) { func (scene *ScenePlay) InitGrid() {
if grid != nil {
// use pre-loaded grid
scene.Grids = []*Grid{
grid,
NewGrid(grid.Width, grid.Height, 0, false),
}
scene.History = NewGrid(grid.Width, grid.Height, 0, false)
return
}
grida := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) grida := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
history := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) history := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
// startup is delayed until user has selected options
grida.FillRandom() grida.FillRandom()
grida.Copy(history) grida.Copy(history)
@@ -603,26 +702,42 @@ func (scene *ScenePlay) InitTiles() {
func (scene *ScenePlay) Init() { func (scene *ScenePlay) Init() {
// setup the scene // setup the scene
var grid *Grid
if scene.Config.StateGrid != nil {
grid = scene.Config.StateGrid
}
scene.Camera = Camera{ scene.Camera = Camera{
ViewPort: f64.Vec2{ ViewPort: f64.Vec2{
float64(scene.Config.ScreenWidth), float64(scene.Config.ScreenWidth),
float64(scene.Config.ScreenHeight), float64(scene.Config.ScreenHeight),
}, },
InitialZoomFactor: scene.Config.Zoomfactor,
InitialPosition: f64.Vec2{
scene.Config.InitialCamPos[0],
scene.Config.InitialCamPos[1],
},
ZoomOutFactor: scene.Config.ZoomOutFactor,
} }
scene.World = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight) scene.World = ebiten.NewImage(
scene.Cache = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight) scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.Cache = ebiten.NewImage(
scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.Pixels = make([]byte, scene.World.Bounds().Dx()*scene.World.Bounds().Dy()*scene.Config.Cellsize)
scene.InitTiles() scene.InitTiles()
scene.InitCache() scene.InitCache()
scene.InitGrid(grid)
if scene.Config.DelayedStart && !scene.Config.Empty {
scene.Config.Empty = true
scene.InitGrid()
scene.Config.Empty = false
} else {
scene.InitGrid()
}
scene.InitPattern() scene.InitPattern()
scene.Index = 0 scene.Index = 0
@@ -633,6 +748,13 @@ func (scene *ScenePlay) Init() {
if scene.Config.Zoomfactor < 0 || scene.Config.Zoomfactor > 0 { if scene.Config.Zoomfactor < 0 || scene.Config.Zoomfactor > 0 {
scene.Camera.ZoomFactor = scene.Config.Zoomfactor scene.Camera.ZoomFactor = scene.Config.Zoomfactor
} }
scene.Camera.Setup()
scene.PixelColor = make([]byte, 3)
scene.PixelColor[0] = 0xff
scene.PixelColor[1] = 0x00
scene.PixelColor[2] = 0xff
} }
// count the living neighbors of a cell // count the living neighbors of a cell
@@ -675,8 +797,8 @@ func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA) {
tile, tile,
float32(1), float32(1),
float32(1), float32(1),
float32(cellsize-1), float32(cellsize),
float32(cellsize-1), float32(cellsize),
col, false, col, false,
) )
} }

View File

@@ -13,10 +13,11 @@ type SceneName int
type Scene interface { type Scene interface {
SetNext(SceneName) SetNext(SceneName)
GetNext() SceneName GetNext() SceneName
SetPrevious(SceneName)
ResetNext() ResetNext()
Clearscreen() bool
Update() error Update() error
Draw(screen *ebiten.Image) Draw(screen *ebiten.Image)
IsPrimary() bool // if true, this scene will be always drawn
} }
const ( const (

View File

@@ -64,28 +64,17 @@ func NewCheckbox(
) )
} }
func NewSeparator() widget.PreferredSizeLocateableWidget { func NewSeparator(padding int) widget.PreferredSizeLocateableWidget {
c := widget.NewContainer( c := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewRowLayout( widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical), widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.Insets{ widget.RowLayoutOpts.Padding(widget.Insets{
Top: 20, Top: padding,
Bottom: 20, Bottom: 0,
}))), }))),
widget.ContainerOpts.WidgetOpts( widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData( widget.WidgetOpts.LayoutData(
widget.RowLayoutData{Stretch: true}))) widget.RowLayoutData{Stretch: true})))
c.AddChild(widget.NewGraphic(
widget.GraphicOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
MaxHeight: 2,
})),
widget.GraphicOpts.ImageNineSlice(
image.NewNineSliceColor(
color.NRGBA{0xdf, 0xf4, 0xff, 0xff})),
))
return c return c
} }
@@ -125,7 +114,7 @@ func NewRowContainer(title string) *RowContainer {
), ),
widget.ContainerOpts.Layout(widget.NewRowLayout( widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical), widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(20)), widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(8)),
widget.RowLayoutOpts.Spacing(0), widget.RowLayoutOpts.Spacing(0),
)), )),
widget.ContainerOpts.BackgroundImage(buttonImageHover), widget.ContainerOpts.BackgroundImage(buttonImageHover),

View File

@@ -0,0 +1,13 @@
module drawminimal
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,362 @@
package main
import (
"fmt"
"image"
"image/color"
"log"
"math/rand"
"os"
"runtime/pprof"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector"
)
var (
blackImage = ebiten.NewImage(3, 3)
blackSubImage = blackImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
)
type Images struct {
Black, White *ebiten.Image
}
type Grid struct {
Data [][]int64
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([][]int64, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]int64, width)
}
return grid
}
// live console output of the grid
func (grid *Grid) Dump() {
/*
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
for y := 0; y < grid.Height; y++ {
for x := 0; x < grid.Width; x++ {
if grid.Data[y][x] == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
*/
fmt.Printf("FPS: %0.2f\n", ebiten.ActualTPS())
}
type Game struct {
Width, Height, Cellsize, Density int
ScreenWidth, ScreenHeight int
Grids []*Grid
Index int
Black, White, Grey color.RGBA
Tiles Images
Cache *ebiten.Image
Elapsed int64
TPG int64 // adjust game speed independently of TPS
Vertices []ebiten.Vertex
Indices []uint16
Pause, Debug bool
}
// fill a cell
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,
)
}
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return game.ScreenWidth, game.ScreenHeight
}
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)
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if rand.Intn(game.Density) == 1 {
grida.Data[y][x] = 1
}
}
}
game.Grids = []*Grid{
grida,
gridb,
}
// setup colors
game.Grey = color.RGBA{128, 128, 128, 0xff}
game.Black = color.RGBA{0, 0, 0, 0xff}
game.White = color.RGBA{200, 200, 200, 0xff}
game.Tiles.White = ebiten.NewImage(game.Cellsize, game.Cellsize)
game.Cache = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
FillCell(game.Tiles.White, game.Cellsize, game.White)
game.Cache.Fill(game.Grey)
// draw the offscreen image
op := &ebiten.DrawImageOptions{}
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))
game.Cache.DrawImage(game.Tiles.White, op)
}
}
blackSubImage.Fill(game.Black)
lenvertices := game.ScreenHeight * game.ScreenWidth
game.Vertices = make([]ebiten.Vertex, lenvertices)
game.Indices = make([]uint16, lenvertices+(lenvertices/2))
}
// count the living neighbors of a cell
func (game *Game) CountNeighbors(x, y int) int64 {
var sum int64
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
// 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 + game.Width) % game.Width
row = (y + nbgY + 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
}
// the heart of the game
func (game *Game) CheckRule(state int64, neighbors int64) int64 {
var nextstate int64
if state == 0 && 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
// reset vertices
// FIXME: fails!
game.ClearVertices()
// 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] // 0|1 == dead or alive
neighbors := game.CountNeighbors(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
}
}
// calculate triangles for rendering
game.UpdateTriangles()
// switch grid for rendering
game.Index ^= 1
game.Elapsed = 0
if game.Debug {
game.Grids[next].Dump()
}
}
func (game *Game) Update() error {
game.UpdateCells()
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
game.Pause = !game.Pause
}
return nil
}
func (game *Game) ClearVertices() {
// FIXME: fails
for i := 0; i < len(game.Vertices); i++ {
game.Vertices[i] = ebiten.Vertex{}
// game.Vertices[i].DstX = 0
// game.Vertices[i].DstY = 1
}
game.Indices = game.Indices[:len(game.Indices)]
}
// create the triangles needed for rendering. Actual rendering doesn't
// happen here but in Draw()
func (game *Game) UpdateTriangles() {
var base uint16 = 0
var index uint16 = 0
idx := 0
// iterate over every cell
for celly := 0; celly < game.Height; celly++ {
for cellx := 0; cellx < game.Width; cellx++ {
// if the cell is alife
if game.Grids[game.Index].Data[celly][cellx] == 1 {
/* iterate over the cell's corners:
0 1
2 3
*/
for i := 0; i < 2; i++ {
for j := 0; j < 2; j++ {
// calculate the corner position
x := (cellx * game.Cellsize) + (i * game.Cellsize) + 1
y := (celly * game.Cellsize) + (j * game.Cellsize) + 1
if i == 1 {
x -= 1
}
if j == 1 {
y -= 1
}
// setup the vertex
game.Vertices[idx].DstX = float32(x)
game.Vertices[idx].DstY = float32(y)
game.Vertices[idx].SrcX = 1
game.Vertices[idx].SrcY = 1
game.Vertices[idx].ColorR = float32(game.Black.R)
game.Vertices[idx].ColorG = float32(game.Black.G)
game.Vertices[idx].ColorB = float32(game.Black.B)
game.Vertices[idx].ColorA = 1
idx++
}
}
}
// indices for first triangle
game.Indices[index] = base
game.Indices[index+1] = base + 1
game.Indices[index+2] = base + 3
// for the second one
game.Indices[index+3] = base
game.Indices[index+4] = base + 2
game.Indices[index+5] = base + 3
index += 6 // 3 indicies per triangle
base += 4 // 4 vertices per cell
}
}
}
func (game *Game) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(0, 0)
screen.DrawImage(game.Cache, op)
triop := &ebiten.DrawTrianglesOptions{}
screen.DrawTriangles(game.Vertices, game.Indices, blackSubImage, triop)
}
func main() {
size := 200
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 5,
TPG: 5,
Debug: true,
}
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)
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,294 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"runtime/pprof"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Images struct {
Black, White *ebiten.Image
}
type Grid struct {
Data [][]int64
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([][]int64, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]int64, width)
}
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] == 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)
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if rand.Intn(game.Density) == 1 {
grida.Data[y][x] = 1
}
}
}
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) int64 {
var sum int64
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
// 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 + game.Width) % game.Width
row = (y + nbgY + 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
}
// the heart of the game
func (game *Game) CheckRule(state int64, neighbors int64) int64 {
var nextstate int64
if state == 0 && 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] // 0|1 == dead or alive
neighbors := game.CountNeighbors(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
}
}
// 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] == 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() {
//x := 1
//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)
}
func _main() {
size := 800
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 8,
TPG: 10,
Debug: false,
Profile: false,
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)
}
}