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
|
||||
|
||||
- [ ] set up build.zig with raylib-zig dependency
|
||||
- [ ] create fixed-point math module (Fixed struct, add/sub/mul/div)
|
||||
- [ ] create trig lookup tables (sin/cos at comptime)
|
||||
- [ ] basic window opens with raylib
|
||||
- [x] set up build.zig with raylib-zig dependency
|
||||
- [x] create fixed-point math module (Fixed struct, add/sub/mul/div)
|
||||
- [x] create trig lookup tables (sin/cos at comptime)
|
||||
- [x] basic window opens with raylib
|
||||
|
||||
## 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