mirror of
https://codeberg.org/scip/golsky.git
synced 2025-12-17 12:40:56 +01:00
Compare commits
5 Commits
dimensions
...
caching
| Author | SHA1 | Date | |
|---|---|---|---|
| ddd8d92a60 | |||
| c78a232ac1 | |||
| f82037a554 | |||
| ba247d0606 | |||
| aeebfb1997 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
golsky
|
golsky
|
||||||
bak
|
bak
|
||||||
dump*
|
dump*
|
||||||
|
rect*
|
||||||
|
*profile
|
||||||
|
|||||||
BIN
assets/fonts/NotoSans-Regular.ttf
Normal file
BIN
assets/fonts/NotoSans-Regular.ttf
Normal file
Binary file not shown.
13
assets/shaders/row.kg
Normal file
13
assets/shaders/row.kg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//kage:unit pixels
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
var Alife int
|
||||||
|
|
||||||
|
func Fragment(_ vec4, pos vec2, _ vec4) vec4 {
|
||||||
|
if Alife == 1 {
|
||||||
|
return vec4(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec4(1.0)
|
||||||
|
}
|
||||||
BIN
assets/sprites/button-9slice1.png
Normal file
BIN
assets/sprites/button-9slice1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 B |
BIN
assets/sprites/button-9slice2.png
Normal file
BIN
assets/sprites/button-9slice2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 274 B |
BIN
assets/sprites/button-9slice3.png
Normal file
BIN
assets/sprites/button-9slice3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 B |
BIN
assets/sprites/checkbox-9slice1.png
Normal file
BIN
assets/sprites/checkbox-9slice1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 289 B |
BIN
assets/sprites/checkbox-9slice2.png
Normal file
BIN
assets/sprites/checkbox-9slice2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 271 B |
BIN
assets/sprites/checkbox-9slice3.png
Normal file
BIN
assets/sprites/checkbox-9slice3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 B |
BIN
assets/src/button-9slice.ase
Normal file
BIN
assets/src/button-9slice.ase
Normal file
Binary file not shown.
BIN
assets/src/button-9slice1.ase
Normal file
BIN
assets/src/button-9slice1.ase
Normal file
Binary file not shown.
BIN
assets/src/checkbox-9slice.ase
Normal file
BIN
assets/src/checkbox-9slice.ase
Normal file
Binary file not shown.
173
config.go
173
config.go
@@ -1,10 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/pprof"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/tlinden/golsky/rle"
|
"github.com/tlinden/golsky/rle"
|
||||||
)
|
)
|
||||||
@@ -22,42 +26,143 @@ type Config struct {
|
|||||||
StateGrid *Grid // a grid from a statefile
|
StateGrid *Grid // a grid from a statefile
|
||||||
Wrap bool // wether wraparound mode is in place or not
|
Wrap bool // wether wraparound mode is in place or not
|
||||||
ShowVersion bool
|
ShowVersion bool
|
||||||
|
UseShader bool // to use a shader to render alife cells
|
||||||
|
|
||||||
|
// for internal profiling
|
||||||
|
ProfileFile string
|
||||||
|
ProfileDraw bool
|
||||||
|
ProfileMaxLoops int64
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VERSION = "v0.0.6"
|
VERSION = "v0.0.7"
|
||||||
Alive = 1
|
Alive = 1
|
||||||
Dead = 0
|
Dead = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRLE(filename string) *rle.RLE {
|
// parse given window geometry and adjust game settings according to it
|
||||||
|
func (config *Config) ParseGeom(geom string) error {
|
||||||
|
if geom == "" {
|
||||||
|
config.ScreenWidth = config.Cellsize * config.Width
|
||||||
|
config.ScreenHeight = config.Cellsize * config.Height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// force a geom
|
||||||
|
geometry := strings.Split(geom, "x")
|
||||||
|
if len(geometry) != 2 {
|
||||||
|
return errors.New("failed to parse -g parameters, expecting WIDTHxHEIGHT")
|
||||||
|
}
|
||||||
|
|
||||||
|
width, err := strconv.Atoi(geometry[0])
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to parse width, expecting integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
height, err := strconv.Atoi(geometry[1])
|
||||||
|
if err != nil {
|
||||||
|
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.Cellsize = config.ScreenWidth / config.Width
|
||||||
|
|
||||||
|
repr.Println(config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have been given an RLE file to load, then load it and
|
||||||
|
// adjust game settings accordingly
|
||||||
|
func (config *Config) ParseRLE(rlefile string) error {
|
||||||
|
if rlefile == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rleobj, err := rle.GetRLE(rlefile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rleobj == nil {
|
||||||
|
return errors.New("failed to load RLE file (uncatched module error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.RLE = rleobj
|
||||||
|
|
||||||
|
// adjust geometry if needed
|
||||||
|
if config.RLE.Width > config.Width || config.RLE.Height > config.Height {
|
||||||
|
config.Width = config.RLE.Width * 2
|
||||||
|
config.Height = config.RLE.Height * 2
|
||||||
|
config.Cellsize = config.ScreenWidth / config.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// RLE needs an empty grid
|
||||||
|
config.Empty = true
|
||||||
|
|
||||||
|
// it may come with its own rule
|
||||||
|
if config.RLE.Rule != "" {
|
||||||
|
config.Rule = ParseGameRule(config.RLE.Rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := os.ReadFile(filename)
|
fd, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedRle, err := rle.Parse(string(content))
|
pprof.StartCPUProfile(fd)
|
||||||
if err != nil {
|
defer pprof.StopCPUProfile()
|
||||||
log.Fatalf("failed to load RLE pattern file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &parsedRle
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCommandline() *Config {
|
func ParseCommandline() (*Config, error) {
|
||||||
config := Config{}
|
config := Config{}
|
||||||
|
|
||||||
var rule string
|
var (
|
||||||
var rlefile string
|
rule, rlefile, geom string
|
||||||
|
)
|
||||||
|
|
||||||
// commandline params, most configure directly config flags
|
// commandline params, most configure directly config flags
|
||||||
pflag.IntVarP(&config.Width, "width", "W", 40, "grid width in cells")
|
pflag.IntVarP(&config.Width, "width", "W", 40, "grid width in cells")
|
||||||
pflag.IntVarP(&config.Height, "height", "H", 40, "grid height in cells")
|
pflag.IntVarP(&config.Height, "height", "H", 40, "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", "", "window geometry in WxH in pixels, overturns -c")
|
||||||
|
|
||||||
pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells")
|
pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells")
|
||||||
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
|
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
|
||||||
"game speed: the higher the slower (default: 10)")
|
"game speed: the higher the slower (default: 10)")
|
||||||
@@ -74,39 +179,23 @@ func ParseCommandline() *Config {
|
|||||||
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 tracks")
|
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution tracks")
|
||||||
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode")
|
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode")
|
||||||
|
pflag.BoolVarP(&config.UseShader, "use-shader", "k", false, "use shader for cell rendering")
|
||||||
|
|
||||||
|
pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling")
|
||||||
|
pflag.BoolVarP(&config.ProfileDraw, "profile-draw", "", false, "profile draw method (default false)")
|
||||||
|
pflag.Int64VarP(&config.ProfileMaxLoops, "profile-max-loops", "", 10, "how many loops to execute (default 10)")
|
||||||
|
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|
||||||
// check if we have been given an RLE file to load
|
err := config.ParseGeom(geom)
|
||||||
config.RLE = GetRLE(rlefile)
|
if err != nil {
|
||||||
if config.RLE != nil {
|
return nil, err
|
||||||
if config.RLE.Width > config.Width || config.RLE.Height > config.Height {
|
|
||||||
config.Width = config.RLE.Width * 2
|
|
||||||
config.Height = config.RLE.Height * 2
|
|
||||||
fmt.Printf("rlew: %d, rleh: %d, w: %d, h: %d\n",
|
|
||||||
config.RLE.Width, config.RLE.Height, config.Width, config.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RLE needs an empty grid
|
|
||||||
config.Empty = true
|
|
||||||
|
|
||||||
// it may come with its own rule
|
|
||||||
if config.RLE.Rule != "" {
|
|
||||||
config.Rule = ParseGameRule(config.RLE.Rule)
|
|
||||||
}
|
|
||||||
} else if config.Statefile != "" {
|
|
||||||
grid, err := LoadState(config.Statefile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to load game state: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Width = grid.Width
|
|
||||||
config.Height = grid.Height
|
|
||||||
config.StateGrid = grid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ScreenWidth = config.Cellsize * config.Width
|
err = config.ParseRLE(rlefile)
|
||||||
config.ScreenHeight = config.Cellsize * config.Height
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// load rule from commandline when no rule came from RLE file,
|
// load rule from commandline when no rule came from RLE file,
|
||||||
// default is B3/S23, aka conways game of life
|
// default is B3/S23, aka conways game of life
|
||||||
@@ -114,5 +203,5 @@ func ParseCommandline() *Config {
|
|||||||
config.Rule = ParseGameRule(rule)
|
config.Rule = ParseGameRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &config
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -13,7 +13,10 @@ require (
|
|||||||
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
|
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
|
||||||
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||||
github.com/ebitengine/purego v0.7.0 // indirect
|
github.com/ebitengine/purego v0.7.0 // 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/tinne26/etxt v0.0.8 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.15.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -6,15 +6,21 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj
|
|||||||
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
|
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 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
|
||||||
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
|
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/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/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/go.mod h1:QM/hlNkstsKC39elTFNKAR34xsMb9QoVosf+g9wlYxM=
|
||||||
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
|
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
|
||||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
|||||||
99
loader-fonts.go
Normal file
99
loader-fonts.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/tinne26/etxt"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FontRenderer = LoadFonts("assets/fonts")
|
||||||
|
|
||||||
|
const (
|
||||||
|
GameFont string = "NotoSans-Regular"
|
||||||
|
FontSizeBig int = 48
|
||||||
|
FontSizeNormal int = 24
|
||||||
|
FontSizeSmall int = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
type Texter struct {
|
||||||
|
Renderer *etxt.Renderer
|
||||||
|
FontNormal *font.Face
|
||||||
|
FontBig *font.Face
|
||||||
|
FontSmall *font.Face
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFonts(dir string) Texter {
|
||||||
|
fontbytes, err := assetfs.ReadFile(dir + "/" + GameFont + ".ttf")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gamefont, err := truetype.Parse(fontbytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gameface := truetype.NewFace(gamefont, &truetype.Options{
|
||||||
|
Size: float64(FontSizeNormal),
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingFull,
|
||||||
|
})
|
||||||
|
|
||||||
|
biggameface := truetype.NewFace(gamefont, &truetype.Options{
|
||||||
|
Size: float64(FontSizeBig),
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingFull,
|
||||||
|
})
|
||||||
|
|
||||||
|
smallgameface := truetype.NewFace(gamefont, &truetype.Options{
|
||||||
|
Size: float64(FontSizeSmall),
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingFull,
|
||||||
|
})
|
||||||
|
|
||||||
|
fontlib := etxt.NewFontLibrary()
|
||||||
|
_, _, err = fontlib.ParseEmbedDirFonts(dir, assetfs)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error while loading fonts: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fontlib.HasFont(GameFont) {
|
||||||
|
log.Fatal("missing font: " + GameFont)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fontlib.EachFont(checkMissingRunes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer := etxt.NewStdRenderer()
|
||||||
|
|
||||||
|
glyphsCache := etxt.NewDefaultCache(10 * 1024 * 1024) // 10MB
|
||||||
|
renderer.SetCacheHandler(glyphsCache.NewHandler())
|
||||||
|
renderer.SetFont(fontlib.GetFont(GameFont))
|
||||||
|
|
||||||
|
return Texter{
|
||||||
|
Renderer: renderer,
|
||||||
|
FontNormal: &gameface,
|
||||||
|
FontBig: &biggameface,
|
||||||
|
FontSmall: &smallgameface,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function used with FontLibrary.EachFont to make sure
|
||||||
|
// all loaded fonts contain the characters or alphabet we want
|
||||||
|
func checkMissingRunes(name string, font *etxt.Font) error {
|
||||||
|
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
const symbols = "0123456789 .,;:!?-()[]{}_&#@"
|
||||||
|
|
||||||
|
missing, err := etxt.GetMissingRunes(font, letters+symbols)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
log.Fatalf("Font '%s' missing runes: %s", name, string(missing))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
loader-shaders.go
Normal file
49
loader-shaders.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShaderRegistry map[string]*ebiten.Shader
|
||||||
|
|
||||||
|
var Shaders = LoadShaders("assets/shaders")
|
||||||
|
|
||||||
|
func LoadShaders(dir string) ShaderRegistry {
|
||||||
|
shaders := ShaderRegistry{}
|
||||||
|
|
||||||
|
entries, err := assetfs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read shaders dir %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range entries {
|
||||||
|
path := path.Join(dir, file.Name())
|
||||||
|
fd, err := assetfs.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open shader file %s: %s", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(file.Name(), ".kg")
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(fd)
|
||||||
|
|
||||||
|
shader, err := ebiten.NewShader([]byte(buf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shaders[name] = shader
|
||||||
|
|
||||||
|
slog.Debug("loaded shader asset", "path", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shaders
|
||||||
|
}
|
||||||
69
loader-sprites.go
Normal file
69
loader-sprites.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Maps image name to image data
|
||||||
|
type AssetRegistry map[string]*ebiten.Image
|
||||||
|
|
||||||
|
// A helper to pass the registry easier around
|
||||||
|
type assetData struct {
|
||||||
|
Registry AssetRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed assets/sprites/*.png assets/fonts/*.ttf assets/shaders/*.kg
|
||||||
|
var assetfs embed.FS
|
||||||
|
|
||||||
|
// Called at build time, creates the global asset and animation registries
|
||||||
|
var Assets = LoadImages("assets/sprites")
|
||||||
|
|
||||||
|
// load pngs and json files
|
||||||
|
func LoadImages(dir string) AssetRegistry {
|
||||||
|
Registry := AssetRegistry{}
|
||||||
|
|
||||||
|
// we use embed.FS to iterate over all files in ./assets/
|
||||||
|
entries, err := assetfs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read assets dir %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, imagefile := range entries {
|
||||||
|
path := path.Join(dir, imagefile.Name())
|
||||||
|
|
||||||
|
fd, err := assetfs.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open file %s: %s", imagefile.Name(), err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(path, ".png"):
|
||||||
|
name, image := ReadImage(imagefile, fd)
|
||||||
|
Registry[name] = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadImage(imagefile fs.DirEntry, fd fs.File) (string, *ebiten.Image) {
|
||||||
|
name := strings.TrimSuffix(imagefile.Name(), ".png")
|
||||||
|
|
||||||
|
img, _, err := image.Decode(fd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to decode image %s: %s", imagefile.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
image := ebiten.NewImageFromImage(img)
|
||||||
|
|
||||||
|
return name, image
|
||||||
|
}
|
||||||
34
main.go
34
main.go
@@ -4,33 +4,39 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/pprof"
|
||||||
|
|
||||||
|
_ "net/http/pprof"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := ParseCommandline()
|
config, err := ParseCommandline()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if config.ShowVersion {
|
if config.ShowVersion {
|
||||||
fmt.Printf("This is golsky version %s\n", VERSION)
|
fmt.Printf("This is golsky version %s\n", VERSION)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// grid := [][]int64{
|
|
||||||
// {0, 1, 1},
|
|
||||||
// {0, 1, 0},
|
|
||||||
// {1, 1, 0},
|
|
||||||
// }
|
|
||||||
|
|
||||||
// err := rle.StoreGridToRLE(grid, "test.rle", "B3/S23", 3, 3)
|
|
||||||
// if err != nil {
|
|
||||||
// panic(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// os.Exit(0)
|
|
||||||
|
|
||||||
game := NewGame(config, Play)
|
game := NewGame(config, Play)
|
||||||
|
|
||||||
|
if config.ProfileFile != "" {
|
||||||
|
// enable cpu profiling. Do NOT use q to stop the game but
|
||||||
|
// close the window to get a profile
|
||||||
|
fd, err := os.Create(config.ProfileFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
pprof.StartCPUProfile(fd)
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
}
|
||||||
|
|
||||||
// main loop
|
// main loop
|
||||||
if err := ebiten.RunGame(game); err != nil {
|
if err := ebiten.RunGame(game); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
19
rle/rle.go
19
rle/rle.go
@@ -20,6 +20,25 @@ type RLE struct {
|
|||||||
patternLineIndex int
|
patternLineIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapper to load a RLE file
|
||||||
|
func GetRLE(filename string) (*RLE, error) {
|
||||||
|
if filename == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedRle, err := Parse(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load RLE pattern file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsedRle, nil
|
||||||
|
}
|
||||||
|
|
||||||
func Parse(input string) (RLE, error) {
|
func Parse(input string) (RLE, error) {
|
||||||
rle := RLE{
|
rle := RLE{
|
||||||
inputLines: strings.Split(input, "\n"),
|
inputLines: strings.Split(input, "\n"),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type ScenePlay struct {
|
|||||||
TicksElapsed int // tick counter for game speed
|
TicksElapsed int // tick counter for game speed
|
||||||
Tiles Images // pre-computed tiles for dead and alife cells
|
Tiles Images // pre-computed tiles for dead and alife cells
|
||||||
Camera Camera // for zoom+move
|
Camera Camera // for zoom+move
|
||||||
World *ebiten.Image // actual image we render to
|
World, Cache *ebiten.Image // actual image we render to
|
||||||
WheelTurned bool // when user turns wheel multiple times, zoom faster
|
WheelTurned bool // when user turns wheel multiple times, zoom faster
|
||||||
Dragging bool // middle mouse is pressed, move canvas
|
Dragging bool // middle mouse is pressed, move canvas
|
||||||
LastCursorPos []int // used to check if the user is dragging
|
LastCursorPos []int // used to check if the user is dragging
|
||||||
@@ -184,14 +184,14 @@ func (scene *ScenePlay) CheckInput() {
|
|||||||
scene.Paused = true // drawing while running makes no sense
|
scene.Paused = true // drawing while running makes no sense
|
||||||
}
|
}
|
||||||
|
|
||||||
if ebiten.IsKeyPressed(ebiten.KeyPageDown) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
|
||||||
if scene.Config.TPG < 120 {
|
if scene.TPG < 120 {
|
||||||
scene.Config.TPG++
|
scene.TPG++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ebiten.IsKeyPressed(ebiten.KeyPageUp) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
|
||||||
if scene.TPG > 1 {
|
if scene.TPG >= 1 {
|
||||||
scene.TPG--
|
scene.TPG--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,25 +258,9 @@ func (scene *ScenePlay) CheckDraggingInput() {
|
|||||||
|
|
||||||
// Zoom
|
// Zoom
|
||||||
_, dy := ebiten.Wheel()
|
_, dy := ebiten.Wheel()
|
||||||
step := 1
|
|
||||||
|
|
||||||
if scene.WheelTurned {
|
if dy != 0 {
|
||||||
// if keep scrolling the wheel, zoom faster
|
scene.Camera.ZoomFactor += (int(dy) * 5)
|
||||||
step = 50
|
|
||||||
} else {
|
|
||||||
scene.WheelTurned = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if dy < 0 {
|
|
||||||
if scene.Camera.ZoomFactor > -2400 {
|
|
||||||
scene.Camera.ZoomFactor -= step
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dy > 0 {
|
|
||||||
if scene.Camera.ZoomFactor < 2400 {
|
|
||||||
scene.Camera.ZoomFactor += step
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
@@ -411,11 +395,8 @@ 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{}
|
||||||
|
|
||||||
if scene.Config.NoGrid {
|
op.GeoM.Translate(0, 0)
|
||||||
scene.World.Fill(scene.White)
|
scene.World.DrawImage(scene.Cache, op)
|
||||||
} else {
|
|
||||||
scene.World.Fill(scene.Grey)
|
|
||||||
}
|
|
||||||
|
|
||||||
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++ {
|
||||||
@@ -428,6 +409,7 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
|
|||||||
case 1:
|
case 1:
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -443,8 +425,6 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
|
|||||||
default:
|
default:
|
||||||
scene.World.DrawImage(scene.Tiles.Age4, op)
|
scene.World.DrawImage(scene.Tiles.Age4, op)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
scene.World.DrawImage(scene.Tiles.White, op)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,11 +455,11 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
|
|||||||
paused = "-- paused --"
|
paused = "-- paused --"
|
||||||
}
|
}
|
||||||
|
|
||||||
ebitenutil.DebugPrint(
|
debug := fmt.Sprintf("FPS: %0.2f, TPG: %d, Mem: %0.2f MB, Generations: %d %s",
|
||||||
screen,
|
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, paused)
|
||||||
fmt.Sprintf("FPS: %0.2f, TPG: %d, Mem: %0.2f MB, Generations: %d %s",
|
|
||||||
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, paused),
|
ebitenutil.DebugPrint(screen, debug)
|
||||||
)
|
fmt.Println(debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +485,25 @@ func (scene *ScenePlay) InitPattern() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (scene *ScenePlay) InitCache() {
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
|
||||||
|
if scene.Config.NoGrid {
|
||||||
|
scene.Cache.Fill(scene.White)
|
||||||
|
} else {
|
||||||
|
scene.Cache.Fill(scene.Grey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for y := 0; y < scene.Config.Height; y++ {
|
||||||
|
for x := 0; x < scene.Config.Width; x++ {
|
||||||
|
op.GeoM.Reset()
|
||||||
|
op.GeoM.Translate(float64(x*scene.Config.Cellsize), float64(y*scene.Config.Cellsize))
|
||||||
|
|
||||||
|
scene.Cache.DrawImage(scene.Tiles.White, op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (scene *ScenePlay) InitGrid(grid *Grid) {
|
func (scene *ScenePlay) InitGrid(grid *Grid) {
|
||||||
if grid != nil {
|
if grid != nil {
|
||||||
// use pre-loaded grid
|
// use pre-loaded grid
|
||||||
@@ -599,10 +598,12 @@ func (scene *ScenePlay) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scene.World = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight)
|
scene.World = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight)
|
||||||
|
scene.Cache = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight)
|
||||||
|
|
||||||
|
scene.InitTiles()
|
||||||
|
scene.InitCache()
|
||||||
scene.InitGrid(grid)
|
scene.InitGrid(grid)
|
||||||
scene.InitPattern()
|
scene.InitPattern()
|
||||||
scene.InitTiles()
|
|
||||||
|
|
||||||
scene.Index = 0
|
scene.Index = 0
|
||||||
scene.TicksElapsed = 0
|
scene.TicksElapsed = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user