const Conway = @This(); const std = @import("std"); const gpa = std.heap.page_allocator; generation: u64 = 0, width: u64, height: u64, world: [][]*Cell, writer: *std.io.Writer, const Cell = struct { /// Technically we don't need to store any neighbors and we could just /// check using the world, but that makes the logic far more terse. /// /// storage of neighbors /// ? 0 ? /// 1 x 2 /// ? 3 ? /// instead of storing all 8 neighbors I've decided to contact them /// through our other neighbors. neighbors: [4]?*Cell = .{ null, null, null, null }, /// the state on the current generation alive: bool = false, /// the state on the next generation will_live: bool = false, }; pub fn init(width: u64, height: u64, writer: *std.Io.Writer) !Conway { std.debug.assert(width > 0); std.debug.assert(height > 0); var self: Conway = .{ .width = width, .height = height, .world = undefined, .writer = writer, }; // create the world and the cells self.world = try gpa.alloc([]*Cell, height); for (self.world, 0..) |col, i| { self.world[i] = try gpa.alloc(*Cell, width); for (col, 0..) |_, j| { if (j >= width) break; self.world[i][j] = try gpa.create(Cell); } } // initialize all the cell properties for (self.world, 0..) |col, i| for (col, 0..) |cell, j| { // the top row has nothing above it cell.neighbors[0] = if (j == 0) null else self.world[i][j - 1]; // the left column has nothing on the left cell.neighbors[1] = if (i == 0) null else self.world[i - 1][j]; // the right row has nothing on the right cell.neighbors[2] = if (i == height - 1) null else self.world[i + 1][j]; // the bottom row has nothing below it cell.neighbors[3] = if (j == width - 1) null else self.world[i][j + 1]; cell.alive = false; }; return self; } pub fn deinit(self: *Conway) void { for (self.world) |col| { for (col) |cell| gpa.destroy(cell); gpa.free(col); } gpa.free(self.world); } pub fn step(self: *Conway) void { for (self.world) |col| for (col) |cell| { var alive_neighbors: u4 = 0; // count the neighbors for (cell.neighbors, 0..) |n, i| { if (n == null) continue; if (i == 0 or i == 3) { if (n.?.neighbors[1]) |nn| if (nn.alive) { alive_neighbors += 1; }; if (n.?.neighbors[2]) |nn| if (nn.alive) { alive_neighbors += 1; }; } if (n.?.alive) alive_neighbors += 1; } // rules if (cell.alive) { cell.will_live = switch (alive_neighbors) { 2, 3 => true, else => false }; } else if (alive_neighbors == 3) cell.will_live = true; }; // the living state must be applied after all the cells have been checked for (self.world) |col| for (col) |cell| { cell.alive = cell.will_live; }; self.generation += 1; } pub fn print(self: *Conway) !void { for (self.world) |col| { for (col) |cell| { try self.writer.print("{s}", .{if (cell.alive) "x" else " "}); } _ = try self.writer.write("\n"); } } pub fn play(width: u64, height: u64, writer: *std.Io.Writer) !void { var con = try Conway.init(width, height - 1, writer); // here we're disabling character echoing and newline requirements on input const oldios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); defer std.posix.tcsetattr(std.posix.STDIN_FILENO, std.posix.TCSA.NOW, oldios) catch { // oh well we've left the terminal in a shitty state, not much we can // do to fix this. }; var newios = oldios; newios.lflag.ECHO = false; newios.lflag.ICANON = false; try std.posix.tcsetattr(std.posix.STDIN_FILENO, std.posix.TCSA.NOW, newios); var fd = [_]std.posix.pollfd{.{ .fd = std.posix.STDIN_FILENO, .events = std.posix.POLL.IN, .revents = 0 }}; var timeout: i32 = 250; var cursor_x: i64 = 1; var cursor_y: i64 = 1; var interactive = true; var running = true; while (running) : (con.step()) run: while (true) { // not sure why but adding one here makes the status bar refresh fully try con.writer.print("\x1B[{}A", .{height + 1}); try con.print(); // keep the cursor in the game window to prevent the terminal from doing // anything we can't/don't account for cursor_x = std.math.clamp(cursor_x, 1, con.width); cursor_y = std.math.clamp(cursor_y, 1, con.height); // statusbar try con.writer.print("ge[n]: {} | {s} [i]nteractive \x1B[0m {},{} {}x{} | [+]{}[-]", .{ con.generation, if (interactive) "\x1B[47;30;1m" else "\x1B[40;37m", cursor_x, cursor_y, con.width, con.height, timeout, }); // position cursor in interactive mode if (interactive) { try con.writer.print("\x1B[{};{}H", .{cursor_y, cursor_x}); // swapped on purpose try con.writer.flush(); } try con.writer.flush(); const size = try std.posix.poll(@constCast(&fd), if (!interactive) @intCast(timeout) else -1); if (size > 0) { var buf: [50]u8 = undefined; _ = try std.fs.File.stdin().read(@constCast(&buf)); for (buf) |c| switch (c) { // we don't wanna worry about case when using + '+', '=' => timeout += 50, '-' => timeout = std.math.clamp(timeout - 50, 1, 1000), 'n' => break :run, // next generation 'q' => { running = false; break :run; }, 'i' => interactive = !interactive, // interactive exclusive keys else => if (interactive) switch (c) { // movement keys, all movement is clamped before it's displayed 'h' => cursor_x -= 1, 'j' => cursor_y += 1, 'k' => cursor_y -= 1, 'l' => cursor_x += 1, '_' => cursor_x = 1, '$' => cursor_x = @intCast(con.width), 'g' => cursor_y = 1, 'G' => cursor_y = @intCast(con.height), ' ' => { // toggle the current value's aliveness const cell = con.world[@intCast(cursor_y - 1)][@intCast(cursor_x - 1)]; cell.alive = !cell.alive; // prevent the cell from coming back cell.will_live = false; }, else => break, }, }; } else break; }; try con.writer.print("\n\x1B[{}A\n", .{height}); try con.writer.flush(); con.deinit(); }