womblic/src/subcmds/arcade/conway.zig

214 lines
7 KiB
Zig

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