From 8f76ba782669bc986a5e450e5a50ac84232c1323 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Wed, 21 Sep 2022 23:19:45 +0200 Subject: Began rewriting the game as a wasm4 cartridge --- .gitmodules | 3 -- build.zig | 61 +++++------------------- lib/spoon | 1 - src/ball.zig | 28 +++++++++++ src/brothers.zig | 131 +++++++--------------------------------------------- src/game.zig | 47 +++++-------------- src/main.zig | 102 ++++------------------------------------ src/playfield.zig | 38 --------------- src/utils.zig | 12 +++++ src/wasm4.zig | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 224 insertions(+), 335 deletions(-) delete mode 160000 lib/spoon create mode 100644 src/ball.zig delete mode 100644 src/playfield.zig create mode 100644 src/utils.zig create mode 100644 src/wasm4.zig diff --git a/.gitmodules b/.gitmodules index 254eeba..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "lib/spoon"] - path = lib/spoon - url = https://git.sr.ht/~leon_plickat/zig-spoon diff --git a/build.zig b/build.zig index f350b17..7431b33 100644 --- a/build.zig +++ b/build.zig @@ -1,57 +1,18 @@ 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. +pub fn build(b: *std.build.Builder) !void { const mode = b.standardReleaseOptions(); + const lib = b.addSharedLibrary("cart", "src/main.zig", .unversioned); - 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); + lib.setBuildMode(mode); + lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + lib.import_memory = true; + lib.initial_memory = 65536; + lib.max_memory = 65536; + lib.stack_size = 14752; - // 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 - }); - } + // Export WASM-4 symbols + lib.export_symbol_names = &[_][]const u8{ "start", "update" }; - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&exe_tests.step); + lib.install(); } diff --git a/lib/spoon b/lib/spoon deleted file mode 160000 index c2be37a..0000000 --- a/lib/spoon +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c2be37a8087ccb6b01981cb617fef7755dc72b68 diff --git a/src/ball.zig b/src/ball.zig new file mode 100644 index 0000000..11737bb --- /dev/null +++ b/src/ball.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const utils = @import("utils.zig"); +const w4 = @import("wasm4.zig"); + +pub const Ball = struct { + x: f64, + y: f64, + dx: f64, + dy: f64, + pub fn draw(self: Ball) void { + var y = @floatToInt(u8, std.math.round(self.y)); + var x = @floatToInt(u8, std.math.round(self.x)); + w4.DRAW_COLORS.* = 0x4321; + w4.blit(&ball, x, y, ball_width, ball_height, ball_flags); + } + pub fn resetRound(self: *Ball, side: utils.side) void { + self.x = @intToFloat(f64, utils.startingX[@enumToInt(side)] + 4); + self.y = 160 - 32 - 8; + self.dx = 0; + self.dy = 0; + } +}; + +//----- Sprite ---------------------------------------------------------------- +const ball_width = 8; +const ball_height = 8; +const ball_flags = 1; // BLIT_2BPP +const ball = [16]u8{ 0x1a, 0xa4, 0x6f, 0xf9, 0xbf, 0xae, 0xbf, 0xae, 0xbf, 0xfe, 0xbf, 0xfe, 0x6f, 0xf9, 0x1a, 0xa4 }; diff --git a/src/brothers.zig b/src/brothers.zig index 6f5fdce..3b92de4 100644 --- a/src/brothers.zig +++ b/src/brothers.zig @@ -1,128 +1,29 @@ const std = @import("std"); -const spoon = @import("spoon"); - -const ball = @import("ball.zig"); - -pub const Side = enum(u1) { - left, - right, -}; - -const startingX = [2]f64{ 15, 60 }; -const colors = [2]spoon.Attribute.Colour{ .blue, .red }; -const leftLimit = [2]f64{ 1, 41 }; -const rightLimit = [2]f64{ 34, 74 }; // (38, 79) minus a brother's width +const utils = @import("utils.zig"); +const w4 = @import("wasm4.zig"); pub const Brother = struct { - side: Side, + side: utils.side, score: u8, - x: f64, + x: u8, y: f64, - dx: f64, - dy: f64, - moveDuration: u64, - 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); - } + pub fn draw(self: Brother) void { + var y = @floatToInt(u8, std.math.round(self.y)); + w4.DRAW_COLORS.* = 0x30; + w4.blit(&brother, self.x, y - brother_height, brother_width, brother_height, brother_flags); } - pub fn moveJump(self: *Brother) void { - if (self.dy == 0) { // no double jumps! TODO allow kicks off the wall - self.dy -= 4 / (1000 / 60.0); - } - } - pub fn moveLeft(self: *Brother) void { - self.dx -= 5 / (1000 / 60.0); - self.moveDuration = 24; - } - pub fn moveRight(self: *Brother) void { - self.dx += 5 / (1000 / 60.0); - self.moveDuration = 24; - } - pub fn reset(self: *Brother, side: Side) void { + pub fn reset(self: *Brother, side: utils.side) void { self.side = side; self.score = 0; } pub fn resetRound(self: *Brother) void { - self.x = startingX[@enumToInt(self.side)]; - self.y = 17; - self.dx = 0; - self.dy = 0; - self.moveDuration = 0; - } - pub fn step(self: *Brother, b: *ball.Ball) void { - // Horizontal movement - const x = self.x + self.dx; - const ll = leftLimit[@enumToInt(self.side)]; - const rl = rightLimit[@enumToInt(self.side)]; - if (x < ll) { - self.x = ll; - self.dx = 0; - self.moveDuration = 0; - } else if (x > rl) { - self.x = rl; - self.dx = 0; - self.moveDuration = 0; - } else { - self.x = x; - if (self.moveDuration > 0) { - self.moveDuration -= 1; - if (self.moveDuration == 0) { - self.dx = 0; - } - } - } - // Vertical movement - const y = self.y + self.dy; - if (y < 12) { // jumping - self.y = 12; - self.dy = -self.dy; - } else if (y > 17) { // falling - self.y = 17; - self.dy = 0; - } else { - self.y = y; - } - // Check for ball collisions - if (b.y >= y and b.y <= y + 2 and b.x >= x and b.x < x + 5) { - if (b.dy > 0) { - b.dy = -b.dy / 1.5; - } - b.dx = b.dx / 2.0; - var strength: f64 = 1; - if (b.dx > 0 and self.dx < 0) - strength *= 2; // moving in opposite directions - if (y < 12) { // jumping - strength *= 2; - } - if (b.x < x + 1) { - b.dx -= strength * 4 / (1000 / 60.0); - } else if (b.x < x + 2) { - b.dx -= strength * 2 / (1000 / 60.0); - } else if (b.x < x + 3) { - var modifier: f64 = 1; - if (self.side == .left) modifier = -1; - b.dx += modifier * strength * 2 / (1000 / 60.0); - } else if (b.x < x + 4) { - b.dx += strength * 2 / (1000 / 60.0); - } else { - b.dx += strength * 4 / (1000 / 60.0); - } - b.dy = b.dy * strength - 0.04; - } + self.x = utils.startingX[@enumToInt(self.side)]; + self.y = 160; } }; -const brother = - \\█ █ - \\█ █ █ - \\█████ - \\█████ - \\█ █ - \\█ █ -; +//----- Sprite ---------------------------------------------------------------- +const brother_width = 16; +const brother_height = 32; +const brother_flags = 0; // BLIT_1BPP +const brother = [64]u8{ 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xfb, 0xdf, 0xfb, 0xdf, 0xfb, 0xdf, 0xfb, 0xdf, 0xf9, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f }; diff --git a/src/game.zig b/src/game.zig index 7f44856..7dbba66 100644 --- a/src/game.zig +++ b/src/game.zig @@ -1,50 +1,27 @@ const std = @import("std"); -const spoon = @import("spoon"); +const utils = @import("utils.zig"); +const w4 = @import("wasm4.zig"); const ball = @import("ball.zig"); const brothers = @import("brothers.zig"); -const playfield = @import("playfield.zig"); pub const Game = struct { ball: ball.Ball = undefined, brothers: [2]brothers.Brother = undefined, - side: 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); - try self.ball.draw(rc); - var score: [1]u8 = undefined; - try rc.moveCursorTo(1, 3); - score[0] = '0' + self.brothers[0].score; - _ = try rc.buffer.writer().write(score[0..]); - try rc.moveCursorTo(1, 76); - score[0] = '0' + self.brothers[1].score; - _ = try rc.buffer.writer().write(score[0..]); + playerSide: utils.side = undefined, + pub fn draw(self: *Game) void { + self.ball.draw(); + self.brothers[0].draw(); + self.brothers[1].draw(); + // draw the net + w4.DRAW_COLORS.* = 0x42; + w4.rect(78, 100, 4, 61); } - pub fn moveJump(self: *Game, side: brothers.Side) void { - self.brothers[@enumToInt(side)].moveJump(); - } - pub fn moveLeft(self: *Game, side: brothers.Side) void { - self.brothers[@enumToInt(side)].moveLeft(); - } - pub fn moveRight(self: *Game, side: brothers.Side) void { - self.brothers[@enumToInt(side)].moveRight(); - } - pub fn reset(self: *Game, side: brothers.Side) void { - self.side = side; + pub fn reset(self: *Game) void { self.brothers[0].reset(.left); self.brothers[1].reset(.right); - self.resetRound(); - } - pub fn resetRound(self: *Game) void { - self.ball.reset(.left); + self.ball.resetRound(.left); self.brothers[0].resetRound(); self.brothers[1].resetRound(); } - pub fn step(self: *Game) void { - self.ball.step(); - self.brothers[0].step(&self.ball); - self.brothers[1].step(&self.ball); - } }; diff --git a/src/main.zig b/src/main.zig index 875b02c..29b7db5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,99 +1,15 @@ -const std = @import("std"); -const spoon = @import("spoon"); - const game = @import("game.zig"); +const std = @import("std"); +const utils = @import("utils.zig"); +const w4 = @import("wasm4.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(.left); - try renderAll(); - - var buf: [16]u8 = undefined; - while (!done) { - // TODO We need to measure how long it took before a key was hit so that we can adjust the timeout on the next loop - // otherwise we will get inconsistent ticks for movement steps - const timeout = try std.os.poll(&fds, @floatToInt(u64, 1000 / 60.0)); - - if (timeout > 0) { // if timeout if not 0 then some fds we are polling have events for us - 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; - } else if (in.eqlDescription("arrow-left")) { - gs.moveLeft(.right); - } else if (in.eqlDescription("arrow-right")) { - gs.moveRight(.right); - } else if (in.eqlDescription("arrow-up")) { - gs.moveJump(.right); - } else if (in.eqlDescription("a")) { - gs.moveLeft(.left); - } else if (in.eqlDescription("d")) { - gs.moveRight(.left); - } else if (in.eqlDescription("space")) { - gs.moveJump(.left); - } - } - } else { - gs.step(); - } - try renderAll(); - } -} - -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); -} +//----- Globals --------------------------------------------------------------- +var Game: game.Game = undefined; -fn handleSigWinch(_: c_int) callconv(.C) void { - term.fetchSize() catch {}; - renderAll() catch {}; +export fn start() void { + Game.reset(); } -/// 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); +export fn update() void { + Game.draw(); } diff --git a/src/playfield.zig b/src/playfield.zig deleted file mode 100644 index 757ec60..0000000 --- a/src/playfield.zig +++ /dev/null @@ -1,38 +0,0 @@ -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 = - \\████████████████████████████████████████████████████████████████████████████████ - \\█ ██ █ - \\████████████████████████████████████████████████████████████████████████████████ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\█ ██ █ - \\████████████████████████████████████████████████████████████████████████████████ -; diff --git a/src/utils.zig b/src/utils.zig new file mode 100644 index 0000000..5fa40f9 --- /dev/null +++ b/src/utils.zig @@ -0,0 +1,12 @@ +//----- Physics --------------------------------------------------------------- +pub const gravity: f64 = 9.807; // m/s² +pub const scale: f64 = 1.0 / 30; // 30 pixels == 1m + +//----- Playground ------------------------------------------------------------ +pub const side = enum(u1) { + left, + right, +}; +pub const startingX = [2]u8{ 23, 160 - 23 - 16 }; +pub const leftLimit = [2]u8{ 0, 81 }; +pub const rightLimit = [2]u8{ 77, 159 }; diff --git a/src/wasm4.zig b/src/wasm4.zig new file mode 100644 index 0000000..c46a261 --- /dev/null +++ b/src/wasm4.zig @@ -0,0 +1,136 @@ +// +// WASM-4: https://wasm4.org/docs + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Platform Constants │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +pub const SCREEN_SIZE: u32 = 160; + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Memory Addresses │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +pub const PALETTE: *[4]u32 = @intToPtr(*[4]u32, 0x04); +pub const DRAW_COLORS: *u16 = @intToPtr(*u16, 0x14); +pub const GAMEPAD1: *const u8 = @intToPtr(*const u8, 0x16); +pub const GAMEPAD2: *const u8 = @intToPtr(*const u8, 0x17); +pub const GAMEPAD3: *const u8 = @intToPtr(*const u8, 0x18); +pub const GAMEPAD4: *const u8 = @intToPtr(*const u8, 0x19); +pub const MOUSE_X: *const i16 = @intToPtr(*const i16, 0x1a); +pub const MOUSE_Y: *const i16 = @intToPtr(*const i16, 0x1c); +pub const MOUSE_BUTTONS: *const u8 = @intToPtr(*const u8, 0x1e); +pub const SYSTEM_FLAGS: *u8 = @intToPtr(*u8, 0x1f); +pub const NETPLAY: *const u8 = @intToPtr(*const u8, 0x20); +pub const FRAMEBUFFER: *[6400]u8 = @intToPtr(*[6400]u8, 0xA0); + +pub const BUTTON_1: u8 = 1; +pub const BUTTON_2: u8 = 2; +pub const BUTTON_LEFT: u8 = 16; +pub const BUTTON_RIGHT: u8 = 32; +pub const BUTTON_UP: u8 = 64; +pub const BUTTON_DOWN: u8 = 128; + +pub const MOUSE_LEFT: u8 = 1; +pub const MOUSE_RIGHT: u8 = 2; +pub const MOUSE_MIDDLE: u8 = 4; + +pub const SYSTEM_PRESERVE_FRAMEBUFFER: u8 = 1; +pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2; + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Drawing Functions │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +/// Copies pixels to the framebuffer. +pub extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: u32) void; + +/// Copies a subregion within a larger sprite atlas to the framebuffer. +pub extern fn blitSub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: u32) void; + +pub const BLIT_2BPP: u32 = 1; +pub const BLIT_1BPP: u32 = 0; +pub const BLIT_FLIP_X: u32 = 2; +pub const BLIT_FLIP_Y: u32 = 4; +pub const BLIT_ROTATE: u32 = 8; + +/// Draws a line between two points. +pub extern fn line(x1: i32, y1: i32, x2: i32, y2: i32) void; + +/// Draws an oval (or circle). +pub extern fn oval(x: i32, y: i32, width: u32, height: u32) void; + +/// Draws a rectangle. +pub extern fn rect(x: i32, y: i32, width: u32, height: u32) void; + +/// Draws text using the built-in system font. +pub fn text(str: []const u8, x: i32, y: i32) void { + textUtf8(str.ptr, str.len, x, y); +} +extern fn textUtf8(strPtr: [*]const u8, strLen: usize, x: i32, y: i32) void; + +/// Draws a vertical line +pub extern fn vline(x: i32, y: i32, len: u32) void; + +/// Draws a horizontal line +pub extern fn hline(x: i32, y: i32, len: u32) void; + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Sound Functions │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +/// Plays a sound tone. +pub extern fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void; + +pub const TONE_PULSE1: u32 = 0; +pub const TONE_PULSE2: u32 = 1; +pub const TONE_TRIANGLE: u32 = 2; +pub const TONE_NOISE: u32 = 3; +pub const TONE_MODE1: u32 = 0; +pub const TONE_MODE2: u32 = 4; +pub const TONE_MODE3: u32 = 8; +pub const TONE_MODE4: u32 = 12; +pub const TONE_PAN_LEFT: u32 = 16; +pub const TONE_PAN_RIGHT: u32 = 32; + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Storage Functions │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +/// Reads up to `size` bytes from persistent storage into the pointer `dest`. +pub extern fn diskr(dest: [*]u8, size: u32) u32; + +/// Writes up to `size` bytes from the pointer `src` into persistent storage. +pub extern fn diskw(src: [*]const u8, size: u32) u32; + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Other Functions │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +/// Prints a message to the debug console. +pub fn trace(x: []const u8) void { + traceUtf8(x.ptr, x.len); +} +extern fn traceUtf8(strPtr: [*]const u8, strLen: usize) void; + +/// Use with caution, as there's no compile-time type checking. +/// +/// * %c, %d, and %x expect 32-bit integers. +/// * %f expects 64-bit floats. +/// * %s expects a *zero-terminated* string pointer. +/// +/// See https://github.com/aduros/wasm4/issues/244 for discussion and type-safe +/// alternatives. +pub extern fn tracef(x: [*:0]const u8, ...) void; -- cgit v1.2.3