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) } }