diff options
-rw-r--r-- | build.zig | 57 | ||||
-rw-r--r-- | src/brothers.zig | 46 | ||||
-rw-r--r-- | src/game.zig | 22 | ||||
-rw-r--r-- | src/main.zig | 80 | ||||
-rw-r--r-- | src/playfield.zig | 38 |
5 files changed, 243 insertions, 0 deletions
diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..f350b17 --- /dev/null +++ b/build.zig @@ -0,0 +1,57 @@ +const std = @import("std"); + +pub fn build(b: *std.build.Builder) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("grenade-brothers", "src/main.zig"); + exe.addPackagePath("spoon", "lib/spoon/import.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const coverage = b.option(bool, "test-coverage", "Generate test coverage") orelse false; + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_tests = b.addTest("src/main.zig"); + exe_tests.addPackagePath("spoon", "lib/spoon/import.zig"); + exe_tests.setTarget(target); + exe_tests.setBuildMode(mode); + + // Code coverage with kcov, we need an allocator for the setup + var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = general_purpose_allocator.deinit(); + const gpa = general_purpose_allocator.allocator(); + // We want to exclude the $HOME/.zig path + const home = std.process.getEnvVarOwned(gpa, "HOME") catch ""; + defer gpa.free(home); + const exclude = std.fmt.allocPrint(gpa, "--exclude-path={s}/.zig/,/usr", .{home}) catch ""; + defer gpa.free(exclude); + if (coverage) { + exe_tests.setExecCmd(&[_]?[]const u8{ + "kcov", + exclude, + //"--path-strip-level=3", // any kcov flags can be specified here + "kcov-output", // output dir for kcov + null, // to get zig to use the --test-cmd-bin flag + }); + } + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&exe_tests.step); +} diff --git a/src/brothers.zig b/src/brothers.zig new file mode 100644 index 0000000..b221ddc --- /dev/null +++ b/src/brothers.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const spoon = @import("spoon"); + +pub const Side = enum(u1) { + left, + right, +}; + +const startingX = [2]f64{ 15, 60 }; +const colors = [2]spoon.Attribute.Colour{ .blue, .red }; + +pub const Brother = struct { + side: Side, + x: f64, + y: f64, + dx: f64, + dy: f64, + pub fn reset(self: *Brother, side: ?Side) void { + if (side) |s| { + self.side = s; + } + self.x = startingX[@enumToInt(self.side)]; + self.y = 17; + self.dx = 0; + self.dy = 0; + } + pub fn draw(self: Brother, rc: *spoon.Term.RenderContext) !void { + try rc.setAttribute(.{ .fg = colors[@enumToInt(self.side)] }); + var iter = std.mem.split(u8, brother, "\n"); + var y = @floatToInt(usize, std.math.round(self.y)); + var x = @floatToInt(usize, std.math.round(self.x)); + while (iter.next()) |line| : (y += 1) { + try rc.moveCursorTo(y, x); + _ = try rc.buffer.writer().write(line); + } + } +}; + +const brother = + \\█ █ + \\█ █ █ + \\█████ + \\█████ + \\█ █ + \\█ █ +; diff --git a/src/game.zig b/src/game.zig new file mode 100644 index 0000000..fc64001 --- /dev/null +++ b/src/game.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const spoon = @import("spoon"); + +const brothers = @import("brothers.zig"); +const playfield = @import("playfield.zig"); + +pub const Game = struct { + brothers: [2]brothers.Brother = undefined, + character: ?brothers.Side = undefined, + pub fn draw(self: Game, rc: *spoon.Term.RenderContext) !void { + try playfield.draw(rc); + try self.brothers[0].draw(rc); + try self.brothers[1].draw(rc); + } + pub fn reset(self: *Game) void { + self.resetRound(); + } + pub fn resetRound(self: *Game) void { + self.brothers[0].reset(brothers.Side.left); + self.brothers[1].reset(brothers.Side.right); + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..cec8688 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,80 @@ +const std = @import("std"); +const spoon = @import("spoon"); + +const game = @import("game.zig"); + +var term: spoon.Term = undefined; +var done: bool = false; + +//----- Game State ----------------------------------------------------------- +var gs: game.Game = undefined; + +//----- Main ----------------------------------------------------------------- +pub fn main() !void { + try term.init(); + defer term.deinit(); + + std.os.sigaction(std.os.SIG.WINCH, &std.os.Sigaction{ + .handler = .{ .handler = handleSigWinch }, + .mask = std.os.empty_sigset, + .flags = 0, + }, null); + + var fds: [1]std.os.pollfd = undefined; + fds[0] = .{ + .fd = term.tty.handle, + .events = std.os.POLL.IN, + .revents = undefined, + }; + + try term.uncook(.{}); + defer term.cook() catch {}; + + try term.fetchSize(); + try term.setWindowTitle("Grenade Brothers", .{}); + + gs.reset(); + try renderAll(); + + var buf: [16]u8 = undefined; + while (!done) { + _ = try std.os.poll(&fds, 100); + + const read = try term.readInput(&buf); + var it = spoon.inputParser(buf[0..read]); + while (it.next()) |in| { + if (in.eqlDescription("escape") or in.eqlDescription("q")) { + done = true; + break; + } + } + } +} + +fn renderAll() !void { + var rc = try term.getRenderContext(); + defer rc.done() catch {}; + + try rc.clear(); + + if (term.width < 80 or term.width < 24) { + try rc.setAttribute(.{ .fg = .red, .bold = true }); + try rc.writeAllWrapping("Terminal too small!"); + return; + } + + try gs.draw(&rc); +} + +fn handleSigWinch(_: c_int) callconv(.C) void { + term.fetchSize() catch {}; + renderAll() catch {}; +} + +/// Custom panic handler, so that we can try to cook the terminal on a crash, +/// as otherwise all messages will be mangled. +pub fn panic(msg: []const u8, trace: ?*std.builtin.StackTrace) noreturn { + @setCold(true); + term.cook() catch {}; + std.builtin.default_panic(msg, trace); +} diff --git a/src/playfield.zig b/src/playfield.zig new file mode 100644 index 0000000..3878323 --- /dev/null +++ b/src/playfield.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const spoon = @import("spoon"); + +pub fn draw(rc: *spoon.Term.RenderContext) !void { + var iter = std.mem.split(u8, field, "\n"); + var y: usize = 0; + while (iter.next()) |line| : (y += 1) { + try rc.moveCursorTo(y, 0); + _ = try rc.buffer.writer().write(line); + } +} + +const field = + \\████████████████████████████████████████████████████████████████████████████████ + \\█ ██ █ + \\████████████████████████████████████████████████████████████████████████████████ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\█ ██ █ + \\████████████████████████████████████████████████████████████████████████████████ +; |