src

Go monorepo.
Log | Files | Refs

commit dc4abee56e21a9f91a9b6d14d18308170ea6abda
parent 3d7e5981589209b7c6ad20d07bf804250b147804
Author: dwrz <dwrz@dwrz.net>
Date:   Sat, 24 Dec 2022 16:16:39 +0000

Add game of life

Diffstat:
Acmd/life/main.go | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apkg/life/life.go | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 357 insertions(+), 0 deletions(-)

diff --git a/cmd/life/main.go b/cmd/life/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "flag" + + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "code.dwrz.net/src/pkg/life" + "code.dwrz.net/src/pkg/log" + "code.dwrz.net/src/pkg/terminal" +) + +var ( + height = flag.Int("h", 0, "height") + width = flag.Int("w", 0, "width") + tick = flag.Int("t", 100, "ms between turns") +) + +func main() { + var l = log.New(os.Stderr) + + // Parse flags. + flag.Parse() + if *height < 0 { + l.Error.Fatalf("invalid height: %d", *height) + } + if *width < 0 { + l.Error.Fatalf("invalid width: %d", *width) + } + if *tick <= 0 { + l.Error.Fatalf("invalid tick: %d", *tick) + } + + // Setup the main context. + ctx, cancel := context.WithCancel(context.Background()) + + // Setup workspace and log file. + cdir, err := os.UserCacheDir() + if err != nil { + l.Error.Fatalf( + "failed to determine user cache directory: %v", err, + ) + } + wdir := filepath.Join(cdir, "life") + + if err := os.MkdirAll(wdir, os.ModeDir|0700); err != nil { + l.Error.Fatalf("failed to create tmp dir: %v", err) + } + + f, err := os.Create(wdir + "/log") + if err != nil { + l.Error.Fatalf("failed to create log file: %v", err) + } + defer f.Close() + + // Retrieve terminal info. + t, err := terminal.New(os.Stdin.Fd()) + if err != nil { + l.Error.Fatalf("failed to get terminal attributes: %v", err) + } + size, err := t.Size() + if err != nil { + l.Error.Fatalf("failed to get terminal size: %v", err) + } + if *height == 0 { + rows := int(size.Rows) + height = &rows + } + if *width == 0 { + cols := int(size.Columns) + 1 + width = &cols + } + + // TODO: refactor; handle sigwinch. + go func() { + var signals = make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) + + // Block until we receive a signal. + s := <-signals + l.Debug.Printf("received signal: %s", s) + + cancel() + }() + + // Setup the game. + var game = life.New(life.Parameters{ + Height: *height, + In: os.Stdin, + Log: log.New(f), + Out: os.Stdout, + Terminal: t, + Tick: time.Duration(*tick) * time.Millisecond, + Width: *width, + }) + if err != nil { + l.Error.Fatalf("failed to create game: %v", err) + } + + // Run the game. + if err := game.Run(ctx); err != nil { + l.Error.Fatal(err) + } +} diff --git a/pkg/life/life.go b/pkg/life/life.go @@ -0,0 +1,248 @@ +package life + +import ( + "bytes" + "context" + "fmt" + "math/rand" + "os" + "time" + + "code.dwrz.net/src/pkg/color" + "code.dwrz.net/src/pkg/log" + "code.dwrz.net/src/pkg/terminal" + "code.dwrz.net/src/pkg/terminal/input" +) + +const ( + alive = color.BackgroundBlack + " " + color.Reset + born = color.BackgroundGreen + " " + color.Reset + died = color.BackgroundRed + " " + color.Reset + dead = " " +) + +var random = rand.New(rand.NewSource(time.Now().UnixNano())) + +type Life struct { + errs chan error + events chan *input.Event + height int + in *os.File + input *input.Reader + last [][]bool + log *log.Logger + out *os.File + terminal *terminal.Terminal + tick time.Duration + ticker *time.Ticker + turn uint + width int + world [][]bool +} + +type Parameters struct { + Height int + In *os.File + Log *log.Logger + Out *os.File + Terminal *terminal.Terminal + Tick time.Duration + Width int +} + +func New(p Parameters) *Life { + // Account for two spaces to represent each cell. + p.Width /= 2 + // Account one line to render the turn count. + p.Height -= 1 + + var life = &Life{ + errs: make(chan error), + events: make(chan *input.Event), + height: p.Height, + in: p.In, + last: make([][]bool, p.Height), + log: p.Log, + out: p.Out, + terminal: p.Terminal, + tick: p.Tick, + ticker: time.NewTicker(p.Tick), + width: p.Width, + world: make([][]bool, p.Height), + } + for i := range life.world { + life.last[i] = make([]bool, p.Width) + life.world[i] = make([]bool, p.Width) + } + for i := 0; i < (p.Height * p.Width / 4); i++ { + x := random.Intn(p.Width) + y := random.Intn(p.Height) + + life.world[y][x] = true + } + + life.input = input.New(input.Parameters{ + Chan: life.events, + In: os.Stdin, + Log: life.log, + }) + + return life +} + +func (l *Life) Run(ctx context.Context) error { + // Setup the terminal. + if err := l.terminal.SetRaw(); err != nil { + return fmt.Errorf( + "failed to set terminal attributes: %v", err, + ) + } + + // Clean up before exit. + defer l.quit() + + // TODO: refactor to use a terminal output package. + l.out.WriteString(terminal.ClearScreen) + l.out.WriteString(terminal.CursorHide) + l.render() + + // Start reading user input. + go func() { + if err := l.input.Run(ctx); err != nil { + l.errs <- err + } + }() + + // Main loop. + for { + select { + case <-ctx.Done(): + return nil + + case <-l.ticker.C: + l.step() + l.render() + + case event := <-l.events: + switch event.Rune { + case 'q': // Quit + return nil + } + } + } +} + +func (l *Life) alive(x, y int) bool { + // Wrap coordinates toroidally. + x += l.width + x %= l.width + y += l.height + y %= l.height + + return l.world[y][x] +} + +func (l *Life) born(x, y int) bool { + // Wrap coordinates toroidally. + x += l.width + x %= l.width + y += l.height + y %= l.height + + return !l.last[y][x] && l.world[y][x] +} + +func (l *Life) died(x, y int) bool { + // Wrap coordinates toroidally. + x += l.width + x %= l.width + y += l.height + y %= l.height + + return l.last[y][x] && !l.world[y][x] +} + +func (l *Life) next(x, y int) bool { + // Count the numbers of living neighbors. + var alive = 0 + for v := -1; v <= 1; v++ { + for h := -1; h <= 1; h++ { + // Ignore self. + if h == 0 && v == 0 { + continue + } + if l.alive(x+v, y+h) { + alive++ + } + } + } + + // Determine the next state. + switch alive { + case 3: // Turn on. + return true + case 2: // Maintain state. + return l.alive(x, y) + default: // Turn off. + return false + } +} + +func (l *Life) quit() { + l.out.Write([]byte(terminal.CursorShow)) + + if err := l.terminal.Reset(); err != nil { + l.log.Error.Printf( + "failed to reset terminal attributes: %v", err, + ) + } +} + +func (l *Life) render() { + var buf bytes.Buffer + + buf.WriteString(terminal.CursorTopLeft) + + for y := 0; y < l.height; y++ { + for x := 0; x < l.width; x++ { + switch { + case l.born(x, y): + buf.WriteString(born) + case l.died(x, y): + buf.WriteString(died) + case l.alive(x, y): + buf.WriteString(alive) + default: + buf.WriteString(dead) + + } + } + buf.WriteString("\r\n") + } + fmt.Fprintf( + &buf, "%sTurn:%s %d\r\n", + color.Bold, color.Reset, l.turn, + ) + + l.out.Write(buf.Bytes()) +} + +func (l *Life) step() { + // Create the next world. + var next = make([][]bool, l.height) + for i := range next { + next[i] = make([]bool, l.width) + } + + // Set the new cells based on the existing cells. + for y := 0; y < l.height; y++ { + for x := 0; x < l.width; x++ { + next[y][x] = l.next(x, y) + } + } + + // Increment the turn and set the next world. + l.turn++ + l.last = l.world + l.world = next +}