From 9bb72f3cdb54742b29d2f8481a11b54ee55484b9 Mon Sep 17 00:00:00 2001 From: Squibid Date: Tue, 23 Dec 2025 20:14:47 -0500 Subject: [PATCH] interactive conways game of life --- src/subcmds/arcade/arcade.zig | 14 +++- src/subcmds/arcade/conway.zig | 125 +++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 24 deletions(-) diff --git a/src/subcmds/arcade/arcade.zig b/src/subcmds/arcade/arcade.zig index 03a425e..7c93c6d 100644 --- a/src/subcmds/arcade/arcade.zig +++ b/src/subcmds/arcade/arcade.zig @@ -2,12 +2,24 @@ const std = @import("std"); const Conway = @import("conway.zig"); +const games = enum{ conway }; + pub export fn arcade_subcmd(_: *anyopaque, argc: c_int, argv: [*c]u8) callconv(.c) void { _ = argc; _ = argv; + var terminfo: std.posix.winsize = undefined; + switch (std.posix.errno(std.posix.system.ioctl( + std.posix.STDOUT_FILENO, + std.posix.T.IOCGWINSZ, + @intFromPtr(&terminfo), + ))) { + .SUCCESS => {}, + else => unreachable, + } + // const title = @embedFile("arcade.txt"); var buf: [4096]u8 = undefined; const writer = std.fs.File.stdout().writer(&buf); - Conway.play(40, 40, @constCast(&writer.interface)) catch unreachable; + Conway.play(terminfo.col, terminfo.row, @constCast(&writer.interface)) catch unreachable; } diff --git a/src/subcmds/arcade/conway.zig b/src/subcmds/arcade/conway.zig index 613781c..2fae118 100644 --- a/src/subcmds/arcade/conway.zig +++ b/src/subcmds/arcade/conway.zig @@ -1,4 +1,3 @@ -// TODO: make interactive const Conway = @This(); const std = @import("std"); @@ -28,6 +27,9 @@ const Cell = struct { }; 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, @@ -36,11 +38,11 @@ pub fn init(width: u64, height: u64, writer: *std.Io.Writer) !Conway { }; // create the world and the cells - self.world = try gpa.alloc([]*Cell, width); + self.world = try gpa.alloc([]*Cell, height); for (self.world, 0..) |col, i| { - self.world[i] = try gpa.alloc(*Cell, height); + self.world[i] = try gpa.alloc(*Cell, width); for (col, 0..) |_, j| { - if (j >= height) break; + if (j >= width) break; self.world[i][j] = try gpa.create(Cell); } } @@ -52,9 +54,9 @@ pub fn init(width: u64, height: u64, writer: *std.Io.Writer) !Conway { // 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 == width - 1) null else self.world[i + 1][j]; + 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 == height - 1) null else self.world[i][j + 1]; + cell.neighbors[3] = if (j == width - 1) null else self.world[i][j + 1]; cell.alive = false; }; @@ -100,6 +102,7 @@ pub fn step(self: *Conway) void { for (self.world) |col| for (col) |cell| { cell.alive = cell.will_live; }; + self.generation += 1; } pub fn print(self: *Conway) !void { @@ -109,27 +112,103 @@ pub fn print(self: *Conway) !void { } _ = try self.writer.write("\n"); } - try self.writer.flush(); } pub fn play(width: u64, height: u64, writer: *std.Io.Writer) !void { - var con = try Conway.init(width, height, writer); + var con = try Conway.init(width, height - 1, writer); - con.world[4][2].alive = true; - con.world[4][3].alive = true; - con.world[4][4].alive = true; - con.world[4][5].alive = true; - con.world[4][6].alive = true; - con.world[4][7].alive = true; - con.world[5][2].alive = true; - con.world[5][3].alive = true; - con.world[5][4].alive = true; - con.world[5][5].alive = true; - con.world[5][6].alive = true; + // 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; - while (true) : (con.step()) { + 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(); - std.posix.nanosleep(0, 250000000); - try con.writer.print("\x1B[{}A", .{con.height}); - } + + // 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(); }