diff --git a/.gitignore b/.gitignore index 8d8da23..ac3dcc2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,17 @@ build/libgoldie-ui.dylib tests/triangulate_test tests/bsp_test tests/bbox_test +tests/collision_test +tests/wad_test +tests/walls_test +tests/player_test triangulate_test bsp_test bbox_test +collision_test +wad_test +walls_test +player_test *.o # Build logs diff --git a/Makefile b/Makefile index f798a28..1b3ff0c 100644 --- a/Makefile +++ b/Makefile @@ -96,7 +96,7 @@ endif OBJS = $(MAPVIEW_OBJS) $(EDITOR_OBJS) $(HEXEN_OBJS) # Targets -.PHONY: all clean test triangulate_test liborion +.PHONY: all clean test triangulate_test bbox_test bsp_test collision_test wad_test walls_test player_test liborion all: liborion mapview @@ -105,7 +105,7 @@ liborion: $(LIBORION) $(LIBORION): @echo "Building liborion via ui/Makefile..." - @$(MAKE) -C $(UI_DIR) all + @$(MAKE) -C $(UI_DIR) library # mapview executable (main executable) mapview: $(OBJS) $(LIBORION) @@ -138,10 +138,15 @@ $(BUILD_DIR)/hexen/%.o: $(HEXEN_DIR)/%.c $(CC) $(CFLAGS) -I. -c $< -o $@ # Test targets -test: triangulate_test bbox_test +test: triangulate_test bbox_test bsp_test collision_test wad_test walls_test player_test @echo "=== Running all tests ===" @./triangulate_test @./bbox_test + @./bsp_test + @./collision_test + @./wad_test + @./walls_test + @./player_test triangulate_test: $(TESTS_DIR)/triangulate_test.c $(MAPVIEW_DIR)/triangulate.c $(CC) -DTEST_MODE -o $@ $^ -I. -lm @@ -149,11 +154,26 @@ triangulate_test: $(TESTS_DIR)/triangulate_test.c $(MAPVIEW_DIR)/triangulate.c bbox_test: $(TESTS_DIR)/bbox_test.c $(CC) -o $@ $< -I. -lm +bsp_test: $(TESTS_DIR)/bsp_test.c + $(CC) -o $@ $< -I. -lm + +collision_test: $(TESTS_DIR)/collision_test.c + $(CC) -o $@ $< -I. -lm + +wad_test: $(TESTS_DIR)/wad_test.c + $(CC) -o $@ $< -I. -lm + +walls_test: $(TESTS_DIR)/walls_test.c + $(CC) -o $@ $< -I. -lm + +player_test: $(TESTS_DIR)/player_test.c + $(CC) -o $@ $< -I. -lm + # Clean clean: -@if [ -f $(UI_DIR)/Makefile ]; then $(MAKE) -C $(UI_DIR) clean; fi rm -rf $(BUILD_DIR) - -rm -f triangulate_test bbox_test doom-ed + -rm -f triangulate_test bbox_test bsp_test collision_test wad_test walls_test player_test doom-ed -test -f mapview && rm -f mapview || true rm -f $(MAPVIEW_DIR)/*.o $(EDITOR_DIR)/*.o $(EDITOR_DIR)/windows/*.o $(EDITOR_DIR)/windows/inspector/*.o rm -f $(HEXEN_DIR)/*.o $(DOOM_DIR)/*.o diff --git a/editor/windows/sprite.c b/editor/windows/sprite.c index 31be1b7..f424db2 100644 --- a/editor/windows/sprite.c +++ b/editor/windows/sprite.c @@ -33,11 +33,11 @@ result_t win_sprite(window_t *win, uint32_t msg, uint32_t wparam, void *lparam) switch (msg) { case evPaint: fill_rect(g_ui_runtime.focused == win ? get_sys_color(brFocusRing) : get_sys_color(brWindowBg), R(win->frame.x-2, win->frame.y-2, win->frame.w+4, win->frame.h+4)); - draw_button(&win->frame, 1, 1, true); + draw_button(win->frame, 1, 1, true); if (!*win->title) return false; if ((spr = find_sprite(win->title))) { rect_t r = fit_sprite(spr, &win->frame); - draw_rect(spr->texture, &r); + draw_rect(spr->texture, r); } else if ((tex = get_flat_texture(win->title))||(tex = get_texture(win->title))) { float scale = fminf(1, fminf(((float)win->frame.w) / tex->width, ((float)win->frame.h) / tex->height)); diff --git a/editor/windows/things.c b/editor/windows/things.c index 91ab6bb..8cb1e72 100644 --- a/editor/windows/things.c +++ b/editor/windows/things.c @@ -53,7 +53,7 @@ result_t win_things(window_t *win, uint32_t msg, uint32_t wparam, void *lparam) uint16_t y = (j / n) * (THING_SIZE+THING_LABEL_HEIGHT); uint16_t tx = x + (THING_SIZE-strwidth(ed_things[i].sprite))/2; rect_t r = fit_sprite(spr, &(rect_t){ x, y, THING_SIZE, THING_SIZE }); - draw_rect(spr->texture, &r); + draw_rect(spr->texture, r); draw_text_small(ed_things[i].sprite, tx, y + THING_SIZE+4, get_sys_color(brTextNormal)); j++; } diff --git a/mapview/map.h b/mapview/map.h index ed83717..32f3f30 100644 --- a/mapview/map.h +++ b/mapview/map.h @@ -387,8 +387,8 @@ int send_message(window_t *win, uint32_t msg, uint32_t wparam, void *lparam); void post_message(window_t *win, uint32_t msg, uint32_t wparam, void *lparam); void invalidate_window(window_t *win); void set_window_item_text(window_t *win, uint32_t id, const char *fmt, ...); -int window_title_bar_y(window_t const *win); window_t *get_window_item(window_t const *win, uint32_t id); + void track_mouse(window_t *win); void set_capture(window_t *win); void set_focus(window_t* win); @@ -433,9 +433,9 @@ void draw_bsp(map_data_t const *map, viewdef_t const *viewdef); void update_player_position_with_sliding(map_data_t const *map, player_t *player, float move_x, float move_y); -void fill_rect(uint32_t color, rect_t const *r); -void draw_rect(int tex, rect_t const *r); -void draw_rect_ex(int tex, rect_t const *r, int type, float alpha); +void fill_rect(uint32_t color, rect_t r); +void draw_rect(int tex, rect_t r); +void draw_rect_ex(int tex, rect_t r, int type, float alpha); void draw_icon8(int icon, int x, int y, uint32_t col); void draw_icon16(int icon, int x, int y, uint32_t col); void draw_palette(map_data_t const *map); diff --git a/mapview/wi_stuff.c b/mapview/wi_stuff.c index 20b425f..3830df6 100644 --- a/mapview/wi_stuff.c +++ b/mapview/wi_stuff.c @@ -94,7 +94,7 @@ void goto_intermisson(void) { void handle_intermission_input(float delta_time) { ui_event_t event; - while (axPollEvent(&event)) { + while (get_message(&event)) { if (event.message == kEventWindowClosed) { g_ui_runtime.running = false; } diff --git a/tests/collision_test.c b/tests/collision_test.c new file mode 100644 index 0000000..1ff3942 --- /dev/null +++ b/tests/collision_test.c @@ -0,0 +1,422 @@ +/* + * Collision Detection Tests + * + * Tests for the pure geometric helper functions in collision.c: + * dist_sq, closest_point_on_line, calc_slide, can_enter_sector + * + * These functions have no external dependencies (no OpenGL, no SDL, no cglm), + * so they can be compiled and tested in isolation. + */ + +#include +#include +#include +#include +#include +#include +#include + +// ── Minimal type stubs ────────────────────────────────────────────────────── + +typedef char texname_t[8]; + +typedef struct { + int16_t x; + int16_t y; +} mapvertex_t; + +typedef struct { + uint16_t start; + uint16_t end; + uint16_t flags; + uint16_t special; + uint16_t tag; + uint16_t sidenum[2]; +} maplinedef_t; + +typedef struct { + int16_t textureoffset; + int16_t rowoffset; + texname_t toptexture; + texname_t bottomtexture; + texname_t midtexture; + uint16_t sector; +} mapsidedef_t; + +typedef struct { + int16_t floorheight; + int16_t ceilingheight; + texname_t floorpic; + texname_t ceilingpic; + int16_t lightlevel; + int16_t special; + int16_t tag; +} mapsector_t; + +typedef struct { + mapvertex_t *vertices; + int num_vertices; + maplinedef_t *linedefs; + int num_linedefs; + mapsidedef_t *sidedefs; + int num_sidedefs; + mapsector_t *sectors; + int num_sectors; +} map_data_t; + +typedef struct { + float x, y, z; + float angle; + float pitch; + float height; + float vel_x, vel_y; + int sector; + int mouse_x_rel; + int mouse_y_rel; + float forward_move; + float strafe_move; +} player_t; + +#define EYE_HEIGHT 48 +#define MAX_STEP 24.0f +#define P_RADIUS 12.0f +#define WALL_DIST 2.0f +#define EPSILON 0.1f + +// ── Functions under test (copied from collision.c to isolate from map.h) ─── + +float dist_sq(float x1, float y1, float x2, float y2) { + float dx = x2 - x1; + float dy = y2 - y1; + return dx*dx + dy*dy; +} + +float closest_point_on_line(float point_x, float point_y, + float line_x1, float line_y1, + float line_x2, float line_y2, + float *closest_x, float *closest_y, + float *t_param) { + float dx = line_x2 - line_x1; + float dy = line_y2 - line_y1; + float len_sq = dx*dx + dy*dy; + + if (len_sq < EPSILON) { + *closest_x = line_x1; + *closest_y = line_y1; + *t_param = 0.0f; + return dist_sq(point_x, point_y, line_x1, line_y1); + } + + float t = ((point_x - line_x1) * dx + (point_y - line_y1) * dy) / len_sq; + t = t < 0.0f ? 0.0f : (t > 1.0f ? 1.0f : t); + + *closest_x = line_x1 + t * dx; + *closest_y = line_y1 + t * dy; + *t_param = t; + + return dist_sq(point_x, point_y, *closest_x, *closest_y); +} + +void calc_slide(float mx, float my, float nx, float ny, float *sx, float *sy) { + float dot = mx * nx + my * ny; + + if (dot > -EPSILON) { + *sx = mx; + *sy = my; + return; + } + + *sx = mx - (nx * dot); + *sy = my - (ny * dot); + + float s_len = sqrt((*sx) * (*sx) + (*sy) * (*sy)); + if (s_len > EPSILON) { + float m_len = sqrt(mx * mx + my * my); + *sx = (*sx) * m_len / s_len; + *sy = (*sy) * m_len / s_len; + } +} + +bool can_enter_sector(mapsector_t const *current, mapsector_t const *new_sector, float player_z) { + if (!new_sector) return false; + + float player_feet = player_z - EYE_HEIGHT; + float floor_diff = new_sector->floorheight - player_feet; + + if (floor_diff > MAX_STEP) return false; + if ((new_sector->ceilingheight - new_sector->floorheight) < EYE_HEIGHT) return false; + + return true; +} + +// ── Test helpers ───────────────────────────────────────────────────────────── + +static int tests_passed = 0; +static int tests_total = 0; + +static int float_eq(float a, float b, float eps) { + return fabsf(a - b) < eps; +} + +#define TEST(name) \ + printf("\nTest %d: %s... ", ++tests_total, name); \ + fflush(stdout) + +#define PASS() \ + printf("PASSED\n"); \ + tests_passed++ + +#define ASSERT(cond, msg) \ + if (!(cond)) { \ + printf("FAILED: %s\n", msg); \ + return; \ + } + +// ── dist_sq tests ──────────────────────────────────────────────────────────── + +static void test_dist_sq_same_point(void) { + TEST("dist_sq: same point returns 0"); + ASSERT(float_eq(dist_sq(5.0f, 5.0f, 5.0f, 5.0f), 0.0f, 1e-6f), + "dist_sq of identical points should be 0"); + PASS(); +} + +static void test_dist_sq_horizontal(void) { + TEST("dist_sq: horizontal distance"); + ASSERT(float_eq(dist_sq(0.0f, 0.0f, 3.0f, 0.0f), 9.0f, 1e-6f), + "dist_sq(0,0,3,0) should be 9"); + PASS(); +} + +static void test_dist_sq_diagonal(void) { + TEST("dist_sq: 3-4-5 triangle"); + ASSERT(float_eq(dist_sq(0.0f, 0.0f, 3.0f, 4.0f), 25.0f, 1e-6f), + "dist_sq(0,0,3,4) should be 25"); + PASS(); +} + +static void test_dist_sq_negative_coords(void) { + TEST("dist_sq: negative coordinates"); + /* (-1,-1) to (2,3): dx=3, dy=4, sq=25 */ + ASSERT(float_eq(dist_sq(-1.0f, -1.0f, 2.0f, 3.0f), 25.0f, 1e-6f), + "dist_sq with negative coords should work"); + PASS(); +} + +// ── closest_point_on_line tests ────────────────────────────────────────────── + +static void test_closest_midpoint(void) { + TEST("closest_point_on_line: point projects to midpoint"); + float cx, cy, t; + /* Horizontal line (0,0)→(10,0); point (5,3) projects to (5,0) */ + float d = closest_point_on_line(5.0f, 3.0f, 0.0f, 0.0f, 10.0f, 0.0f, &cx, &cy, &t); + ASSERT(float_eq(cx, 5.0f, 1e-4f), "Closest x should be 5"); + ASSERT(float_eq(cy, 0.0f, 1e-4f), "Closest y should be 0"); + ASSERT(float_eq(t, 0.5f, 1e-4f), "t should be 0.5"); + ASSERT(float_eq(d, 9.0f, 1e-4f), "Squared distance should be 9"); + PASS(); +} + +static void test_closest_clamped_to_start(void) { + TEST("closest_point_on_line: point behind start clamped to start"); + float cx, cy, t; + /* Line (10,0)→(20,0); point (-5,0) is behind start */ + closest_point_on_line(-5.0f, 0.0f, 10.0f, 0.0f, 20.0f, 0.0f, &cx, &cy, &t); + ASSERT(float_eq(cx, 10.0f, 1e-4f), "Closest x should be clamped to line start"); + ASSERT(float_eq(t, 0.0f, 1e-4f), "t should be 0 (clamped)"); + PASS(); +} + +static void test_closest_clamped_to_end(void) { + TEST("closest_point_on_line: point beyond end clamped to end"); + float cx, cy, t; + /* Line (0,0)→(10,0); point (20,0) is beyond end */ + closest_point_on_line(20.0f, 0.0f, 0.0f, 0.0f, 10.0f, 0.0f, &cx, &cy, &t); + ASSERT(float_eq(cx, 10.0f, 1e-4f), "Closest x should be clamped to line end"); + ASSERT(float_eq(t, 1.0f, 1e-4f), "t should be 1 (clamped)"); + PASS(); +} + +static void test_closest_degenerate_line(void) { + TEST("closest_point_on_line: degenerate (zero-length) line"); + float cx, cy, t; + /* Degenerate line: both endpoints identical */ + float d = closest_point_on_line(3.0f, 4.0f, 0.0f, 0.0f, 0.0f, 0.0f, &cx, &cy, &t); + ASSERT(float_eq(cx, 0.0f, 1e-4f), "Closest x should equal line point"); + ASSERT(float_eq(cy, 0.0f, 1e-4f), "Closest y should equal line point"); + ASSERT(float_eq(t, 0.0f, 1e-4f), "t should be 0"); + ASSERT(float_eq(d, 25.0f, 1e-4f), "Squared distance should be 25 (3-4-5)"); + PASS(); +} + +static void test_closest_diagonal_line(void) { + TEST("closest_point_on_line: diagonal line"); + float cx, cy, t; + /* Line (0,0)→(10,10); point (0,10) projects to (5,5) */ + float d = closest_point_on_line(0.0f, 10.0f, 0.0f, 0.0f, 10.0f, 10.0f, &cx, &cy, &t); + ASSERT(float_eq(cx, 5.0f, 1e-4f), "Closest x should be 5"); + ASSERT(float_eq(cy, 5.0f, 1e-4f), "Closest y should be 5"); + ASSERT(float_eq(t, 0.5f, 1e-4f), "t should be 0.5"); + /* Distance from (0,10) to (5,5) is sqrt(50), squared = 50 */ + ASSERT(float_eq(d, 50.0f, 1e-3f), "Squared distance should be 50"); + PASS(); +} + +// ── calc_slide tests ───────────────────────────────────────────────────────── + +static void test_slide_perpendicular_wall(void) { + TEST("calc_slide: movement into perpendicular wall gives zero slide"); + float sx, sy; + /* Moving straight into a wall with normal (1,0) */ + calc_slide(-5.0f, 0.0f, 1.0f, 0.0f, &sx, &sy); + /* Projection: dot = -5; slide = (-5 - 1*(-5), 0) = (0, 0) */ + ASSERT(float_eq(sx, 0.0f, 1e-3f), "Slide x should be 0 for head-on impact"); + ASSERT(float_eq(sy, 0.0f, 1e-3f), "Slide y should be 0 for head-on impact"); + PASS(); +} + +static void test_slide_along_wall(void) { + TEST("calc_slide: movement parallel to wall is unchanged"); + float sx, sy; + /* Moving along a wall: move=(0,5), normal=(1,0) — dot=0 (not into wall) */ + calc_slide(0.0f, 5.0f, 1.0f, 0.0f, &sx, &sy); + ASSERT(float_eq(sx, 0.0f, 1e-3f), "Slide x should be 0"); + ASSERT(float_eq(sy, 5.0f, 1e-3f), "Slide y should be unchanged (5)"); + PASS(); +} + +static void test_slide_diagonal_into_wall(void) { + TEST("calc_slide: diagonal movement into wall slides along wall"); + float sx, sy; + /* Move (-3,-3), normal (1,0): dot=-3 (into wall) */ + /* slide = (-3 - 1*(-3), -3 - 0*(-3)) = (0, -3) */ + calc_slide(-3.0f, -3.0f, 1.0f, 0.0f, &sx, &sy); + /* Result should be (0, -3) normalized to same speed as original */ + float speed_orig = sqrtf((-3.0f)*(-3.0f) + (-3.0f)*(-3.0f)); + float speed_slide = sqrtf(sx*sx + sy*sy); + ASSERT(float_eq(speed_slide, speed_orig, 1e-3f), "Slide speed should equal move speed"); + ASSERT(sx > -1e-3f, "Slide x should be ~0 (no movement into wall)"); + PASS(); +} + +static void test_slide_away_from_wall(void) { + TEST("calc_slide: movement away from wall is unchanged"); + float sx, sy; + /* Moving away: move=(5,0), normal=(1,0): dot=5 > 0 → pass-through */ + calc_slide(5.0f, 0.0f, 1.0f, 0.0f, &sx, &sy); + ASSERT(float_eq(sx, 5.0f, 1e-3f), "Movement away from wall should be unchanged"); + ASSERT(float_eq(sy, 0.0f, 1e-3f), "Slide y should be 0"); + PASS(); +} + +// ── can_enter_sector tests ─────────────────────────────────────────────────── + +static void test_can_enter_null_sector(void) { + TEST("can_enter_sector: NULL new_sector returns false"); + mapsector_t cur = { .floorheight = 0, .ceilingheight = 128 }; + ASSERT(!can_enter_sector(&cur, NULL, EYE_HEIGHT), "NULL sector should return false"); + PASS(); +} + +static void test_can_enter_flat_floor(void) { + TEST("can_enter_sector: flat floor transition is passable"); + mapsector_t cur = { .floorheight = 0, .ceilingheight = 128 }; + mapsector_t next = { .floorheight = 0, .ceilingheight = 128 }; + ASSERT(can_enter_sector(&cur, &next, (float)EYE_HEIGHT), + "Same-height floors should be passable"); + PASS(); +} + +static void test_can_enter_small_step_up(void) { + TEST("can_enter_sector: step up within MAX_STEP is passable"); + mapsector_t cur = { .floorheight = 0, .ceilingheight = 128 }; + /* floor_diff = 16 - (EYE_HEIGHT - EYE_HEIGHT) = 16 ≤ MAX_STEP(24) */ + mapsector_t next = { .floorheight = 16, .ceilingheight = 144 }; + ASSERT(can_enter_sector(&cur, &next, (float)EYE_HEIGHT), + "Step up of 16 should be passable"); + PASS(); +} + +static void test_can_enter_step_too_high(void) { + TEST("can_enter_sector: step exceeding MAX_STEP is blocked"); + mapsector_t cur = { .floorheight = 0, .ceilingheight = 128 }; + /* floor_diff = 32 > MAX_STEP(24) */ + mapsector_t next = { .floorheight = 32, .ceilingheight = 160 }; + ASSERT(!can_enter_sector(&cur, &next, (float)EYE_HEIGHT), + "Step up of 32 should be blocked"); + PASS(); +} + +static void test_can_enter_low_ceiling(void) { + TEST("can_enter_sector: ceiling too low is blocked"); + mapsector_t cur = { .floorheight = 0, .ceilingheight = 128 }; + /* ceiling - floor = 40 < EYE_HEIGHT(48) */ + mapsector_t next = { .floorheight = 0, .ceilingheight = 40 }; + ASSERT(!can_enter_sector(&cur, &next, (float)EYE_HEIGHT), + "Sector with ceiling too low should be blocked"); + PASS(); +} + +static void test_can_enter_step_down(void) { + TEST("can_enter_sector: step down is always passable"); + mapsector_t cur = { .floorheight = 32, .ceilingheight = 160 }; + /* floor_diff = 0 - 32 = -32 ≤ MAX_STEP */ + mapsector_t next = { .floorheight = 0, .ceilingheight = 128 }; + ASSERT(can_enter_sector(&cur, &next, 32.0f + EYE_HEIGHT), + "Stepping down should always be passable"); + PASS(); +} + +static void test_can_enter_exact_max_step(void) { + TEST("can_enter_sector: step equal to MAX_STEP is passable"); + mapsector_t cur = { .floorheight = 0, .ceilingheight = 128 }; + /* floor_diff = 24 == MAX_STEP → should pass (≤ MAX_STEP) */ + mapsector_t next = { .floorheight = (int16_t)MAX_STEP, .ceilingheight = 128 + (int16_t)MAX_STEP }; + ASSERT(can_enter_sector(&cur, &next, (float)EYE_HEIGHT), + "Step equal to MAX_STEP should be passable"); + PASS(); +} + +// ── main ───────────────────────────────────────────────────────────────────── + +int main(void) { + printf("\n=== Running Collision Detection Tests ===\n"); + + /* dist_sq */ + test_dist_sq_same_point(); + test_dist_sq_horizontal(); + test_dist_sq_diagonal(); + test_dist_sq_negative_coords(); + + /* closest_point_on_line */ + test_closest_midpoint(); + test_closest_clamped_to_start(); + test_closest_clamped_to_end(); + test_closest_degenerate_line(); + test_closest_diagonal_line(); + + /* calc_slide */ + test_slide_perpendicular_wall(); + test_slide_along_wall(); + test_slide_diagonal_into_wall(); + test_slide_away_from_wall(); + + /* can_enter_sector */ + test_can_enter_null_sector(); + test_can_enter_flat_floor(); + test_can_enter_small_step_up(); + test_can_enter_step_too_high(); + test_can_enter_low_ceiling(); + test_can_enter_step_down(); + test_can_enter_exact_max_step(); + + printf("\n=== Test Results ===\n"); + printf("Passed: %d/%d\n", tests_passed, tests_total); + + if (tests_passed == tests_total) { + printf("\n=== All Tests Passed! ===\n"); + return 0; + } + printf("\n=== Some Tests Failed ===\n"); + return 1; +} diff --git a/tests/player_test.c b/tests/player_test.c new file mode 100644 index 0000000..bc8a215 --- /dev/null +++ b/tests/player_test.c @@ -0,0 +1,297 @@ +/* + * Player Physics Tests + * + * Tests for the mathematical operations governing player movement in input.c: + * - Angle wrapping (kept in 0–360 range) + * - Pitch clamping (kept in –89 to +89) + * - Velocity clamping (speed capped at MAX_SPEED) + * - Friction / deceleration (speed reduced toward 0 without going negative) + * + * These are pure floating-point calculations with no external dependencies, + * so they can be exercised entirely in isolation. + */ + +#include +#include +#include +#include +#include +#include +#include + +// ── Constants (from map.h / input.c) ───────────────────────────────────────── + +#define MAX_SPEED 300.0f +#define ACCELERATION 1000.0f +#define FRICTION 1200.0f + +// ── Pure-math helpers extracted from game_tick() in input.c ────────────────── + +/* Wrap angle into [0, 360). */ +static float wrap_angle(float angle) { + if (angle < 0.0f) angle += 360.0f; + if (angle >= 360.0f) angle -= 360.0f; + return angle; +} + +/* Clamp pitch into [-89, 89]. */ +static float clamp_pitch(float pitch) { + if (pitch > 89.0f) pitch = 89.0f; + if (pitch < -89.0f) pitch = -89.0f; + return pitch; +} + +/* Clamp a velocity vector (vx, vy) so its magnitude never exceeds max_speed. + * Direction is preserved; if speed <= max_speed the vector is unchanged. */ +static void clamp_velocity(float *vx, float *vy, float max_speed) { + float speed = sqrtf(*vx * *vx + *vy * *vy); + if (speed > max_speed) { + float scale = max_speed / speed; + *vx *= scale; + *vy *= scale; + } +} + +/* Apply one step of friction to a velocity vector. + * Reduces speed by (friction * dt) but never below zero. */ +static void apply_friction(float *vx, float *vy, float friction, float dt) { + float speed = sqrtf(*vx * *vx + *vy * *vy); + if (speed <= 0.0f) return; + float decel = friction * dt; + float new_speed = fmaxf(0.0f, speed - decel); + float scale = new_speed / speed; + *vx *= scale; + *vy *= scale; +} + +// ── Test helpers ───────────────────────────────────────────────────────────── + +static int tests_passed = 0; +static int tests_total = 0; + +static int float_eq(float a, float b, float eps) { + return fabsf(a - b) < eps; +} + +#define TEST(name) \ + printf("\nTest %d: %s... ", ++tests_total, name); \ + fflush(stdout) + +#define PASS() \ + printf("PASSED\n"); \ + tests_passed++ + +#define ASSERT(cond, msg) \ + if (!(cond)) { \ + printf("FAILED: %s\n", msg); \ + return; \ + } + +// ── Angle wrapping tests ───────────────────────────────────────────────────── + +static void test_angle_wrap_below_zero(void) { + TEST("angle wrap: -10 wraps to 350"); + float a = wrap_angle(-10.0f); + ASSERT(float_eq(a, 350.0f, 1e-4f), "angle -10 should wrap to 350"); + PASS(); +} + +static void test_angle_wrap_exactly_zero(void) { + TEST("angle wrap: 0 stays 0"); + float a = wrap_angle(0.0f); + ASSERT(float_eq(a, 0.0f, 1e-4f), "angle 0 should stay 0"); + PASS(); +} + +static void test_angle_wrap_below_360(void) { + TEST("angle wrap: 359 stays 359"); + float a = wrap_angle(359.0f); + ASSERT(float_eq(a, 359.0f, 1e-4f), "angle 359 should be unchanged"); + PASS(); +} + +static void test_angle_wrap_exactly_360(void) { + TEST("angle wrap: 360 wraps to 0"); + float a = wrap_angle(360.0f); + ASSERT(float_eq(a, 0.0f, 1e-4f), "angle 360 should wrap to 0"); + PASS(); +} + +static void test_angle_wrap_above_360(void) { + TEST("angle wrap: 370 wraps to 10"); + float a = wrap_angle(370.0f); + ASSERT(float_eq(a, 10.0f, 1e-4f), "angle 370 should wrap to 10"); + PASS(); +} + +// ── Pitch clamping tests ────────────────────────────────────────────────────── + +static void test_pitch_clamp_above_max(void) { + TEST("pitch clamp: 100 clamped to 89"); + float p = clamp_pitch(100.0f); + ASSERT(float_eq(p, 89.0f, 1e-4f), "pitch above 89 should be clamped to 89"); + PASS(); +} + +static void test_pitch_clamp_below_min(void) { + TEST("pitch clamp: -100 clamped to -89"); + float p = clamp_pitch(-100.0f); + ASSERT(float_eq(p, -89.0f, 1e-4f), "pitch below -89 should be clamped to -89"); + PASS(); +} + +static void test_pitch_clamp_at_max(void) { + TEST("pitch clamp: exactly 89 stays 89"); + float p = clamp_pitch(89.0f); + ASSERT(float_eq(p, 89.0f, 1e-4f), "pitch at exactly 89 should not be clamped"); + PASS(); +} + +static void test_pitch_clamp_at_min(void) { + TEST("pitch clamp: exactly -89 stays -89"); + float p = clamp_pitch(-89.0f); + ASSERT(float_eq(p, -89.0f, 1e-4f), "pitch at exactly -89 should not be clamped"); + PASS(); +} + +static void test_pitch_clamp_in_range(void) { + TEST("pitch clamp: 45 unchanged"); + float p = clamp_pitch(45.0f); + ASSERT(float_eq(p, 45.0f, 1e-4f), "pitch in valid range should be unchanged"); + PASS(); +} + +// ── Velocity clamping tests ─────────────────────────────────────────────────── + +static void test_velocity_clamp_above_max(void) { + TEST("velocity clamp: speed above MAX_SPEED gets clamped"); + float vx = MAX_SPEED * 2.0f; + float vy = 0.0f; + clamp_velocity(&vx, &vy, MAX_SPEED); + float speed = sqrtf(vx*vx + vy*vy); + ASSERT(float_eq(speed, MAX_SPEED, 1e-3f), "speed should be clamped to MAX_SPEED"); + PASS(); +} + +static void test_velocity_clamp_below_max(void) { + TEST("velocity clamp: speed below MAX_SPEED unchanged"); + float vx = MAX_SPEED * 0.5f; + float vy = 0.0f; + float orig_vx = vx; + clamp_velocity(&vx, &vy, MAX_SPEED); + ASSERT(float_eq(vx, orig_vx, 1e-4f), "vx should not change when speed < MAX_SPEED"); + ASSERT(float_eq(vy, 0.0f, 1e-4f), "vy should remain 0"); + PASS(); +} + +static void test_velocity_clamp_preserves_direction(void) { + TEST("velocity clamp: direction preserved after clamping"); + float vx = MAX_SPEED * 3.0f; + float vy = MAX_SPEED * 4.0f; /* 3-4-5 scaled by MAX_SPEED */ + float orig_angle = atan2f(vy, vx); + clamp_velocity(&vx, &vy, MAX_SPEED); + float new_angle = atan2f(vy, vx); + ASSERT(float_eq(orig_angle, new_angle, 1e-4f), "direction should be preserved"); + float speed = sqrtf(vx*vx + vy*vy); + ASSERT(float_eq(speed, MAX_SPEED, 1e-3f), "speed should be clamped to MAX_SPEED"); + PASS(); +} + +static void test_velocity_clamp_zero(void) { + TEST("velocity clamp: zero vector stays zero"); + float vx = 0.0f, vy = 0.0f; + clamp_velocity(&vx, &vy, MAX_SPEED); + ASSERT(float_eq(vx, 0.0f, 1e-6f), "zero vx stays zero"); + ASSERT(float_eq(vy, 0.0f, 1e-6f), "zero vy stays zero"); + PASS(); +} + +// ── Friction tests ──────────────────────────────────────────────────────────── + +static void test_friction_reduces_speed(void) { + TEST("friction: speed decreases after one tick"); + float vx = MAX_SPEED; + float vy = 0.0f; + float dt = 1.0f / 60.0f; /* one frame at 60 fps */ + apply_friction(&vx, &vy, FRICTION, dt); + float speed = sqrtf(vx*vx + vy*vy); + ASSERT(speed < MAX_SPEED, "speed should decrease after friction"); + ASSERT(speed >= 0.0f, "speed should not go negative"); + PASS(); +} + +static void test_friction_stops_at_zero(void) { + TEST("friction: large dt brings speed to zero, not below"); + float vx = 1.0f; + float vy = 0.0f; + apply_friction(&vx, &vy, FRICTION, 100.0f); /* huge dt */ + float speed = sqrtf(vx*vx + vy*vy); + ASSERT(float_eq(speed, 0.0f, 1e-6f), "speed should reach 0, not go negative"); + PASS(); +} + +static void test_friction_stationary_object(void) { + TEST("friction: stationary object stays stationary"); + float vx = 0.0f, vy = 0.0f; + apply_friction(&vx, &vy, FRICTION, 1.0f / 60.0f); + ASSERT(float_eq(vx, 0.0f, 1e-6f), "stationary vx should stay 0"); + ASSERT(float_eq(vy, 0.0f, 1e-6f), "stationary vy should stay 0"); + PASS(); +} + +static void test_friction_preserves_direction(void) { + TEST("friction: direction preserved while decelerating"); + float vx = MAX_SPEED * 0.7f; + float vy = MAX_SPEED * 0.7f; + float orig_angle = atan2f(vy, vx); + float dt = 1.0f / 60.0f; + apply_friction(&vx, &vy, FRICTION, dt); + float speed = sqrtf(vx*vx + vy*vy); + if (speed > 0.0f) { + float new_angle = atan2f(vy, vx); + ASSERT(float_eq(orig_angle, new_angle, 1e-4f), "direction should be preserved"); + } + PASS(); +} + +// ── main ───────────────────────────────────────────────────────────────────── + +int main(void) { + printf("\n=== Running Player Physics Tests ===\n"); + + /* Angle wrapping */ + test_angle_wrap_below_zero(); + test_angle_wrap_exactly_zero(); + test_angle_wrap_below_360(); + test_angle_wrap_exactly_360(); + test_angle_wrap_above_360(); + + /* Pitch clamping */ + test_pitch_clamp_above_max(); + test_pitch_clamp_below_min(); + test_pitch_clamp_at_max(); + test_pitch_clamp_at_min(); + test_pitch_clamp_in_range(); + + /* Velocity clamping */ + test_velocity_clamp_above_max(); + test_velocity_clamp_below_max(); + test_velocity_clamp_preserves_direction(); + test_velocity_clamp_zero(); + + /* Friction */ + test_friction_reduces_speed(); + test_friction_stops_at_zero(); + test_friction_stationary_object(); + test_friction_preserves_direction(); + + printf("\n=== Test Results ===\n"); + printf("Passed: %d/%d\n", tests_passed, tests_total); + + if (tests_passed == tests_total) { + printf("\n=== All Tests Passed! ===\n"); + return 0; + } + printf("\n=== Some Tests Failed ===\n"); + return 1; +} diff --git a/tests/wad_test.c b/tests/wad_test.c new file mode 100644 index 0000000..1362301 --- /dev/null +++ b/tests/wad_test.c @@ -0,0 +1,314 @@ +/* + * WAD Structure Tests + * + * Tests for WAD file parsing logic that can be exercised without a real + * WAD file on disk: + * - is_map_block_valid (lump-sequence validation) + * - find_lump / find_lump_num style name matching + * - WAD header and directory structure layout + */ + +#include +#include +#include +#include +#include +#include + +// ── Minimal type stubs ────────────────────────────────────────────────────── + +typedef char lumpname_t[8]; + +typedef struct { + uint32_t filepos; + uint32_t size; + lumpname_t name; +} filelump_t; + +typedef struct { + char identification[4]; + uint32_t numlumps; + uint32_t infotableofs; +} wadheader_t; + +// ── Functions under test (copied from wad.c to isolate from map.h) ────────── + +bool is_map_block_valid(filelump_t *dir, int index, int total_lumps) { + static const char *expected[] = { + "THINGS", "LINEDEFS", "SIDEDEFS", "VERTEXES", + "SEGS", "SSECTORS", "NODES", "SECTORS", "REJECT", "BLOCKMAP" + }; + for (int i = 0; i < 10; i++) { + if (index + 1 + i >= total_lumps) return false; + if (strncmp(dir[index + 1 + i].name, expected[i], 8) != 0) return false; + } + return true; +} + +static int find_lump_num_impl(filelump_t const *dir, int num_lumps, const char *name) { + for (int i = 0; i < num_lumps; i++) { + if (strncmp(dir[i].name, name, sizeof(lumpname_t)) == 0) + return i; + } + return -1; +} + +// ── Test helpers ───────────────────────────────────────────────────────────── + +static int tests_passed = 0; +static int tests_total = 0; + +#define TEST(name) \ + printf("\nTest %d: %s... ", ++tests_total, name); \ + fflush(stdout) + +#define PASS() \ + printf("PASSED\n"); \ + tests_passed++ + +#define ASSERT(cond, msg) \ + if (!(cond)) { \ + printf("FAILED: %s\n", msg); \ + return; \ + } + +// Helper: set a lump name, zero-padding to 8 bytes (WAD convention) +static void set_lump_name(filelump_t *lump, const char *name) { + memset(lump->name, 0, sizeof(lumpname_t)); + strncpy(lump->name, name, sizeof(lumpname_t)); +} + +// Build a minimal but valid map directory block starting at index 0: +// [0]=marker, [1]=THINGS, ..., [10]=BLOCKMAP +static void build_valid_map_block(filelump_t *dir, const char *marker) { + static const char *lumps[] = { + "THINGS", "LINEDEFS", "SIDEDEFS", "VERTEXES", + "SEGS", "SSECTORS", "NODES", "SECTORS", "REJECT", "BLOCKMAP" + }; + set_lump_name(&dir[0], marker); + for (int i = 0; i < 10; i++) { + set_lump_name(&dir[i + 1], lumps[i]); + dir[i + 1].filepos = 0; + dir[i + 1].size = 0; + } +} + +// ── is_map_block_valid tests ───────────────────────────────────────────────── + +static void test_valid_doom_map_e1m1(void) { + TEST("is_map_block_valid: valid E1M1 marker block"); + filelump_t dir[11]; + memset(dir, 0, sizeof(dir)); + build_valid_map_block(dir, "E1M1"); + ASSERT(is_map_block_valid(dir, 0, 11), "Valid E1M1 map block should pass"); + PASS(); +} + +static void test_valid_doom2_map01(void) { + TEST("is_map_block_valid: valid MAP01 marker block"); + filelump_t dir[11]; + memset(dir, 0, sizeof(dir)); + build_valid_map_block(dir, "MAP01"); + ASSERT(is_map_block_valid(dir, 0, 11), "Valid MAP01 map block should pass"); + PASS(); +} + +static void test_valid_map_block_not_at_start(void) { + TEST("is_map_block_valid: valid block not at directory start"); + /* Put some other lumps before the map block */ + filelump_t dir[15]; + memset(dir, 0, sizeof(dir)); + set_lump_name(&dir[0], "PLAYPAL"); + set_lump_name(&dir[1], "COLORMAP"); + set_lump_name(&dir[2], "TEXTURE1"); + /* Map block starts at index 3 */ + build_valid_map_block(&dir[3], "E2M1"); + ASSERT(is_map_block_valid(dir, 3, 14), "Valid block at offset 3 should pass"); + PASS(); +} + +static void test_invalid_wrong_lump_name(void) { + TEST("is_map_block_valid: wrong lump name fails"); + filelump_t dir[11]; + memset(dir, 0, sizeof(dir)); + build_valid_map_block(dir, "E1M1"); + /* Corrupt the SECTORS lump name */ + set_lump_name(&dir[8], "JUNK"); + ASSERT(!is_map_block_valid(dir, 0, 11), + "Block with wrong lump name should fail"); + PASS(); +} + +static void test_invalid_too_few_lumps(void) { + TEST("is_map_block_valid: too few lumps in directory"); + filelump_t dir[11]; + memset(dir, 0, sizeof(dir)); + build_valid_map_block(dir, "E1M1"); + /* total_lumps = 5 means we can't reach all 10 required lumps */ + ASSERT(!is_map_block_valid(dir, 0, 5), + "Block with insufficient total_lumps should fail"); + PASS(); +} + +static void test_invalid_index_near_end(void) { + TEST("is_map_block_valid: index too close to end of directory"); + filelump_t dir[11]; + memset(dir, 0, sizeof(dir)); + build_valid_map_block(dir, "E1M1"); + /* index=8 means there are only 2 lumps after it, not 10 */ + ASSERT(!is_map_block_valid(dir, 8, 11), + "Block index leaving too few trailing lumps should fail"); + PASS(); +} + +static void test_valid_all_doom2_maps(void) { + TEST("is_map_block_valid: all 32 MAP0x/MAP1x/MAP2x/MAP3x markers validate"); + const char *markers[] = { + "MAP01","MAP02","MAP03","MAP04","MAP05","MAP06","MAP07","MAP08","MAP09","MAP10", + "MAP11","MAP12","MAP13","MAP14","MAP15","MAP16","MAP17","MAP18","MAP19","MAP20", + "MAP21","MAP22","MAP23","MAP24","MAP25","MAP26","MAP27","MAP28","MAP29","MAP30", + "MAP31","MAP32" + }; + for (int m = 0; m < 32; m++) { + filelump_t dir[11]; + memset(dir, 0, sizeof(dir)); + build_valid_map_block(dir, markers[m]); + ASSERT(is_map_block_valid(dir, 0, 11), markers[m]); + } + PASS(); +} + +// ── Lump name matching tests ───────────────────────────────────────────────── + +static void test_find_lump_found(void) { + TEST("find_lump_num: finds existing lump by name"); + filelump_t dir[4]; + memset(dir, 0, sizeof(dir)); + set_lump_name(&dir[0], "PLAYPAL"); + set_lump_name(&dir[1], "COLORMAP"); + set_lump_name(&dir[2], "TEXTURE1"); + set_lump_name(&dir[3], "PNAMES"); + + ASSERT(find_lump_num_impl(dir, 4, "TEXTURE1") == 2, "TEXTURE1 should be at index 2"); + PASS(); +} + +static void test_find_lump_not_found(void) { + TEST("find_lump_num: returns -1 for missing lump"); + filelump_t dir[2]; + memset(dir, 0, sizeof(dir)); + set_lump_name(&dir[0], "PLAYPAL"); + set_lump_name(&dir[1], "COLORMAP"); + + ASSERT(find_lump_num_impl(dir, 2, "MISSING") == -1, + "Missing lump should return -1"); + PASS(); +} + +static void test_find_lump_first_match(void) { + TEST("find_lump_num: returns first matching lump when duplicates exist"); + filelump_t dir[4]; + memset(dir, 0, sizeof(dir)); + set_lump_name(&dir[0], "FLAT1"); + set_lump_name(&dir[1], "FLAT2"); + set_lump_name(&dir[2], "FLAT1"); // duplicate + set_lump_name(&dir[3], "FLAT3"); + + ASSERT(find_lump_num_impl(dir, 4, "FLAT1") == 0, + "Should return first occurrence of duplicate lump"); + PASS(); +} + +static void test_find_lump_empty_directory(void) { + TEST("find_lump_num: empty directory returns -1"); + ASSERT(find_lump_num_impl(NULL, 0, "ANYTHING") == -1, + "Empty directory should return -1"); + PASS(); +} + +static void test_find_lump_8char_name(void) { + TEST("find_lump_num: 8-character lump name (no null terminator) matches"); + filelump_t dir[1]; + memset(dir, 0, sizeof(dir)); + /* Write all 8 bytes without a null terminator */ + memcpy(dir[0].name, "LONGNAME", 8); + + ASSERT(find_lump_num_impl(dir, 1, "LONGNAME") == 0, + "8-character lump name should match"); + PASS(); +} + +// ── WAD header structure tests ─────────────────────────────────────────────── + +static void test_wadheader_size(void) { + TEST("wadheader_t: struct size is 12 bytes (WAD spec)"); + ASSERT(sizeof(wadheader_t) == 12, + "WAD header must be exactly 12 bytes per specification"); + PASS(); +} + +static void test_filelump_size(void) { + TEST("filelump_t: struct size is 16 bytes (WAD spec)"); + /* 4 (filepos) + 4 (size) + 8 (name) = 16 */ + ASSERT(sizeof(filelump_t) == 16, + "WAD directory entry must be exactly 16 bytes per specification"); + PASS(); +} + +static void test_wadheader_identification_iwad(void) { + TEST("wadheader_t: IWAD identification field parses correctly"); + wadheader_t hdr; + memset(&hdr, 0, sizeof(hdr)); + memcpy(hdr.identification, "IWAD", 4); + ASSERT(strncmp(hdr.identification, "IWAD", 4) == 0, + "IWAD identification should parse correctly"); + PASS(); +} + +static void test_wadheader_identification_pwad(void) { + TEST("wadheader_t: PWAD identification field parses correctly"); + wadheader_t hdr; + memset(&hdr, 0, sizeof(hdr)); + memcpy(hdr.identification, "PWAD", 4); + ASSERT(strncmp(hdr.identification, "PWAD", 4) == 0, + "PWAD identification should parse correctly"); + PASS(); +} + +// ── main ───────────────────────────────────────────────────────────────────── + +int main(void) { + printf("\n=== Running WAD Structure Tests ===\n"); + + /* is_map_block_valid */ + test_valid_doom_map_e1m1(); + test_valid_doom2_map01(); + test_valid_map_block_not_at_start(); + test_invalid_wrong_lump_name(); + test_invalid_too_few_lumps(); + test_invalid_index_near_end(); + test_valid_all_doom2_maps(); + + /* find_lump_num */ + test_find_lump_found(); + test_find_lump_not_found(); + test_find_lump_first_match(); + test_find_lump_empty_directory(); + test_find_lump_8char_name(); + + /* WAD struct layout */ + test_wadheader_size(); + test_filelump_size(); + test_wadheader_identification_iwad(); + test_wadheader_identification_pwad(); + + printf("\n=== Test Results ===\n"); + printf("Passed: %d/%d\n", tests_passed, tests_total); + + if (tests_passed == tests_total) { + printf("\n=== All Tests Passed! ===\n"); + return 0; + } + printf("\n=== Some Tests Failed ===\n"); + return 1; +} diff --git a/tests/walls_test.c b/tests/walls_test.c new file mode 100644 index 0000000..e7c855c --- /dev/null +++ b/tests/walls_test.c @@ -0,0 +1,239 @@ +/* + * Wall Normal Tests + * + * Tests for compute_normal_packed() from walls.c: + * - Packs a 2D perpendicular normal into signed 8-bit components. + * - Input (dx, dy) is the wall direction vector. + * - Normal = (-dy, dx) normalised to unit length, packed into int8_t * 127. + * - Returns the length of the input vector (useful as a wall length). + */ + +#include +#include +#include +#include +#include +#include +#include + +// ── Function under test ────────────────────────────────────────────────────── +// +// Copied verbatim from walls.c. walls.c cannot be #included in a standalone +// test because it pulls in OpenGL, cglm and map.h (which in turn requires SDL2 +// and the ui submodule). All other test files in this repo use the same +// copy-and-compile pattern for the same reason. If the production +// implementation changes, update this copy to match. + +static float compute_normal_packed(float dx, float dy, int8_t out[3]) { + float nx = -dy; + float ny = dx; + + float length = sqrtf(nx * nx + ny * ny); + if (length == 0.0f) { + out[0] = out[1] = out[2] = 0; + return 0; + } + + nx /= length; + ny /= length; + + out[0] = (int8_t)(nx * 127.0f); + out[1] = (int8_t)(ny * 127.0f); + out[2] = 0; + + return length; +} + +// ── Test helpers ───────────────────────────────────────────────────────────── + +static int tests_passed = 0; +static int tests_total = 0; + +#define TEST(name) \ + printf("\nTest %d: %s... ", ++tests_total, name); \ + fflush(stdout) + +#define PASS() \ + printf("PASSED\n"); \ + tests_passed++ + +#define ASSERT(cond, msg) \ + if (!(cond)) { \ + printf("FAILED: %s\n", msg); \ + return; \ + } + +static int float_eq(float a, float b, float eps) { + return fabsf(a - b) < eps; +} + +/* Packed normals are rounded toward zero; allow ±2 for platform rounding. */ +static int i8_near(int8_t a, int expected) { + return abs((int)a - expected) <= 2; +} + +// ── Tests ──────────────────────────────────────────────────────────────────── + +/* Rightward wall: dx=1, dy=0 → normal = (-0, 1) = (0,+1) + * out[0]=0, out[1]=+127, return=1 */ +static void test_rightward_wall(void) { + TEST("rightward wall (dx=1, dy=0): normal is (0, +127)"); + int8_t n[3]; + float len = compute_normal_packed(1.0f, 0.0f, n); + ASSERT(i8_near(n[0], 0), "out[0] should be 0"); + ASSERT(i8_near(n[1], 127), "out[1] should be +127"); + ASSERT(n[2] == 0, "out[2] should always be 0"); + ASSERT(float_eq(len, 1.0f, 1e-5f), "return value should be 1.0"); + PASS(); +} + +/* Leftward wall: dx=-1, dy=0 → normal = (0, -1) + * out[0]=0, out[1]=-127 */ +static void test_leftward_wall(void) { + TEST("leftward wall (dx=-1, dy=0): normal is (0, -127)"); + int8_t n[3]; + float len = compute_normal_packed(-1.0f, 0.0f, n); + ASSERT(i8_near(n[0], 0), "out[0] should be 0"); + ASSERT(i8_near(n[1], -127), "out[1] should be -127"); + ASSERT(n[2] == 0, "out[2] should always be 0"); + ASSERT(float_eq(len, 1.0f, 1e-5f), "return value should be 1.0"); + PASS(); +} + +/* Upward wall: dx=0, dy=1 → normal = (-1, 0) + * out[0]=-127, out[1]=0 */ +static void test_upward_wall(void) { + TEST("upward wall (dx=0, dy=1): normal is (-127, 0)"); + int8_t n[3]; + float len = compute_normal_packed(0.0f, 1.0f, n); + ASSERT(i8_near(n[0], -127), "out[0] should be -127"); + ASSERT(i8_near(n[1], 0), "out[1] should be 0"); + ASSERT(n[2] == 0, "out[2] should always be 0"); + ASSERT(float_eq(len, 1.0f, 1e-5f), "return value should be 1.0"); + PASS(); +} + +/* Downward wall: dx=0, dy=-1 → normal = (+1, 0) + * out[0]=+127, out[1]=0 */ +static void test_downward_wall(void) { + TEST("downward wall (dx=0, dy=-1): normal is (+127, 0)"); + int8_t n[3]; + float len = compute_normal_packed(0.0f, -1.0f, n); + ASSERT(i8_near(n[0], 127), "out[0] should be +127"); + ASSERT(i8_near(n[1], 0), "out[1] should be 0"); + ASSERT(n[2] == 0, "out[2] should always be 0"); + ASSERT(float_eq(len, 1.0f, 1e-5f), "return value should be 1.0"); + PASS(); +} + +/* Degenerate wall (zero vector): length==0 → all zeros, return 0 */ +static void test_degenerate_wall(void) { + TEST("degenerate wall (dx=0, dy=0): returns 0 and all-zero normal"); + int8_t n[3] = {99, 99, 99}; + float len = compute_normal_packed(0.0f, 0.0f, n); + ASSERT(float_eq(len, 0.0f, 1e-10f), "return value should be 0"); + ASSERT(n[0] == 0, "out[0] should be 0 for degenerate input"); + ASSERT(n[1] == 0, "out[1] should be 0 for degenerate input"); + ASSERT(n[2] == 0, "out[2] should be 0 for degenerate input"); + PASS(); +} + +/* 45° diagonal wall (dx=1, dy=1): + * nx = -1/√2 ≈ -0.707 → out[0] ≈ -89 or -90 + * ny = 1/√2 ≈ +0.707 → out[1] ≈ +89 or +90 + * length = √2 */ +static void test_diagonal_wall(void) { + TEST("45° diagonal wall (dx=1, dy=1)"); + int8_t n[3]; + float len = compute_normal_packed(1.0f, 1.0f, n); + ASSERT(float_eq(len, sqrtf(2.0f), 1e-5f), "return should be sqrt(2)"); + /* nx = -1/√2 → packed ≈ -90; ny = 1/√2 → packed ≈ 90 */ + ASSERT(n[0] < 0, "out[0] should be negative for 45° diagonal"); + ASSERT(n[1] > 0, "out[1] should be positive for 45° diagonal"); + ASSERT(n[2] == 0, "out[2] should always be 0"); + /* The two components should be roughly equal in magnitude */ + ASSERT(abs((int)n[0]) == abs((int)n[1]) || abs(abs((int)n[0]) - abs((int)n[1])) <= 1, + "components should have equal magnitude for 45°"); + PASS(); +} + +/* 3-4-5 right triangle: dx=3, dy=4 + * nx = -4/5 = -0.8 → out[0] = (int8_t)(-101.6) = -101 + * ny = 3/5 = +0.6 → out[1] = (int8_t)( 76.2) = 76 + * length = 5 */ +static void test_3_4_5_wall(void) { + TEST("3-4-5 wall (dx=3, dy=4): return=5, normals correct"); + int8_t n[3]; + float len = compute_normal_packed(3.0f, 4.0f, n); + ASSERT(float_eq(len, 5.0f, 1e-4f), "return value should be 5 for 3-4-5"); + ASSERT(i8_near(n[0], -101), "out[0] ≈ -101 for 3-4-5"); + ASSERT(i8_near(n[1], 76), "out[1] ≈ 76 for 3-4-5"); + ASSERT(n[2] == 0, "out[2] should always be 0"); + PASS(); +} + +/* Scale independence: doubling the input length doubles the return value + * but should produce the same packed normal components. */ +static void test_scale_independence(void) { + TEST("scale independence: double length → same normal, double return"); + int8_t n1[3], n2[3]; + float len1 = compute_normal_packed(1.0f, 0.0f, n1); + float len2 = compute_normal_packed(2.0f, 0.0f, n2); + ASSERT(float_eq(len2, 2.0f * len1, 1e-5f), "return should scale with input length"); + ASSERT(n1[0] == n2[0], "packed normal x should be same regardless of scale"); + ASSERT(n1[1] == n2[1], "packed normal y should be same regardless of scale"); + ASSERT(n2[2] == 0, "out[2] should always be 0"); + PASS(); +} + +/* The z component should always be zero regardless of input. */ +static void test_z_component_always_zero(void) { + TEST("z component is always 0 for any valid input"); + int8_t n[3]; + compute_normal_packed(1.0f, 1.0f, n); + ASSERT(n[2] == 0, "out[2] should be 0 for diagonal"); + compute_normal_packed(0.0f, 5.0f, n); + ASSERT(n[2] == 0, "out[2] should be 0 for vertical"); + compute_normal_packed(-3.0f, -7.0f, n); + ASSERT(n[2] == 0, "out[2] should be 0 for arbitrary direction"); + PASS(); +} + +/* Opposite directions produce opposite normals (anti-parallel walls). */ +static void test_opposite_directions_flip_normal(void) { + TEST("opposite wall directions produce negated normals"); + int8_t n_fwd[3], n_rev[3]; + compute_normal_packed( 1.0f, 0.0f, n_fwd); + compute_normal_packed(-1.0f, 0.0f, n_rev); + /* n_fwd and n_rev should be negations of each other (within rounding) */ + ASSERT(i8_near(n_fwd[0], -n_rev[0]), "x components should be negated"); + ASSERT(i8_near(n_fwd[1], -n_rev[1]), "y components should be negated"); + PASS(); +} + +// ── main ───────────────────────────────────────────────────────────────────── + +int main(void) { + printf("\n=== Running Wall Normal Tests ===\n"); + + test_rightward_wall(); + test_leftward_wall(); + test_upward_wall(); + test_downward_wall(); + test_degenerate_wall(); + test_diagonal_wall(); + test_3_4_5_wall(); + test_scale_independence(); + test_z_component_always_zero(); + test_opposite_directions_flip_normal(); + + printf("\n=== Test Results ===\n"); + printf("Passed: %d/%d\n", tests_passed, tests_total); + + if (tests_passed == tests_total) { + printf("\n=== All Tests Passed! ===\n"); + return 0; + } + printf("\n=== Some Tests Failed ===\n"); + return 1; +} diff --git a/ui b/ui index 0dd5b62..b570da0 160000 --- a/ui +++ b/ui @@ -1 +1 @@ -Subproject commit 0dd5b62fa96c2b21a87f04de4aeee4079d9da18d +Subproject commit b570da0e2fb712ec47571b69dc6d1f01d7dfae05