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