life.go (4555B)
1 package life 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "math/rand" 8 "os" 9 "time" 10 11 "code.dwrz.net/src/pkg/color" 12 "code.dwrz.net/src/pkg/log" 13 "code.dwrz.net/src/pkg/terminal" 14 "code.dwrz.net/src/pkg/terminal/input" 15 ) 16 17 const ( 18 alive = color.BackgroundBlack + " " + color.Reset 19 born = color.BackgroundGreen + " " + color.Reset 20 died = color.BackgroundRed + " " + color.Reset 21 dead = " " 22 ) 23 24 var random = rand.New(rand.NewSource(time.Now().UnixNano())) 25 26 type Life struct { 27 errs chan error 28 events chan *input.Event 29 height int 30 in *os.File 31 input *input.Reader 32 last [][]bool 33 log *log.Logger 34 out *os.File 35 terminal *terminal.Terminal 36 tick time.Duration 37 ticker *time.Ticker 38 turn uint 39 width int 40 world [][]bool 41 } 42 43 type Parameters struct { 44 Height int 45 In *os.File 46 Log *log.Logger 47 Out *os.File 48 Terminal *terminal.Terminal 49 Tick time.Duration 50 Width int 51 } 52 53 func New(p Parameters) *Life { 54 // Account for two spaces to represent each cell. 55 p.Width /= 2 56 // Account one line to render the turn count. 57 p.Height -= 1 58 59 var life = &Life{ 60 errs: make(chan error), 61 events: make(chan *input.Event), 62 height: p.Height, 63 in: p.In, 64 last: make([][]bool, p.Height), 65 log: p.Log, 66 out: p.Out, 67 terminal: p.Terminal, 68 tick: p.Tick, 69 ticker: time.NewTicker(p.Tick), 70 width: p.Width, 71 world: make([][]bool, p.Height), 72 } 73 for i := range life.world { 74 life.last[i] = make([]bool, p.Width) 75 life.world[i] = make([]bool, p.Width) 76 } 77 for i := 0; i < (p.Height * p.Width / 4); i++ { 78 x := random.Intn(p.Width) 79 y := random.Intn(p.Height) 80 81 life.world[y][x] = true 82 } 83 84 life.input = input.New(input.Parameters{ 85 Chan: life.events, 86 In: os.Stdin, 87 Log: life.log, 88 }) 89 90 return life 91 } 92 93 func (l *Life) Run(ctx context.Context) error { 94 // Setup the terminal. 95 if err := l.terminal.SetRaw(); err != nil { 96 return fmt.Errorf( 97 "failed to set terminal attributes: %v", err, 98 ) 99 } 100 101 // Clean up before exit. 102 defer l.quit() 103 104 // TODO: refactor to use a terminal output package. 105 l.out.WriteString(terminal.ClearScreen) 106 l.out.WriteString(terminal.CursorHide) 107 l.render() 108 109 // Start reading user input. 110 go func() { 111 if err := l.input.Run(ctx); err != nil { 112 l.errs <- err 113 } 114 }() 115 116 // Main loop. 117 for { 118 select { 119 case <-ctx.Done(): 120 return nil 121 122 case <-l.ticker.C: 123 l.step() 124 l.render() 125 126 case event := <-l.events: 127 switch event.Rune { 128 case 'q': // Quit 129 return nil 130 } 131 } 132 } 133 } 134 135 func (l *Life) alive(x, y int) bool { 136 // Wrap coordinates toroidally. 137 x += l.width 138 x %= l.width 139 y += l.height 140 y %= l.height 141 142 return l.world[y][x] 143 } 144 145 func (l *Life) born(x, y int) bool { 146 // Wrap coordinates toroidally. 147 x += l.width 148 x %= l.width 149 y += l.height 150 y %= l.height 151 152 return !l.last[y][x] && l.world[y][x] 153 } 154 155 func (l *Life) died(x, y int) bool { 156 // Wrap coordinates toroidally. 157 x += l.width 158 x %= l.width 159 y += l.height 160 y %= l.height 161 162 return l.last[y][x] && !l.world[y][x] 163 } 164 165 func (l *Life) next(x, y int) bool { 166 // Count the numbers of living neighbors. 167 var alive = 0 168 for v := -1; v <= 1; v++ { 169 for h := -1; h <= 1; h++ { 170 // Ignore self. 171 if h == 0 && v == 0 { 172 continue 173 } 174 if l.alive(x+v, y+h) { 175 alive++ 176 } 177 } 178 } 179 180 // Determine the next state. 181 switch alive { 182 case 3: // Turn on. 183 return true 184 case 2: // Maintain state. 185 return l.alive(x, y) 186 default: // Turn off. 187 return false 188 } 189 } 190 191 func (l *Life) quit() { 192 l.out.Write([]byte(terminal.CursorShow)) 193 194 if err := l.terminal.Reset(); err != nil { 195 l.log.Error.Printf( 196 "failed to reset terminal attributes: %v", err, 197 ) 198 } 199 } 200 201 func (l *Life) render() { 202 var buf bytes.Buffer 203 204 buf.WriteString(terminal.CursorTopLeft) 205 206 for y := 0; y < l.height; y++ { 207 for x := 0; x < l.width; x++ { 208 switch { 209 case l.born(x, y): 210 buf.WriteString(born) 211 case l.died(x, y): 212 buf.WriteString(died) 213 case l.alive(x, y): 214 buf.WriteString(alive) 215 default: 216 buf.WriteString(dead) 217 218 } 219 } 220 buf.WriteString("\r\n") 221 } 222 fmt.Fprintf( 223 &buf, "%sTurn:%s %d\r\n", 224 color.Bold, color.Reset, l.turn, 225 ) 226 227 l.out.Write(buf.Bytes()) 228 } 229 230 func (l *Life) step() { 231 // Create the next world. 232 var next = make([][]bool, l.height) 233 for i := range next { 234 next[i] = make([]bool, l.width) 235 } 236 237 // Set the new cells based on the existing cells. 238 for y := 0; y < l.height; y++ { 239 for x := 0; x < l.width; x++ { 240 next[y][x] = l.next(x, y) 241 } 242 } 243 244 // Increment the turn and set the next world. 245 l.turn++ 246 l.last = l.world 247 l.world = next 248 }