commit 38794017c71a664440f4a09554c199f0187e7b71
parent 80b94934ca998277ca99b7757525f1ad1c37b96b
Author: Brian C. Lane <bcl@brianlane.com>
Date:   Thu, 24 Nov 2022 11:09:34 -0800
Move pattern loading to functions on LifeGame
This will allow them to modify the existing cells.
Also fixes a bug in the life 1.05 loader, when I moved to loading
everything into a []string I apparently only tested cells, not 1.05
formatted files.
Also fixed wrapping at the edges when loading patterns. Previously it
would panic with a negative cell index.
This also moves cell initialization into InitializeCells so that the
pattern loader can be used without resetting the world.
Diffstat:
| M | main.go | | | 97 | +++++++++++++++++++++++++++++++++---------------------------------------------- | 
1 file changed, 40 insertions(+), 57 deletions(-)
diff --git a/main.go b/main.go
@@ -125,6 +125,15 @@ func (g *LifeGame) cleanup() {
 func (g *LifeGame) InitializeCells() {
 	g.age = 0
 
+	// Fill it with dead cells first
+	g.cells = make([][]*Cell, cfg.Rows, cfg.Columns)
+	for y := 0; y < cfg.Rows; y++ {
+		for x := 0; x < cfg.Columns; x++ {
+			c := &Cell{x: x, y: y}
+			g.cells[y] = append(g.cells[y], c)
+		}
+	}
+
 	if len(cfg.PatternFile) == 0 {
 		g.InitializeRandomCells()
 	} else {
@@ -146,11 +155,11 @@ func (g *LifeGame) InitializeCells() {
 		}
 
 		if strings.HasPrefix(lines[0], "#Life 1.05") {
-			g.cells, err = ParseLife105(lines)
+			err = g.ParseLife105(lines)
 		} else if strings.HasPrefix(lines[0], "#Life 1.06") {
 			log.Fatal("Life 1.06 file format is not supported")
 		} else {
-			g.cells, err = ParsePlaintext(lines)
+			err = g.ParsePlaintext(lines)
 		}
 
 		if err != nil {
@@ -179,39 +188,24 @@ func (g *LifeGame) InitializeRandomCells() {
 		rand.Seed(cfg.Seed)
 	}
 
-	g.cells = make([][]*Cell, cfg.Rows, cfg.Columns)
 	for y := 0; y < cfg.Rows; y++ {
 		for x := 0; x < cfg.Columns; x++ {
-			c := &Cell{x: x, y: y}
-			c.alive = rand.Float64() < threshold
-			c.aliveNext = c.alive
-
-			g.cells[y] = append(g.cells[y], c)
+			g.cells[y][x].alive = rand.Float64() < threshold
+			g.cells[y][x].aliveNext = g.cells[y][x].alive
 		}
 	}
 }
 
 // ParseLife105 pattern file
-// The header has already been read from the buffer when this is called
 // #D Descriptions lines (0+)
 // #R Rule line (0/1)
 // #P -1 4 (Upper left corner, required, center is 0,0)
 // The pattern is . for dead and * for live
-func ParseLife105(lines []string) ([][]*Cell, error) {
-	cells := make([][]*Cell, cfg.Rows, cfg.Columns)
-
-	// Fill it with dead cells first
-	for y := 0; y < cfg.Rows; y++ {
-		for x := 0; x < cfg.Columns; x++ {
-			c := &Cell{x: x, y: y}
-			cells[y] = append(cells[y], c)
-		}
-	}
-
+func (g *LifeGame) ParseLife105(lines []string) error {
 	var x, y int
 	var err error
 	for _, line := range lines {
-		if strings.HasPrefix(line, "#D") {
+		if strings.HasPrefix(line, "#D") || strings.HasPrefix(line, "#Life") {
 			continue
 		} else if strings.HasPrefix(line, "#N") {
 			// Use default rules (from the cmdline in this case)
@@ -223,21 +217,21 @@ func ParseLife105(lines []string) ([][]*Cell, error) {
 
 			// Make sure the rule has a / in it
 			if !strings.Contains(line, "/") {
-				return nil, fmt.Errorf("ERROR: Rule must contain /")
+				return fmt.Errorf("ERROR: Rule must contain /")
 			}
 
 			fields := strings.Split(line[3:], "/")
 			if len(fields) != 2 {
-				return nil, fmt.Errorf("ERROR: Problem splitting rule on /")
+				return fmt.Errorf("ERROR: Problem splitting rule on /")
 			}
 
 			var stay, birth int
 			if stay, err = strconv.Atoi(fields[0]); err != nil {
-				return nil, fmt.Errorf("Error parsing alive value: %s", err)
+				return fmt.Errorf("Error parsing alive value: %s", err)
 			}
 
 			if birth, err = strconv.Atoi(fields[1]); err != nil {
-				return nil, fmt.Errorf("Error parsing birth value: %s", err)
+				return fmt.Errorf("Error parsing birth value: %s", err)
 			}
 
 			cfg.Rule = fmt.Sprintf("B%d/S%d", birth, stay)
@@ -245,52 +239,43 @@ func ParseLife105(lines []string) ([][]*Cell, error) {
 			// Initial position
 			fields := strings.Split(line, " ")
 			if len(fields) != 3 {
-				return nil, fmt.Errorf("Cannot parse position line: %s", line)
+				return fmt.Errorf("Cannot parse position line: %s", line)
 			}
 			if y, err = strconv.Atoi(fields[1]); err != nil {
-				return nil, fmt.Errorf("Error parsing position: %s", err)
+				return fmt.Errorf("Error parsing position: %s", err)
 			}
 			if x, err = strconv.Atoi(fields[2]); err != nil {
-				return nil, fmt.Errorf("Error parsing position: %s", err)
+				return fmt.Errorf("Error parsing position: %s", err)
 			}
 
-			// Move x, y to center of field
-			x = x + cfg.Columns/2
-			y = y + cfg.Rows/2
+			// Move x, y to center of field and wrap at the edges
+			// NOTE: % in go preserves sign of a, unlike Python :)
+			x = cfg.Columns/2 + x
+			x = (x%cfg.Columns + cfg.Columns) % cfg.Columns
+			y = cfg.Rows/2 + y
+			y = (y%cfg.Rows + cfg.Rows) % cfg.Rows
 		} else {
 			// Parse the line, error if it isn't . or *
 			xLine := x
 			for _, c := range line {
 				if c != '.' && c != '*' {
-					return nil, fmt.Errorf("Illegal characters in pattern: %s", line)
+					return fmt.Errorf("Illegal characters in pattern: %s", line)
 				}
-				if c == '*' {
-					cells[y][xLine].alive = true
-					cells[y][xLine].aliveNext = true
-				}
-				xLine = xLine + 1
+				g.cells[y][xLine].alive = bool(c == '*')
+				g.cells[y][xLine].aliveNext = bool(c == '*')
+				xLine = (xLine + 1) % cfg.Columns
 			}
-			y = y + 1
+			y = (y + 1) % cfg.Rows
 		}
 	}
-	return cells, nil
+	return nil
 }
 
 // ParsePlaintext pattern file
 // The header has already been read from the buffer when this is called
 // This is a bit more generic than the spec, skip lines starting with !
 // and assume the pattern is . for dead cells any anything else for live.
-func ParsePlaintext(lines []string) ([][]*Cell, error) {
-	cells := make([][]*Cell, cfg.Rows, cfg.Columns)
-
-	// Fill it with dead cells first
-	for y := 0; y < cfg.Rows; y++ {
-		for x := 0; x < cfg.Columns; x++ {
-			c := &Cell{x: x, y: y}
-			cells[y] = append(cells[y], c)
-		}
-	}
-
+func (g *LifeGame) ParsePlaintext(lines []string) error {
 	var x, y int
 
 	// Move x, y to center of field
@@ -304,17 +289,15 @@ func ParsePlaintext(lines []string) ([][]*Cell, error) {
 			// Parse the line, . is dead, anything else is alive.
 			xLine := x
 			for _, c := range line {
-				if c != '.' {
-					cells[y][xLine].alive = true
-					cells[y][xLine].aliveNext = true
-				}
-				xLine = xLine + 1
+				g.cells[y][xLine].alive = bool(c != '.')
+				g.cells[y][xLine].aliveNext = bool(c != '.')
+				xLine = (xLine + 1) % cfg.Columns
 			}
-			y = y + 1
+			y = (y + 1) % cfg.Rows
 		}
 	}
 
-	return cells, nil
+	return nil
 }
 
 // checkState determines the state of the cell for the next tick of the game.