1
0
Fork 0

Began rewriting the game as a wasm4 cartridge

This commit is contained in:
Julien Dessaux 2022-09-21 23:19:45 +02:00
parent a9378b5785
commit 8f76ba7826
Signed by: adyxax
GPG key ID: F92E51B86E07177E
10 changed files with 224 additions and 335 deletions

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "lib/spoon"]
path = lib/spoon
url = https://git.sr.ht/~leon_plickat/zig-spoon

View file

@ -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();
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;
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
// Export WASM-4 symbols
lib.export_symbol_names = &[_][]const u8{ "start", "update" };
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);
lib.install();
}

@ -1 +0,0 @@
Subproject commit c2be37a8087ccb6b01981cb617fef7755dc72b68

28
src/ball.zig Normal file
View file

@ -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 };

View file

@ -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 };

View file

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

View file

@ -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;
//----- Globals ---------------------------------------------------------------
var Game: game.Game = undefined;
//----- 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();
}
export fn start() void {
Game.reset();
}
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);
export fn update() void {
Game.draw();
}

View file

@ -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 =
\\████████████████████████████████████████████████████████████████████████████████
\\█ ██ █
\\████████████████████████████████████████████████████████████████████████████████
\\█ █
\\█ █
\\█ █
\\█ █
\\█ █
\\█ █
\\█ █
\\█ █
\\█ █
\\█ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\█ ██ █
\\████████████████████████████████████████████████████████████████████████████████
;

12
src/utils.zig Normal file
View file

@ -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 };

136
src/wasm4.zig Normal file
View file

@ -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;