2025-11-13 21:30:44 +01:00
|
|
|
package cmd
|
2024-05-26 12:29:43 +02:00
|
|
|
|
|
|
|
|
import (
|
2024-05-27 13:38:14 +02:00
|
|
|
"errors"
|
2024-05-26 12:29:43 +02:00
|
|
|
"fmt"
|
2024-06-02 19:42:06 +02:00
|
|
|
"math"
|
2024-05-26 13:08:36 +02:00
|
|
|
"os"
|
2024-05-27 13:38:14 +02:00
|
|
|
"runtime/pprof"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2024-05-26 12:29:43 +02:00
|
|
|
|
|
|
|
|
"github.com/spf13/pflag"
|
2025-11-13 21:30:44 +01:00
|
|
|
"codeberg.org/scip/golsky/rle"
|
2024-05-26 12:29:43 +02:00
|
|
|
)
|
|
|
|
|
|
2024-05-26 12:33:16 +02:00
|
|
|
// all the settings comming from commandline, but maybe tweaked later from the UI
|
2024-05-26 12:29:43 +02:00
|
|
|
type Config struct {
|
2024-06-08 16:19:54 +02:00
|
|
|
Width, Height, Cellsize, Density int // measurements
|
|
|
|
|
ScreenWidth, ScreenHeight int
|
|
|
|
|
TPG int // ticks per generation/game speed, 1==max
|
|
|
|
|
Debug, Empty, Paused, Markmode, Drawmode bool // game modi
|
|
|
|
|
ShowEvolution, ShowGrid, RunOneStep bool // flags
|
|
|
|
|
Rule *Rule // which rule to use, default: B3/S23
|
|
|
|
|
RLE *rle.RLE // loaded GOL pattern from RLE file
|
|
|
|
|
Statefile string // load game state from it if non-nil
|
|
|
|
|
StateGrid *Grid // a grid from a statefile
|
|
|
|
|
Wrap bool // wether wraparound mode is in place or not
|
|
|
|
|
ShowVersion bool
|
|
|
|
|
UseShader bool // to use a shader to render alife cells
|
|
|
|
|
Restart, RestartGrid, RestartCache bool
|
|
|
|
|
StartWithMenu bool
|
|
|
|
|
Zoomfactor int
|
|
|
|
|
ZoomOutFactor int
|
|
|
|
|
InitialCamPos []float64
|
|
|
|
|
DelayedStart bool // if true game, we wait. like pause but program induced
|
|
|
|
|
Theme string
|
|
|
|
|
ThemeManager ThemeManager
|
2024-05-27 13:38:14 +02:00
|
|
|
|
|
|
|
|
// for internal profiling
|
|
|
|
|
ProfileFile string
|
|
|
|
|
ProfileDraw bool
|
|
|
|
|
ProfileMaxLoops int64
|
2024-05-26 12:29:43 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-26 13:08:36 +02:00
|
|
|
const (
|
2024-07-12 23:01:46 +02:00
|
|
|
VERSION = "v0.0.9"
|
2024-06-15 12:06:17 +02:00
|
|
|
Alive = 1
|
|
|
|
|
Dead = 0
|
2024-05-31 14:19:30 +02:00
|
|
|
|
2024-06-01 20:22:28 +02:00
|
|
|
DEFAULT_GRID_WIDTH = 600
|
|
|
|
|
DEFAULT_GRID_HEIGHT = 400
|
|
|
|
|
DEFAULT_CELLSIZE = 4
|
2024-06-08 16:29:09 +02:00
|
|
|
DEFAULT_ZOOMFACTOR = 400
|
2024-06-01 20:22:28 +02:00
|
|
|
DEFAULT_GEOM = "640x384"
|
2024-07-13 19:31:25 +02:00
|
|
|
DEFAULT_THEME = "standard"
|
2024-05-26 13:08:36 +02:00
|
|
|
)
|
|
|
|
|
|
2024-06-06 19:55:16 +02:00
|
|
|
const KEYBINDINGS string = `
|
|
|
|
|
- SPACE: pause or resume the game
|
|
|
|
|
- N: while game is paused: forward one step
|
|
|
|
|
- PAGE UP: speed up
|
|
|
|
|
- PAGE DOWN: slow down
|
|
|
|
|
- MOUSE WHEEL: zoom in or out
|
|
|
|
|
- LEFT MOUSE BUTTON: use to drag canvas, keep clicked and move mouse
|
2024-07-13 19:31:25 +02:00
|
|
|
- I: enter "insert" (draw) mode: use left mouse to toggle a cells alife state.
|
|
|
|
|
Leave with insert mode with "space". While in insert mode, use middle mouse
|
|
|
|
|
button to drag the grid.
|
2024-06-06 19:55:16 +02:00
|
|
|
- R: reset to 1:1 zoom
|
|
|
|
|
- ESCAPE: open menu, o: open options menu
|
|
|
|
|
- S: save game state to file (can be loaded with -l)
|
|
|
|
|
- C: enter mark mode. Mark a rectangle with the mouse, when you
|
|
|
|
|
release the mouse buttonx it is being saved to an RLE file
|
|
|
|
|
- D: toggle debug output
|
|
|
|
|
- Q: quit game
|
|
|
|
|
`
|
|
|
|
|
|
2024-06-03 17:44:17 +02:00
|
|
|
func (config *Config) SetupCamera() {
|
2024-06-08 16:29:09 +02:00
|
|
|
config.Zoomfactor = DEFAULT_ZOOMFACTOR / config.Cellsize
|
2024-06-03 17:44:17 +02:00
|
|
|
|
|
|
|
|
// 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])
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-04 18:46:59 +02:00
|
|
|
// same for Y
|
|
|
|
|
config.InitialCamPos[1] = float64(((config.ScreenHeight - (config.Height * config.Cellsize)) / 2) * -1)
|
2024-06-03 17:44:17 +02:00
|
|
|
if config.Height*config.Cellsize > config.ScreenHeight {
|
2024-06-04 18:46:59 +02:00
|
|
|
config.InitialCamPos[1] = math.Abs(config.InitialCamPos[1])
|
2024-06-03 17:44:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2024-06-04 18:46:59 +02:00
|
|
|
// width instead of height
|
2024-06-03 17:44:17 +02:00
|
|
|
config.ZoomOutFactor = int(
|
|
|
|
|
math.Log(float64(config.ScreenHeight)/(float64(config.Height)*float64(config.Cellsize))) /
|
|
|
|
|
math.Log(1.01))
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
// parse given window geometry and adjust game settings according to it
|
|
|
|
|
func (config *Config) ParseGeom(geom string) error {
|
|
|
|
|
// 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])
|
2024-05-26 13:08:36 +02:00
|
|
|
if err != nil {
|
2024-05-27 13:38:14 +02:00
|
|
|
return errors.New("failed to parse height, expecting integer")
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 14:19:30 +02:00
|
|
|
config.ScreenWidth = width
|
|
|
|
|
config.ScreenHeight = height
|
|
|
|
|
|
2024-06-08 16:29:09 +02:00
|
|
|
//config.Cellsize = DEFAULT_CELLSIZE
|
2024-06-02 19:01:40 +02:00
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-04 14:09:40 +02:00
|
|
|
// check if we have been given an RLE or LIF file to load, then load
|
|
|
|
|
// it and adjust game settings accordingly
|
2024-05-27 13:38:14 +02:00
|
|
|
func (config *Config) ParseRLE(rlefile string) error {
|
|
|
|
|
if rlefile == "" {
|
|
|
|
|
return nil
|
2024-05-26 13:08:36 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-04 14:09:40 +02:00
|
|
|
var rleobj *rle.RLE
|
|
|
|
|
|
|
|
|
|
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
|
2024-05-27 13:38:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if rleobj == nil {
|
2024-06-04 14:09:40 +02:00
|
|
|
return errors.New("failed to load pattern file (uncatched module error)")
|
2024-05-27 13:38:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2024-05-26 13:08:36 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-01 20:22:28 +02:00
|
|
|
fmt.Printf("width: %d, screenwidth: %d, rlewidth: %d, cellsize: %d\n",
|
|
|
|
|
config.Width, config.ScreenWidth, config.RLE.Width, config.Cellsize)
|
|
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
// 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
|
2024-05-26 13:08:36 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
func (config *Config) EnableCPUProfiling(filename string) error {
|
|
|
|
|
if filename == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fd, err := os.Create(filename)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pprof.StartCPUProfile(fd)
|
|
|
|
|
defer pprof.StopCPUProfile()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ParseCommandline() (*Config, error) {
|
2024-05-26 12:29:43 +02:00
|
|
|
config := Config{}
|
|
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
var (
|
|
|
|
|
rule, rlefile, geom string
|
|
|
|
|
)
|
2024-05-26 12:29:43 +02:00
|
|
|
|
|
|
|
|
// commandline params, most configure directly config flags
|
2024-06-01 20:22:28 +02:00
|
|
|
pflag.IntVarP(&config.Width, "width", "W", DEFAULT_GRID_WIDTH, "grid width in cells")
|
|
|
|
|
pflag.IntVarP(&config.Height, "height", "H", DEFAULT_GRID_HEIGHT, "grid height in cells")
|
2024-05-26 12:29:43 +02:00
|
|
|
pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels")
|
2024-05-31 14:19:30 +02:00
|
|
|
pflag.StringVarP(&geom, "geom", "G", DEFAULT_GEOM, "window geometry in WxH in pixels, overturns -c")
|
2024-05-27 13:38:14 +02:00
|
|
|
|
2024-05-26 12:29:43 +02:00
|
|
|
pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells")
|
|
|
|
|
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
|
|
|
|
|
"game speed: the higher the slower (default: 10)")
|
|
|
|
|
|
|
|
|
|
pflag.StringVarP(&rule, "rule", "r", "B3/S23", "game rule")
|
2024-06-04 14:09:40 +02:00
|
|
|
pflag.StringVarP(&rlefile, "pattern-file", "f", "", "RLE or LIF pattern file")
|
2024-05-26 12:29:43 +02:00
|
|
|
|
2024-05-26 12:33:16 +02:00
|
|
|
pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version")
|
2024-06-08 19:52:20 +02:00
|
|
|
pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", false, "draw grid lines")
|
|
|
|
|
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution traces")
|
|
|
|
|
|
2024-05-26 12:29:43 +02:00
|
|
|
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.Empty, "empty", "e", false, "start with an empty screen")
|
2024-06-07 17:27:08 +02:00
|
|
|
|
|
|
|
|
// style
|
2024-06-08 16:19:54 +02:00
|
|
|
pflag.StringVarP(&config.Theme, "theme", "T", DEFAULT_THEME, "color theme: standard, dark, light (default: standard)")
|
2024-06-07 17:27:08 +02:00
|
|
|
|
2024-05-26 12:29:43 +02:00
|
|
|
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode")
|
2024-05-28 13:07:36 +02:00
|
|
|
pflag.BoolVarP(&config.UseShader, "use-shader", "k", false, "use shader for cell rendering")
|
2024-05-26 12:29:43 +02:00
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling")
|
|
|
|
|
|
2024-05-26 12:29:43 +02:00
|
|
|
pflag.Parse()
|
|
|
|
|
|
2024-05-27 13:38:14 +02:00
|
|
|
err := config.ParseGeom(geom)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = config.ParseRLE(rlefile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-05-26 12:29:43 +02:00
|
|
|
|
|
|
|
|
// load rule from commandline when no rule came from RLE file,
|
|
|
|
|
// default is B3/S23, aka conways game of life
|
|
|
|
|
if config.Rule == nil {
|
|
|
|
|
config.Rule = ParseGameRule(rule)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-03 17:44:17 +02:00
|
|
|
config.SetupCamera()
|
|
|
|
|
|
2024-06-07 17:27:08 +02:00
|
|
|
config.ThemeManager = NewThemeManager(config.Theme, config.Cellsize)
|
|
|
|
|
|
2024-06-01 20:22:28 +02:00
|
|
|
//repr.Println(config)
|
2024-05-27 13:38:14 +02:00
|
|
|
return &config, nil
|
2024-05-26 12:29:43 +02:00
|
|
|
}
|
2024-05-30 12:32:58 +02:00
|
|
|
|
|
|
|
|
func (config *Config) TogglePaused() {
|
|
|
|
|
config.Paused = !config.Paused
|
|
|
|
|
}
|
2024-05-30 19:45:13 +02:00
|
|
|
|
|
|
|
|
func (config *Config) ToggleDebugging() {
|
|
|
|
|
config.Debug = !config.Debug
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-07 17:27:08 +02:00
|
|
|
func (config *Config) SwitchTheme(theme string) {
|
|
|
|
|
config.ThemeManager.SetCurrentTheme(theme)
|
2024-06-09 18:00:06 +02:00
|
|
|
config.RestartCache = true
|
2024-06-07 17:27:08 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-30 19:45:13 +02:00
|
|
|
func (config *Config) ToggleGridlines() {
|
2024-05-31 14:19:30 +02:00
|
|
|
config.ShowGrid = !config.ShowGrid
|
|
|
|
|
config.RestartCache = true
|
2024-05-30 19:45:13 +02:00
|
|
|
}
|
2024-06-03 18:38:18 +02:00
|
|
|
|
|
|
|
|
func (config *Config) ToggleEvolution() {
|
|
|
|
|
config.ShowEvolution = !config.ShowEvolution
|
|
|
|
|
}
|
2024-06-04 18:50:29 +02:00
|
|
|
|
|
|
|
|
func (config *Config) ToggleWrap() {
|
|
|
|
|
config.Wrap = !config.Wrap
|
|
|
|
|
}
|