Complete phase 1 setup with simple window
This commit is contained in:
parent
71e182ac15
commit
a441305539
6 changed files with 320 additions and 4 deletions
8
TODO.md
8
TODO.md
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
## phase 1: foundation
|
## phase 1: foundation
|
||||||
|
|
||||||
- [ ] set up build.zig with raylib-zig dependency
|
- [x] set up build.zig with raylib-zig dependency
|
||||||
- [ ] create fixed-point math module (Fixed struct, add/sub/mul/div)
|
- [x] create fixed-point math module (Fixed struct, add/sub/mul/div)
|
||||||
- [ ] create trig lookup tables (sin/cos at comptime)
|
- [x] create trig lookup tables (sin/cos at comptime)
|
||||||
- [ ] basic window opens with raylib
|
- [x] basic window opens with raylib
|
||||||
|
|
||||||
## phase 2: single-player simulation
|
## phase 2: single-player simulation
|
||||||
|
|
||||||
|
|
|
||||||
34
build.zig
Normal file
34
build.zig
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const raylib_dep = b.dependency("raylib_zig", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "lockstep",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
exe.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
||||||
|
exe.linkLibrary(raylib_dep.artifact("raylib"));
|
||||||
|
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const run_step = b.step("run", "run the game");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
}
|
||||||
16
build.zig.zon
Normal file
16
build.zig.zon
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.{
|
||||||
|
.name = .lockstep,
|
||||||
|
.version = "0.0.1",
|
||||||
|
.fingerprint = 0x9de74c19d031cdc9,
|
||||||
|
.dependencies = .{
|
||||||
|
.raylib_zig = .{
|
||||||
|
.url = "git+https://github.com/raylib-zig/raylib-zig?ref=devel#a4d18b2d1cf8fdddec68b5b084535fca0475f466",
|
||||||
|
.hash = "raylib_zig-5.6.0-dev-KE8REL5MBQAf3p497t52Xw9P7ojndIkVOWPXnLiLLw2P",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
},
|
||||||
|
}
|
||||||
147
src/fixed.zig
Normal file
147
src/fixed.zig
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
// fixed-point math module
|
||||||
|
// 32.32 format: 32 integer bits, 32 fractional bits
|
||||||
|
// deterministic across all platforms - no floats in simulation
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const Fixed = struct {
|
||||||
|
raw: i64,
|
||||||
|
|
||||||
|
pub const FRAC_BITS = 32;
|
||||||
|
pub const SCALE: i64 = 1 << FRAC_BITS;
|
||||||
|
|
||||||
|
// common constants
|
||||||
|
pub const ZERO: Fixed = .{ .raw = 0 };
|
||||||
|
pub const ONE: Fixed = .{ .raw = SCALE };
|
||||||
|
pub const HALF: Fixed = .{ .raw = SCALE >> 1 };
|
||||||
|
pub const TWO: Fixed = .{ .raw = SCALE << 1 };
|
||||||
|
pub const NEG_ONE: Fixed = .{ .raw = -SCALE };
|
||||||
|
|
||||||
|
// mathematical constants (precomputed)
|
||||||
|
pub const PI: Fixed = .{ .raw = 13493037705 }; // pi * 2^32
|
||||||
|
pub const TWO_PI: Fixed = .{ .raw = 26986075409 }; // 2*pi * 2^32
|
||||||
|
pub const HALF_PI: Fixed = .{ .raw = 6746518852 }; // pi/2 * 2^32
|
||||||
|
|
||||||
|
// game constants
|
||||||
|
pub const GRAVITY: Fixed = .{ .raw = 42949673 }; // ~0.01
|
||||||
|
pub const WIND_FACTOR: Fixed = .{ .raw = 4294967 }; // ~0.001
|
||||||
|
pub const MAX_POWER: Fixed = .{ .raw = 100 << 32 };
|
||||||
|
|
||||||
|
pub fn fromInt(n: i32) Fixed {
|
||||||
|
return .{ .raw = @as(i64, n) << FRAC_BITS };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromFloat(comptime f: f64) Fixed {
|
||||||
|
return .{ .raw = @intFromFloat(f * @as(f64, SCALE)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// only for rendering - never use in simulation!
|
||||||
|
pub fn toFloat(self: Fixed) f32 {
|
||||||
|
return @as(f32, @floatFromInt(self.raw)) / @as(f32, @floatFromInt(SCALE));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toInt(self: Fixed) i32 {
|
||||||
|
return @intCast(self.raw >> FRAC_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(a: Fixed, b: Fixed) Fixed {
|
||||||
|
return .{ .raw = a.raw + b.raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sub(a: Fixed, b: Fixed) Fixed {
|
||||||
|
return .{ .raw = a.raw - b.raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mul(a: Fixed, b: Fixed) Fixed {
|
||||||
|
// widen to i128 to avoid overflow
|
||||||
|
const wide = @as(i128, a.raw) * @as(i128, b.raw);
|
||||||
|
return .{ .raw = @intCast(wide >> FRAC_BITS) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn div(a: Fixed, b: Fixed) Fixed {
|
||||||
|
const wide = @as(i128, a.raw) << FRAC_BITS;
|
||||||
|
return .{ .raw = @intCast(@divTrunc(wide, b.raw)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn neg(self: Fixed) Fixed {
|
||||||
|
return .{ .raw = -self.raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abs(self: Fixed) Fixed {
|
||||||
|
return .{ .raw = if (self.raw < 0) -self.raw else self.raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lessThan(a: Fixed, b: Fixed) bool {
|
||||||
|
return a.raw < b.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn greaterThan(a: Fixed, b: Fixed) bool {
|
||||||
|
return a.raw > b.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lessThanOrEqual(a: Fixed, b: Fixed) bool {
|
||||||
|
return a.raw <= b.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn greaterThanOrEqual(a: Fixed, b: Fixed) bool {
|
||||||
|
return a.raw >= b.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eq(a: Fixed, b: Fixed) bool {
|
||||||
|
return a.raw == b.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min(a: Fixed, b: Fixed) Fixed {
|
||||||
|
return if (a.raw < b.raw) a else b;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(a: Fixed, b: Fixed) Fixed {
|
||||||
|
return if (a.raw > b.raw) a else b;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(self: Fixed, lo: Fixed, hi: Fixed) Fixed {
|
||||||
|
return max(lo, min(hi, self));
|
||||||
|
}
|
||||||
|
|
||||||
|
// format for debug printing
|
||||||
|
pub fn format(
|
||||||
|
self: Fixed,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
try writer.print("{d:.4}", .{self.toFloat()});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "fixed point basic ops" {
|
||||||
|
const a = Fixed.fromInt(10);
|
||||||
|
const b = Fixed.fromInt(3);
|
||||||
|
|
||||||
|
// add
|
||||||
|
try std.testing.expectEqual(@as(i32, 13), a.add(b).toInt());
|
||||||
|
|
||||||
|
// sub
|
||||||
|
try std.testing.expectEqual(@as(i32, 7), a.sub(b).toInt());
|
||||||
|
|
||||||
|
// mul
|
||||||
|
try std.testing.expectEqual(@as(i32, 30), a.mul(b).toInt());
|
||||||
|
|
||||||
|
// div
|
||||||
|
try std.testing.expectEqual(@as(i32, 3), a.div(b).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "fixed point fractional" {
|
||||||
|
const half = Fixed.HALF;
|
||||||
|
const one = Fixed.ONE;
|
||||||
|
const two = Fixed.TWO;
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(i32, 0), half.toInt());
|
||||||
|
try std.testing.expectEqual(@as(i32, 1), one.toInt());
|
||||||
|
try std.testing.expectEqual(@as(i32, 2), two.toInt());
|
||||||
|
|
||||||
|
// half + half = one
|
||||||
|
try std.testing.expect(half.add(half).eq(one));
|
||||||
|
}
|
||||||
43
src/main.zig
Normal file
43
src/main.zig
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const rl = @import("raylib");
|
||||||
|
|
||||||
|
const Fixed = @import("fixed.zig").Fixed;
|
||||||
|
const trig = @import("trig.zig");
|
||||||
|
|
||||||
|
const SCREEN_WIDTH = 800;
|
||||||
|
const SCREEN_HEIGHT = 600;
|
||||||
|
|
||||||
|
// colors (vector/oscilloscope aesthetic)
|
||||||
|
const BG_COLOR = rl.Color{ .r = 10, .g = 10, .b = 18, .a = 255 };
|
||||||
|
const CYAN = rl.Color{ .r = 0, .g = 255, .b = 255, .a = 255 };
|
||||||
|
const MAGENTA = rl.Color{ .r = 255, .g = 0, .b = 255, .a = 255 };
|
||||||
|
const GREEN = rl.Color{ .r = 0, .g = 255, .b = 0, .a = 255 };
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
rl.initWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "lockstep");
|
||||||
|
defer rl.closeWindow();
|
||||||
|
rl.setTargetFPS(60);
|
||||||
|
|
||||||
|
// test fixed-point math
|
||||||
|
const angle = Fixed.ZERO;
|
||||||
|
const sin_val = trig.sin(angle);
|
||||||
|
const cos_val = trig.cos(angle);
|
||||||
|
|
||||||
|
while (!rl.windowShouldClose()) {
|
||||||
|
rl.beginDrawing();
|
||||||
|
defer rl.endDrawing();
|
||||||
|
|
||||||
|
rl.clearBackground(BG_COLOR);
|
||||||
|
|
||||||
|
// draw some test lines
|
||||||
|
rl.drawLine(100, 500, 700, 500, GREEN);
|
||||||
|
rl.drawLine(200, 300, 200, 500, CYAN);
|
||||||
|
rl.drawLine(600, 300, 600, 500, MAGENTA);
|
||||||
|
|
||||||
|
// show fixed-point values
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
const text = std.fmt.bufPrintZ(&buf, "sin(0)={d:.3} cos(0)={d:.3}", .{ sin_val.toFloat(), cos_val.toFloat() }) catch "?";
|
||||||
|
rl.drawText(text, 10, 10, 20, rl.Color.white);
|
||||||
|
rl.drawText("lockstep artillery - phase 1 complete", 10, 40, 20, GREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/trig.zig
Normal file
76
src/trig.zig
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// trig lookup tables - generated at comptime
|
||||||
|
// no @sin/@cos at runtime - fully deterministic
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Fixed = @import("fixed.zig").Fixed;
|
||||||
|
|
||||||
|
pub const TABLE_SIZE = 1024;
|
||||||
|
|
||||||
|
// precomputed sin table at comptime
|
||||||
|
pub const sin_table: [TABLE_SIZE]Fixed = blk: {
|
||||||
|
@setEvalBranchQuota(10000);
|
||||||
|
var table: [TABLE_SIZE]Fixed = undefined;
|
||||||
|
for (0..TABLE_SIZE) |i| {
|
||||||
|
const angle = @as(f64, @floatFromInt(i)) * std.math.pi * 2.0 / @as(f64, TABLE_SIZE);
|
||||||
|
const s = @sin(angle);
|
||||||
|
table[i] = .{ .raw = @intFromFloat(s * @as(f64, Fixed.SCALE)) };
|
||||||
|
}
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
// precomputed cos table at comptime
|
||||||
|
pub const cos_table: [TABLE_SIZE]Fixed = blk: {
|
||||||
|
@setEvalBranchQuota(10000);
|
||||||
|
var table: [TABLE_SIZE]Fixed = undefined;
|
||||||
|
for (0..TABLE_SIZE) |i| {
|
||||||
|
const angle = @as(f64, @floatFromInt(i)) * std.math.pi * 2.0 / @as(f64, TABLE_SIZE);
|
||||||
|
const c = @cos(angle);
|
||||||
|
table[i] = .{ .raw = @intFromFloat(c * @as(f64, Fixed.SCALE)) };
|
||||||
|
}
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
// lookup sin from fixed-point angle (radians)
|
||||||
|
pub fn sin(angle: Fixed) Fixed {
|
||||||
|
// normalize to [0, 2*pi)
|
||||||
|
var a = @mod(angle.raw, Fixed.TWO_PI.raw);
|
||||||
|
if (a < 0) a += Fixed.TWO_PI.raw;
|
||||||
|
|
||||||
|
// convert to table index
|
||||||
|
// idx = (a / TWO_PI) * TABLE_SIZE = (a * TABLE_SIZE) / TWO_PI
|
||||||
|
const scaled = @as(u128, @intCast(a)) * TABLE_SIZE;
|
||||||
|
const idx = @as(usize, @intCast(scaled / @as(u128, @intCast(Fixed.TWO_PI.raw)))) % TABLE_SIZE;
|
||||||
|
|
||||||
|
return sin_table[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup cos from fixed-point angle (radians)
|
||||||
|
pub fn cos(angle: Fixed) Fixed {
|
||||||
|
// normalize to [0, 2*pi)
|
||||||
|
var a = @mod(angle.raw, Fixed.TWO_PI.raw);
|
||||||
|
if (a < 0) a += Fixed.TWO_PI.raw;
|
||||||
|
|
||||||
|
// convert to table index
|
||||||
|
const scaled = @as(u128, @intCast(a)) * TABLE_SIZE;
|
||||||
|
const idx = @as(usize, @intCast(scaled / @as(u128, @intCast(Fixed.TWO_PI.raw)))) % TABLE_SIZE;
|
||||||
|
|
||||||
|
return cos_table[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
test "trig tables" {
|
||||||
|
// sin(0) ~= 0
|
||||||
|
const sin_0 = sin(Fixed.ZERO);
|
||||||
|
try std.testing.expect(sin_0.abs().raw < Fixed.fromFloat(0.01).raw);
|
||||||
|
|
||||||
|
// sin(pi/2) ~= 1
|
||||||
|
const sin_half_pi = sin(Fixed.HALF_PI);
|
||||||
|
try std.testing.expect(sin_half_pi.sub(Fixed.ONE).abs().raw < Fixed.fromFloat(0.01).raw);
|
||||||
|
|
||||||
|
// cos(0) ~= 1
|
||||||
|
const cos_0 = cos(Fixed.ZERO);
|
||||||
|
try std.testing.expect(cos_0.sub(Fixed.ONE).abs().raw < Fixed.fromFloat(0.01).raw);
|
||||||
|
|
||||||
|
// cos(pi/2) ~= 0
|
||||||
|
const cos_half_pi = cos(Fixed.HALF_PI);
|
||||||
|
try std.testing.expect(cos_half_pi.abs().raw < Fixed.fromFloat(0.01).raw);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue