summaryrefslogtreecommitdiffstats
path: root/life.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-09-02 21:44:39 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-09-02 21:45:30 -0700
commit9b2f49093442098c23b5274906bad2e682dcb0ad (patch)
tree8dc58fadc4c1931ed80f5625f473109f417905c5 /life.go
downloadlife-main.tar.zst
Initial commitHEADmain
Diffstat (limited to 'life.go')
-rw-r--r--life.go214
1 files changed, 214 insertions, 0 deletions
diff --git a/life.go b/life.go
new file mode 100644
index 0000000..078e72c
--- /dev/null
+++ b/life.go
@@ -0,0 +1,214 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "math/rand/v2"
+ "os"
+ "runtime"
+ "sync"
+ "time"
+
+ "golang.org/x/term"
+)
+
+var stdout = bufio.NewWriter(os.Stdout)
+
+const (
+ left = -(1 + iota)
+ right
+)
+
+func readKeys(out chan<- rune) {
+ defer close(out)
+ stdin := bufio.NewReader(os.Stdin)
+ for {
+ b, err := stdin.ReadByte()
+ if err != nil {
+ break
+ }
+ if b == '\x1b' {
+ buf := make([]byte, 2)
+ n, err := stdin.Read(buf)
+ switch string(buf[:n]) {
+ case "[D":
+ out <- left
+ case "[C":
+ out <- right
+ }
+ if err != nil {
+ break
+ }
+ } else {
+ out <- rune(b)
+ }
+ }
+}
+
+type board struct {
+ buf []bool
+ rows int
+ cols int
+}
+
+func newBoard(rows, cols int) board {
+ return board{
+ buf: make([]bool, rows*cols),
+ rows: rows,
+ cols: cols,
+ }
+}
+
+func (b board) row(i int) []bool {
+ start := i * b.cols
+ return b.buf[start : start+b.cols]
+}
+
+func (b board) neighborCount(i, j int) int {
+ count := 0
+ for di := -1; di <= 1; di++ {
+ for dj := -1; dj <= 1; dj++ {
+ if di == 0 && dj == 0 {
+ continue
+ }
+ ii := i + di
+ jj := j + dj
+ if ii < 0 {
+ ii = b.rows - 1
+ } else if ii == b.rows {
+ ii = 0
+ }
+ if jj < 0 {
+ jj = b.cols - 1
+ } else if jj == b.cols {
+ jj = 0
+ }
+ if b.row(ii)[jj] {
+ count++
+ }
+ }
+ }
+ return count
+}
+
+var numCPU = runtime.NumCPU()
+
+func step(nextBoard, activeBoard board) {
+ chunkSize := (activeBoard.rows + numCPU - 1) / numCPU
+ wg := new(sync.WaitGroup)
+ for start := 0; start < activeBoard.rows; start += chunkSize {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := start; i < min(start+chunkSize, activeBoard.rows); i++ {
+ for j := range activeBoard.cols {
+ count := activeBoard.neighborCount(i, j)
+ nextBoard.row(i)[j] = count == 3 || count == 2 && activeBoard.row(i)[j]
+ }
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+func life() error {
+ cols, rows, err := term.GetSize(int(os.Stdout.Fd()))
+ if err != nil {
+ return err
+ }
+ oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
+ if err != nil {
+ return err
+ }
+ defer term.Restore(int(os.Stdin.Fd()), oldState)
+
+ board1 := newBoard(rows, cols)
+ for i := range board1.rows {
+ for j := range board1.cols {
+ board1.row(i)[j] = rand.IntN(2) != 0
+ }
+ }
+ board2 := newBoard(rows, cols)
+ activeBoard := board1
+ nextBoard := board2
+
+ const minDelay = time.Second / 300
+
+ delay := 250 * time.Millisecond
+ running := true
+ oneTick := false
+
+ frameTimer := time.NewTicker(minDelay)
+ defer frameTimer.Stop()
+ lastFrameTime := time.Now()
+ accumulator := time.Duration(0)
+
+ keyCh := make(chan rune, 1024)
+ go readKeys(keyCh)
+
+ for now := range frameTimer.C {
+ Read:
+ for {
+ var key rune
+ select {
+ case key = <-keyCh:
+ default:
+ break Read
+ }
+ switch key {
+ case 'q':
+ return nil
+ case ' ':
+ running = !running
+ case '.':
+ if !running {
+ oneTick = true
+ }
+ case left:
+ delay += delay / 3
+ case right:
+ delay = max(delay-delay/3, minDelay)
+ }
+ }
+
+ accumulator += now.Sub(lastFrameTime)
+ lastFrameTime = now
+ if oneTick || accumulator >= delay {
+ if accumulator >= delay {
+ accumulator -= delay
+ }
+ if running || oneTick {
+ oneTick = false
+ step(nextBoard, activeBoard)
+ activeBoard, nextBoard = nextBoard, activeBoard
+ }
+ }
+ // Prevent falling behind.
+ if accumulator > delay {
+ accumulator = delay
+ }
+
+ stdout.WriteString("\x1b[H")
+ for i := range activeBoard.rows {
+ if i > 0 {
+ stdout.WriteString("\r\n")
+ }
+ for j := range activeBoard.cols {
+ if activeBoard.row(i)[j] {
+ stdout.WriteByte('#')
+ } else {
+ stdout.WriteByte(' ')
+ }
+ }
+ }
+ stdout.Flush()
+ }
+ panic("unreachable")
+}
+
+func main() {
+ if err := life(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}