main.go (30028B)
1 // sdl2-life 2 // by Brian C. Lane <bcl@brianlane.com> 3 package main 4 5 import ( 6 "bufio" 7 "flag" 8 "fmt" 9 "log" 10 "math" 11 "math/rand" 12 "net/http" 13 "os" 14 "regexp" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/veandco/go-sdl2/sdl" 20 "github.com/veandco/go-sdl2/ttf" 21 ) 22 23 const ( 24 threshold = 0.15 25 // LinearGradient cmdline selection 26 LinearGradient = 0 27 // PolylinearGradient cmdline selection 28 PolylinearGradient = 1 29 // BezierGradient cmdline selection 30 BezierGradient = 2 31 ) 32 33 // RLE header with variable spacing and optional rules 34 // Matches it with or without rule at the end, and with 0 or more spaces between elements. 35 var rleHeaderRegex = regexp.MustCompile(`x\s*=\s*(\d+)\s*,\s*y\s*=\s*(\d+)(?:\s*,\s*rule\s*=\s*(.*))*`) 36 37 /* commandline flags */ 38 type cmdlineArgs struct { 39 Width int // Width of window in pixels 40 Height int // Height of window in pixels 41 Rows int // Number of cell rows 42 Columns int // Number of cell columns 43 Seed int64 // Seed for PRNG 44 Border bool // Border around cells 45 Font string // Path to TTF to use for status bar 46 FontSize int // Size of font in points 47 Rule string // Rulestring to use 48 Fps int // Frames per Second 49 PatternFile string // File with initial pattern 50 Pause bool // Start the game paused 51 Empty bool // Start with empty world 52 Color bool // Color the cells based on age 53 Colors string // Comma separated color hex triplets 54 Gradient int // Gradient algorithm to use 55 MaxAge int // Maximum age for gradient colors 56 Port int // Port to listen to 57 Host string // Host IP to bind to 58 Server bool // Launch an API server when true 59 } 60 61 /* commandline defaults */ 62 var cfg = cmdlineArgs{ 63 Width: 500, 64 Height: 500, 65 Rows: 100, 66 Columns: 100, 67 Seed: 0, 68 Border: false, 69 Font: "", 70 FontSize: 14, 71 Rule: "B3/S23", 72 Fps: 10, 73 PatternFile: "", 74 Pause: false, 75 Empty: false, 76 Color: false, 77 Colors: "#4682b4,#ffffff", 78 Gradient: 0, 79 MaxAge: 255, 80 Port: 3051, 81 Host: "127.0.0.1", 82 Server: false, 83 } 84 85 /* parseArgs handles parsing the cmdline args and setting values in the global cfg struct */ 86 func parseArgs() { 87 flag.IntVar(&cfg.Width, "width", cfg.Width, "Width of window in pixels") 88 flag.IntVar(&cfg.Height, "height", cfg.Height, "Height of window in pixels") 89 flag.IntVar(&cfg.Rows, "rows", cfg.Rows, "Number of cell rows") 90 flag.IntVar(&cfg.Columns, "columns", cfg.Columns, "Number of cell columns") 91 flag.Int64Var(&cfg.Seed, "seed", cfg.Seed, "PRNG seed") 92 flag.BoolVar(&cfg.Border, "border", cfg.Border, "Border around cells") 93 flag.StringVar(&cfg.Font, "font", cfg.Font, "Path to TTF to use for status bar") 94 flag.IntVar(&cfg.FontSize, "font-size", cfg.FontSize, "Size of font in points") 95 flag.StringVar(&cfg.Rule, "rule", cfg.Rule, "Rulestring Bn.../Sn... (B3/S23)") 96 flag.IntVar(&cfg.Fps, "fps", cfg.Fps, "Frames per Second update rate (10fps)") 97 flag.StringVar(&cfg.PatternFile, "pattern", cfg.PatternFile, "File with initial pattern to load") 98 flag.BoolVar(&cfg.Pause, "pause", cfg.Pause, "Start the game paused") 99 flag.BoolVar(&cfg.Empty, "empty", cfg.Empty, "Start with empty world") 100 flag.BoolVar(&cfg.Color, "color", cfg.Color, "Color cells based on age") 101 flag.StringVar(&cfg.Colors, "colors", cfg.Colors, "Comma separated color hex triplets") 102 flag.IntVar(&cfg.Gradient, "gradient", cfg.Gradient, "Gradient type. 0=Linear 1=Polylinear 2=Bezier") 103 flag.IntVar(&cfg.MaxAge, "age", cfg.MaxAge, "Maximum age for gradient colors") 104 flag.IntVar(&cfg.Port, "port", cfg.Port, "Port to listen to") 105 flag.StringVar(&cfg.Host, "host", cfg.Host, "Host IP to bind to") 106 flag.BoolVar(&cfg.Server, "server", cfg.Server, "Launch an API server") 107 108 flag.Parse() 109 } 110 111 // Possible default fonts to search for 112 var defaultFonts = []string{"/usr/share/fonts/liberation/LiberationMono-Regular.ttf", 113 "/usr/local/share/fonts/TerminusTTF/TerminusTTF-4.49.2.ttf", 114 "/usr/X11/share/fonts/TTF/LiberationMono-Regular.ttf"} 115 116 // RGBAColor holds a color 117 type RGBAColor struct { 118 r, g, b, a uint8 119 } 120 121 // Gradient holds the colors to use for displaying cell age 122 type Gradient struct { 123 controls []RGBAColor 124 points []RGBAColor 125 } 126 127 // Print prints the gradient values to the console 128 func (g *Gradient) Print() { 129 fmt.Printf("controls:\n%#v\n\n", g.controls) 130 for i := range g.points { 131 fmt.Printf("%d = %#v\n", i, g.points[i]) 132 } 133 } 134 135 // Append adds the points from a gradient to this one at an insertion point 136 func (g *Gradient) Append(from Gradient, start int) { 137 for i, p := range from.points { 138 g.points[start+i] = p 139 } 140 } 141 142 // NewLinearGradient returns a Linear Gradient with pre-computed colors for every age 143 // from https://bsouthga.dev/posts/color-gradients-with-python 144 // 145 // Only uses the first and last color passed in 146 func NewLinearGradient(colors []RGBAColor, maxAge int) (Gradient, error) { 147 if len(colors) < 1 { 148 return Gradient{}, fmt.Errorf("Linear Gradient requires at least 1 color") 149 } 150 151 // Use the first and last color in controls as start and end 152 gradient := Gradient{controls: []RGBAColor{colors[0], colors[len(colors)-1]}, points: make([]RGBAColor, maxAge)} 153 154 start := gradient.controls[0] 155 end := gradient.controls[1] 156 157 for t := 0; t < maxAge; t++ { 158 r := uint8(float64(start.r) + (float64(t)/float64(maxAge-1))*(float64(end.r)-float64(start.r))) 159 g := uint8(float64(start.g) + (float64(t)/float64(maxAge-1))*(float64(end.g)-float64(start.g))) 160 b := uint8(float64(start.b) + (float64(t)/float64(maxAge-1))*(float64(end.b)-float64(start.b))) 161 gradient.points[t] = RGBAColor{r, g, b, 255} 162 } 163 return gradient, nil 164 } 165 166 // NewPolylinearGradient returns a gradient that is linear between all control colors 167 func NewPolylinearGradient(colors []RGBAColor, maxAge int) (Gradient, error) { 168 if len(colors) < 2 { 169 return Gradient{}, fmt.Errorf("Polylinear Gradient requires at least 2 colors") 170 } 171 172 gradient := Gradient{controls: colors, points: make([]RGBAColor, maxAge)} 173 174 n := int(float64(maxAge) / float64(len(colors)-1)) 175 g, _ := NewLinearGradient(colors, n) 176 gradient.Append(g, 0) 177 178 if len(colors) == 2 { 179 return gradient, nil 180 } 181 182 for i := 1; i < len(colors)-1; i++ { 183 if i == len(colors)-2 { 184 // The last group may need to be extended if it doesn't fill all the way to the end 185 remainder := maxAge - ((i + 1) * n) 186 g, _ := NewLinearGradient(colors[i:i+1], n+remainder) 187 gradient.Append(g, (i * n)) 188 } else { 189 g, _ := NewLinearGradient(colors[i:i+1], n) 190 gradient.Append(g, (i * n)) 191 } 192 } 193 194 return gradient, nil 195 } 196 197 // FactorialCache saves the results for factorial calculations for faster access 198 type FactorialCache struct { 199 cache map[int]float64 200 } 201 202 // NewFactorialCache returns a new empty cache 203 func NewFactorialCache() *FactorialCache { 204 return &FactorialCache{cache: make(map[int]float64)} 205 } 206 207 // Fact calculates the n! and caches the results 208 func (fc *FactorialCache) Fact(n int) float64 { 209 f, ok := fc.cache[n] 210 if ok { 211 return f 212 } 213 var result float64 214 if n == 1 || n == 0 { 215 result = 1 216 } else { 217 result = float64(n) * fc.Fact(n-1) 218 } 219 220 fc.cache[n] = result 221 return result 222 } 223 224 // Bernstein calculates the bernstein coefficient 225 // 226 // t runs from 0 -> 1 and is the 'position' on the curve (age / maxAge-1) 227 // n is the number of control colors -1 228 // i is the current control color from 0 -> n 229 func (fc *FactorialCache) Bernstein(t float64, n, i int) float64 { 230 b := fc.Fact(n) / (fc.Fact(i) * fc.Fact(n-i)) 231 b = b * math.Pow(1-t, float64(n-i)) * math.Pow(t, float64(i)) 232 return b 233 } 234 235 // NewBezierGradient returns pre-computed colors using control colors and bezier curve 236 // from https://bsouthga.dev/posts/color-gradients-with-python 237 func NewBezierGradient(controls []RGBAColor, maxAge int) Gradient { 238 gradient := Gradient{controls: controls, points: make([]RGBAColor, maxAge)} 239 fc := NewFactorialCache() 240 241 for t := 0; t < maxAge; t++ { 242 color := RGBAColor{} 243 for i, c := range controls { 244 color.r += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.r)) 245 color.g += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.g)) 246 color.b += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.b)) 247 } 248 color.a = 255 249 gradient.points[t] = color 250 } 251 252 return gradient 253 } 254 255 // Cell describes the location and state of a cell 256 type Cell struct { 257 alive bool 258 aliveNext bool 259 260 x int 261 y int 262 263 age int 264 } 265 266 // Pattern is used to pass patterns from the API to the game 267 type Pattern []string 268 269 // LifeGame holds all the global state of the game and the methods to operate on it 270 type LifeGame struct { 271 mp bool 272 erase bool 273 cells [][]*Cell // NOTE: This is an array of [row][columns] not x,y coordinates 274 liveCells int 275 age int64 276 birth map[int]bool 277 stayAlive map[int]bool 278 279 // Graphics 280 window *sdl.Window 281 renderer *sdl.Renderer 282 font *ttf.Font 283 bg RGBAColor 284 fg RGBAColor 285 cellWidth int32 286 cellHeight int32 287 gradient Gradient 288 pChan <-chan Pattern 289 } 290 291 // cleanup will handle cleanup of allocated resources 292 func (g *LifeGame) cleanup() { 293 // Clean up all the allocated memory 294 295 g.renderer.Destroy() 296 g.window.Destroy() 297 g.font.Close() 298 ttf.Quit() 299 sdl.Quit() 300 } 301 302 // InitializeCells resets the world, either randomly or from a pattern file 303 func (g *LifeGame) InitializeCells() { 304 g.age = 0 305 306 // Fill it with dead cells first 307 g.cells = make([][]*Cell, cfg.Rows, cfg.Columns) 308 for y := 0; y < cfg.Rows; y++ { 309 for x := 0; x < cfg.Columns; x++ { 310 c := &Cell{x: x, y: y} 311 g.cells[y] = append(g.cells[y], c) 312 } 313 } 314 315 if len(cfg.PatternFile) > 0 { 316 // Read all of the pattern file for parsing 317 f, err := os.Open(cfg.PatternFile) 318 if err != nil { 319 log.Fatalf("Error reading pattern file: %s", err) 320 } 321 defer f.Close() 322 323 scanner := bufio.NewScanner(f) 324 var lines []string 325 for scanner.Scan() { 326 lines = append(lines, scanner.Text()) 327 } 328 if len(lines) == 0 { 329 log.Fatalf("%s is empty.", cfg.PatternFile) 330 } 331 332 if strings.HasPrefix(lines[0], "#Life 1.05") { 333 err = g.ParseLife105(lines) 334 } else if strings.HasPrefix(lines[0], "#Life 1.06") { 335 log.Fatal("Life 1.06 file format is not supported") 336 } else if isRLE(lines) { 337 err = g.ParseRLE(lines, 0, 0) 338 } else { 339 err = g.ParsePlaintext(lines) 340 } 341 342 if err != nil { 343 log.Fatalf("Error reading pattern file: %s", err) 344 } 345 } else if !cfg.Empty { 346 g.InitializeRandomCells() 347 } 348 349 var err error 350 if g.birth, g.stayAlive, err = ParseRulestring(cfg.Rule); err != nil { 351 log.Fatalf("Failed to parse the rule string (%s): %s\n", cfg.Rule, err) 352 } 353 354 // Draw initial world 355 g.Draw("") 356 } 357 358 // TranslateXY move the x, y coordinates so that 0, 0 is the center of the world 359 // and handle wrapping at the edges 360 func (g *LifeGame) TranslateXY(x, y int) (int, int) { 361 // Move x, y to center of field and wrap at the edges 362 // NOTE: % in go preserves sign of a, unlike Python :) 363 x = cfg.Columns/2 + x 364 x = (x%cfg.Columns + cfg.Columns) % cfg.Columns 365 y = cfg.Rows/2 + y 366 y = (y%cfg.Rows + cfg.Rows) % cfg.Rows 367 368 return x, y 369 } 370 371 // SetCellState sets the cell alive state 372 // it also wraps the x and y at the edges and returns the new value 373 func (g *LifeGame) SetCellState(x, y int, alive bool) (int, int) { 374 x = x % cfg.Columns 375 y = y % cfg.Rows 376 g.cells[y][x].alive = alive 377 g.cells[y][x].aliveNext = alive 378 379 if !alive { 380 g.cells[y][x].age = 0 381 } 382 383 return x, y 384 } 385 386 // PrintCellDetails prints the details for a cell, located by the window coordinates x, y 387 func (g *LifeGame) PrintCellDetails(x, y int32) { 388 cellX := int(x / g.cellWidth) 389 cellY := int(y / g.cellHeight) 390 391 log.Printf("%d, %d = %#v\n", cellX, cellY, g.cells[cellY][cellX]) 392 } 393 394 // FillDead makes sure the rest of a line, width long, is filled with dead cells 395 // xEdge is the left side of the box of width length 396 // x is the starting point for the first line, any further lines start at xEdge 397 func (g *LifeGame) FillDead(xEdge, x, y, width, height int) { 398 for i := 0; i < height; i++ { 399 jlen := width - x 400 for j := 0; j < jlen; j++ { 401 x, y = g.SetCellState(x, y, false) 402 x++ 403 } 404 y++ 405 x = xEdge 406 } 407 } 408 409 // InitializeRandomCells resets the world to a random state 410 func (g *LifeGame) InitializeRandomCells() { 411 412 if cfg.Seed == 0 { 413 seed := time.Now().UnixNano() 414 log.Printf("seed = %d\n", seed) 415 rand.Seed(seed) 416 } else { 417 log.Printf("seed = %d\n", cfg.Seed) 418 rand.Seed(cfg.Seed) 419 } 420 421 for y := 0; y < cfg.Rows; y++ { 422 for x := 0; x < cfg.Columns; x++ { 423 g.SetCellState(x, y, rand.Float64() < threshold) 424 } 425 } 426 } 427 428 // ParseLife105 pattern file 429 // #D Descriptions lines (0+) 430 // #R Rule line (0/1) 431 // #P -1 4 (Upper left corner, required, center is 0,0) 432 // The pattern is . for dead and * for live 433 func (g *LifeGame) ParseLife105(lines []string) error { 434 var x, y int 435 var err error 436 for _, line := range lines { 437 if strings.HasPrefix(line, "#D") || strings.HasPrefix(line, "#Life") { 438 continue 439 } else if strings.HasPrefix(line, "#N") { 440 // Use default rules (from the cmdline in this case) 441 continue 442 } else if strings.HasPrefix(line, "#R ") { 443 // TODO Parse rule and return it or setup cfg.Rule 444 // Format is: sss/bbb where s is stay alive and b are birth values 445 // Need to flip it to Bbbb/Ssss format 446 447 // Make sure the rule has a / in it 448 if !strings.Contains(line, "/") { 449 return fmt.Errorf("ERROR: Rule must contain /") 450 } 451 452 fields := strings.Split(line[3:], "/") 453 if len(fields) != 2 { 454 return fmt.Errorf("ERROR: Problem splitting rule on /") 455 } 456 457 var stay, birth int 458 if stay, err = strconv.Atoi(fields[0]); err != nil { 459 return fmt.Errorf("Error parsing alive value: %s", err) 460 } 461 462 if birth, err = strconv.Atoi(fields[1]); err != nil { 463 return fmt.Errorf("Error parsing birth value: %s", err) 464 } 465 466 cfg.Rule = fmt.Sprintf("B%d/S%d", birth, stay) 467 } else if strings.HasPrefix(line, "#P") { 468 // Initial position 469 fields := strings.Split(line, " ") 470 if len(fields) != 3 { 471 return fmt.Errorf("Cannot parse position line: %s", line) 472 } 473 if y, err = strconv.Atoi(fields[1]); err != nil { 474 return fmt.Errorf("Error parsing position: %s", err) 475 } 476 if x, err = strconv.Atoi(fields[2]); err != nil { 477 return fmt.Errorf("Error parsing position: %s", err) 478 } 479 480 // Move to 0, 0 at the center of the world 481 x, y = g.TranslateXY(x, y) 482 } else { 483 // Parse the line, error if it isn't . or * 484 xLine := x 485 for _, c := range line { 486 if c != '.' && c != '*' { 487 return fmt.Errorf("Illegal characters in pattern: %s", line) 488 } 489 xLine, y = g.SetCellState(xLine, y, c == '*') 490 xLine++ 491 } 492 y++ 493 } 494 } 495 return nil 496 } 497 498 // ParsePlaintext pattern file 499 // The header has already been read from the buffer when this is called 500 // This is a bit more generic than the spec, skip lines starting with ! 501 // and assume the pattern is . for dead cells any anything else for live. 502 func (g *LifeGame) ParsePlaintext(lines []string) error { 503 var x, y int 504 505 // Move x, y to center of field 506 x = cfg.Columns / 2 507 y = cfg.Rows / 2 508 509 for _, line := range lines { 510 if strings.HasPrefix(line, "!") { 511 continue 512 } else { 513 // Parse the line, . is dead, anything else is alive. 514 xLine := x 515 for _, c := range line { 516 g.SetCellState(xLine, y, c != '.') 517 xLine++ 518 } 519 y++ 520 } 521 } 522 523 return nil 524 } 525 526 // isRLEPattern checks the lines to determine if it is a RLE pattern 527 func isRLE(lines []string) bool { 528 for _, line := range lines { 529 if rleHeaderRegex.MatchString(line) { 530 return true 531 } 532 } 533 return false 534 } 535 536 // ParseRLE pattern file 537 // Parses files matching the RLE specification - https://conwaylife.com/wiki/Run_Length_Encoded 538 // Optional x, y starting position for later use 539 func (g *LifeGame) ParseRLE(lines []string, x, y int) error { 540 // Move to 0, 0 at the center of the world 541 x, y = g.TranslateXY(x, y) 542 543 var header []string 544 var first int 545 for i, line := range lines { 546 header = rleHeaderRegex.FindStringSubmatch(line) 547 if len(header) > 0 { 548 first = i + 1 549 break 550 } 551 // All lines before the header must be a # line 552 if line[0] != '#' { 553 return fmt.Errorf("Incorrect or missing RLE header") 554 } 555 } 556 if len(header) < 3 { 557 return fmt.Errorf("Incorrect or missing RLE header") 558 } 559 if first > len(lines)-1 { 560 return fmt.Errorf("Missing lines after RLE header") 561 } 562 width, err := strconv.Atoi(header[1]) 563 if err != nil { 564 return fmt.Errorf("Error parsing width: %s", err) 565 } 566 height, err := strconv.Atoi(header[2]) 567 if err != nil { 568 return fmt.Errorf("Error parsing height: %s", err) 569 } 570 571 // Were there rules? Use them. TODO override if cmdline rules passed in 572 if len(header) == 4 { 573 cfg.Rule = header[3] 574 } 575 576 count := 0 577 xLine := x 578 yStart := y 579 for _, line := range lines[first:] { 580 for _, c := range line { 581 if c == '$' { 582 // End of this line (which can have a count) 583 if count == 0 { 584 count = 1 585 } 586 // Blank cells to the edge of the pattern, and full empty lines 587 g.FillDead(x, xLine, y, width, count) 588 589 xLine = x 590 y = y + count 591 count = 0 592 continue 593 } 594 if c == '!' { 595 // Finished 596 // Fill in any remaining space with dead cells 597 g.FillDead(x, xLine, y, width, height-(y-yStart)) 598 return nil 599 } 600 601 // Is it a digit? 602 digit, err := strconv.Atoi(string(c)) 603 if err == nil { 604 count = (count * 10) + digit 605 continue 606 } 607 608 if count == 0 { 609 count = 1 610 } 611 612 for i := 0; i < count; i++ { 613 xLine, y = g.SetCellState(xLine, y, c != 'b') 614 xLine++ 615 } 616 count = 0 617 } 618 } 619 return nil 620 } 621 622 // checkState determines the state of the cell for the next tick of the game. 623 func (g *LifeGame) checkState(c *Cell) { 624 liveCount, avgAge := g.liveNeighbors(c) 625 if c.alive { 626 // Stay alive if the number of neighbors is in stayAlive 627 _, c.aliveNext = g.stayAlive[liveCount] 628 } else { 629 // Birth a new cell if number of neighbors is in birth 630 _, c.aliveNext = g.birth[liveCount] 631 632 // New cells inherit their age from parents 633 // TODO make this optional 634 if c.aliveNext { 635 c.age = avgAge 636 } 637 } 638 639 if c.aliveNext { 640 c.age++ 641 } else { 642 c.age = 0 643 } 644 } 645 646 // liveNeighbors returns the number of live neighbors for a cell and their average age 647 func (g *LifeGame) liveNeighbors(c *Cell) (int, int) { 648 var liveCount int 649 var ageSum int 650 add := func(x, y int) { 651 // If we're at an edge, check the other side of the board. 652 if y == len(g.cells) { 653 y = 0 654 } else if y == -1 { 655 y = len(g.cells) - 1 656 } 657 if x == len(g.cells[y]) { 658 x = 0 659 } else if x == -1 { 660 x = len(g.cells[y]) - 1 661 } 662 663 if g.cells[y][x].alive { 664 liveCount++ 665 ageSum += g.cells[y][x].age 666 } 667 } 668 669 add(c.x-1, c.y) // To the left 670 add(c.x+1, c.y) // To the right 671 add(c.x, c.y+1) // up 672 add(c.x, c.y-1) // down 673 add(c.x-1, c.y+1) // top-left 674 add(c.x+1, c.y+1) // top-right 675 add(c.x-1, c.y-1) // bottom-left 676 add(c.x+1, c.y-1) // bottom-right 677 678 if liveCount > 0 { 679 return liveCount, int(ageSum / liveCount) 680 } 681 return liveCount, 0 682 } 683 684 // SetColorFromAge uses the cell's age to color it 685 func (g *LifeGame) SetColorFromAge(age int) { 686 if age >= len(g.gradient.points) { 687 age = len(g.gradient.points) - 1 688 } 689 color := g.gradient.points[age] 690 g.renderer.SetDrawColor(color.r, color.g, color.b, color.a) 691 } 692 693 // Draw draws the current state of the world 694 func (g *LifeGame) Draw(status string) { 695 // Clear the world to the background color 696 g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a) 697 g.renderer.Clear() 698 g.renderer.SetDrawColor(g.fg.r, g.fg.g, g.fg.b, g.fg.a) 699 for y := range g.cells { 700 for _, c := range g.cells[y] { 701 c.alive = c.aliveNext 702 if !c.alive { 703 continue 704 } 705 if cfg.Color { 706 g.SetColorFromAge(c.age) 707 } 708 g.DrawCell(*c) 709 } 710 } 711 // Default to background color 712 g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a) 713 714 g.UpdateStatus(status) 715 716 g.renderer.Present() 717 } 718 719 // DrawCell draws a new cell on an empty background 720 func (g *LifeGame) DrawCell(c Cell) { 721 x := int32(c.x) * g.cellWidth 722 y := int32(c.y) * g.cellHeight 723 if cfg.Border { 724 g.renderer.FillRect(&sdl.Rect{x + 1, y + 1, g.cellWidth - 2, g.cellHeight - 2}) 725 } else { 726 g.renderer.FillRect(&sdl.Rect{x, y, g.cellWidth, g.cellHeight}) 727 } 728 } 729 730 // UpdateCell redraws an existing cell, optionally erasing it 731 func (g *LifeGame) UpdateCell(x, y int, erase bool) { 732 g.cells[y][x].alive = !erase 733 734 // Update the image right now 735 if erase { 736 g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a) 737 } else { 738 g.renderer.SetDrawColor(g.fg.r, g.fg.g, g.fg.b, g.fg.a) 739 } 740 g.DrawCell(*g.cells[y][x]) 741 742 // Default to background color 743 g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a) 744 g.renderer.Present() 745 } 746 747 // UpdateStatus draws the status bar 748 func (g *LifeGame) UpdateStatus(status string) { 749 if len(status) == 0 { 750 return 751 } 752 text, err := g.font.RenderUTF8Solid(status, sdl.Color{255, 255, 255, 255}) 753 if err != nil { 754 log.Printf("Failed to render text: %s\n", err) 755 return 756 } 757 defer text.Free() 758 759 texture, err := g.renderer.CreateTextureFromSurface(text) 760 if err != nil { 761 log.Printf("Failed to render text: %s\n", err) 762 return 763 } 764 defer texture.Destroy() 765 766 w, h, err := g.font.SizeUTF8(status) 767 if err != nil { 768 log.Printf("Failed to get size: %s\n", err) 769 return 770 } 771 772 x := int32((cfg.Width - w) / 2) 773 rect := &sdl.Rect{x, int32(cfg.Height + 2), int32(w), int32(h)} 774 if err = g.renderer.Copy(texture, nil, rect); err != nil { 775 log.Printf("Failed to copy texture: %s\n", err) 776 return 777 } 778 } 779 780 // NextFrame executes the next screen of the game 781 func (g *LifeGame) NextFrame() { 782 last := g.liveCells 783 g.liveCells = 0 784 for y := range g.cells { 785 for _, c := range g.cells[y] { 786 g.checkState(c) 787 if c.aliveNext { 788 g.liveCells++ 789 } 790 } 791 } 792 if g.liveCells-last != 0 { 793 g.age++ 794 } 795 796 // Draw a new screen 797 status := fmt.Sprintf("age: %5d alive: %5d change: %5d", g.age, g.liveCells, g.liveCells-last) 798 g.Draw(status) 799 } 800 801 // ShowKeysHelp prints the keys that are reconized to control behavior 802 func ShowKeysHelp() { 803 fmt.Println("h - Print help") 804 fmt.Println("<space> - Toggle pause/play") 805 fmt.Println("c - Toggle color") 806 fmt.Println("q - Quit") 807 fmt.Println("s - Single step") 808 fmt.Println("r - Reset the game") 809 } 810 811 // Run executes the main loop of the game 812 // it handles user input and updating the display at the selected update rate 813 func (g *LifeGame) Run() { 814 fpsTime := sdl.GetTicks() 815 816 running := true 817 oneStep := false 818 pause := cfg.Pause 819 for running { 820 for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { 821 switch t := event.(type) { 822 case *sdl.QuitEvent: 823 running = false 824 break 825 case *sdl.KeyboardEvent: 826 if t.GetType() == sdl.KEYDOWN { 827 switch t.Keysym.Sym { 828 case sdl.K_h: 829 ShowKeysHelp() 830 case sdl.K_q: 831 running = false 832 break 833 case sdl.K_SPACE: 834 pause = !pause 835 case sdl.K_s: 836 pause = true 837 oneStep = true 838 case sdl.K_r: 839 g.InitializeCells() 840 case sdl.K_c: 841 cfg.Color = !cfg.Color 842 } 843 844 } 845 case *sdl.MouseButtonEvent: 846 if t.GetType() == sdl.MOUSEBUTTONDOWN { 847 // log.Printf("x=%d y=%d\n", t.X, t.Y) 848 g.PrintCellDetails(t.X, t.Y) 849 } 850 } 851 } 852 // Delay a small amount 853 time.Sleep(1 * time.Millisecond) 854 if sdl.GetTicks() > fpsTime+(1000/uint32(cfg.Fps)) { 855 if !pause || oneStep { 856 g.NextFrame() 857 fpsTime = sdl.GetTicks() 858 oneStep = false 859 } 860 } 861 862 if g.pChan != nil { 863 select { 864 case pattern := <-g.pChan: 865 var err error 866 if strings.HasPrefix(pattern[0], "#Life 1.05") { 867 err = g.ParseLife105(pattern) 868 } else if isRLE(pattern) { 869 err = g.ParseRLE(pattern, 0, 0) 870 } else { 871 err = g.ParsePlaintext(pattern) 872 } 873 if err != nil { 874 log.Printf("Pattern error: %s\n", err) 875 } 876 default: 877 } 878 } 879 } 880 } 881 882 // InitializeGame sets up the game struct and the SDL library 883 // It also creates the main window 884 func InitializeGame() *LifeGame { 885 game := &LifeGame{} 886 887 var err error 888 if err = sdl.Init(sdl.INIT_EVERYTHING); err != nil { 889 log.Fatalf("Problem initializing SDL: %s", err) 890 } 891 892 if err = ttf.Init(); err != nil { 893 log.Fatalf("Failed to initialize TTF: %s\n", err) 894 } 895 896 if game.font, err = ttf.OpenFont(cfg.Font, cfg.FontSize); err != nil { 897 log.Fatalf("Failed to open font: %s\n", err) 898 } 899 log.Printf("Font height is %d", game.font.Height()) 900 901 game.font.SetHinting(ttf.HINTING_NORMAL) 902 game.font.SetKerning(true) 903 904 game.window, err = sdl.CreateWindow( 905 "Conway's Game of Life", 906 sdl.WINDOWPOS_UNDEFINED, 907 sdl.WINDOWPOS_UNDEFINED, 908 int32(cfg.Width), 909 int32(cfg.Height+4+game.font.Height()), 910 sdl.WINDOW_SHOWN) 911 if err != nil { 912 log.Fatalf("Problem initializing SDL window: %s", err) 913 } 914 915 game.renderer, err = sdl.CreateRenderer(game.window, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC) 916 if err != nil { 917 log.Fatalf("Problem initializing SDL renderer: %s", err) 918 } 919 920 // White on Black background 921 game.bg = RGBAColor{0, 0, 0, 255} 922 game.fg = RGBAColor{255, 255, 255, 255} 923 924 // Calculate square cell size, take into account --border selection 925 w := cfg.Width / cfg.Columns 926 h := cfg.Height / cfg.Rows 927 if w < h { 928 h = w 929 } else { 930 w = h 931 } 932 game.cellWidth = int32(w) 933 game.cellHeight = int32(h) 934 935 // Parse the hex triplets 936 colors, err := ParseColorTriplets(cfg.Colors) 937 if err != nil { 938 log.Fatalf("Problem parsing colors: %s", err) 939 } 940 941 // Build the color gradient 942 switch cfg.Gradient { 943 case LinearGradient: 944 game.gradient, err = NewLinearGradient(colors, cfg.MaxAge) 945 if err != nil { 946 log.Fatalf("ERROR: %s", err) 947 } 948 case PolylinearGradient: 949 game.gradient, err = NewPolylinearGradient(colors, cfg.MaxAge) 950 if err != nil { 951 log.Fatalf("ERROR: %s", err) 952 } 953 case BezierGradient: 954 game.gradient = NewBezierGradient(colors, cfg.MaxAge) 955 } 956 957 return game 958 } 959 960 // ParseColorTriplets parses color hex values into an array 961 // like ffffff,000000 or #ffffff,#000000 or #ffffff#000000 962 func ParseColorTriplets(s string) ([]RGBAColor, error) { 963 964 // Remove leading # if present 965 s = strings.TrimPrefix(s, "#") 966 // Replace ,# combinations with just , 967 s = strings.ReplaceAll(s, ",#", ",") 968 // Replace # alone with , 969 s = strings.ReplaceAll(s, "#", ",") 970 971 var colors []RGBAColor 972 // Convert the tuples into RGBAColor 973 for _, c := range strings.Split(s, ",") { 974 r, err := strconv.ParseUint(c[:2], 16, 8) 975 if err != nil { 976 return colors, err 977 } 978 g, err := strconv.ParseUint(c[2:4], 16, 8) 979 if err != nil { 980 return colors, err 981 } 982 b, err := strconv.ParseUint(c[4:6], 16, 8) 983 if err != nil { 984 return colors, err 985 } 986 987 colors = append(colors, RGBAColor{uint8(r), uint8(g), uint8(b), 255}) 988 } 989 990 return colors, nil 991 } 992 993 // Parse digits into a map of ints from 0-9 994 // 995 // Returns an error if they aren't digits, or if there are more than 10 of them 996 func parseDigits(digits string) (map[int]bool, error) { 997 ruleMap := make(map[int]bool, 10) 998 999 var errors bool 1000 var err error 1001 var value int 1002 if value, err = strconv.Atoi(digits); err != nil { 1003 log.Printf("%s must be digits from 0-9\n", digits) 1004 errors = true 1005 } 1006 if value > 9999999999 { 1007 log.Printf("%d has more than 10 digits", value) 1008 errors = true 1009 } 1010 if errors { 1011 return nil, fmt.Errorf("ERROR: Problem parsing digits") 1012 } 1013 1014 // Add the digits to the map (order doesn't matter) 1015 for value > 0 { 1016 ruleMap[value%10] = true 1017 value = value / 10 1018 } 1019 1020 return ruleMap, nil 1021 } 1022 1023 // ParseRulestring parses the rules that control the game 1024 // 1025 // Rulestrings are of the form Bn.../Sn... which list the number of neighbors to birth a new one, 1026 // and the number of neighbors to stay alive. 1027 // 1028 func ParseRulestring(rule string) (birth map[int]bool, stayAlive map[int]bool, e error) { 1029 var errors bool 1030 1031 // Make sure the rule starts with a B 1032 if !strings.HasPrefix(rule, "B") { 1033 log.Println("ERROR: Rule must start with a 'B'") 1034 errors = true 1035 } 1036 1037 // Make sure the rule has a /S in it 1038 if !strings.Contains(rule, "/S") { 1039 log.Println("ERROR: Rule must contain /S") 1040 errors = true 1041 } 1042 if errors { 1043 return nil, nil, fmt.Errorf("The Rule string should look similar to: B2/S23") 1044 } 1045 1046 // Split on the / returning 2 results like Bnn and Snn 1047 fields := strings.Split(rule, "/") 1048 if len(fields) != 2 { 1049 return nil, nil, fmt.Errorf("ERROR: Problem splitting rule on /") 1050 } 1051 1052 var err error 1053 // Convert the values to maps 1054 birth, err = parseDigits(strings.TrimPrefix(fields[0], "B")) 1055 if err != nil { 1056 errors = true 1057 } 1058 stayAlive, err = parseDigits(strings.TrimPrefix(fields[1], "S")) 1059 if err != nil { 1060 errors = true 1061 } 1062 if errors { 1063 return nil, nil, fmt.Errorf("ERROR: Problem with Birth or Stay alive values") 1064 } 1065 1066 return birth, stayAlive, nil 1067 } 1068 1069 // Server starts an API server to receive patterns 1070 func Server(host string, port int, pChan chan<- Pattern) { 1071 1072 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 1073 if r.Method != "POST" { 1074 http.Error(w, "", http.StatusMethodNotAllowed) 1075 return 1076 } 1077 1078 scanner := bufio.NewScanner(r.Body) 1079 var pattern Pattern 1080 for scanner.Scan() { 1081 pattern = append(pattern, scanner.Text()) 1082 } 1083 if len(pattern) == 0 { 1084 http.Error(w, "Empty pattern", http.StatusServiceUnavailable) 1085 return 1086 } 1087 1088 // Splat this pattern onto the world 1089 pChan <- pattern 1090 }) 1091 1092 log.Printf("Starting server on %s:%d", host, port) 1093 log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), nil)) 1094 } 1095 1096 func main() { 1097 parseArgs() 1098 1099 // If the user didn't specify a font, try to find a default one 1100 if len(cfg.Font) == 0 { 1101 for _, f := range defaultFonts { 1102 if _, err := os.Stat(f); !os.IsNotExist(err) { 1103 cfg.Font = f 1104 break 1105 } 1106 } 1107 } 1108 1109 if len(cfg.Font) == 0 { 1110 log.Fatal("Failed to find a font for the statusbar. Use -font to Pass the path to a monospaced font") 1111 } 1112 1113 // Initialize the main window 1114 game := InitializeGame() 1115 defer game.cleanup() 1116 1117 // TODO 1118 // * resize events? 1119 // * add a status bar (either add to height, or subtract from it) 1120 1121 // Setup the initial state of the world 1122 game.InitializeCells() 1123 1124 ShowKeysHelp() 1125 1126 if cfg.Server { 1127 ch := make(chan Pattern, 2) 1128 game.pChan = ch 1129 go Server(cfg.Host, cfg.Port, ch) 1130 } 1131 1132 game.Run() 1133 }