From 4b38bea5db05c939692b095f82d4b5c646401fa7 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Sun, 26 May 2024 20:26:13 +0200 Subject: [PATCH] completed save rect to RLE file feature --- README.md | 3 ++ grid.go | 5 +++ main.go | 13 +++++++ rle/rle.go | 80 ++++++++++++++++++++++++++++++++++++++++-- rule.go | 7 ++-- sample-rles/glider.rle | 2 ++ scene-play.go | 55 +++++++++++++++++++++++++++++ 7 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 sample-rles/glider.rle diff --git a/README.md b/README.md index e01b930..0f24f7f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Based on: https://youtu.be/FWSR_7kZuYg?si=ix1dmo76D8AmF25F * you can paint your own patterns in the game * the game can also be started with an empty grid, which is easier to paint patterns * wrap around grid mode can be enabled +* you can also save rectangles of the grid to RLE files # Install @@ -78,6 +79,8 @@ While it runs, there are a couple of commands you can use: * move mouse while middle mouse button pressed: move canvas * escape: reset to 1:1 zoom * s: save game state to file (can be loaded with -l) +* c: enter copy mode. Mark a rectangle with the mouse, when you + release the mous button it is being saved to an RLE file * q: quit # Report bugs diff --git a/grid.go b/grid.go index a20de67..5d3eede 100644 --- a/grid.go +++ b/grid.go @@ -69,6 +69,11 @@ func GetFilename(generations int64) string { return fmt.Sprintf("dump-%s-%d.gol", now.Format("20060102150405"), generations) } +func GetFilenameRLE(generations int64) string { + now := time.Now() + return fmt.Sprintf("rect-%s-%d.rle", now.Format("20060102150405"), generations) +} + func (grid *Grid) SaveState(filename string) error { file, err := os.Create(filename) if err != nil { diff --git a/main.go b/main.go index a940993..cdb6864 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,19 @@ func main() { 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) // main loop diff --git a/rle/rle.go b/rle/rle.go index a44e241..ac81d5a 100644 --- a/rle/rle.go +++ b/rle/rle.go @@ -1,8 +1,9 @@ -// source: https://github.com/nhoffmann/life by N.Hoffmann 2020. +// original source: https://github.com/nhoffmann/life by N.Hoffmann 2020. package rle import ( "fmt" + "os" "regexp" "strconv" "strings" @@ -52,7 +53,7 @@ func (rle *RLE) partitionFile() error { } } - return fmt.Errorf("Invlaid input: Header is missing") + return fmt.Errorf("invalid input: Header is missing") } func (rle *RLE) parseComments() error { @@ -98,3 +99,78 @@ func removeWhitespace(input string) string { re := regexp.MustCompile(` *\t*\r*\n*`) return re.ReplaceAllString(input, "") } + +// Store a grid to an RLE file +func StoreGridToRLE(grid [][]int64, filename, rule string, width, height int) error { + fd, err := os.Create(filename) + if err != nil { + return err + } + + var pattern string + + for y := 0; y < height; y++ { + line := "" + for x := 0; x < width; x++ { + switch grid[y][x] { + case 0: + line += "b" + case 1: + line += "o" + } + } + + // if first row is: 001011110, then line is now: + // bboboooob + + encoded := RunLengthEncode(line) + + // and now its: 2bob4ob + pattern += encoded + + if y != height-1 { + pattern += "$" + } + } + + pattern += "!" + + wrapped := "" + for idx, char := range pattern { + if idx%70 == 0 && idx != 0 { + wrapped += "\n" + } + wrapped += string(char) + } + + _, err = fmt.Fprintf(fd, "#N %s\nx = %d, y = %d, rule = %s\n%s\n", + filename, width, height, rule, wrapped) + + if err != nil { + return err + } + + return nil +} + +// by peterSO on +// https://codereview.stackexchange.com/questions/238893/run-length-encoding-in-golang +func RunLengthEncode(s string) string { + e := make([]byte, 0, len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + j := i + 1 + for ; j <= len(s); j++ { + if j < len(s) && s[j] == c { + continue + } + if j-i > 1 { + e = strconv.AppendInt(e, int64(j-i), 10) + } + e = append(e, c) + break + } + i = j - 1 + } + return string(e) +} diff --git a/rule.go b/rule.go index bd09525..d2fab97 100644 --- a/rule.go +++ b/rule.go @@ -8,8 +8,9 @@ import ( // a GOL rule type Rule struct { - Birth []int64 - Death []int64 + Definition string + Birth []int64 + Death []int64 } // parse one part of a GOL rule into rule slice @@ -37,7 +38,7 @@ func ParseGameRule(rule string) *Rule { log.Fatalf("Invalid game rule <%s>", rule) } - golrule := &Rule{} + golrule := &Rule{Definition: rule} for _, part := range parts { if part[0] == 'B' { diff --git a/sample-rles/glider.rle b/sample-rles/glider.rle new file mode 100644 index 0000000..a15732f --- /dev/null +++ b/sample-rles/glider.rle @@ -0,0 +1,2 @@ +x = 3, y = 3, rule = B3/S23 +3o$2bo$bo! diff --git a/scene-play.go b/scene-play.go index ba824e8..1034170 100644 --- a/scene-play.go +++ b/scene-play.go @@ -12,6 +12,7 @@ import ( "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/vector" + "github.com/tlinden/golsky/rle" "golang.org/x/image/math/f64" ) @@ -310,6 +311,8 @@ func (scene *ScenePlay) CheckMarkInput() { scene.Markmode = false scene.MarkTaken = false scene.MarkDone = true + + scene.SaveRectRLE() } } @@ -322,6 +325,58 @@ func (scene *ScenePlay) SaveState() { log.Printf("saved game state to %s at generation %d\n", filename, scene.Generations) } +func (scene *ScenePlay) SaveRectRLE() { + filename := GetFilenameRLE(scene.Generations) + + if scene.Mark.X == scene.Point.X || scene.Mark.Y == scene.Point.Y { + log.Printf("can't save non-rectangle\n") + return + } + + var width int + var height int + var startx int + var starty int + + if scene.Mark.X < scene.Point.X { + // mark left point + startx = scene.Mark.X + width = scene.Point.X - scene.Mark.X + } else { + // mark right point + startx = scene.Point.X + width = scene.Mark.X - scene.Point.X + } + + if scene.Mark.Y < scene.Point.Y { + // mark above point + starty = scene.Mark.Y + height = scene.Point.Y - scene.Mark.Y + } else { + // mark below point + starty = scene.Point.Y + height = scene.Mark.Y - scene.Point.Y + } + + grid := make([][]int64, height) + + for y := 0; y < height; y++ { + grid[y] = make([]int64, width) + + for x := 0; x < width; x++ { + grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx] + } + } + + err := rle.StoreGridToRLE(grid, filename, scene.Config.Rule.Definition, width, height) + if err != nil { + log.Printf("failed to save rect to %s: %s\n", filename, err) + } else { + log.Printf("saved selected rect to %s at generation %d\n", filename, scene.Generations) + } + +} + func (scene *ScenePlay) Update() error { scene.CheckInput() scene.CheckDraggingInput()