From 53a5ce314e920d9409bfd8d556aa0c327a1bd2b6 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 10:27:16 -0500 Subject: [PATCH 01/40] Initial LM-generated file --- .gitignore | 8 + pufferlib/ocean/codeball/.gitignore | 1 + pufferlib/ocean/codeball/Makefile | 14 + pufferlib/ocean/codeball/codeball.c | 601 ++++++++++++++++++++++++ pufferlib/ocean/codeball/cy_squared.pyx | 70 +++ pufferlib/ocean/codeball/squared.c | 33 ++ pufferlib/ocean/codeball/squared.h | 134 ++++++ pufferlib/ocean/codeball/squared.py | 45 ++ 8 files changed, 906 insertions(+) create mode 100644 pufferlib/ocean/codeball/.gitignore create mode 100644 pufferlib/ocean/codeball/Makefile create mode 100644 pufferlib/ocean/codeball/codeball.c create mode 100644 pufferlib/ocean/codeball/cy_squared.pyx create mode 100644 pufferlib/ocean/codeball/squared.c create mode 100644 pufferlib/ocean/codeball/squared.h create mode 100644 pufferlib/ocean/codeball/squared.py diff --git a/.gitignore b/.gitignore index 5bb9cfc8..4f9ea8bd 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,11 @@ checkpoints/ experiments/ wandb/ raylib/ + +raylib_wasm/* +c_gae.c +pufferlib/puffernet.c +pufferlib/extensions.c +pufferlib/ocean/tactical/c_tactical.c +pufferlib/ocean/grid/c_grid.c +.idea/ \ No newline at end of file diff --git a/pufferlib/ocean/codeball/.gitignore b/pufferlib/ocean/codeball/.gitignore new file mode 100644 index 00000000..7763a1e2 --- /dev/null +++ b/pufferlib/ocean/codeball/.gitignore @@ -0,0 +1 @@ +codeball \ No newline at end of file diff --git a/pufferlib/ocean/codeball/Makefile b/pufferlib/ocean/codeball/Makefile new file mode 100644 index 00000000..b6dfcc54 --- /dev/null +++ b/pufferlib/ocean/codeball/Makefile @@ -0,0 +1,14 @@ +CFILES=codeball.c +CFLAGS=\ + -lm -I../../../raylib/include -I../../../pufferlib -lpthread ../../../raylib/lib/libraylib.a -lglfw -lobjc \ + -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -framework GLUT -framework AppKit + +# -std=c99 -Wall -pedantic -Wextra -Werror -Wstrict-prototypes -Wold-style-definition -Werror=vla -lm \ + -I../../../raylib/include -I../../../pufferlib -lpthread +ARCH=$(shell uname -m) + +codeball: $(CFILES) + gcc $(CFLAGS) $(CFILES) -o codeball + +run: codeball + ./codeball \ No newline at end of file diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c new file mode 100644 index 00000000..e762528e --- /dev/null +++ b/pufferlib/ocean/codeball/codeball.c @@ -0,0 +1,601 @@ +#include +#include +#include +#include +#include +#include "puffernet.h" +#include "raylib.h" + +// Constants (from the description) +#define ROBOT_MIN_RADIUS 1.0 +#define ROBOT_MAX_RADIUS 1.05 +#define ROBOT_MAX_JUMP_SPEED 15.0 +#define ROBOT_ACCELERATION 100.0 +#define ROBOT_NITRO_ACCELERATION 30.0 +#define ROBOT_MAX_GROUND_SPEED 30.0 +#define ROBOT_ARENA_E 0.0 +#define ROBOT_MASS 2.0 +#define TICKS_PER_SECOND 60 +#define MICROTICKS_PER_TICK 100 +#define RESET_TICKS (2 * TICKS_PER_SECOND) +#define BALL_ARENA_E 0.7 +#define BALL_RADIUS 2.0 +#define BALL_MASS 1.0 +#define MIN_HIT_E 0.4 +#define MAX_HIT_E 0.5 +#define MAX_ENTITY_SPEED 100.0 +#define MAX_NITRO_AMOUNT 100.0 +#define START_NITRO_AMOUNT 50.0 +#define NITRO_POINT_VELOCITY_CHANGE 0.6 +#define NITRO_PACK_X 20.0 +#define NITRO_PACK_Y 1.0 +#define NITRO_PACK_Z 30.0 +#define NITRO_PACK_RADIUS 0.5 +#define NITRO_PACK_AMOUNT 4 // Corrected: There are 4 nitro packs +#define NITRO_PACK_RESPAWN_TICKS (10 * TICKS_PER_SECOND) +#define GRAVITY 30.0 + +// Arena parameters (from the description) +typedef struct { + double width; + double height; + double depth; + double bottom_radius; + double top_radius; + double corner_radius; + double goal_top_radius; + double goal_width; + double goal_depth; + double goal_height; + double goal_side_radius; +} CodeBallArena; + +CodeBallArena arena = {60, 20, 80, 3, 7, 13, 3, 30, 10, 10, 1}; + +// 3D Vector +typedef struct { + double x; + double y; + double z; +} Vec3D; + +// Action structure +typedef struct { + Vec3D target_velocity; + double jump_speed; + bool use_nitro; +} Action; + +// Entity (Robot or Ball) +typedef struct { + Vec3D position; + Vec3D velocity; + double radius; + double radius_change_speed; + double mass; + double arena_e; + bool touch; + Vec3D touch_normal; + double nitro; + Action action; +} Entity; + +//Nitro Pack +typedef struct{ + Vec3D position; + bool alive; + int respawn_ticks; + double radius; //Nitro pack has radius +} NitroPack; + +NitroPack nitro_packs[NITRO_PACK_AMOUNT]; + +//Helper functions +double vec3d_length(Vec3D v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } +Vec3D vec3d_normalize(Vec3D v) { + double l = vec3d_length(v); + if (l == 0) return (Vec3D){0, 0, 0}; // Avoid division by zero + return (Vec3D){v.x / l, v.y / l, v.z / l}; +} + +double vec3d_dot(Vec3D a, Vec3D b) { return a.x * b.x + a.y * b.y + a.z * b.z; } + +Vec3D vec3d_subtract(Vec3D a, Vec3D b) { return (Vec3D){a.x - b.x, a.y - b.y, a.z - b.z}; } +Vec3D vec3d_add(Vec3D a, Vec3D b) { return (Vec3D){a.x + b.x, a.y + b.y, a.z + b.z}; } +Vec3D vec3d_multiply(Vec3D v, double s) { return (Vec3D){v.x * s, v.y * s, v.z * s}; } + +double clamp(double val, double min, double max) { + if (val < min) return min; + if (val > max) return max; + return val; +} +Vec3D vec3d_clamp(Vec3D v, double max_length) { + double length = vec3d_length(v); + if (length > max_length) { + return vec3d_multiply(v, max_length / length); + } + return v; +} + +typedef struct { + double distance; + Vec3D normal; +} DistanceAndNormal; + +// Distance and Normal functions +DistanceAndNormal dan_to_plane(Vec3D point, Vec3D point_on_plane, Vec3D plane_normal) { + return (DistanceAndNormal){vec3d_dot(vec3d_subtract(point, point_on_plane), plane_normal), plane_normal}; +} + +DistanceAndNormal dan_to_sphere_inner(Vec3D point, Vec3D sphere_center, double sphere_radius) { + return (DistanceAndNormal){sphere_radius - vec3d_length(vec3d_subtract(point, sphere_center)), vec3d_normalize(vec3d_subtract(sphere_center, point))}; +} + +DistanceAndNormal dan_to_sphere_outer(Vec3D point, Vec3D sphere_center, double sphere_radius) { + return (DistanceAndNormal){vec3d_length(vec3d_subtract(point, sphere_center)) - sphere_radius, vec3d_normalize(vec3d_subtract(point, sphere_center))}; +} + +DistanceAndNormal dan_to_arena_quarter(Vec3D point) { + DistanceAndNormal dan; + dan.distance = 1e9; // Initialize with a large value + + // Ground + DistanceAndNormal temp_dan = dan_to_plane(point, (Vec3D){0, 0, 0}, (Vec3D){0, 1, 0}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Ceiling + temp_dan = dan_to_plane(point, (Vec3D){0, arena.height, 0}, (Vec3D){0, -1, 0}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Side x + temp_dan = dan_to_plane(point, (Vec3D){arena.width / 2.0, 0, 0}, (Vec3D){-1, 0, 0}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Side z (goal) + temp_dan = dan_to_plane(point, (Vec3D){0, 0, (arena.depth / 2.0) + arena.goal_depth}, (Vec3D){0, 0, -1}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Side z (regular) + Vec3D v = {point.x - ((arena.goal_width / 2.0) - arena.goal_top_radius), point.y - (arena.goal_height - arena.goal_top_radius), 0}; + if (point.x >= (arena.goal_width / 2.0) + arena.goal_side_radius || + point.y >= arena.goal_height + arena.goal_side_radius || + (v.x > 0 && v.y > 0 && vec3d_length(v) >= arena.goal_top_radius + arena.goal_side_radius)) { + temp_dan = dan_to_plane(point, (Vec3D){0, 0, arena.depth / 2.0}, (Vec3D){0, 0, -1}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + + // Goal back corners + if (point.z > (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius) { + temp_dan = dan_to_sphere_inner(point, (Vec3D){clamp(point.x, arena.bottom_radius - (arena.goal_width / 2.0), (arena.goal_width / 2.0) - arena.bottom_radius), clamp(point.y, arena.bottom_radius, arena.goal_height - arena.goal_top_radius), (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + + // Corner + if (point.x > (arena.width / 2.0) - arena.corner_radius && point.z > (arena.depth / 2.0) - arena.corner_radius) { + temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.width / 2.0) - arena.corner_radius, point.y, (arena.depth / 2.0) - arena.corner_radius}, arena.corner_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + + // Goal outer corner + if (point.z < (arena.depth / 2.0) + arena.goal_side_radius) { + // Side x + if (point.x < (arena.goal_width / 2.0) + arena.goal_side_radius) { + temp_dan = dan_to_sphere_outer(point, (Vec3D){(arena.goal_width / 2.0) + arena.goal_side_radius, point.y, (arena.depth / 2.0) + arena.goal_side_radius}, arena.goal_side_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Ceiling + if (point.y < arena.goal_height + arena.goal_side_radius) { + temp_dan = dan_to_sphere_outer(point, (Vec3D){point.x, arena.goal_height + arena.goal_side_radius, (arena.depth / 2.0) + arena.goal_side_radius}, arena.goal_side_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Top corner + Vec3D o = {(arena.goal_width / 2.0) - arena.goal_top_radius, arena.goal_height - arena.goal_top_radius, 0}; + v = (Vec3D){point.x - o.x, point.y - o.y, 0}; + if (v.x > 0 && v.y > 0) { + o.x += vec3d_normalize(v).x * (arena.goal_top_radius + arena.goal_side_radius); + o.y += vec3d_normalize(v).y * (arena.goal_top_radius + arena.goal_side_radius); + temp_dan = dan_to_sphere_outer(point, (Vec3D){o.x, o.y, (arena.depth / 2.0) + arena.goal_side_radius}, arena.goal_side_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + + // Goal inside top corners + if (point.z > (arena.depth / 2.0) + arena.goal_side_radius && point.y > arena.goal_height - arena.goal_top_radius) { + // Side x + if (point.x > (arena.goal_width / 2.0) - arena.goal_top_radius) { + temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.goal_width / 2.0) - arena.goal_top_radius, arena.goal_height - arena.goal_top_radius, point.z}, arena.goal_top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z + if (point.z > (arena.depth / 2.0) + arena.goal_depth - arena.goal_top_radius) { + temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.goal_height - arena.goal_top_radius, (arena.depth / 2.0) + arena.goal_depth - arena.goal_top_radius}, arena.goal_top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + + // Bottom corners + if (point.y < arena.bottom_radius) { + // Side x + if (point.x > (arena.width / 2.0) - arena.bottom_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.width / 2.0) - arena.bottom_radius, arena.bottom_radius, point.z}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z + if (point.z > (arena.depth / 2.0) - arena.bottom_radius && point.x >= (arena.goal_width / 2.0) + arena.goal_side_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.bottom_radius, (arena.depth / 2.0) - arena.bottom_radius}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z (goal) + if (point.z > (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.bottom_radius, (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Goal outer corner + Vec3D o = {(arena.goal_width / 2.0) + arena.goal_side_radius, (arena.depth / 2.0) + arena.goal_side_radius, 0}; + Vec3D v = {point.x - o.x, point.z - o.y, 0}; + if (v.x < 0 && v.y < 0 && vec3d_length(v) < arena.goal_side_radius + arena.bottom_radius) { + o.x += vec3d_normalize(v).x * (arena.goal_side_radius + arena.bottom_radius); + o.y += vec3d_normalize(v).y * (arena.goal_side_radius + arena.bottom_radius); + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){o.x, arena.bottom_radius, o.y}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side x (goal) + if (point.z >= (arena.depth / 2.0) + arena.goal_side_radius && point.x > (arena.goal_width / 2.0) - arena.bottom_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.goal_width / 2.0) - arena.bottom_radius, arena.bottom_radius, point.z}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Corner + if (point.x > (arena.width / 2.0) - arena.corner_radius && point.z > (arena.depth / 2.0) - arena.corner_radius) { + Vec3D corner_o = {(arena.width / 2.0) - arena.corner_radius, (arena.depth / 2.0) - arena.corner_radius, 0}; + Vec3D n = {point.x - corner_o.x, point.z - corner_o.y, 0}; + double dist = vec3d_length(n); + if (dist > arena.corner_radius - arena.bottom_radius) { + n = vec3d_normalize(n); + Vec3D o2 = {corner_o.x + n.x * (arena.corner_radius - arena.bottom_radius), corner_o.y + n.y * (arena.corner_radius - arena.bottom_radius), 0}; + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){o2.x, arena.bottom_radius, o2.y}, arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + } + + // Ceiling corners + if (point.y > arena.height - arena.top_radius) { + // Side x + if (point.x > (arena.width / 2.0) - arena.top_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.width / 2.0) - arena.top_radius, arena.height - arena.top_radius, point.z}, arena.top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z + if (point.z > (arena.depth / 2.0) - arena.top_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.height - arena.top_radius, (arena.depth / 2.0) - arena.top_radius}, arena.top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Corner + if (point.x > (arena.width / 2.0) - arena.corner_radius && point.z > (arena.depth / 2.0) - arena.corner_radius) { + Vec3D corner_o = {(arena.width / 2.0) - arena.corner_radius, (arena.depth / 2.0) - arena.corner_radius, 0}; + Vec3D dv = {point.x - corner_o.x, point.z - corner_o.y, 0}; + if (vec3d_length(dv) > arena.corner_radius - arena.top_radius) { + Vec3D n = vec3d_normalize(dv); + Vec3D o2 = {corner_o.x + n.x * (arena.corner_radius - arena.top_radius), arena.height - arena.top_radius, o2.y}; + DistanceAndNormal temp_dan = dan_to_sphere_inner(point, o2, arena.top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + } + return dan; +} + +DistanceAndNormal dan_to_arena(Vec3D point) { + bool negate_x = point.x < 0; + bool negate_z = point.z < 0; + if (negate_x) point.x = -point.x; + if (negate_z) point.z = -point.z; + DistanceAndNormal result = dan_to_arena_quarter(point); + if (negate_x) result.normal.x = -result.normal.x; + if (negate_z) result.normal.z = -result.normal.z; + return result; +} + +void collide_entities(Entity *a, Entity *b) { + Vec3D delta_position = {b->position.x - a->position.x, b->position.y - a->position.y, b->position.z - a->position.z}; + double distance = vec3d_length(delta_position); + double penetration = a->radius + b->radius - distance; + if (penetration > 0) { + double k_a = (1.0 / a->mass) / ((1.0 / a->mass) + (1.0 / b->mass)); + double k_b = (1.0 / b->mass) / ((1.0 / a->mass) + (1.0 / b->mass)); + Vec3D normal = vec3d_normalize(delta_position); + a->position.x -= normal.x * penetration * k_a; + a->position.y -= normal.y * penetration * k_a; + a->position.z -= normal.z * penetration * k_a; + b->position.x += normal.x * penetration * k_b; + b->position.y += normal.y * penetration * k_b; + b->position.z += normal.z * penetration * k_b; + + double delta_velocity = vec3d_dot((Vec3D){b->velocity.x - a->velocity.x, b->velocity.y - a->velocity.y, b->velocity.z - a->velocity.z}, normal) - b->radius_change_speed - a->radius_change_speed; + + if (delta_velocity < 0) { + double e = ((double)rand() / RAND_MAX) * (MAX_HIT_E - MIN_HIT_E) + MIN_HIT_E; + Vec3D impulse = {normal.x * (1 + e) * delta_velocity, normal.y * (1 + e) * delta_velocity, normal.z * (1 + e) * delta_velocity}; + a->velocity.x += impulse.x * k_a; + a->velocity.y += impulse.y * k_a; + a->velocity.z += impulse.z * k_a; + b->velocity.x -= impulse.x * k_b; + b->velocity.y -= impulse.y * k_b; + b->velocity.z -= impulse.z * k_b; + } + } +} + +Vec3D collide_with_arena(Entity *e) { + DistanceAndNormal dan = dan_to_arena(e->position); + double penetration = e->radius - dan.distance; + if (penetration > 0) { + e->position.x += dan.normal.x * penetration; + e->position.y += dan.normal.y * penetration; + e->position.z += dan.normal.z * penetration; + + double velocity = vec3d_dot(e->velocity, dan.normal) - e->radius_change_speed; + if (velocity < 0) { + e->velocity.x -= (1 + e->arena_e) * velocity * dan.normal.x; + e->velocity.y -= (1 + e->arena_e) * velocity * dan.normal.y; + e->velocity.z -= (1 + e->arena_e) * velocity * dan.normal.z; + } + return dan.normal; + } + return (Vec3D){0,0,0}; // Return zero vector to indicate no collision +} + +void move(Entity *e, double delta_time) { + e->velocity = vec3d_clamp(e->velocity, MAX_ENTITY_SPEED); + e->position.x += e->velocity.x * delta_time; + e->position.y += e->velocity.y * delta_time; + e->position.z += e->velocity.z * delta_time; + e->position.y -= GRAVITY * delta_time * delta_time / 2.0; + e->velocity.y -= GRAVITY * delta_time; +} + +void goal_scored(void) { + printf("Goal!\n"); + // Reset game state (not implemented here, as it depends on game structure) +} + +void update(double delta_time, Entity robots[], Entity* ball) { + for (int i = 0; i < 2; i++) { // Assuming 2 robots + Vec3D target_velocity = vec3d_clamp(robots[i].action.target_velocity, ROBOT_MAX_GROUND_SPEED); + + if (robots[i].touch) { + target_velocity.x -= robots[i].touch_normal.x * vec3d_dot(robots[i].touch_normal, target_velocity); + target_velocity.y -= robots[i].touch_normal.y * vec3d_dot(robots[i].touch_normal, target_velocity); + target_velocity.z -= robots[i].touch_normal.z * vec3d_dot(robots[i].touch_normal, target_velocity); + + Vec3D target_velocity_change = {target_velocity.x - robots[i].velocity.x, target_velocity.y - robots[i].velocity.y, target_velocity.z - robots[i].velocity.z}; + if (vec3d_length(target_velocity_change) > 0) { + double acceleration = ROBOT_ACCELERATION * fmax(0, robots[i].touch_normal.y); + Vec3D acceleration_vec = vec3d_clamp((Vec3D){target_velocity_change.x * acceleration * delta_time / vec3d_length(target_velocity_change), target_velocity_change.y * acceleration * delta_time / vec3d_length(target_velocity_change), target_velocity_change.z * acceleration * delta_time / vec3d_length(target_velocity_change)}, vec3d_length(target_velocity_change)); + robots[i].velocity.x += acceleration_vec.x; + robots[i].velocity.y += acceleration_vec.y; + robots[i].velocity.z += acceleration_vec.z; + } + } + + if (robots[i].action.use_nitro && robots[i].nitro > 0) { // Check if nitro is available + Vec3D target_velocity_change = {robots[i].action.target_velocity.x - robots[i].velocity.x, robots[i].action.target_velocity.y - robots[i].velocity.y, robots[i].action.target_velocity.z - robots[i].velocity.z}; + target_velocity_change = vec3d_clamp(target_velocity_change, robots[i].nitro * NITRO_POINT_VELOCITY_CHANGE); + if (vec3d_length(target_velocity_change) > 0) { + Vec3D acceleration = vec3d_normalize(target_velocity_change); + acceleration.x *= ROBOT_NITRO_ACCELERATION; + acceleration.y *= ROBOT_NITRO_ACCELERATION; + acceleration.z *= ROBOT_NITRO_ACCELERATION; + Vec3D velocity_change = vec3d_clamp((Vec3D){acceleration.x * delta_time, acceleration.y * delta_time, acceleration.z * delta_time}, vec3d_length(target_velocity_change)); + robots[i].velocity.x += velocity_change.x; + robots[i].velocity.y += velocity_change.y; + robots[i].velocity.z += velocity_change.z; + robots[i].nitro -= vec3d_length(velocity_change) / NITRO_POINT_VELOCITY_CHANGE; + if (robots[i].nitro < 0) robots[i].nitro = 0; // Ensure nitro doesn't go negative + } + } + + move(&robots[i], delta_time); + robots[i].radius = ROBOT_MIN_RADIUS + (ROBOT_MAX_RADIUS - ROBOT_MIN_RADIUS) * robots[i].action.jump_speed / ROBOT_MAX_JUMP_SPEED; + robots[i].radius_change_speed = robots[i].action.jump_speed; + } + + move(ball, delta_time); + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < i; j++) { + collide_entities(&robots[i], &robots[j]); + } + } + + for (int i = 0; i < 2; i++) { + collide_entities(&robots[i], ball); + Vec3D collision_normal = collide_with_arena(&robots[i]); + robots[i].touch = (collision_normal.x != 0 || collision_normal.y != 0 || collision_normal.z != 0); + if (robots[i].touch) { + robots[i].touch_normal = collision_normal; + } + } + collide_with_arena(ball); + + if (fabs(ball->position.z) > arena.depth / 2.0 + ball->radius) { + goal_scored(); + } + + for (int i = 0; i < 2; i++) { + if (robots[i].nitro == MAX_NITRO_AMOUNT) continue; + for (int j = 0; j < NITRO_PACK_AMOUNT; j++){ + if (!nitro_packs[j].alive) continue; + Vec3D diff = vec3d_subtract(robots[i].position, nitro_packs[j].position); + + if (vec3d_length(diff) <= robots[i].radius + nitro_packs[j].radius) { + robots[i].nitro = MAX_NITRO_AMOUNT; + nitro_packs[j].alive = false; + nitro_packs[j].respawn_ticks = NITRO_PACK_RESPAWN_TICKS; + } + } + } +} + +void step(Entity robots[], Entity* ball) { + double delta_time = 1.0 / TICKS_PER_SECOND; + for (int i = 0; i < MICROTICKS_PER_TICK; i++) { + update(delta_time / MICROTICKS_PER_TICK, robots, ball); + } + for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { + if (!nitro_packs[i].alive) { + nitro_packs[i].respawn_ticks--; + if (nitro_packs[i].respawn_ticks == 0) { + nitro_packs[i].alive = true; + } + } + } +} + +typedef struct Client Client; +struct Client { + float width; + float height; + Color robot_color[2]; + Color ball_color; + Color nitro_color; +}; + +Client* make_client() { + Client* client = (Client*)calloc(1, sizeof(Client)); + client->width = 800; // Example width + client->height = 600; // Example height + client->robot_color[0] = RED; + client->robot_color[1] = BLUE; + client->ball_color = WHITE; + client->nitro_color = GREEN; + + InitWindow(client->width, client->height, "CodeBall"); + SetTargetFPS(60); + + return client; +} + +void close_client(Client* client) { + CloseWindow(); + free(client); +} + +void render(Client* client, Entity robots[], Entity* ball, NitroPack nitro_packs[]) { + BeginDrawing(); + ClearBackground(DARKGRAY); + + // Draw arena (simplified rectangle for now) + DrawRectangle(0, 0, client->width, client->height, GRAY); + + //Draw goal lines + DrawLine(client->width/2 - arena.goal_width/2,0,client->width/2 - arena.goal_width/2, arena.goal_height, BLUE); + DrawLine(client->width/2 + arena.goal_width/2,0,client->width/2 + arena.goal_width/2, arena.goal_height, RED); + + double depth = arena.depth + arena.goal_depth * 2.0; + + // Draw robots + for (int i = 0; i < 2; i++) { + DrawCircle((int)(client->width / 2 + robots[i].position.x * client->width / (arena.width)), + (int)(client->height / 2 - robots[i].position.z * client->height / (depth)), + (int)(robots[i].radius * client->width / (arena.width)), client->robot_color[i]); + } + + // Draw ball + DrawCircle((int)(client->width / 2 + ball->position.x * client->width / (arena.width)), + (int)(client->height / 2 - ball->position.z * client->height / (depth)), + (int)(ball->radius * client->width / (arena.width)), client->ball_color); + + + // Draw nitro packs + for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { + if (nitro_packs[i].alive) { + DrawCircle((int)(client->width / 2 + nitro_packs[i].position.x * client->width / (arena.width)), + (int)(client->height / 2 - nitro_packs[i].position.z * client->height / (depth)), + (int)(nitro_packs[i].radius * client->width / (arena.width)), client->nitro_color); + } + } + + DrawFPS(10, 10); + EndDrawing(); +} + +int main() { + srand(time(NULL)); // Seed the random number generator + Client* client = make_client(); + + // Initialize robots + Entity robots[2]; + for (int i = 0; i < 2; i++) { + robots[i].position.x = ((double)rand() / RAND_MAX) * (arena.width/2) * (i == 0 ? -1: 1); + robots[i].position.z = ((double)rand() / RAND_MAX) * (arena.depth/2) * (i == 0 ? -1: 1); + robots[i].position.y = 0; + robots[i].velocity = (Vec3D){0, 0, 0}; + robots[i].radius = ROBOT_MIN_RADIUS; + robots[i].radius_change_speed = 0; + robots[i].mass = ROBOT_MASS; + robots[i].arena_e = ROBOT_ARENA_E; + robots[i].touch = false; + robots[i].touch_normal = (Vec3D){0, 0, 0}; + robots[i].nitro = START_NITRO_AMOUNT; + robots[i].action.target_velocity = (Vec3D){0, 0, 0}; + robots[i].action.jump_speed = 0; + robots[i].action.use_nitro = false; + } + + // Initialize ball + Entity ball; + ball.position.x = 0; + ball.position.z = 0; + ball.position.y = ((double)rand() / RAND_MAX) * (3 * BALL_RADIUS) + BALL_RADIUS; // Random height + ball.velocity = (Vec3D){4, 0, 0}; + ball.radius = BALL_RADIUS; + ball.radius_change_speed = 0; + ball.mass = BALL_MASS; + ball.arena_e = BALL_ARENA_E; + ball.touch = false; + ball.touch_normal = (Vec3D){0, 0, 0}; + ball.nitro = 0; + ball.action.target_velocity = (Vec3D){0, 0, 0}; + ball.action.jump_speed = 0; + ball.action.use_nitro = false; + + // Initialize nitro packs + for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { + nitro_packs[i].position.x = (i % 2 == 0) ? NITRO_PACK_X : -NITRO_PACK_X; + nitro_packs[i].position.z = (i < 2) ? NITRO_PACK_Z : -NITRO_PACK_Z; + nitro_packs[i].position.y = NITRO_PACK_Y; + nitro_packs[i].alive = true; + nitro_packs[i].respawn_ticks = 0; + nitro_packs[i].radius = NITRO_PACK_RADIUS; + } + + // Example game loop with printing + for (int i = 0; i < 10000; i++) { + step(robots, &ball); + + for (int j = 0; j < 2; j++) + { + robots[j].action = (Action){ + .target_velocity = vec3d_multiply(vec3d_subtract(ball.position, robots[j].position), 4.0), + .jump_speed = 0, .use_nitro = false + }; + } + + // printf("Tick %d: ", i); + // for (int j = 0; j < 2; j++) { + // printf("Robot %d: Pos(%.2f, %.2f, %.2f), Vel(%.2f, %.2f, %.2f), Nitro: %.2f; ", j, robots[j].position.x, robots[j].position.y, robots[j].position.z, robots[j].velocity.x, robots[j].velocity.y, robots[j].velocity.z, robots[j].nitro); + // } + // printf("Ball: Pos(%.2f, %.2f, %.2f), Vel(%.2f, %.2f, %.2f); ", ball.position.x, ball.position.y, ball.position.z, ball.velocity.x, ball.velocity.y, ball.velocity.z); + // for (int j = 0; j < NITRO_PACK_AMOUNT; j++) { + // printf("NitroPack %d: Pos(%.2f, %.2f, %.2f), Alive: %d; ", j, nitro_packs[j].position.x, nitro_packs[j].position.y, nitro_packs[j].position.z, nitro_packs[j].alive); + // } + // printf("\n"); + + render(client, robots, &ball, nitro_packs); + } + + close_client(client); + + return 0; +} diff --git a/pufferlib/ocean/codeball/cy_squared.pyx b/pufferlib/ocean/codeball/cy_squared.pyx new file mode 100644 index 00000000..249f0376 --- /dev/null +++ b/pufferlib/ocean/codeball/cy_squared.pyx @@ -0,0 +1,70 @@ +cimport numpy as cnp +from libc.stdlib cimport calloc, free + +cdef extern from "squared.h": + ctypedef struct Squared: + unsigned char* observations + int* actions + float* rewards + unsigned char* terminals + int size + int tick + int r + int c + + ctypedef struct Client + + void reset(Squared* env) + void step(Squared* env) + + Client* make_client(Squared* env) + void close_client(Client* client) + void render(Client* client, Squared* env) + +cdef class CySquared: + cdef: + Squared* envs + Client* client + int num_envs + int size + + def __init__(self, unsigned char[:, :] observations, int[:] actions, + float[:] rewards, unsigned char[:] terminals, int num_envs, int size): + + self.envs = calloc(num_envs, sizeof(Squared)) + self.num_envs = num_envs + self.client = NULL + + cdef int i + for i in range(num_envs): + self.envs[i] = Squared( + observations = &observations[i, 0], + actions = &actions[i], + rewards = &rewards[i], + terminals = &terminals[i], + size=size, + ) + + def reset(self): + cdef int i + for i in range(self.num_envs): + reset(&self.envs[i]) + + def step(self): + cdef int i + for i in range(self.num_envs): + step(&self.envs[i]) + + def render(self): + cdef Squared* env = &self.envs[0] + if self.client == NULL: + self.client = make_client(env) + + render(self.client, env) + + def close(self): + if self.client != NULL: + close_client(self.client) + self.client = NULL + + free(self.envs) diff --git a/pufferlib/ocean/codeball/squared.c b/pufferlib/ocean/codeball/squared.c new file mode 100644 index 00000000..34332090 --- /dev/null +++ b/pufferlib/ocean/codeball/squared.c @@ -0,0 +1,33 @@ +#include "squared.h" +#include "puffernet.h" + +int main() { + //Weights* weights = load_weights("resources/pong_weights.bin", 133764); + //LinearLSTM* net = make_linearlstm(weights, 1, 8, 3); + + Squared env = {.size = 11}; + allocate(&env); + + Client* client = make_client(&env); + + reset(&env); + while (!WindowShouldClose()) { + if (IsKeyDown(KEY_LEFT_SHIFT)) { + env.actions[0] = 0; + if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) env.actions[0] = UP; + if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) env.actions[0] = DOWN; + if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) env.actions[0] = LEFT; + if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) env.actions[0] = RIGHT; + } else { + env.actions[0] = NOOP; + //forward_linearlstm(net, env.observations, env.actions); + } + step(&env); + render(client, &env); + } + //free_linearlstm(net); + //free(weights); + free_allocated(&env); + close_client(client); +} + diff --git a/pufferlib/ocean/codeball/squared.h b/pufferlib/ocean/codeball/squared.h new file mode 100644 index 00000000..74a7de6f --- /dev/null +++ b/pufferlib/ocean/codeball/squared.h @@ -0,0 +1,134 @@ +#include +#include +#include "raylib.h" + +const unsigned char NOOP = 0; +const unsigned char DOWN = 1; +const unsigned char UP = 2; +const unsigned char LEFT = 3; +const unsigned char RIGHT = 4; + +const unsigned char EMPTY = 0; +const unsigned char AGENT = 1; +const unsigned char TARGET = 2; + +typedef struct Squared Squared; +struct Squared { + unsigned char* observations; + int* actions; + float* rewards; + unsigned char* terminals; + int size; + int tick; + int r; + int c; +}; + +void allocate(Squared* env) { + env->observations = (unsigned char*)calloc(env->size*env->size, sizeof(unsigned char)); + env->actions = (int*)calloc(1, sizeof(int)); + env->rewards = (float*)calloc(1, sizeof(float)); + env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char)); +} + +void free_allocated(Squared* env) { + free(env->observations); + free(env->actions); + free(env->rewards); + free(env->terminals); +} + +void reset(Squared* env) { + memset(env->observations, 0, env->size*env->size*sizeof(unsigned char)); + env->observations[env->size*env->size/2] = AGENT; + env->r = env->size/2; + env->c = env->size/2; + env->tick = 0; + int target_idx; + do { + target_idx = rand() % (env->size*env->size); + } while (target_idx == env->size*env->size/2); + env->observations[target_idx] = TARGET; +} + +void step(Squared* env) { + int action = env->actions[0]; + env->terminals[0] = 0; + env->rewards[0] = 0; + + env->observations[env->r*env->size + env->c] = EMPTY; + + if (action == DOWN) { + env->r += 1; + } else if (action == RIGHT) { + env->c += 1; + } else if (action == UP) { + env->r -= 1; + } else if (action == LEFT) { + env->c -= 1; + } + + if (env->tick > 3*env->size + || env->r < 0 + || env->c < 0 + || env->r >= env->size + || env->c >= env->size) { + env->terminals[0] = 1; + env->rewards[0] = -1.0; + reset(env); + return; + } + + int pos = env->r*env->size + env->c; + if (env->observations[pos] == TARGET) { + env->terminals[0] = 1; + env->rewards[0] = 1.0; + reset(env); + return; + } + + env->observations[pos] = AGENT; + env->tick += 1; +} + +typedef struct Client Client; +struct Client { + Texture2D ball; +}; + +Client* make_client(Squared* env) { + Client* client = (Client*)calloc(1, sizeof(Client)); + int px = 64*env->size; + InitWindow(px, px, "PufferLib Squared"); + SetTargetFPS(5); + + //client->agent = LoadTexture("resources/puffers_128.png"); + return client; +} + +void close_client(Client* client) { + CloseWindow(); + free(client); +} + +void render(Client* client, Squared* env) { + if (IsKeyDown(KEY_ESCAPE)) { + exit(0); + } + + BeginDrawing(); + ClearBackground((Color){6, 24, 24, 255}); + + int px = 64; + for (int i = 0; i < env->size; i++) { + for (int j = 0; j < env->size; j++) { + int tex = env->observations[i*env->size + j]; + if (tex == EMPTY) { + continue; + } + Color color = (tex == AGENT) ? (Color){0, 255, 255, 255} : (Color){255, 0, 0, 255}; + DrawRectangle(j*px, i*px, px, px, color); + } + } + EndDrawing(); +} diff --git a/pufferlib/ocean/codeball/squared.py b/pufferlib/ocean/codeball/squared.py new file mode 100644 index 00000000..29a3c84d --- /dev/null +++ b/pufferlib/ocean/codeball/squared.py @@ -0,0 +1,45 @@ +'''A simple sample environment. Use this as a template for your own envs.''' + +import gymnasium +import numpy as np + +import pufferlib +from pufferlib.ocean.squared.cy_squared import CySquared + + +class Squared(pufferlib.PufferEnv): + def __init__(self, num_envs=1, render_mode=None, size=11, buf=None): + self.single_observation_space = gymnasium.spaces.Box(low=0, high=1, + shape=(size*size,), dtype=np.uint8) + self.single_action_space = gymnasium.spaces.Discrete(5) + self.render_mode = render_mode + self.num_agents = num_envs + + super().__init__(buf) + self.c_envs = CySquared(self.observations, self.actions, + self.rewards, self.terminals, num_envs, size) + + def reset(self, seed=None): + self.c_envs.reset() + return self.observations, [] + + def step(self, actions): + self.actions[:] = actions + self.c_envs.step() + + episode_returns = self.rewards[self.terminals] + + info = [] + if len(episode_returns) > 0: + info = [{ + 'reward': np.mean(episode_returns), + }] + + return (self.observations, self.rewards, + self.terminals, self.truncations, info) + + def render(self): + self.c_envs.render() + + def close(self): + self.c_envs.close() From 9f0ab279fecd96dadd8d5037068bda548e8625fb Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 11:27:27 -0500 Subject: [PATCH 02/40] More interesting robot dynamics --- pufferlib/ocean/codeball/codeball.c | 76 ++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index e762528e..ea68492f 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -88,8 +88,6 @@ typedef struct{ double radius; //Nitro pack has radius } NitroPack; -NitroPack nitro_packs[NITRO_PACK_AMOUNT]; - //Helper functions double vec3d_length(Vec3D v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } Vec3D vec3d_normalize(Vec3D v) { @@ -359,8 +357,20 @@ void goal_scored(void) { // Reset game state (not implemented here, as it depends on game structure) } -void update(double delta_time, Entity robots[], Entity* ball) { - for (int i = 0; i < 2; i++) { // Assuming 2 robots +typedef struct CodeBall { + Entity* ball; + int n_robots; + Entity* robots; + int n_nitros; + NitroPack* nitro_packs; +} CodeBall; + +void update(double delta_time, CodeBall* env) { + Entity* robots = env->robots; + Entity* ball = env->ball; + NitroPack* nitro_packs = env->nitro_packs; + + for (int i = 0; i < env->n_robots; i++) { // Assuming 2 robots Vec3D target_velocity = vec3d_clamp(robots[i].action.target_velocity, ROBOT_MAX_GROUND_SPEED); if (robots[i].touch) { @@ -402,13 +412,13 @@ void update(double delta_time, Entity robots[], Entity* ball) { move(ball, delta_time); - for (int i = 0; i < 2; i++) { + for (int i = 0; i < env->n_robots; i++) { for (int j = 0; j < i; j++) { collide_entities(&robots[i], &robots[j]); } } - for (int i = 0; i < 2; i++) { + for (int i = 0; i < env->n_robots; i++) { collide_entities(&robots[i], ball); Vec3D collision_normal = collide_with_arena(&robots[i]); robots[i].touch = (collision_normal.x != 0 || collision_normal.y != 0 || collision_normal.z != 0); @@ -422,9 +432,9 @@ void update(double delta_time, Entity robots[], Entity* ball) { goal_scored(); } - for (int i = 0; i < 2; i++) { + for (int i = 0; i < env->n_robots; i++) { if (robots[i].nitro == MAX_NITRO_AMOUNT) continue; - for (int j = 0; j < NITRO_PACK_AMOUNT; j++){ + for (int j = 0; j < env->n_nitros; j++){ if (!nitro_packs[j].alive) continue; Vec3D diff = vec3d_subtract(robots[i].position, nitro_packs[j].position); @@ -437,12 +447,13 @@ void update(double delta_time, Entity robots[], Entity* ball) { } } -void step(Entity robots[], Entity* ball) { +void step(CodeBall *env) { double delta_time = 1.0 / TICKS_PER_SECOND; for (int i = 0; i < MICROTICKS_PER_TICK; i++) { - update(delta_time / MICROTICKS_PER_TICK, robots, ball); + update(delta_time / MICROTICKS_PER_TICK, env); } - for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { + NitroPack *nitro_packs = env->nitro_packs; + for (int i = 0; i < env->n_nitros; i++) { if (!nitro_packs[i].alive) { nitro_packs[i].respawn_ticks--; if (nitro_packs[i].respawn_ticks == 0) { @@ -481,10 +492,14 @@ void close_client(Client* client) { free(client); } -void render(Client* client, Entity robots[], Entity* ball, NitroPack nitro_packs[]) { +void render(Client* client, CodeBall *env) { BeginDrawing(); ClearBackground(DARKGRAY); + Entity* robots = env->robots; + Entity* ball = env->ball; + NitroPack* nitro_packs = env->nitro_packs; + // Draw arena (simplified rectangle for now) DrawRectangle(0, 0, client->width, client->height, GRAY); @@ -495,7 +510,7 @@ void render(Client* client, Entity robots[], Entity* ball, NitroPack nitro_packs double depth = arena.depth + arena.goal_depth * 2.0; // Draw robots - for (int i = 0; i < 2; i++) { + for (int i = 0; i < env->n_robots; i++) { DrawCircle((int)(client->width / 2 + robots[i].position.x * client->width / (arena.width)), (int)(client->height / 2 - robots[i].position.z * client->height / (depth)), (int)(robots[i].radius * client->width / (arena.width)), client->robot_color[i]); @@ -508,7 +523,7 @@ void render(Client* client, Entity robots[], Entity* ball, NitroPack nitro_packs // Draw nitro packs - for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { + for (int i = 0; i < env->n_nitros; i++) { if (nitro_packs[i].alive) { DrawCircle((int)(client->width / 2 + nitro_packs[i].position.x * client->width / (arena.width)), (int)(client->height / 2 - nitro_packs[i].position.z * client->height / (depth)), @@ -525,8 +540,8 @@ int main() { Client* client = make_client(); // Initialize robots - Entity robots[2]; - for (int i = 0; i < 2; i++) { + Entity robots[6]; + for (int i = 0; i < 6; i++) { robots[i].position.x = ((double)rand() / RAND_MAX) * (arena.width/2) * (i == 0 ? -1: 1); robots[i].position.z = ((double)rand() / RAND_MAX) * (arena.depth/2) * (i == 0 ? -1: 1); robots[i].position.y = 0; @@ -561,6 +576,7 @@ int main() { ball.action.use_nitro = false; // Initialize nitro packs + NitroPack nitro_packs[NITRO_PACK_AMOUNT]; for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { nitro_packs[i].position.x = (i % 2 == 0) ? NITRO_PACK_X : -NITRO_PACK_X; nitro_packs[i].position.z = (i < 2) ? NITRO_PACK_Z : -NITRO_PACK_Z; @@ -570,16 +586,30 @@ int main() { nitro_packs[i].radius = NITRO_PACK_RADIUS; } + CodeBall env = {&ball, 6, robots, NITRO_PACK_AMOUNT, nitro_packs}; + env.nitro_packs = nitro_packs; + // Example game loop with printing for (int i = 0; i < 10000; i++) { - step(robots, &ball); + step(&env); - for (int j = 0; j < 2; j++) + for (int j = 0; j < env.n_robots; j++) { - robots[j].action = (Action){ - .target_velocity = vec3d_multiply(vec3d_subtract(ball.position, robots[j].position), 4.0), - .jump_speed = 0, .use_nitro = false - }; + Vec3D tgt = + vec3d_subtract(ball.position, robots[j].position); + for (int k = 0; k < env.n_robots; k++) { + if (k != j) { + Vec3D diff = vec3d_subtract(robots[k].position, robots[j].position); + double diff_len = vec3d_length(diff); + if (diff_len < 2.5) { + tgt = vec3d_multiply(diff, -1.0); + } + } + } + robots[j].action = + (Action){.target_velocity = vec3d_multiply(tgt, 4.0), + .jump_speed = 0, + .use_nitro = false}; } // printf("Tick %d: ", i); @@ -592,7 +622,7 @@ int main() { // } // printf("\n"); - render(client, robots, &ball, nitro_packs); + render(client, &env); } close_client(client); From c55db3bb6e87fba6320d8a4e1c74e02ce966aac2 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 15:31:02 -0500 Subject: [PATCH 03/40] Separate out header file --- pufferlib/ocean/codeball/Makefile | 5 +- pufferlib/ocean/codeball/codeball.c | 630 +--------------------- pufferlib/ocean/codeball/codeball.h | 789 ++++++++++++++++++++++++++++ 3 files changed, 819 insertions(+), 605 deletions(-) create mode 100644 pufferlib/ocean/codeball/codeball.h diff --git a/pufferlib/ocean/codeball/Makefile b/pufferlib/ocean/codeball/Makefile index b6dfcc54..05335499 100644 --- a/pufferlib/ocean/codeball/Makefile +++ b/pufferlib/ocean/codeball/Makefile @@ -1,13 +1,14 @@ CFILES=codeball.c CFLAGS=\ -lm -I../../../raylib/include -I../../../pufferlib -lpthread ../../../raylib/lib/libraylib.a -lglfw -lobjc \ - -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -framework GLUT -framework AppKit + -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -framework GLUT -framework AppKit \ + -O3 -march=native -funroll-loops # -std=c99 -Wall -pedantic -Wextra -Werror -Wstrict-prototypes -Wold-style-definition -Werror=vla -lm \ -I../../../raylib/include -I../../../pufferlib -lpthread ARCH=$(shell uname -m) -codeball: $(CFILES) +codeball: $(CFILES) codeball.h gcc $(CFLAGS) $(CFILES) -o codeball run: codeball diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index ea68492f..5823afab 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -1,628 +1,52 @@ -#include -#include -#include -#include -#include -#include "puffernet.h" -#include "raylib.h" +#include "codeball.h" +#include -// Constants (from the description) -#define ROBOT_MIN_RADIUS 1.0 -#define ROBOT_MAX_RADIUS 1.05 -#define ROBOT_MAX_JUMP_SPEED 15.0 -#define ROBOT_ACCELERATION 100.0 -#define ROBOT_NITRO_ACCELERATION 30.0 -#define ROBOT_MAX_GROUND_SPEED 30.0 -#define ROBOT_ARENA_E 0.0 -#define ROBOT_MASS 2.0 -#define TICKS_PER_SECOND 60 -#define MICROTICKS_PER_TICK 100 -#define RESET_TICKS (2 * TICKS_PER_SECOND) -#define BALL_ARENA_E 0.7 -#define BALL_RADIUS 2.0 -#define BALL_MASS 1.0 -#define MIN_HIT_E 0.4 -#define MAX_HIT_E 0.5 -#define MAX_ENTITY_SPEED 100.0 -#define MAX_NITRO_AMOUNT 100.0 -#define START_NITRO_AMOUNT 50.0 -#define NITRO_POINT_VELOCITY_CHANGE 0.6 -#define NITRO_PACK_X 20.0 -#define NITRO_PACK_Y 1.0 -#define NITRO_PACK_Z 30.0 -#define NITRO_PACK_RADIUS 0.5 -#define NITRO_PACK_AMOUNT 4 // Corrected: There are 4 nitro packs -#define NITRO_PACK_RESPAWN_TICKS (10 * TICKS_PER_SECOND) -#define GRAVITY 30.0 - -// Arena parameters (from the description) -typedef struct { - double width; - double height; - double depth; - double bottom_radius; - double top_radius; - double corner_radius; - double goal_top_radius; - double goal_width; - double goal_depth; - double goal_height; - double goal_side_radius; -} CodeBallArena; - -CodeBallArena arena = {60, 20, 80, 3, 7, 13, 3, 30, 10, 10, 1}; - -// 3D Vector -typedef struct { - double x; - double y; - double z; -} Vec3D; - -// Action structure -typedef struct { - Vec3D target_velocity; - double jump_speed; - bool use_nitro; -} Action; - -// Entity (Robot or Ball) -typedef struct { - Vec3D position; - Vec3D velocity; - double radius; - double radius_change_speed; - double mass; - double arena_e; - bool touch; - Vec3D touch_normal; - double nitro; - Action action; -} Entity; - -//Nitro Pack -typedef struct{ - Vec3D position; - bool alive; - int respawn_ticks; - double radius; //Nitro pack has radius -} NitroPack; - -//Helper functions -double vec3d_length(Vec3D v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } -Vec3D vec3d_normalize(Vec3D v) { - double l = vec3d_length(v); - if (l == 0) return (Vec3D){0, 0, 0}; // Avoid division by zero - return (Vec3D){v.x / l, v.y / l, v.z / l}; -} - -double vec3d_dot(Vec3D a, Vec3D b) { return a.x * b.x + a.y * b.y + a.z * b.z; } - -Vec3D vec3d_subtract(Vec3D a, Vec3D b) { return (Vec3D){a.x - b.x, a.y - b.y, a.z - b.z}; } -Vec3D vec3d_add(Vec3D a, Vec3D b) { return (Vec3D){a.x + b.x, a.y + b.y, a.z + b.z}; } -Vec3D vec3d_multiply(Vec3D v, double s) { return (Vec3D){v.x * s, v.y * s, v.z * s}; } - -double clamp(double val, double min, double max) { - if (val < min) return min; - if (val > max) return max; - return val; -} -Vec3D vec3d_clamp(Vec3D v, double max_length) { - double length = vec3d_length(v); - if (length > max_length) { - return vec3d_multiply(v, max_length / length); - } - return v; -} - -typedef struct { - double distance; - Vec3D normal; -} DistanceAndNormal; - -// Distance and Normal functions -DistanceAndNormal dan_to_plane(Vec3D point, Vec3D point_on_plane, Vec3D plane_normal) { - return (DistanceAndNormal){vec3d_dot(vec3d_subtract(point, point_on_plane), plane_normal), plane_normal}; -} - -DistanceAndNormal dan_to_sphere_inner(Vec3D point, Vec3D sphere_center, double sphere_radius) { - return (DistanceAndNormal){sphere_radius - vec3d_length(vec3d_subtract(point, sphere_center)), vec3d_normalize(vec3d_subtract(sphere_center, point))}; -} - -DistanceAndNormal dan_to_sphere_outer(Vec3D point, Vec3D sphere_center, double sphere_radius) { - return (DistanceAndNormal){vec3d_length(vec3d_subtract(point, sphere_center)) - sphere_radius, vec3d_normalize(vec3d_subtract(point, sphere_center))}; -} - -DistanceAndNormal dan_to_arena_quarter(Vec3D point) { - DistanceAndNormal dan; - dan.distance = 1e9; // Initialize with a large value - - // Ground - DistanceAndNormal temp_dan = dan_to_plane(point, (Vec3D){0, 0, 0}, (Vec3D){0, 1, 0}); - if (temp_dan.distance < dan.distance) dan = temp_dan; - - // Ceiling - temp_dan = dan_to_plane(point, (Vec3D){0, arena.height, 0}, (Vec3D){0, -1, 0}); - if (temp_dan.distance < dan.distance) dan = temp_dan; - - // Side x - temp_dan = dan_to_plane(point, (Vec3D){arena.width / 2.0, 0, 0}, (Vec3D){-1, 0, 0}); - if (temp_dan.distance < dan.distance) dan = temp_dan; - - // Side z (goal) - temp_dan = dan_to_plane(point, (Vec3D){0, 0, (arena.depth / 2.0) + arena.goal_depth}, (Vec3D){0, 0, -1}); - if (temp_dan.distance < dan.distance) dan = temp_dan; - - // Side z (regular) - Vec3D v = {point.x - ((arena.goal_width / 2.0) - arena.goal_top_radius), point.y - (arena.goal_height - arena.goal_top_radius), 0}; - if (point.x >= (arena.goal_width / 2.0) + arena.goal_side_radius || - point.y >= arena.goal_height + arena.goal_side_radius || - (v.x > 0 && v.y > 0 && vec3d_length(v) >= arena.goal_top_radius + arena.goal_side_radius)) { - temp_dan = dan_to_plane(point, (Vec3D){0, 0, arena.depth / 2.0}, (Vec3D){0, 0, -1}); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - - // Goal back corners - if (point.z > (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius) { - temp_dan = dan_to_sphere_inner(point, (Vec3D){clamp(point.x, arena.bottom_radius - (arena.goal_width / 2.0), (arena.goal_width / 2.0) - arena.bottom_radius), clamp(point.y, arena.bottom_radius, arena.goal_height - arena.goal_top_radius), (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - - // Corner - if (point.x > (arena.width / 2.0) - arena.corner_radius && point.z > (arena.depth / 2.0) - arena.corner_radius) { - temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.width / 2.0) - arena.corner_radius, point.y, (arena.depth / 2.0) - arena.corner_radius}, arena.corner_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - - // Goal outer corner - if (point.z < (arena.depth / 2.0) + arena.goal_side_radius) { - // Side x - if (point.x < (arena.goal_width / 2.0) + arena.goal_side_radius) { - temp_dan = dan_to_sphere_outer(point, (Vec3D){(arena.goal_width / 2.0) + arena.goal_side_radius, point.y, (arena.depth / 2.0) + arena.goal_side_radius}, arena.goal_side_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Ceiling - if (point.y < arena.goal_height + arena.goal_side_radius) { - temp_dan = dan_to_sphere_outer(point, (Vec3D){point.x, arena.goal_height + arena.goal_side_radius, (arena.depth / 2.0) + arena.goal_side_radius}, arena.goal_side_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Top corner - Vec3D o = {(arena.goal_width / 2.0) - arena.goal_top_radius, arena.goal_height - arena.goal_top_radius, 0}; - v = (Vec3D){point.x - o.x, point.y - o.y, 0}; - if (v.x > 0 && v.y > 0) { - o.x += vec3d_normalize(v).x * (arena.goal_top_radius + arena.goal_side_radius); - o.y += vec3d_normalize(v).y * (arena.goal_top_radius + arena.goal_side_radius); - temp_dan = dan_to_sphere_outer(point, (Vec3D){o.x, o.y, (arena.depth / 2.0) + arena.goal_side_radius}, arena.goal_side_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - } - - // Goal inside top corners - if (point.z > (arena.depth / 2.0) + arena.goal_side_radius && point.y > arena.goal_height - arena.goal_top_radius) { - // Side x - if (point.x > (arena.goal_width / 2.0) - arena.goal_top_radius) { - temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.goal_width / 2.0) - arena.goal_top_radius, arena.goal_height - arena.goal_top_radius, point.z}, arena.goal_top_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Side z - if (point.z > (arena.depth / 2.0) + arena.goal_depth - arena.goal_top_radius) { - temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.goal_height - arena.goal_top_radius, (arena.depth / 2.0) + arena.goal_depth - arena.goal_top_radius}, arena.goal_top_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - } - - // Bottom corners - if (point.y < arena.bottom_radius) { - // Side x - if (point.x > (arena.width / 2.0) - arena.bottom_radius) { - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.width / 2.0) - arena.bottom_radius, arena.bottom_radius, point.z}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Side z - if (point.z > (arena.depth / 2.0) - arena.bottom_radius && point.x >= (arena.goal_width / 2.0) + arena.goal_side_radius) { - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.bottom_radius, (arena.depth / 2.0) - arena.bottom_radius}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Side z (goal) - if (point.z > (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius) { - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.bottom_radius, (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Goal outer corner - Vec3D o = {(arena.goal_width / 2.0) + arena.goal_side_radius, (arena.depth / 2.0) + arena.goal_side_radius, 0}; - Vec3D v = {point.x - o.x, point.z - o.y, 0}; - if (v.x < 0 && v.y < 0 && vec3d_length(v) < arena.goal_side_radius + arena.bottom_radius) { - o.x += vec3d_normalize(v).x * (arena.goal_side_radius + arena.bottom_radius); - o.y += vec3d_normalize(v).y * (arena.goal_side_radius + arena.bottom_radius); - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){o.x, arena.bottom_radius, o.y}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Side x (goal) - if (point.z >= (arena.depth / 2.0) + arena.goal_side_radius && point.x > (arena.goal_width / 2.0) - arena.bottom_radius) { - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.goal_width / 2.0) - arena.bottom_radius, arena.bottom_radius, point.z}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Corner - if (point.x > (arena.width / 2.0) - arena.corner_radius && point.z > (arena.depth / 2.0) - arena.corner_radius) { - Vec3D corner_o = {(arena.width / 2.0) - arena.corner_radius, (arena.depth / 2.0) - arena.corner_radius, 0}; - Vec3D n = {point.x - corner_o.x, point.z - corner_o.y, 0}; - double dist = vec3d_length(n); - if (dist > arena.corner_radius - arena.bottom_radius) { - n = vec3d_normalize(n); - Vec3D o2 = {corner_o.x + n.x * (arena.corner_radius - arena.bottom_radius), corner_o.y + n.y * (arena.corner_radius - arena.bottom_radius), 0}; - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){o2.x, arena.bottom_radius, o2.y}, arena.bottom_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - } - } - - // Ceiling corners - if (point.y > arena.height - arena.top_radius) { - // Side x - if (point.x > (arena.width / 2.0) - arena.top_radius) { - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){(arena.width / 2.0) - arena.top_radius, arena.height - arena.top_radius, point.z}, arena.top_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Side z - if (point.z > (arena.depth / 2.0) - arena.top_radius) { - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, (Vec3D){point.x, arena.height - arena.top_radius, (arena.depth / 2.0) - arena.top_radius}, arena.top_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - // Corner - if (point.x > (arena.width / 2.0) - arena.corner_radius && point.z > (arena.depth / 2.0) - arena.corner_radius) { - Vec3D corner_o = {(arena.width / 2.0) - arena.corner_radius, (arena.depth / 2.0) - arena.corner_radius, 0}; - Vec3D dv = {point.x - corner_o.x, point.z - corner_o.y, 0}; - if (vec3d_length(dv) > arena.corner_radius - arena.top_radius) { - Vec3D n = vec3d_normalize(dv); - Vec3D o2 = {corner_o.x + n.x * (arena.corner_radius - arena.top_radius), arena.height - arena.top_radius, o2.y}; - DistanceAndNormal temp_dan = dan_to_sphere_inner(point, o2, arena.top_radius); - if (temp_dan.distance < dan.distance) dan = temp_dan; - } - } - } - return dan; -} - -DistanceAndNormal dan_to_arena(Vec3D point) { - bool negate_x = point.x < 0; - bool negate_z = point.z < 0; - if (negate_x) point.x = -point.x; - if (negate_z) point.z = -point.z; - DistanceAndNormal result = dan_to_arena_quarter(point); - if (negate_x) result.normal.x = -result.normal.x; - if (negate_z) result.normal.z = -result.normal.z; - return result; -} - -void collide_entities(Entity *a, Entity *b) { - Vec3D delta_position = {b->position.x - a->position.x, b->position.y - a->position.y, b->position.z - a->position.z}; - double distance = vec3d_length(delta_position); - double penetration = a->radius + b->radius - distance; - if (penetration > 0) { - double k_a = (1.0 / a->mass) / ((1.0 / a->mass) + (1.0 / b->mass)); - double k_b = (1.0 / b->mass) / ((1.0 / a->mass) + (1.0 / b->mass)); - Vec3D normal = vec3d_normalize(delta_position); - a->position.x -= normal.x * penetration * k_a; - a->position.y -= normal.y * penetration * k_a; - a->position.z -= normal.z * penetration * k_a; - b->position.x += normal.x * penetration * k_b; - b->position.y += normal.y * penetration * k_b; - b->position.z += normal.z * penetration * k_b; - - double delta_velocity = vec3d_dot((Vec3D){b->velocity.x - a->velocity.x, b->velocity.y - a->velocity.y, b->velocity.z - a->velocity.z}, normal) - b->radius_change_speed - a->radius_change_speed; - - if (delta_velocity < 0) { - double e = ((double)rand() / RAND_MAX) * (MAX_HIT_E - MIN_HIT_E) + MIN_HIT_E; - Vec3D impulse = {normal.x * (1 + e) * delta_velocity, normal.y * (1 + e) * delta_velocity, normal.z * (1 + e) * delta_velocity}; - a->velocity.x += impulse.x * k_a; - a->velocity.y += impulse.y * k_a; - a->velocity.z += impulse.z * k_a; - b->velocity.x -= impulse.x * k_b; - b->velocity.y -= impulse.y * k_b; - b->velocity.z -= impulse.z * k_b; - } - } -} - -Vec3D collide_with_arena(Entity *e) { - DistanceAndNormal dan = dan_to_arena(e->position); - double penetration = e->radius - dan.distance; - if (penetration > 0) { - e->position.x += dan.normal.x * penetration; - e->position.y += dan.normal.y * penetration; - e->position.z += dan.normal.z * penetration; - - double velocity = vec3d_dot(e->velocity, dan.normal) - e->radius_change_speed; - if (velocity < 0) { - e->velocity.x -= (1 + e->arena_e) * velocity * dan.normal.x; - e->velocity.y -= (1 + e->arena_e) * velocity * dan.normal.y; - e->velocity.z -= (1 + e->arena_e) * velocity * dan.normal.z; - } - return dan.normal; - } - return (Vec3D){0,0,0}; // Return zero vector to indicate no collision -} - -void move(Entity *e, double delta_time) { - e->velocity = vec3d_clamp(e->velocity, MAX_ENTITY_SPEED); - e->position.x += e->velocity.x * delta_time; - e->position.y += e->velocity.y * delta_time; - e->position.z += e->velocity.z * delta_time; - e->position.y -= GRAVITY * delta_time * delta_time / 2.0; - e->velocity.y -= GRAVITY * delta_time; -} - -void goal_scored(void) { - printf("Goal!\n"); - // Reset game state (not implemented here, as it depends on game structure) -} - -typedef struct CodeBall { - Entity* ball; - int n_robots; - Entity* robots; - int n_nitros; - NitroPack* nitro_packs; -} CodeBall; - -void update(double delta_time, CodeBall* env) { - Entity* robots = env->robots; - Entity* ball = env->ball; - NitroPack* nitro_packs = env->nitro_packs; - - for (int i = 0; i < env->n_robots; i++) { // Assuming 2 robots - Vec3D target_velocity = vec3d_clamp(robots[i].action.target_velocity, ROBOT_MAX_GROUND_SPEED); - - if (robots[i].touch) { - target_velocity.x -= robots[i].touch_normal.x * vec3d_dot(robots[i].touch_normal, target_velocity); - target_velocity.y -= robots[i].touch_normal.y * vec3d_dot(robots[i].touch_normal, target_velocity); - target_velocity.z -= robots[i].touch_normal.z * vec3d_dot(robots[i].touch_normal, target_velocity); - - Vec3D target_velocity_change = {target_velocity.x - robots[i].velocity.x, target_velocity.y - robots[i].velocity.y, target_velocity.z - robots[i].velocity.z}; - if (vec3d_length(target_velocity_change) > 0) { - double acceleration = ROBOT_ACCELERATION * fmax(0, robots[i].touch_normal.y); - Vec3D acceleration_vec = vec3d_clamp((Vec3D){target_velocity_change.x * acceleration * delta_time / vec3d_length(target_velocity_change), target_velocity_change.y * acceleration * delta_time / vec3d_length(target_velocity_change), target_velocity_change.z * acceleration * delta_time / vec3d_length(target_velocity_change)}, vec3d_length(target_velocity_change)); - robots[i].velocity.x += acceleration_vec.x; - robots[i].velocity.y += acceleration_vec.y; - robots[i].velocity.z += acceleration_vec.z; - } - } - - if (robots[i].action.use_nitro && robots[i].nitro > 0) { // Check if nitro is available - Vec3D target_velocity_change = {robots[i].action.target_velocity.x - robots[i].velocity.x, robots[i].action.target_velocity.y - robots[i].velocity.y, robots[i].action.target_velocity.z - robots[i].velocity.z}; - target_velocity_change = vec3d_clamp(target_velocity_change, robots[i].nitro * NITRO_POINT_VELOCITY_CHANGE); - if (vec3d_length(target_velocity_change) > 0) { - Vec3D acceleration = vec3d_normalize(target_velocity_change); - acceleration.x *= ROBOT_NITRO_ACCELERATION; - acceleration.y *= ROBOT_NITRO_ACCELERATION; - acceleration.z *= ROBOT_NITRO_ACCELERATION; - Vec3D velocity_change = vec3d_clamp((Vec3D){acceleration.x * delta_time, acceleration.y * delta_time, acceleration.z * delta_time}, vec3d_length(target_velocity_change)); - robots[i].velocity.x += velocity_change.x; - robots[i].velocity.y += velocity_change.y; - robots[i].velocity.z += velocity_change.z; - robots[i].nitro -= vec3d_length(velocity_change) / NITRO_POINT_VELOCITY_CHANGE; - if (robots[i].nitro < 0) robots[i].nitro = 0; // Ensure nitro doesn't go negative - } - } - - move(&robots[i], delta_time); - robots[i].radius = ROBOT_MIN_RADIUS + (ROBOT_MAX_RADIUS - ROBOT_MIN_RADIUS) * robots[i].action.jump_speed / ROBOT_MAX_JUMP_SPEED; - robots[i].radius_change_speed = robots[i].action.jump_speed; - } - - move(ball, delta_time); - - for (int i = 0; i < env->n_robots; i++) { - for (int j = 0; j < i; j++) { - collide_entities(&robots[i], &robots[j]); - } - } - - for (int i = 0; i < env->n_robots; i++) { - collide_entities(&robots[i], ball); - Vec3D collision_normal = collide_with_arena(&robots[i]); - robots[i].touch = (collision_normal.x != 0 || collision_normal.y != 0 || collision_normal.z != 0); - if (robots[i].touch) { - robots[i].touch_normal = collision_normal; - } - } - collide_with_arena(ball); - - if (fabs(ball->position.z) > arena.depth / 2.0 + ball->radius) { - goal_scored(); - } - - for (int i = 0; i < env->n_robots; i++) { - if (robots[i].nitro == MAX_NITRO_AMOUNT) continue; - for (int j = 0; j < env->n_nitros; j++){ - if (!nitro_packs[j].alive) continue; - Vec3D diff = vec3d_subtract(robots[i].position, nitro_packs[j].position); - - if (vec3d_length(diff) <= robots[i].radius + nitro_packs[j].radius) { - robots[i].nitro = MAX_NITRO_AMOUNT; - nitro_packs[j].alive = false; - nitro_packs[j].respawn_ticks = NITRO_PACK_RESPAWN_TICKS; - } - } - } -} - -void step(CodeBall *env) { - double delta_time = 1.0 / TICKS_PER_SECOND; - for (int i = 0; i < MICROTICKS_PER_TICK; i++) { - update(delta_time / MICROTICKS_PER_TICK, env); - } - NitroPack *nitro_packs = env->nitro_packs; - for (int i = 0; i < env->n_nitros; i++) { - if (!nitro_packs[i].alive) { - nitro_packs[i].respawn_ticks--; - if (nitro_packs[i].respawn_ticks == 0) { - nitro_packs[i].alive = true; - } - } - } -} - -typedef struct Client Client; -struct Client { - float width; - float height; - Color robot_color[2]; - Color ball_color; - Color nitro_color; -}; - -Client* make_client() { - Client* client = (Client*)calloc(1, sizeof(Client)); - client->width = 800; // Example width - client->height = 600; // Example height - client->robot_color[0] = RED; - client->robot_color[1] = BLUE; - client->ball_color = WHITE; - client->nitro_color = GREEN; - - InitWindow(client->width, client->height, "CodeBall"); - SetTargetFPS(60); - - return client; -} - -void close_client(Client* client) { - CloseWindow(); - free(client); -} - -void render(Client* client, CodeBall *env) { - BeginDrawing(); - ClearBackground(DARKGRAY); - - Entity* robots = env->robots; - Entity* ball = env->ball; - NitroPack* nitro_packs = env->nitro_packs; - - // Draw arena (simplified rectangle for now) - DrawRectangle(0, 0, client->width, client->height, GRAY); - - //Draw goal lines - DrawLine(client->width/2 - arena.goal_width/2,0,client->width/2 - arena.goal_width/2, arena.goal_height, BLUE); - DrawLine(client->width/2 + arena.goal_width/2,0,client->width/2 + arena.goal_width/2, arena.goal_height, RED); - - double depth = arena.depth + arena.goal_depth * 2.0; - - // Draw robots - for (int i = 0; i < env->n_robots; i++) { - DrawCircle((int)(client->width / 2 + robots[i].position.x * client->width / (arena.width)), - (int)(client->height / 2 - robots[i].position.z * client->height / (depth)), - (int)(robots[i].radius * client->width / (arena.width)), client->robot_color[i]); - } - - // Draw ball - DrawCircle((int)(client->width / 2 + ball->position.x * client->width / (arena.width)), - (int)(client->height / 2 - ball->position.z * client->height / (depth)), - (int)(ball->radius * client->width / (arena.width)), client->ball_color); - - - // Draw nitro packs - for (int i = 0; i < env->n_nitros; i++) { - if (nitro_packs[i].alive) { - DrawCircle((int)(client->width / 2 + nitro_packs[i].position.x * client->width / (arena.width)), - (int)(client->height / 2 - nitro_packs[i].position.z * client->height / (depth)), - (int)(nitro_packs[i].radius * client->width / (arena.width)), client->nitro_color); - } - } - - DrawFPS(10, 10); - EndDrawing(); -} int main() { srand(time(NULL)); // Seed the random number generator Client* client = make_client(); - // Initialize robots - Entity robots[6]; - for (int i = 0; i < 6; i++) { - robots[i].position.x = ((double)rand() / RAND_MAX) * (arena.width/2) * (i == 0 ? -1: 1); - robots[i].position.z = ((double)rand() / RAND_MAX) * (arena.depth/2) * (i == 0 ? -1: 1); - robots[i].position.y = 0; - robots[i].velocity = (Vec3D){0, 0, 0}; - robots[i].radius = ROBOT_MIN_RADIUS; - robots[i].radius_change_speed = 0; - robots[i].mass = ROBOT_MASS; - robots[i].arena_e = ROBOT_ARENA_E; - robots[i].touch = false; - robots[i].touch_normal = (Vec3D){0, 0, 0}; - robots[i].nitro = START_NITRO_AMOUNT; - robots[i].action.target_velocity = (Vec3D){0, 0, 0}; - robots[i].action.jump_speed = 0; - robots[i].action.use_nitro = false; - } + CodeBall env; + env.n_robots = 6; + env.n_nitros = 4; + allocate(&env); + reset(&env); - // Initialize ball - Entity ball; - ball.position.x = 0; - ball.position.z = 0; - ball.position.y = ((double)rand() / RAND_MAX) * (3 * BALL_RADIUS) + BALL_RADIUS; // Random height - ball.velocity = (Vec3D){4, 0, 0}; - ball.radius = BALL_RADIUS; - ball.radius_change_speed = 0; - ball.mass = BALL_MASS; - ball.arena_e = BALL_ARENA_E; - ball.touch = false; - ball.touch_normal = (Vec3D){0, 0, 0}; - ball.nitro = 0; - ball.action.target_velocity = (Vec3D){0, 0, 0}; - ball.action.jump_speed = 0; - ball.action.use_nitro = false; + struct timeval start, end; + gettimeofday(&start, NULL); - // Initialize nitro packs - NitroPack nitro_packs[NITRO_PACK_AMOUNT]; - for (int i = 0; i < NITRO_PACK_AMOUNT; i++) { - nitro_packs[i].position.x = (i % 2 == 0) ? NITRO_PACK_X : -NITRO_PACK_X; - nitro_packs[i].position.z = (i < 2) ? NITRO_PACK_Z : -NITRO_PACK_Z; - nitro_packs[i].position.y = NITRO_PACK_Y; - nitro_packs[i].alive = true; - nitro_packs[i].respawn_ticks = 0; - nitro_packs[i].radius = NITRO_PACK_RADIUS; - } + int initial_steps = 5000; - CodeBall env = {&ball, 6, robots, NITRO_PACK_AMOUNT, nitro_packs}; - env.nitro_packs = nitro_packs; - - // Example game loop with printing for (int i = 0; i < 10000; i++) { - step(&env); - for (int j = 0; j < env.n_robots; j++) { Vec3D tgt = - vec3d_subtract(ball.position, robots[j].position); + vec3d_subtract(env.ball.position, env.robots[j].position); for (int k = 0; k < env.n_robots; k++) { if (k != j) { - Vec3D diff = vec3d_subtract(robots[k].position, robots[j].position); + Vec3D diff = vec3d_subtract(env.robots[k].position, env.robots[j].position); double diff_len = vec3d_length(diff); if (diff_len < 2.5) { tgt = vec3d_multiply(diff, -1.0); } } } - robots[j].action = - (Action){.target_velocity = vec3d_multiply(tgt, 4.0), - .jump_speed = 0, - .use_nitro = false}; + tgt = vec3d_multiply(tgt, 4.0); + env.actions[j * 4] = tgt.x; + env.actions[j * 4 + 1] = tgt.y; + env.actions[j * 4 + 2] = tgt.z; + } + step(&env); + if (i == initial_steps) { + gettimeofday(&end, NULL); + double elapsed = (end.tv_sec - start.tv_sec) + + (end.tv_usec - start.tv_usec) / 1000000.0; + printf("%d steps took %f seconds\n", initial_steps, elapsed); + printf("SPS: \t%f\n", ((double)initial_steps) / elapsed); + } + if (i > initial_steps) { + render(client, &env); } - - // printf("Tick %d: ", i); - // for (int j = 0; j < 2; j++) { - // printf("Robot %d: Pos(%.2f, %.2f, %.2f), Vel(%.2f, %.2f, %.2f), Nitro: %.2f; ", j, robots[j].position.x, robots[j].position.y, robots[j].position.z, robots[j].velocity.x, robots[j].velocity.y, robots[j].velocity.z, robots[j].nitro); - // } - // printf("Ball: Pos(%.2f, %.2f, %.2f), Vel(%.2f, %.2f, %.2f); ", ball.position.x, ball.position.y, ball.position.z, ball.velocity.x, ball.velocity.y, ball.velocity.z); - // for (int j = 0; j < NITRO_PACK_AMOUNT; j++) { - // printf("NitroPack %d: Pos(%.2f, %.2f, %.2f), Alive: %d; ", j, nitro_packs[j].position.x, nitro_packs[j].position.y, nitro_packs[j].position.z, nitro_packs[j].alive); - // } - // printf("\n"); - - render(client, &env); } close_client(client); diff --git a/pufferlib/ocean/codeball/codeball.h b/pufferlib/ocean/codeball/codeball.h new file mode 100644 index 00000000..0e8d657e --- /dev/null +++ b/pufferlib/ocean/codeball/codeball.h @@ -0,0 +1,789 @@ +#include +#include +#include +#include +#include +#include "puffernet.h" +#include "raylib.h" + +// Constants (from the description) +#define ROBOT_MIN_RADIUS 1.0 +#define ROBOT_MAX_RADIUS 1.05 +#define ROBOT_MAX_JUMP_SPEED 15.0 +#define ROBOT_ACCELERATION 100.0 +#define ROBOT_NITRO_ACCELERATION 30.0 +#define ROBOT_MAX_GROUND_SPEED 30.0 +#define ROBOT_ARENA_E 0.0 +#define ROBOT_MASS 2.0 +#define TICKS_PER_SECOND 60 +// #define MICROTICKS_PER_TICK 100 +#define MICROTICKS_PER_TICK 20 +#define RESET_TICKS (2 * TICKS_PER_SECOND) +#define BALL_ARENA_E 0.7 +#define BALL_RADIUS 2.0 +#define BALL_MASS 1.0 +#define MIN_HIT_E 0.4 +#define MAX_HIT_E 0.5 +#define MAX_ENTITY_SPEED 100.0 +#define MAX_NITRO_AMOUNT 100.0 +#define START_NITRO_AMOUNT 50.0 +#define NITRO_POINT_VELOCITY_CHANGE 0.6 +#define NITRO_PACK_X 20.0 +#define NITRO_PACK_Y 1.0 +#define NITRO_PACK_Z 30.0 +#define NITRO_PACK_RADIUS 0.5 +#define NITRO_PACK_AMOUNT 4 // Corrected: There are 4 nitro packs +#define NITRO_PACK_RESPAWN_TICKS (10 * TICKS_PER_SECOND) +#define GRAVITY 30.0 + +typedef double sim_dtype; + +// Arena parameters (from the description) +typedef struct { + sim_dtype width; + sim_dtype height; + sim_dtype depth; + sim_dtype bottom_radius; + sim_dtype top_radius; + sim_dtype corner_radius; + sim_dtype goal_top_radius; + sim_dtype goal_width; + sim_dtype goal_depth; + sim_dtype goal_height; + sim_dtype goal_side_radius; +} CodeBallArena; + +CodeBallArena arena = {60, 20, 80, 3, 7, 13, 3, 30, 10, 10, 1}; + +// 3D Vector +typedef struct { + sim_dtype x; + sim_dtype y; + sim_dtype z; +} Vec3D; + +// Action structure +typedef struct { + Vec3D target_velocity; + sim_dtype jump_speed; + bool use_nitro; +} Action; + +// Entity (Robot or Ball) +typedef struct { + Vec3D position; + Vec3D velocity; + sim_dtype radius; + sim_dtype radius_change_speed; + sim_dtype mass; + sim_dtype arena_e; + bool touch; + Vec3D touch_normal; + sim_dtype nitro; + Action action; + bool side; +} Entity; + +// Nitro Pack +typedef struct { + Vec3D position; + bool alive; + int respawn_ticks; + sim_dtype radius; // Nitro pack has radius +} NitroPack; + +// Helper functions +sim_dtype vec3d_length(Vec3D v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } +Vec3D vec3d_normalize(Vec3D v) { + sim_dtype l = vec3d_length(v); + if (l == 0) return (Vec3D){0, 0, 0}; // Avoid division by zero + return (Vec3D){v.x / l, v.y / l, v.z / l}; +} + +sim_dtype vec3d_dot(Vec3D a, Vec3D b) { return a.x * b.x + a.y * b.y + a.z * b.z; } + +Vec3D vec3d_subtract(Vec3D a, Vec3D b) { + return (Vec3D){a.x - b.x, a.y - b.y, a.z - b.z}; +} +Vec3D vec3d_add(Vec3D a, Vec3D b) { + return (Vec3D){a.x + b.x, a.y + b.y, a.z + b.z}; +} +Vec3D vec3d_multiply(Vec3D v, sim_dtype s) { + return (Vec3D){v.x * s, v.y * s, v.z * s}; +} + +sim_dtype clamp(sim_dtype val, sim_dtype min, sim_dtype max) { + if (val < min) return min; + if (val > max) return max; + return val; +} +Vec3D vec3d_clamp(Vec3D v, sim_dtype max_length) { + sim_dtype length = vec3d_length(v); + if (length > max_length) { + return vec3d_multiply(v, max_length / length); + } + return v; +} + +typedef struct { + sim_dtype distance; + Vec3D normal; +} DistanceAndNormal; + +// Distance and Normal functions +DistanceAndNormal dan_to_plane(Vec3D point, Vec3D point_on_plane, + Vec3D plane_normal) { + return (DistanceAndNormal){ + vec3d_dot(vec3d_subtract(point, point_on_plane), plane_normal), + plane_normal}; +} + +DistanceAndNormal dan_to_sphere_inner(Vec3D point, Vec3D sphere_center, + sim_dtype sphere_radius) { + return (DistanceAndNormal){ + sphere_radius - vec3d_length(vec3d_subtract(point, sphere_center)), + vec3d_normalize(vec3d_subtract(sphere_center, point))}; +} + +DistanceAndNormal dan_to_sphere_outer(Vec3D point, Vec3D sphere_center, + sim_dtype sphere_radius) { + return (DistanceAndNormal){ + vec3d_length(vec3d_subtract(point, sphere_center)) - sphere_radius, + vec3d_normalize(vec3d_subtract(point, sphere_center))}; +} + +DistanceAndNormal dan_to_arena_quarter(Vec3D point) { + DistanceAndNormal dan; + dan.distance = 1e9; // Initialize with a large value + + // Ground + DistanceAndNormal temp_dan = + dan_to_plane(point, (Vec3D){0, 0, 0}, (Vec3D){0, 1, 0}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Ceiling + temp_dan = + dan_to_plane(point, (Vec3D){0, arena.height, 0}, (Vec3D){0, -1, 0}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Side x + temp_dan = dan_to_plane(point, (Vec3D){arena.width / 2.0, 0, 0}, + (Vec3D){-1, 0, 0}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Side z (goal) + temp_dan = dan_to_plane( + point, (Vec3D){0, 0, (arena.depth / 2.0) + arena.goal_depth}, + (Vec3D){0, 0, -1}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + + // Side z (regular) + Vec3D v = {point.x - ((arena.goal_width / 2.0) - arena.goal_top_radius), + point.y - (arena.goal_height - arena.goal_top_radius), 0}; + if (point.x >= (arena.goal_width / 2.0) + arena.goal_side_radius || + point.y >= arena.goal_height + arena.goal_side_radius || + (v.x > 0 && v.y > 0 && + vec3d_length(v) >= arena.goal_top_radius + arena.goal_side_radius)) { + temp_dan = dan_to_plane(point, (Vec3D){0, 0, arena.depth / 2.0}, + (Vec3D){0, 0, -1}); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + + // Goal back corners + if (point.z > + (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius) { + temp_dan = dan_to_sphere_inner( + point, + (Vec3D){ + clamp(point.x, arena.bottom_radius - (arena.goal_width / 2.0), + (arena.goal_width / 2.0) - arena.bottom_radius), + clamp(point.y, arena.bottom_radius, + arena.goal_height - arena.goal_top_radius), + (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + + // Corner + if (point.x > (arena.width / 2.0) - arena.corner_radius && + point.z > (arena.depth / 2.0) - arena.corner_radius) { + temp_dan = dan_to_sphere_inner( + point, + (Vec3D){(arena.width / 2.0) - arena.corner_radius, point.y, + (arena.depth / 2.0) - arena.corner_radius}, + arena.corner_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + + // Goal outer corner + if (point.z < (arena.depth / 2.0) + arena.goal_side_radius) { + // Side x + if (point.x < (arena.goal_width / 2.0) + arena.goal_side_radius) { + temp_dan = dan_to_sphere_outer( + point, + (Vec3D){(arena.goal_width / 2.0) + arena.goal_side_radius, + point.y, (arena.depth / 2.0) + arena.goal_side_radius}, + arena.goal_side_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Ceiling + if (point.y < arena.goal_height + arena.goal_side_radius) { + temp_dan = dan_to_sphere_outer( + point, + (Vec3D){point.x, arena.goal_height + arena.goal_side_radius, + (arena.depth / 2.0) + arena.goal_side_radius}, + arena.goal_side_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Top corner + Vec3D o = {(arena.goal_width / 2.0) - arena.goal_top_radius, + arena.goal_height - arena.goal_top_radius, 0}; + v = (Vec3D){point.x - o.x, point.y - o.y, 0}; + if (v.x > 0 && v.y > 0) { + o.x += vec3d_normalize(v).x * + (arena.goal_top_radius + arena.goal_side_radius); + o.y += vec3d_normalize(v).y * + (arena.goal_top_radius + arena.goal_side_radius); + temp_dan = dan_to_sphere_outer( + point, + (Vec3D){o.x, o.y, (arena.depth / 2.0) + arena.goal_side_radius}, + arena.goal_side_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + + // Goal inside top corners + if (point.z > (arena.depth / 2.0) + arena.goal_side_radius && + point.y > arena.goal_height - arena.goal_top_radius) { + // Side x + if (point.x > (arena.goal_width / 2.0) - arena.goal_top_radius) { + temp_dan = dan_to_sphere_inner( + point, + (Vec3D){(arena.goal_width / 2.0) - arena.goal_top_radius, + arena.goal_height - arena.goal_top_radius, point.z}, + arena.goal_top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z + if (point.z > + (arena.depth / 2.0) + arena.goal_depth - arena.goal_top_radius) { + temp_dan = dan_to_sphere_inner( + point, + (Vec3D){point.x, arena.goal_height - arena.goal_top_radius, + (arena.depth / 2.0) + arena.goal_depth - + arena.goal_top_radius}, + arena.goal_top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + + // Bottom corners + if (point.y < arena.bottom_radius) { + // Side x + if (point.x > (arena.width / 2.0) - arena.bottom_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, + (Vec3D){(arena.width / 2.0) - arena.bottom_radius, + arena.bottom_radius, point.z}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z + if (point.z > (arena.depth / 2.0) - arena.bottom_radius && + point.x >= (arena.goal_width / 2.0) + arena.goal_side_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, + (Vec3D){point.x, arena.bottom_radius, + (arena.depth / 2.0) - arena.bottom_radius}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z (goal) + if (point.z > + (arena.depth / 2.0) + arena.goal_depth - arena.bottom_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, + (Vec3D){point.x, arena.bottom_radius, + (arena.depth / 2.0) + arena.goal_depth - + arena.bottom_radius}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Goal outer corner + Vec3D o = {(arena.goal_width / 2.0) + arena.goal_side_radius, + (arena.depth / 2.0) + arena.goal_side_radius, 0}; + Vec3D v = {point.x - o.x, point.z - o.y, 0}; + if (v.x < 0 && v.y < 0 && + vec3d_length(v) < arena.goal_side_radius + arena.bottom_radius) { + o.x += vec3d_normalize(v).x * + (arena.goal_side_radius + arena.bottom_radius); + o.y += vec3d_normalize(v).y * + (arena.goal_side_radius + arena.bottom_radius); + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, (Vec3D){o.x, arena.bottom_radius, o.y}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side x (goal) + if (point.z >= (arena.depth / 2.0) + arena.goal_side_radius && + point.x > (arena.goal_width / 2.0) - arena.bottom_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, + (Vec3D){(arena.goal_width / 2.0) - arena.bottom_radius, + arena.bottom_radius, point.z}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Corner + if (point.x > (arena.width / 2.0) - arena.corner_radius && + point.z > (arena.depth / 2.0) - arena.corner_radius) { + Vec3D corner_o = {(arena.width / 2.0) - arena.corner_radius, + (arena.depth / 2.0) - arena.corner_radius, 0}; + Vec3D n = {point.x - corner_o.x, point.z - corner_o.y, 0}; + sim_dtype dist = vec3d_length(n); + if (dist > arena.corner_radius - arena.bottom_radius) { + n = vec3d_normalize(n); + Vec3D o2 = {corner_o.x + n.x * (arena.corner_radius - + arena.bottom_radius), + corner_o.y + n.y * (arena.corner_radius - + arena.bottom_radius), + 0}; + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, (Vec3D){o2.x, arena.bottom_radius, o2.y}, + arena.bottom_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + } + + // Ceiling corners + if (point.y > arena.height - arena.top_radius) { + // Side x + if (point.x > (arena.width / 2.0) - arena.top_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, + (Vec3D){(arena.width / 2.0) - arena.top_radius, + arena.height - arena.top_radius, point.z}, + arena.top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Side z + if (point.z > (arena.depth / 2.0) - arena.top_radius) { + DistanceAndNormal temp_dan = dan_to_sphere_inner( + point, + (Vec3D){point.x, arena.height - arena.top_radius, + (arena.depth / 2.0) - arena.top_radius}, + arena.top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + // Corner + if (point.x > (arena.width / 2.0) - arena.corner_radius && + point.z > (arena.depth / 2.0) - arena.corner_radius) { + Vec3D corner_o = {(arena.width / 2.0) - arena.corner_radius, + (arena.depth / 2.0) - arena.corner_radius, 0}; + Vec3D dv = {point.x - corner_o.x, point.z - corner_o.y, 0}; + if (vec3d_length(dv) > arena.corner_radius - arena.top_radius) { + Vec3D n = vec3d_normalize(dv); + Vec3D o2 = { + corner_o.x + n.x * (arena.corner_radius - arena.top_radius), + arena.height - arena.top_radius, o2.y}; + DistanceAndNormal temp_dan = + dan_to_sphere_inner(point, o2, arena.top_radius); + if (temp_dan.distance < dan.distance) dan = temp_dan; + } + } + } + return dan; +} + +DistanceAndNormal dan_to_arena(Vec3D point) { + bool negate_x = point.x < 0; + bool negate_z = point.z < 0; + if (negate_x) point.x = -point.x; + if (negate_z) point.z = -point.z; + DistanceAndNormal result = dan_to_arena_quarter(point); + if (negate_x) result.normal.x = -result.normal.x; + if (negate_z) result.normal.z = -result.normal.z; + return result; +} + +void collide_entities(Entity* a, Entity* b) { + Vec3D delta_position = {b->position.x - a->position.x, + b->position.y - a->position.y, + b->position.z - a->position.z}; + sim_dtype distance = vec3d_length(delta_position); + sim_dtype penetration = a->radius + b->radius - distance; + if (penetration > 0) { + sim_dtype k_a = (1.0 / a->mass) / ((1.0 / a->mass) + (1.0 / b->mass)); + sim_dtype k_b = (1.0 / b->mass) / ((1.0 / a->mass) + (1.0 / b->mass)); + Vec3D normal = vec3d_normalize(delta_position); + a->position.x -= normal.x * penetration * k_a; + a->position.y -= normal.y * penetration * k_a; + a->position.z -= normal.z * penetration * k_a; + b->position.x += normal.x * penetration * k_b; + b->position.y += normal.y * penetration * k_b; + b->position.z += normal.z * penetration * k_b; + + sim_dtype delta_velocity = + vec3d_dot((Vec3D){b->velocity.x - a->velocity.x, + b->velocity.y - a->velocity.y, + b->velocity.z - a->velocity.z}, + normal) - + b->radius_change_speed - a->radius_change_speed; + + if (delta_velocity < 0) { + sim_dtype e = ((sim_dtype)rand() / RAND_MAX) * (MAX_HIT_E - MIN_HIT_E) + + MIN_HIT_E; + Vec3D impulse = {normal.x * (1 + e) * delta_velocity, + normal.y * (1 + e) * delta_velocity, + normal.z * (1 + e) * delta_velocity}; + a->velocity.x += impulse.x * k_a; + a->velocity.y += impulse.y * k_a; + a->velocity.z += impulse.z * k_a; + b->velocity.x -= impulse.x * k_b; + b->velocity.y -= impulse.y * k_b; + b->velocity.z -= impulse.z * k_b; + } + } +} + +Vec3D collide_with_arena(Entity* e) { + DistanceAndNormal dan = dan_to_arena(e->position); + sim_dtype penetration = e->radius - dan.distance; + if (penetration > 0) { + e->position.x += dan.normal.x * penetration; + e->position.y += dan.normal.y * penetration; + e->position.z += dan.normal.z * penetration; + + sim_dtype velocity = + vec3d_dot(e->velocity, dan.normal) - e->radius_change_speed; + if (velocity < 0) { + e->velocity.x -= (1 + e->arena_e) * velocity * dan.normal.x; + e->velocity.y -= (1 + e->arena_e) * velocity * dan.normal.y; + e->velocity.z -= (1 + e->arena_e) * velocity * dan.normal.z; + } + return dan.normal; + } + return (Vec3D){0, 0, 0}; // Return zero vector to indicate no collision +} + +void move(Entity* e, sim_dtype delta_time) { + e->velocity = vec3d_clamp(e->velocity, MAX_ENTITY_SPEED); + e->position.x += e->velocity.x * delta_time; + e->position.y += e->velocity.y * delta_time; + e->position.z += e->velocity.z * delta_time; + e->position.y -= GRAVITY * delta_time * delta_time / 2.0; + e->velocity.y -= GRAVITY * delta_time; +} + +typedef struct CodeBall { + Entity ball; + int n_robots; + Entity* robots; + int n_nitros; + NitroPack* nitro_packs; + int tick; + double* actions; + double* rewards; +} CodeBall; + +void goal_scored(CodeBall *env, bool side) { + for (int i = 0; i < env->n_robots; i++) { + env->rewards[i] = env->robots[i].side == side ? 1.0 : -1.0; + } +} + +void allocate(CodeBall* env) { + if (env->n_robots < 2 || env->n_robots > 6) { + printf("Invalid number of robots\n"); + exit(1); + } + if (env->n_nitros != 4 && env->n_nitros != 0) { + printf("Invalid number of nitro packs\n"); + exit(1); + } + env->robots = (Entity*)calloc(env->n_robots, sizeof(Entity)); + env->nitro_packs = (NitroPack*)calloc(env->n_nitros, sizeof(NitroPack)); + env->actions = (double*)calloc(env->n_robots * 4, sizeof(double)); + env->rewards = (double*)calloc(env->n_robots, sizeof(double)); +} + +void free_allocated(CodeBall* env) { + free(env->robots); + free(env->nitro_packs); + free(env->actions); + free(env->rewards); +} + +void reset(CodeBall* env) { + Entity* robots = env->robots; + for (int i = 0; i < env->n_robots; i++) { + robots[i].position.x = + ((sim_dtype)rand() / RAND_MAX) * (arena.width / 2) * (i == 0 ? -1 : 1); + robots[i].position.z = + ((sim_dtype)rand() / RAND_MAX) * (arena.depth / 2) * (i == 0 ? -1 : 1); + robots[i].position.y = 0; + robots[i].velocity = (Vec3D){0, 0, 0}; + robots[i].radius = ROBOT_MIN_RADIUS; + robots[i].radius_change_speed = 0; + robots[i].mass = ROBOT_MASS; + robots[i].arena_e = ROBOT_ARENA_E; + robots[i].touch = false; + robots[i].touch_normal = (Vec3D){0, 0, 0}; + robots[i].nitro = START_NITRO_AMOUNT; + robots[i].action.target_velocity = (Vec3D){0, 0, 0}; + robots[i].action.jump_speed = 0; + robots[i].action.use_nitro = false; + robots[i].side = robots[i].position.z > 0; + } + + Entity ball; + ball.position.x = 0; + ball.position.z = 0; + ball.position.y = ((sim_dtype)rand() / RAND_MAX) * (3 * BALL_RADIUS) + + BALL_RADIUS; // Random height + ball.velocity = (Vec3D){4, 0, 0}; + ball.radius = BALL_RADIUS; + ball.radius_change_speed = 0; + ball.mass = BALL_MASS; + ball.arena_e = BALL_ARENA_E; + ball.touch = false; + ball.touch_normal = (Vec3D){0, 0, 0}; + ball.nitro = 0; + ball.action.target_velocity = (Vec3D){0, 0, 0}; + ball.action.jump_speed = 0; + ball.action.use_nitro = false; + env->ball = ball; + + memset(env->actions, 0, env->n_robots * 4 * sizeof(double)); + memset(env->rewards, 0, env->n_robots * sizeof(double)); + + env->tick = 0; +} + +void update(sim_dtype delta_time, CodeBall* env) { + Entity* robots = env->robots; + Entity* ball = &env->ball; + NitroPack* nitro_packs = env->nitro_packs; + + for (int i = 0; i < env->n_robots; i++) { + Vec3D target_velocity = vec3d_clamp(robots[i].action.target_velocity, + ROBOT_MAX_GROUND_SPEED); + + if (robots[i].touch) { + target_velocity.x -= + robots[i].touch_normal.x * + vec3d_dot(robots[i].touch_normal, target_velocity); + target_velocity.y -= + robots[i].touch_normal.y * + vec3d_dot(robots[i].touch_normal, target_velocity); + target_velocity.z -= + robots[i].touch_normal.z * + vec3d_dot(robots[i].touch_normal, target_velocity); + + Vec3D target_velocity_change = { + target_velocity.x - robots[i].velocity.x, + target_velocity.y - robots[i].velocity.y, + target_velocity.z - robots[i].velocity.z}; + if (vec3d_length(target_velocity_change) > 0) { + sim_dtype acceleration = + ROBOT_ACCELERATION * fmax(0, robots[i].touch_normal.y); + Vec3D acceleration_vec = vec3d_clamp( + (Vec3D){ + target_velocity_change.x * acceleration * delta_time / + vec3d_length(target_velocity_change), + target_velocity_change.y * acceleration * delta_time / + vec3d_length(target_velocity_change), + target_velocity_change.z * acceleration * delta_time / + vec3d_length(target_velocity_change)}, + vec3d_length(target_velocity_change)); + robots[i].velocity.x += acceleration_vec.x; + robots[i].velocity.y += acceleration_vec.y; + robots[i].velocity.z += acceleration_vec.z; + } + } + + if (robots[i].action.use_nitro && + robots[i].nitro > 0) { // Check if nitro is available + Vec3D target_velocity_change = { + robots[i].action.target_velocity.x - robots[i].velocity.x, + robots[i].action.target_velocity.y - robots[i].velocity.y, + robots[i].action.target_velocity.z - robots[i].velocity.z}; + target_velocity_change = + vec3d_clamp(target_velocity_change, + robots[i].nitro * NITRO_POINT_VELOCITY_CHANGE); + if (vec3d_length(target_velocity_change) > 0) { + Vec3D acceleration = vec3d_normalize(target_velocity_change); + acceleration.x *= ROBOT_NITRO_ACCELERATION; + acceleration.y *= ROBOT_NITRO_ACCELERATION; + acceleration.z *= ROBOT_NITRO_ACCELERATION; + Vec3D velocity_change = + vec3d_clamp((Vec3D){acceleration.x * delta_time, + acceleration.y * delta_time, + acceleration.z * delta_time}, + vec3d_length(target_velocity_change)); + robots[i].velocity.x += velocity_change.x; + robots[i].velocity.y += velocity_change.y; + robots[i].velocity.z += velocity_change.z; + robots[i].nitro -= + vec3d_length(velocity_change) / NITRO_POINT_VELOCITY_CHANGE; + if (robots[i].nitro < 0) + robots[i].nitro = 0; // Ensure nitro doesn't go negative + } + } + + move(&robots[i], delta_time); + robots[i].radius = + ROBOT_MIN_RADIUS + (ROBOT_MAX_RADIUS - ROBOT_MIN_RADIUS) * + robots[i].action.jump_speed / + ROBOT_MAX_JUMP_SPEED; + robots[i].radius_change_speed = robots[i].action.jump_speed; + } + + move(ball, delta_time); + + for (int i = 0; i < env->n_robots; i++) { + for (int j = 0; j < i; j++) { + collide_entities(&robots[i], &robots[j]); + } + } + + for (int i = 0; i < env->n_robots; i++) { + collide_entities(&robots[i], ball); + Vec3D collision_normal = collide_with_arena(&robots[i]); + robots[i].touch = (collision_normal.x != 0 || collision_normal.y != 0 || + collision_normal.z != 0); + if (robots[i].touch) { + robots[i].touch_normal = collision_normal; + } + } + collide_with_arena(ball); + + if (fabs(ball->position.z) > arena.depth / 2.0 + ball->radius) { + goal_scored(env, ball->position.z > 0); + } + + for (int i = 0; i < env->n_robots; i++) { + if (robots[i].nitro == MAX_NITRO_AMOUNT) continue; + for (int j = 0; j < env->n_nitros; j++) { + if (!nitro_packs[j].alive) continue; + Vec3D diff = + vec3d_subtract(robots[i].position, nitro_packs[j].position); + + if (vec3d_length(diff) <= + robots[i].radius + nitro_packs[j].radius) { + robots[i].nitro = MAX_NITRO_AMOUNT; + nitro_packs[j].alive = false; + nitro_packs[j].respawn_ticks = NITRO_PACK_RESPAWN_TICKS; + } + } + } +} + +void step(CodeBall* env) { + for (int i = 0; i < env->n_robots; i++) { + env->robots[i].action = (Action){ + .target_velocity = {env->actions[i * 4], env->actions[i * 4 + 1], + env->actions[i * 4 + 2]}, + .jump_speed = env->actions[i * 4 + 3], + .use_nitro = false}; + } + + sim_dtype delta_time = 1.0 / TICKS_PER_SECOND; + for (int i = 0; i < MICROTICKS_PER_TICK; i++) { + update(delta_time / MICROTICKS_PER_TICK, env); + } + NitroPack* nitro_packs = env->nitro_packs; + for (int i = 0; i < env->n_nitros; i++) { + if (!nitro_packs[i].alive) { + nitro_packs[i].respawn_ticks--; + if (nitro_packs[i].respawn_ticks == 0) { + nitro_packs[i].alive = true; + } + } + } + env->tick++; +} + +typedef struct Client Client; +struct Client { + float width; + float height; + Color robot_color[2]; + Color ball_color; + Color nitro_color; +}; + +Client* make_client() { + Client* client = (Client*)calloc(1, sizeof(Client)); + client->width = 800; // Example width + client->height = 600; // Example height + client->robot_color[0] = RED; + client->robot_color[1] = BLUE; + client->ball_color = WHITE; + client->nitro_color = GREEN; + + InitWindow(client->width, client->height, "CodeBall"); + SetTargetFPS(60); + + return client; +} + +void close_client(Client* client) { + CloseWindow(); + free(client); +} + +void render(Client* client, CodeBall* env) { + BeginDrawing(); + ClearBackground(DARKGRAY); + + Entity* robots = env->robots; + Entity ball = env->ball; + NitroPack* nitro_packs = env->nitro_packs; + + // Draw arena (simplified rectangle for now) + DrawRectangle(0, 0, client->width, client->height, GRAY); + + // Draw goal lines + DrawLine(client->width / 2 - arena.goal_width / 2, 0, + client->width / 2 - arena.goal_width / 2, arena.goal_height, BLUE); + DrawLine(client->width / 2 + arena.goal_width / 2, 0, + client->width / 2 + arena.goal_width / 2, arena.goal_height, RED); + + sim_dtype depth = arena.depth + arena.goal_depth * 2.0; + + // Draw robots + for (int i = 0; i < env->n_robots; i++) { + DrawCircle((int)(client->width / 2 + + robots[i].position.x * client->width / (arena.width)), + (int)(client->height / 2 - + robots[i].position.z * client->height / (depth)), + (int)(robots[i].radius * client->width / (arena.width)), + client->robot_color[i]); + } + + // Draw ball + DrawCircle( + (int)(client->width / 2 + + ball.position.x * client->width / (arena.width)), + (int)(client->height / 2 - ball.position.z * client->height / (depth)), + (int)(ball.radius * client->width / (arena.width)), + client->ball_color); + + // Draw nitro packs + for (int i = 0; i < env->n_nitros; i++) { + if (nitro_packs[i].alive) { + DrawCircle( + (int)(client->width / 2 + nitro_packs[i].position.x * + client->width / (arena.width)), + (int)(client->height / 2 - + nitro_packs[i].position.z * client->height / (depth)), + (int)(nitro_packs[i].radius * client->width / (arena.width)), + client->nitro_color); + } + } + + DrawFPS(10, 10); + EndDrawing(); +} \ No newline at end of file From f0857eeeb98802b1fee983f55e5f26359b3952a9 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 16:04:24 -0500 Subject: [PATCH 04/40] Improve 2D rendering --- pufferlib/ocean/codeball/codeball.c | 113 ++++++++++++++++++++++++++- pufferlib/ocean/codeball/codeball.h | 114 ++-------------------------- 2 files changed, 117 insertions(+), 110 deletions(-) diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index 5823afab..657e75ff 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -1,6 +1,117 @@ #include "codeball.h" +#include "puffernet.h" #include +void allocate(CodeBall* env) { + if (env->n_robots < 2 || env->n_robots > 6) { + printf("Invalid number of robots\n"); + exit(1); + } + if (env->n_nitros != 4 && env->n_nitros != 0) { + printf("Invalid number of nitro packs\n"); + exit(1); + } + env->robots = (Entity*)calloc(env->n_robots, sizeof(Entity)); + env->nitro_packs = (NitroPack*)calloc(env->n_nitros, sizeof(NitroPack)); + env->actions = (double*)calloc(env->n_robots * 4, sizeof(double)); + env->rewards = (double*)calloc(env->n_robots, sizeof(double)); +} + +void free_allocated(CodeBall* env) { + free(env->robots); + free(env->nitro_packs); + free(env->actions); + free(env->rewards); +} + +typedef struct Client Client; +struct Client { + float width; + float height; + Color robot_color[2]; + Color ball_color; + Color nitro_color; +}; + +Client* make_client() { + Client* client = (Client*)calloc(1, sizeof(Client)); + client->width = 800; // Example width + client->height = 600; // Example height + client->robot_color[0] = RED; + client->robot_color[1] = BLUE; + client->ball_color = WHITE; + client->nitro_color = GREEN; + + InitWindow(client->width, client->height, "CodeBall"); + SetTargetFPS(60); + + return client; +} + +void close_client(Client* client) { + CloseWindow(); + free(client); +} + +void render(Client* client, CodeBall* env) { + BeginDrawing(); + ClearBackground(DARKGRAY); + + Entity* robots = env->robots; + Entity ball = env->ball; + NitroPack* nitro_packs = env->nitro_packs; + + // Arena dimensions for scaling + float arena_width_scaled = arena.width * client->width / arena.width; + float arena_depth_scaled = (arena.depth + 2 * arena.goal_depth) * client->height / (arena.depth + 2 * arena.goal_depth); + float arena_x_offset = client->width / 2.0f; + float arena_z_offset = client->height / 2.0f; + + // Draw background + DrawRectangle(0, 0, client->width, client->height, DARKGRAY); + + // Draw arena (now a separate colored rectangle) + DrawRectangle(arena_x_offset - arena_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, arena_width_scaled, arena_depth_scaled, LIGHTGRAY); + + // Draw goal areas (Corrected positioning and size) + float goal_width_scaled = arena.goal_width * client->width / arena.width; + float goal_depth_scaled = arena.goal_depth * client->height / (arena.depth + 2 * arena.goal_depth); + float goal_height_scaled = arena.goal_height * client->height / (arena.depth + 2 * arena.goal_depth); + + DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset + arena_depth_scaled / 2 - goal_depth_scaled, goal_width_scaled, goal_height_scaled, GREEN); // Blue goal + DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, goal_width_scaled, goal_height_scaled, YELLOW); // Red goal + + // Draw robots (Colored by side) + for (int i = 0; i < env->n_robots; i++) { + Color robot_color = robots[i].side ? client->robot_color[1] : client->robot_color[0]; // Right is blue, left is red + Vector2 robot_pos = { + arena_x_offset + robots[i].position.x * arena_width_scaled / arena.width, + arena_z_offset - robots[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) + }; + DrawCircleV(robot_pos, robots[i].radius * arena_width_scaled / arena.width, robot_color); + } + + // Draw ball + Vector2 ball_pos = { + arena_x_offset + ball.position.x * arena_width_scaled / arena.width, + arena_z_offset - ball.position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) + }; + DrawCircleV(ball_pos, ball.radius * arena_width_scaled / arena.width, client->ball_color); + + // Draw nitro packs + for (int i = 0; i < env->n_nitros; i++) { + if (nitro_packs[i].alive) { + Vector2 nitro_pos = { + arena_x_offset + nitro_packs[i].position.x * arena_width_scaled / arena.width, + arena_z_offset - nitro_packs[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) + }; + DrawCircleV(nitro_pos, nitro_packs[i].radius * arena_width_scaled / arena.width, client->nitro_color); + } + } + + DrawFPS(10, 10); + EndDrawing(); +} int main() { srand(time(NULL)); // Seed the random number generator @@ -15,7 +126,7 @@ int main() { struct timeval start, end; gettimeofday(&start, NULL); - int initial_steps = 5000; + int initial_steps = 10; for (int i = 0; i < 10000; i++) { for (int j = 0; j < env.n_robots; j++) diff --git a/pufferlib/ocean/codeball/codeball.h b/pufferlib/ocean/codeball/codeball.h index 0e8d657e..ff5cad9a 100644 --- a/pufferlib/ocean/codeball/codeball.h +++ b/pufferlib/ocean/codeball/codeball.h @@ -3,7 +3,7 @@ #include #include #include -#include "puffernet.h" +#include #include "raylib.h" // Constants (from the description) @@ -17,7 +17,8 @@ #define ROBOT_MASS 2.0 #define TICKS_PER_SECOND 60 // #define MICROTICKS_PER_TICK 100 -#define MICROTICKS_PER_TICK 20 +// #define MICROTICKS_PER_TICK 20 +#define MICROTICKS_PER_TICK 1 #define RESET_TICKS (2 * TICKS_PER_SECOND) #define BALL_ARENA_E 0.7 #define BALL_RADIUS 2.0 @@ -493,35 +494,13 @@ void goal_scored(CodeBall *env, bool side) { } } -void allocate(CodeBall* env) { - if (env->n_robots < 2 || env->n_robots > 6) { - printf("Invalid number of robots\n"); - exit(1); - } - if (env->n_nitros != 4 && env->n_nitros != 0) { - printf("Invalid number of nitro packs\n"); - exit(1); - } - env->robots = (Entity*)calloc(env->n_robots, sizeof(Entity)); - env->nitro_packs = (NitroPack*)calloc(env->n_nitros, sizeof(NitroPack)); - env->actions = (double*)calloc(env->n_robots * 4, sizeof(double)); - env->rewards = (double*)calloc(env->n_robots, sizeof(double)); -} - -void free_allocated(CodeBall* env) { - free(env->robots); - free(env->nitro_packs); - free(env->actions); - free(env->rewards); -} - void reset(CodeBall* env) { Entity* robots = env->robots; for (int i = 0; i < env->n_robots; i++) { robots[i].position.x = - ((sim_dtype)rand() / RAND_MAX) * (arena.width / 2) * (i == 0 ? -1 : 1); + ((sim_dtype)rand() / RAND_MAX) * (arena.width / 2) * (i % 2 == 0 ? -1 : 1); robots[i].position.z = - ((sim_dtype)rand() / RAND_MAX) * (arena.depth / 2) * (i == 0 ? -1 : 1); + ((sim_dtype)rand() / RAND_MAX) * (arena.depth / 2) * (i % 2 == 0 ? -1 : 1); robots[i].position.y = 0; robots[i].velocity = (Vec3D){0, 0, 0}; robots[i].radius = ROBOT_MIN_RADIUS; @@ -704,86 +683,3 @@ void step(CodeBall* env) { } env->tick++; } - -typedef struct Client Client; -struct Client { - float width; - float height; - Color robot_color[2]; - Color ball_color; - Color nitro_color; -}; - -Client* make_client() { - Client* client = (Client*)calloc(1, sizeof(Client)); - client->width = 800; // Example width - client->height = 600; // Example height - client->robot_color[0] = RED; - client->robot_color[1] = BLUE; - client->ball_color = WHITE; - client->nitro_color = GREEN; - - InitWindow(client->width, client->height, "CodeBall"); - SetTargetFPS(60); - - return client; -} - -void close_client(Client* client) { - CloseWindow(); - free(client); -} - -void render(Client* client, CodeBall* env) { - BeginDrawing(); - ClearBackground(DARKGRAY); - - Entity* robots = env->robots; - Entity ball = env->ball; - NitroPack* nitro_packs = env->nitro_packs; - - // Draw arena (simplified rectangle for now) - DrawRectangle(0, 0, client->width, client->height, GRAY); - - // Draw goal lines - DrawLine(client->width / 2 - arena.goal_width / 2, 0, - client->width / 2 - arena.goal_width / 2, arena.goal_height, BLUE); - DrawLine(client->width / 2 + arena.goal_width / 2, 0, - client->width / 2 + arena.goal_width / 2, arena.goal_height, RED); - - sim_dtype depth = arena.depth + arena.goal_depth * 2.0; - - // Draw robots - for (int i = 0; i < env->n_robots; i++) { - DrawCircle((int)(client->width / 2 + - robots[i].position.x * client->width / (arena.width)), - (int)(client->height / 2 - - robots[i].position.z * client->height / (depth)), - (int)(robots[i].radius * client->width / (arena.width)), - client->robot_color[i]); - } - - // Draw ball - DrawCircle( - (int)(client->width / 2 + - ball.position.x * client->width / (arena.width)), - (int)(client->height / 2 - ball.position.z * client->height / (depth)), - (int)(ball.radius * client->width / (arena.width)), - client->ball_color); - - // Draw nitro packs - for (int i = 0; i < env->n_nitros; i++) { - if (nitro_packs[i].alive) { - DrawCircle( - (int)(client->width / 2 + nitro_packs[i].position.x * - client->width / (arena.width)), - (int)(client->height / 2 - - nitro_packs[i].position.z * client->height / (depth)), - (int)(nitro_packs[i].radius * client->width / (arena.width)), - client->nitro_color); - } - } - - DrawFPS(10, 10); - EndDrawing(); -} \ No newline at end of file From c309df63f0ea2a845e7468d7e57060af6ffcbdcb Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 16:51:38 -0500 Subject: [PATCH 05/40] 3D rendering --- pufferlib/ocean/codeball/codeball.c | 246 +++++++++++++++++++++++----- 1 file changed, 209 insertions(+), 37 deletions(-) diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index 657e75ff..a9c5bccb 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -1,7 +1,9 @@ #include "codeball.h" #include "puffernet.h" +#include "raymath.h" #include + void allocate(CodeBall* env) { if (env->n_robots < 2 || env->n_robots > 6) { printf("Invalid number of robots\n"); @@ -31,6 +33,7 @@ struct Client { Color robot_color[2]; Color ball_color; Color nitro_color; + Camera3D camera; }; Client* make_client() { @@ -41,8 +44,14 @@ Client* make_client() { client->robot_color[1] = BLUE; client->ball_color = WHITE; client->nitro_color = GREEN; + client->camera = + // (Camera3D){(Vector3){0.0f, 40.0f, -60.0f}, (Vector3){0.0f, 0.0f, + // 0.0f}, + (Camera3D){(Vector3){0.0f, 60.0f, -90.0f}, (Vector3){0.0f, 0.0f, 0.0f}, + (Vector3){0.0f, 1.0f, 0.0f}, 45.0f, 0}; InitWindow(client->width, client->height, "CodeBall"); + SetTargetFPS(60); return client; @@ -53,66 +62,205 @@ void close_client(Client* client) { free(client); } -void render(Client* client, CodeBall* env) { - BeginDrawing(); - ClearBackground(DARKGRAY); +Vector3 cvt(Vec3D vec) { + return (Vector3){vec.x, vec.y, vec.z}; +} + +// Custom DrawPlane function for arbitrary orientation +void MyDrawPlane(Vector3 center, Vector3 normal, float width, float height, + Color color) { + Vector3 u, v; + + if (fabsf(normal.y) < 0.999f) { + u = (Vector3){normal.z, 0, -normal.x}; + } else { + u = (Vector3){1, 0, 0}; + } + v = Vector3CrossProduct(normal, u); + + u = Vector3Normalize(u); + v = Vector3Normalize(v); + Vector3 p1 = + Vector3Subtract(Vector3Subtract(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + Vector3 p2 = Vector3Add(Vector3Subtract(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + Vector3 p3 = Vector3Add(Vector3Add(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + Vector3 p4 = Vector3Subtract(Vector3Add(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + + DrawTriangleStrip3D((Vector3[]){p1, p2, p4, p3}, 4, color); +} + +void render(Client* client, CodeBall* env) { Entity* robots = env->robots; Entity ball = env->ball; NitroPack* nitro_packs = env->nitro_packs; - // Arena dimensions for scaling - float arena_width_scaled = arena.width * client->width / arena.width; - float arena_depth_scaled = (arena.depth + 2 * arena.goal_depth) * client->height / (arena.depth + 2 * arena.goal_depth); - float arena_x_offset = client->width / 2.0f; - float arena_z_offset = client->height / 2.0f; + BeginDrawing(); + + ClearBackground(DARKGRAY); + + BeginMode3D(client->camera); // Begin 3D mode - // Draw background - DrawRectangle(0, 0, client->width, client->height, DARKGRAY); + // Arena dimensions + Vector3 arena_size = {arena.width, arena.height, + arena.depth}; - // Draw arena (now a separate colored rectangle) - DrawRectangle(arena_x_offset - arena_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, arena_width_scaled, arena_depth_scaled, LIGHTGRAY); + // Draw arena (box) + // DrawCube((Vector3){0, arena.height / 2, 0}, arena_size.x, arena_size.y, + // arena_size.z, LIGHTGRAY); + // DrawCube((Vector3){0, -arena.height / 2, 0}, arena_size.x, arena_size.y, + // arena_size.z, LIGHTGRAY); + + MyDrawPlane((Vector3){0, 0, 0}, (Vector3){0, -1, 0}, arena_size.x, + arena_size.z, LIGHTGRAY); // Bottom (CORRECTED NORMAL) + MyDrawPlane((Vector3){arena_size.x / 2, arena_size.y / 2, 0}, + (Vector3){1, 0, 0}, arena_size.z, arena_size.y, + LIGHTGRAY); // Right + MyDrawPlane((Vector3){-arena_size.x / 2, arena_size.y / 2, 0}, + (Vector3){-1, 0, 0}, arena_size.z, arena_size.y, + LIGHTGRAY); // Left + + for (int fb = 0; fb < 2; fb++) { + int sign = fb == 0 ? 1 : -1; + Vector3 normal = {0, 0, sign}; + MyDrawPlane( + (Vector3){0, arena.goal_height + (arena_size.y - arena.goal_height) / 2, + arena_size.z / 2 * sign}, + normal, arena.goal_width, + (arena_size.y - arena.goal_height), + LIGHTGRAY); // Back middle + double remaining = (arena_size.x - arena.goal_width) / 2; + MyDrawPlane((Vector3){-arena.goal_width / 2 - remaining / 2, arena_size.y / 2, + arena_size.z / 2 * sign}, + normal, remaining, arena_size.y, + LIGHTGRAY); // Back right + MyDrawPlane((Vector3){arena.goal_width / 2 + remaining / 2, + arena_size.y / 2, arena_size.z / 2 * sign}, + normal, remaining, arena_size.y, + LIGHTGRAY); // Back left + } - // Draw goal areas (Corrected positioning and size) - float goal_width_scaled = arena.goal_width * client->width / arena.width; - float goal_depth_scaled = arena.goal_depth * client->height / (arena.depth + 2 * arena.goal_depth); - float goal_height_scaled = arena.goal_height * client->height / (arena.depth + 2 * arena.goal_depth); + MyDrawPlane((Vector3){0, arena_size.y / 2, -arena_size.z / 2}, + (Vector3){0, 0, -1}, arena_size.x, arena_size.y, + LIGHTGRAY); // Front - DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset + arena_depth_scaled / 2 - goal_depth_scaled, goal_width_scaled, goal_height_scaled, GREEN); // Blue goal - DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, goal_width_scaled, goal_height_scaled, YELLOW); // Red goal + // Draw Goals (as boxes) + Vector3 goal_size = {arena.goal_width, arena.goal_height, arena.goal_depth}; - // Draw robots (Colored by side) + Color goal_colors[2] = {GREEN, YELLOW}; + for (int gi = 0; gi < 2; gi++) { + Color goal_color = goal_colors[gi]; + int sign = gi == 0 ? 1 : -1; + MyDrawPlane( + (Vector3){0, goal_size.y / 2, sign * (arena.depth / 2 + arena.goal_depth)}, + (Vector3){0, 0, sign}, goal_size.x, goal_size.y, goal_color); // Back + MyDrawPlane((Vector3){-goal_size.x / 2, goal_size.y / 2, + sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){-1, 0, 0}, goal_size.z, goal_size.y, + goal_color); // Left + MyDrawPlane((Vector3){goal_size.x / 2, goal_size.y / 2, + sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){1, 0, 0}, goal_size.z, goal_size.y, + goal_color); // Right + MyDrawPlane( + (Vector3){0, goal_size.y, sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){0, 1, 0}, goal_size.x, goal_size.z, goal_color); // Top + + MyDrawPlane((Vector3){0, 0, sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){0, -1, 0}, goal_size.x, goal_size.z, goal_color); // Bottom + } + + // Draw robots (as boxes, colored by side) for (int i = 0; i < env->n_robots; i++) { - Color robot_color = robots[i].side ? client->robot_color[1] : client->robot_color[0]; // Right is blue, left is red - Vector2 robot_pos = { - arena_x_offset + robots[i].position.x * arena_width_scaled / arena.width, - arena_z_offset - robots[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) - }; - DrawCircleV(robot_pos, robots[i].radius * arena_width_scaled / arena.width, robot_color); + Color robot_color = + robots[i].side ? client->robot_color[1] : client->robot_color[0]; + DrawCube(cvt(robots[i].position), robots[i].radius * 2, robots[i].radius * 2, + robots[i].radius * 2, robot_color); } - // Draw ball - Vector2 ball_pos = { - arena_x_offset + ball.position.x * arena_width_scaled / arena.width, - arena_z_offset - ball.position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) - }; - DrawCircleV(ball_pos, ball.radius * arena_width_scaled / arena.width, client->ball_color); + // Draw ball (as a box) + DrawCube(cvt(ball.position), ball.radius * 2, ball.radius * 2, ball.radius * 2, + client->ball_color); - // Draw nitro packs + // Draw nitro packs (as boxes) for (int i = 0; i < env->n_nitros; i++) { if (nitro_packs[i].alive) { - Vector2 nitro_pos = { - arena_x_offset + nitro_packs[i].position.x * arena_width_scaled / arena.width, - arena_z_offset - nitro_packs[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) - }; - DrawCircleV(nitro_pos, nitro_packs[i].radius * arena_width_scaled / arena.width, client->nitro_color); + DrawCube(cvt(nitro_packs[i].position), nitro_packs[i].radius * 2, + nitro_packs[i].radius * 2, nitro_packs[i].radius * 2, + client->nitro_color); } } + EndMode3D(); // End 3D mode + DrawFPS(10, 10); EndDrawing(); } +// void render(Client* client, CodeBall* env) { +// BeginDrawing(); +// ClearBackground(DARKGRAY); + +// Entity* robots = env->robots; +// Entity ball = env->ball; +// NitroPack* nitro_packs = env->nitro_packs; + +// // Arena dimensions for scaling +// float arena_width_scaled = arena.width * client->width / arena.width; +// float arena_depth_scaled = (arena.depth + 2 * arena.goal_depth) * client->height / (arena.depth + 2 * arena.goal_depth); +// float arena_x_offset = client->width / 2.0f; +// float arena_z_offset = client->height / 2.0f; + +// // Draw background +// DrawRectangle(0, 0, client->width, client->height, DARKGRAY); + +// // Draw arena (now a separate colored rectangle) +// DrawRectangle(arena_x_offset - arena_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, arena_width_scaled, arena_depth_scaled, LIGHTGRAY); + +// // Draw goal areas (Corrected positioning and size) +// float goal_width_scaled = arena.goal_width * client->width / arena.width; +// float goal_depth_scaled = arena.goal_depth * client->height / (arena.depth + 2 * arena.goal_depth); +// float goal_height_scaled = arena.goal_height * client->height / (arena.depth + 2 * arena.goal_depth); + +// DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset + arena_depth_scaled / 2 - goal_depth_scaled, goal_width_scaled, goal_height_scaled, GREEN); // Blue goal +// DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, goal_width_scaled, goal_height_scaled, YELLOW); // Red goal + +// // Draw robots (Colored by side) +// for (int i = 0; i < env->n_robots; i++) { +// Color robot_color = robots[i].side ? client->robot_color[1] : client->robot_color[0]; // Right is blue, left is red +// Vector2 robot_pos = { +// arena_x_offset + robots[i].position.x * arena_width_scaled / arena.width, +// arena_z_offset - robots[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) +// }; +// DrawCircleV(robot_pos, robots[i].radius * arena_width_scaled / arena.width, robot_color); +// } + +// // Draw ball +// Vector2 ball_pos = { +// arena_x_offset + ball.position.x * arena_width_scaled / arena.width, +// arena_z_offset - ball.position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) +// }; +// DrawCircleV(ball_pos, ball.radius * arena_width_scaled / arena.width, client->ball_color); + +// // Draw nitro packs +// for (int i = 0; i < env->n_nitros; i++) { +// if (nitro_packs[i].alive) { +// Vector2 nitro_pos = { +// arena_x_offset + nitro_packs[i].position.x * arena_width_scaled / arena.width, +// arena_z_offset - nitro_packs[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) +// }; +// DrawCircleV(nitro_pos, nitro_packs[i].radius * arena_width_scaled / arena.width, client->nitro_color); +// } +// } + +// DrawFPS(10, 10); +// EndDrawing(); +// } + int main() { srand(time(NULL)); // Seed the random number generator Client* client = make_client(); @@ -129,6 +277,30 @@ int main() { int initial_steps = 10; for (int i = 0; i < 10000; i++) { + if (WindowShouldClose()) break; + + // Camera controls + Vector3 addition = {0, 0, 0}; + Vector3 camera_forward = Vector3Normalize(Vector3Subtract(client->camera.target, client->camera.position)); + Vector3 camera_right = Vector3Normalize(Vector3CrossProduct(camera_forward, (Vector3){0, 1, 0})); + Vector3 camera_up = Vector3Normalize(Vector3CrossProduct(camera_right, camera_forward)); + + if (IsKeyDown(KEY_W)) addition = Vector3Add(addition, camera_forward); + if (IsKeyDown(KEY_S)) addition = Vector3Subtract(addition, camera_forward); + if (IsKeyDown(KEY_D)) addition = Vector3Add(addition, camera_right); + if (IsKeyDown(KEY_A)) addition = Vector3Subtract(addition, camera_right); + if (IsKeyDown(KEY_E)) addition = Vector3Add(addition, camera_up); + if (IsKeyDown(KEY_Q)) addition = Vector3Subtract(addition, camera_up); + addition = Vector3Scale(Vector3Normalize(addition), 0.5f); + client->camera.position = Vector3Add(client->camera.position, addition); + + // if (IsKeyDown(KEY_D)) client->camera.position.x -= 0.5f; + // if (IsKeyDown(KEY_A)) client->camera.position.x += 0.5f; + // if (IsKeyDown(KEY_E)) client->camera.position.y += 0.5f; + // if (IsKeyDown(KEY_Q)) client->camera.position.y -= 0.5f; + // if (IsKeyDown(KEY_S)) client->camera.position.z -= 0.5f; + // if (IsKeyDown(KEY_W)) client->camera.position.z += 0.5f; + for (int j = 0; j < env.n_robots; j++) { Vec3D tgt = From c44e771b5912b5ede84949c6e7bed82488e4c341 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 17:04:24 -0500 Subject: [PATCH 06/40] Cube -> sphere --- pufferlib/ocean/codeball/codeball.c | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index a9c5bccb..85e59438 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -5,14 +5,6 @@ void allocate(CodeBall* env) { - if (env->n_robots < 2 || env->n_robots > 6) { - printf("Invalid number of robots\n"); - exit(1); - } - if (env->n_nitros != 4 && env->n_nitros != 0) { - printf("Invalid number of nitro packs\n"); - exit(1); - } env->robots = (Entity*)calloc(env->n_robots, sizeof(Entity)); env->nitro_packs = (NitroPack*)calloc(env->n_nitros, sizeof(NitroPack)); env->actions = (double*)calloc(env->n_robots * 4, sizeof(double)); @@ -174,17 +166,19 @@ void render(Client* client, CodeBall* env) { (Vector3){0, -1, 0}, goal_size.x, goal_size.z, goal_color); // Bottom } - // Draw robots (as boxes, colored by side) + // Draw robots (colored by side) for (int i = 0; i < env->n_robots; i++) { Color robot_color = robots[i].side ? client->robot_color[1] : client->robot_color[0]; - DrawCube(cvt(robots[i].position), robots[i].radius * 2, robots[i].radius * 2, - robots[i].radius * 2, robot_color); + // DrawCube(cvt(robots[i].position), robots[i].radius * 2, robots[i].radius * 2, + // robots[i].radius * 2, robot_color); + DrawSphere(cvt(robots[i].position), robots[i].radius, robot_color); } // Draw ball (as a box) - DrawCube(cvt(ball.position), ball.radius * 2, ball.radius * 2, ball.radius * 2, - client->ball_color); + // DrawCube(cvt(ball.position), ball.radius * 2, ball.radius * 2, ball.radius * 2, + // client->ball_color); + DrawSphere(cvt(ball.position), ball.radius, client->ball_color); // Draw nitro packs (as boxes) for (int i = 0; i < env->n_nitros; i++) { @@ -266,7 +260,7 @@ int main() { Client* client = make_client(); CodeBall env; - env.n_robots = 6; + env.n_robots = 50; env.n_nitros = 4; allocate(&env); reset(&env); @@ -274,7 +268,7 @@ int main() { struct timeval start, end; gettimeofday(&start, NULL); - int initial_steps = 10; + int initial_steps = 1; for (int i = 0; i < 10000; i++) { if (WindowShouldClose()) break; From d8a16aa8a5632f1df4621accdebc7dc776cfef3b Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 18:13:23 -0500 Subject: [PATCH 07/40] Write shaders. nothing works --- pufferlib/ocean/codeball/base.vs | 31 ++++++++++++++ pufferlib/ocean/codeball/codeball.c | 35 +++++++++++++++- pufferlib/ocean/codeball/fragment.fs | 61 ++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 pufferlib/ocean/codeball/base.vs create mode 100644 pufferlib/ocean/codeball/fragment.fs diff --git a/pufferlib/ocean/codeball/base.vs b/pufferlib/ocean/codeball/base.vs new file mode 100644 index 00000000..b268514e --- /dev/null +++ b/pufferlib/ocean/codeball/base.vs @@ -0,0 +1,31 @@ +#version 330 + +// Input vertex attributes +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; +in vec4 vertexColor; + +// Input uniform values +uniform mat4 mvp; +uniform mat4 matModel; +uniform mat4 matNormal; + +// Output vertex attributes (to fragment shader) +out vec3 fragPosition; +out vec2 fragTexCoord; +out vec4 fragColor; +out vec3 fragNormal; + +// NOTE: Add here your custom variables + +void main() { + // Send vertex attributes to fragment shader + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 1.0))); + + // Calculate final vertex position + gl_Position = mvp * vec4(vertexPosition, 1.0); +} \ No newline at end of file diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index 85e59438..00df426c 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -1,8 +1,14 @@ #include "codeball.h" #include "puffernet.h" #include "raymath.h" +#include "rlgl.h" #include +#if defined(PLATFORM_DESKTOP) +#define GLSL_VERSION 330 +#else // PLATFORM_ANDROID, PLATFORM_WEB +#define GLSL_VERSION 100 +#endif void allocate(CodeBall* env) { env->robots = (Entity*)calloc(env->n_robots, sizeof(Entity)); @@ -26,6 +32,8 @@ struct Client { Color ball_color; Color nitro_color; Camera3D camera; + Shader fragment; + RenderTexture2D render_target; }; Client* make_client() { @@ -44,12 +52,18 @@ Client* make_client() { InitWindow(client->width, client->height, "CodeBall"); + client->fragment = LoadShader(TextFormat("base.vs", GLSL_VERSION), + TextFormat("fragment.fs", GLSL_VERSION)); + client->render_target = + LoadRenderTexture(client->width, client->height); + SetTargetFPS(60); return client; } void close_client(Client* client) { + UnloadRenderTexture(client->render_target); CloseWindow(); free(client); } @@ -91,12 +105,12 @@ void render(Client* client, CodeBall* env) { Entity ball = env->ball; NitroPack* nitro_packs = env->nitro_packs; - BeginDrawing(); + BeginTextureMode(client->render_target); ClearBackground(DARKGRAY); - BeginMode3D(client->camera); // Begin 3D mode + // Arena dimensions Vector3 arena_size = {arena.width, arena.height, arena.depth}; @@ -190,9 +204,26 @@ void render(Client* client, CodeBall* env) { } EndMode3D(); // End 3D mode + EndTextureMode(); + + + BeginDrawing(); + + ClearBackground(RAYWHITE); + BeginShaderMode(client->fragment); + // NOTE: Render texture must be y-flipped due to default OpenGL coordinates + // (left-bottom) + DrawTextureRec(client->render_target.texture, + (Rectangle){0, 0, (float)client->render_target.texture.width, + (float)-client->render_target.texture.height}, + (Vector2){0, 0}, WHITE); + EndShaderMode(); DrawFPS(10, 10); + EndDrawing(); + + } // void render(Client* client, CodeBall* env) { diff --git a/pufferlib/ocean/codeball/fragment.fs b/pufferlib/ocean/codeball/fragment.fs new file mode 100644 index 00000000..b849316a --- /dev/null +++ b/pufferlib/ocean/codeball/fragment.fs @@ -0,0 +1,61 @@ +#version 330 + +// Input vertex attributes (from vertex shader) +in vec2 fragTexCoord; +in vec4 fragColor; +in vec3 fragPosition; +in vec3 fragNormal; +in float fragDepth; + +// Input uniform values +uniform sampler2D texture0; + +// Output fragment color +out vec4 finalColor; + +// NOTE: Add here your custom variables + +const vec2 size = vec2(800, 450); // Framebuffer size +const float samples = 5.0; // Pixels per axis; higher = bigger glow, worse performance +const float quality = 2.5; // Defines size factor: Lower = smaller glow, better quality + +void main() { + // vec4 sum = vec4(0); + // vec2 sizeFactor = vec2(1) / size * quality; + + // // Texel color fetching from texture sampler + // vec4 source = texture(texture0, fragTexCoord); + + // const int range = 2; // should be = (samples - 1)/2; + + // for(int x = -range; x <= range; x++) { + // for(int y = -range; y <= range; y++) { + // sum += texture(texture0, fragTexCoord + vec2(x, y) * sizeFactor); + // } + // } + + // // Calculate final fragment color + // finalColor = ((sum / (samples * samples)) + source) * colDiffuse / range; + // finalColor = gl_FragDepth > 10 ? texture(texture0, fragTexCoord) : 0; + // finalColor = texture(texture0, fragTexCoord) * (clamp(length(fragPosition) / 1000.0, 0.0, 1.0)); + // finalColor = texture(texture0, fragTexCoord) * (clamp(length(fragPosition) / 1000.0, 0.0, 1.0)); + // finalColor = texture(texture0, fragTexCoord) * (fragDepth / 100.0); + // float fd = clamp(fragDepth / 100.0, 0, 1); + // float fd = gl_FragCoord.z / 10.0; + // float fd = gl_FragDepth / 10.0; + // finalColor = vec4(fd, fd, fd, 1.0); + // float fd = -fragNormal.z; + // float fd = gl_FragCoord.z; + // finalColor = vec4(fd, fd, fd, 1.0); + // finalColor = fragColor; + // finalColor = texture(texture0, fragTexCoord) * 0.8; + // float fd = dot(normalize(fragNormal), normalize(vec3(0, 0.3, -1))); + // float fd = max(0.0, dot(fragNormal, -normalize(vec3(0, 0.3, -1)))); + // fd = clamp(fd, 0.2, 0.8); + // float fd = gl_FragCoord.x; + // float fd = fragTexCoord.x; + // float fd = texture(texture0, fragTexCoord).x; + float fd = fragColor.x; + finalColor = vec4(fd, fd, fd, 1.0); + // finalColor = texture(texture0, fragTexCoord); +} \ No newline at end of file From abe5a849548aefcb7d53d0c801b6bb6d4c4ad67c Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 18:34:00 -0500 Subject: [PATCH 08/40] Maybe I should remove the render texture --- pufferlib/ocean/codeball/Makefile | 3 +++ pufferlib/ocean/codeball/base.vs | 11 +++++------ pufferlib/ocean/codeball/codeball.c | 24 ++++++++++++++++-------- pufferlib/ocean/codeball/fragment.fs | 14 +++++--------- setup.py | 11 +++++++---- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/pufferlib/ocean/codeball/Makefile b/pufferlib/ocean/codeball/Makefile index 05335499..c0950a44 100644 --- a/pufferlib/ocean/codeball/Makefile +++ b/pufferlib/ocean/codeball/Makefile @@ -8,6 +8,9 @@ CFLAGS=\ -I../../../raylib/include -I../../../pufferlib -lpthread ARCH=$(shell uname -m) +clean: + rm -f codeball + codeball: $(CFILES) codeball.h gcc $(CFLAGS) $(CFILES) -o codeball diff --git a/pufferlib/ocean/codeball/base.vs b/pufferlib/ocean/codeball/base.vs index b268514e..f9999066 100644 --- a/pufferlib/ocean/codeball/base.vs +++ b/pufferlib/ocean/codeball/base.vs @@ -8,24 +8,23 @@ in vec4 vertexColor; // Input uniform values uniform mat4 mvp; -uniform mat4 matModel; -uniform mat4 matNormal; // Output vertex attributes (to fragment shader) -out vec3 fragPosition; out vec2 fragTexCoord; out vec4 fragColor; out vec3 fragNormal; +out vec3 fragPosition; // NOTE: Add here your custom variables void main() { // Send vertex attributes to fragment shader - fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); fragTexCoord = vertexTexCoord; - fragColor = vertexColor; - fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 1.0))); + // fragColor = vertexColor; + fragColor = vec4(1.0, 1.0, 0.0, 1.0) * vec4(fragNormal.x, fragNormal.x, fragNormal.x, 1.0); // Calculate final vertex position gl_Position = mvp * vec4(vertexPosition, 1.0); + fragNormal = vertexNormal; + fragPosition = vertexPosition; } \ No newline at end of file diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index 00df426c..1f705d4d 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -32,8 +32,9 @@ struct Client { Color ball_color; Color nitro_color; Camera3D camera; - Shader fragment; + Shader shader; RenderTexture2D render_target; + Model sphere; }; Client* make_client() { @@ -52,8 +53,8 @@ Client* make_client() { InitWindow(client->width, client->height, "CodeBall"); - client->fragment = LoadShader(TextFormat("base.vs", GLSL_VERSION), - TextFormat("fragment.fs", GLSL_VERSION)); + client->sphere = LoadModelFromMesh(GenMeshSphere(1.0f, 32, 32)); + client->shader = LoadShader("base.vs", "fragment.fs"); client->render_target = LoadRenderTexture(client->width, client->height); @@ -72,6 +73,11 @@ Vector3 cvt(Vec3D vec) { return (Vector3){vec.x, vec.y, vec.z}; } +void MyDrawSphere(Client *client, Vector3 center, float radius, + Color color) { + DrawModel(client->sphere, center, radius, color); +} + // Custom DrawPlane function for arbitrary orientation void MyDrawPlane(Vector3 center, Vector3 normal, float width, float height, Color color) { @@ -110,6 +116,7 @@ void render(Client* client, CodeBall* env) { ClearBackground(DARKGRAY); BeginMode3D(client->camera); // Begin 3D mode + BeginShaderMode(client->shader); // Arena dimensions Vector3 arena_size = {arena.width, arena.height, @@ -186,23 +193,24 @@ void render(Client* client, CodeBall* env) { robots[i].side ? client->robot_color[1] : client->robot_color[0]; // DrawCube(cvt(robots[i].position), robots[i].radius * 2, robots[i].radius * 2, // robots[i].radius * 2, robot_color); - DrawSphere(cvt(robots[i].position), robots[i].radius, robot_color); + MyDrawSphere(client, cvt(robots[i].position), robots[i].radius, robot_color); } // Draw ball (as a box) // DrawCube(cvt(ball.position), ball.radius * 2, ball.radius * 2, ball.radius * 2, // client->ball_color); - DrawSphere(cvt(ball.position), ball.radius, client->ball_color); + MyDrawSphere(client, cvt(ball.position), ball.radius, client->ball_color); // Draw nitro packs (as boxes) for (int i = 0; i < env->n_nitros; i++) { if (nitro_packs[i].alive) { - DrawCube(cvt(nitro_packs[i].position), nitro_packs[i].radius * 2, - nitro_packs[i].radius * 2, nitro_packs[i].radius * 2, + MyDrawSphere(client, cvt(nitro_packs[i].position), nitro_packs[i].radius * 2, client->nitro_color); } } + EndShaderMode(); + EndMode3D(); // End 3D mode EndTextureMode(); @@ -210,7 +218,7 @@ void render(Client* client, CodeBall* env) { BeginDrawing(); ClearBackground(RAYWHITE); - BeginShaderMode(client->fragment); + BeginShaderMode(client->shader); // NOTE: Render texture must be y-flipped due to default OpenGL coordinates // (left-bottom) DrawTextureRec(client->render_target.texture, diff --git a/pufferlib/ocean/codeball/fragment.fs b/pufferlib/ocean/codeball/fragment.fs index b849316a..37f3583a 100644 --- a/pufferlib/ocean/codeball/fragment.fs +++ b/pufferlib/ocean/codeball/fragment.fs @@ -5,7 +5,6 @@ in vec2 fragTexCoord; in vec4 fragColor; in vec3 fragPosition; in vec3 fragNormal; -in float fragDepth; // Input uniform values uniform sampler2D texture0; @@ -49,13 +48,10 @@ void main() { // finalColor = vec4(fd, fd, fd, 1.0); // finalColor = fragColor; // finalColor = texture(texture0, fragTexCoord) * 0.8; - // float fd = dot(normalize(fragNormal), normalize(vec3(0, 0.3, -1))); - // float fd = max(0.0, dot(fragNormal, -normalize(vec3(0, 0.3, -1)))); + // float fd = max(0.0, dot(fragNormal, normalize(vec3(0, 0.3, -1)))); // fd = clamp(fd, 0.2, 0.8); - // float fd = gl_FragCoord.x; - // float fd = fragTexCoord.x; - // float fd = texture(texture0, fragTexCoord).x; - float fd = fragColor.x; - finalColor = vec4(fd, fd, fd, 1.0); - // finalColor = texture(texture0, fragTexCoord); + // float fd = fragNormal.z; + // finalColor = vec4(fd, fd, fd, 1.0); + // finalColor = texture(texture0, fragTexCoord) * (fragNormal.x + 0.5); + finalColor = fragColor; } \ No newline at end of file diff --git a/setup.py b/setup.py index 58f9501f..4683c8c7 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,12 @@ VERSION = '2.0.3' -RAYLIB_BASE = 'https://github.com/raysan5/raylib/releases/download/5.0/' -RAYLIB_NAME = 'raylib-5.0_macos' if platform.system() == "Darwin" else 'raylib-5.0_linux_amd64' -RAYLIB_WASM_URL = RAYLIB_BASE + 'raylib-5.0_webassembly.zip' +# RAYLIB_BASE = 'https://github.com/raysan5/raylib/releases/download/5.0/' +# RAYLIB_NAME = 'raylib-5.0_macos' if platform.system() == "Darwin" else 'raylib-5.0_linux_amd64' +# RAYLIB_WASM_URL = RAYLIB_BASE + 'raylib-5.0_webassembly.zip' +RAYLIB_BASE = 'https://github.com/raysan5/raylib/releases/download/5.5/' +RAYLIB_NAME = 'raylib-5.5_macos' if platform.system() == "Darwin" else 'raylib-5.5_linux_amd64' +RAYLIB_WASM_URL = RAYLIB_BASE + 'raylib-5.5_webassembly.zip' RAYLIB_URL = RAYLIB_BASE + RAYLIB_NAME + '.tar.gz' if not os.path.exists('raylib'): @@ -30,7 +33,7 @@ urllib.request.urlretrieve(RAYLIB_WASM_URL, 'raylib.zip') with zipfile.ZipFile('raylib.zip', 'r') as zip_ref: zip_ref.extractall() - os.rename('raylib-5.0_webassembly', 'raylib_wasm') + os.rename('raylib-5.5_webassembly', 'raylib_wasm') os.remove('raylib.zip') From 77757716d477d66bdc95bb140a6d72c0645c943e Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 18:49:17 -0500 Subject: [PATCH 09/40] Position works (but not normals) --- pufferlib/ocean/codeball/base.vs | 16 ++++---- pufferlib/ocean/codeball/codeball.c | 26 +++---------- pufferlib/ocean/codeball/fragment.fs | 58 ++++++---------------------- 3 files changed, 25 insertions(+), 75 deletions(-) diff --git a/pufferlib/ocean/codeball/base.vs b/pufferlib/ocean/codeball/base.vs index f9999066..ce24a180 100644 --- a/pufferlib/ocean/codeball/base.vs +++ b/pufferlib/ocean/codeball/base.vs @@ -12,19 +12,21 @@ uniform mat4 mvp; // Output vertex attributes (to fragment shader) out vec2 fragTexCoord; out vec4 fragColor; -out vec3 fragNormal; -out vec3 fragPosition; +out vec3 fragPositionOriginal; +out vec3 fragPositionView; +out vec3 fragNormalOriginal; +out vec3 fragNormalView; -// NOTE: Add here your custom variables void main() { // Send vertex attributes to fragment shader fragTexCoord = vertexTexCoord; - // fragColor = vertexColor; - fragColor = vec4(1.0, 1.0, 0.0, 1.0) * vec4(fragNormal.x, fragNormal.x, fragNormal.x, 1.0); + fragColor = vertexColor; // Calculate final vertex position gl_Position = mvp * vec4(vertexPosition, 1.0); - fragNormal = vertexNormal; - fragPosition = vertexPosition; + fragNormalOriginal = vertexNormal; + fragNormalView = (mvp * vec4(vertexNormal, 1.0)).xyz; + fragPositionView = gl_Position.xyz; + fragPositionOriginal = vertexPosition; } \ No newline at end of file diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index 1f705d4d..a14a320c 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -33,7 +33,6 @@ struct Client { Color nitro_color; Camera3D camera; Shader shader; - RenderTexture2D render_target; Model sphere; }; @@ -55,8 +54,6 @@ Client* make_client() { client->sphere = LoadModelFromMesh(GenMeshSphere(1.0f, 32, 32)); client->shader = LoadShader("base.vs", "fragment.fs"); - client->render_target = - LoadRenderTexture(client->width, client->height); SetTargetFPS(60); @@ -64,7 +61,6 @@ Client* make_client() { } void close_client(Client* client) { - UnloadRenderTexture(client->render_target); CloseWindow(); free(client); } @@ -73,9 +69,11 @@ Vector3 cvt(Vec3D vec) { return (Vector3){vec.x, vec.y, vec.z}; } +// TODO instancing... void MyDrawSphere(Client *client, Vector3 center, float radius, Color color) { - DrawModel(client->sphere, center, radius, color); + // DrawModel(client->sphere, center, radius, color); + DrawSphere(center, radius, color); } // Custom DrawPlane function for arbitrary orientation @@ -111,7 +109,7 @@ void render(Client* client, CodeBall* env) { Entity ball = env->ball; NitroPack* nitro_packs = env->nitro_packs; - BeginTextureMode(client->render_target); + BeginDrawing(); ClearBackground(DARKGRAY); BeginMode3D(client->camera); // Begin 3D mode @@ -211,21 +209,7 @@ void render(Client* client, CodeBall* env) { EndShaderMode(); - EndMode3D(); // End 3D mode - EndTextureMode(); - - - BeginDrawing(); - - ClearBackground(RAYWHITE); - BeginShaderMode(client->shader); - // NOTE: Render texture must be y-flipped due to default OpenGL coordinates - // (left-bottom) - DrawTextureRec(client->render_target.texture, - (Rectangle){0, 0, (float)client->render_target.texture.width, - (float)-client->render_target.texture.height}, - (Vector2){0, 0}, WHITE); - EndShaderMode(); + EndMode3D(); DrawFPS(10, 10); diff --git a/pufferlib/ocean/codeball/fragment.fs b/pufferlib/ocean/codeball/fragment.fs index 37f3583a..b6b234e5 100644 --- a/pufferlib/ocean/codeball/fragment.fs +++ b/pufferlib/ocean/codeball/fragment.fs @@ -3,55 +3,19 @@ // Input vertex attributes (from vertex shader) in vec2 fragTexCoord; in vec4 fragColor; -in vec3 fragPosition; -in vec3 fragNormal; - -// Input uniform values -uniform sampler2D texture0; +in vec3 fragPositionOriginal; +in vec3 fragPositionView; +in vec3 fragNormalOriginal; +in vec3 fragNormalView; // Output fragment color out vec4 finalColor; - -// NOTE: Add here your custom variables - -const vec2 size = vec2(800, 450); // Framebuffer size -const float samples = 5.0; // Pixels per axis; higher = bigger glow, worse performance -const float quality = 2.5; // Defines size factor: Lower = smaller glow, better quality - void main() { - // vec4 sum = vec4(0); - // vec2 sizeFactor = vec2(1) / size * quality; - - // // Texel color fetching from texture sampler - // vec4 source = texture(texture0, fragTexCoord); - - // const int range = 2; // should be = (samples - 1)/2; - - // for(int x = -range; x <= range; x++) { - // for(int y = -range; y <= range; y++) { - // sum += texture(texture0, fragTexCoord + vec2(x, y) * sizeFactor); - // } - // } - - // // Calculate final fragment color - // finalColor = ((sum / (samples * samples)) + source) * colDiffuse / range; - // finalColor = gl_FragDepth > 10 ? texture(texture0, fragTexCoord) : 0; - // finalColor = texture(texture0, fragTexCoord) * (clamp(length(fragPosition) / 1000.0, 0.0, 1.0)); - // finalColor = texture(texture0, fragTexCoord) * (clamp(length(fragPosition) / 1000.0, 0.0, 1.0)); - // finalColor = texture(texture0, fragTexCoord) * (fragDepth / 100.0); - // float fd = clamp(fragDepth / 100.0, 0, 1); - // float fd = gl_FragCoord.z / 10.0; - // float fd = gl_FragDepth / 10.0; - // finalColor = vec4(fd, fd, fd, 1.0); - // float fd = -fragNormal.z; - // float fd = gl_FragCoord.z; - // finalColor = vec4(fd, fd, fd, 1.0); - // finalColor = fragColor; - // finalColor = texture(texture0, fragTexCoord) * 0.8; - // float fd = max(0.0, dot(fragNormal, normalize(vec3(0, 0.3, -1)))); - // fd = clamp(fd, 0.2, 0.8); - // float fd = fragNormal.z; - // finalColor = vec4(fd, fd, fd, 1.0); - // finalColor = texture(texture0, fragTexCoord) * (fragNormal.x + 0.5); - finalColor = fragColor; + float height = 5.0; + float sun_intensity = clamp(fragPositionOriginal.y, 0.0, height) / height; + float spotlight_radius = 60.0; + float spotlight_distance = spotlight_radius - length(fragPositionOriginal.xz); + float spotlight_intensity = max(sqrt(max(spotlight_distance, 0.0) / spotlight_radius), 0.3); + float mul = spotlight_intensity + sun_intensity * 0.4; + finalColor = fragColor * vec4(mul, mul, mul, 1.0); } \ No newline at end of file From 5d58f369d6c32dbec25ec73dc9ac926c09dea5a9 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 19:06:32 -0500 Subject: [PATCH 10/40] Add fragment shader --- pufferlib/ocean/codeball/base.vs | 16 ++++++++-------- pufferlib/ocean/codeball/codeball.c | 14 +++++++++++--- pufferlib/ocean/codeball/fragment.fs | 14 +++++++------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pufferlib/ocean/codeball/base.vs b/pufferlib/ocean/codeball/base.vs index ce24a180..8ccda414 100644 --- a/pufferlib/ocean/codeball/base.vs +++ b/pufferlib/ocean/codeball/base.vs @@ -8,25 +8,25 @@ in vec4 vertexColor; // Input uniform values uniform mat4 mvp; +uniform mat4 matModel; +uniform mat4 matNormal; // Output vertex attributes (to fragment shader) +out vec3 fragPosition; out vec2 fragTexCoord; out vec4 fragColor; -out vec3 fragPositionOriginal; -out vec3 fragPositionView; -out vec3 fragNormalOriginal; -out vec3 fragNormalView; +out vec3 fragNormal; +// NOTE: Add here your custom variables void main() { // Send vertex attributes to fragment shader + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); fragTexCoord = vertexTexCoord; fragColor = vertexColor; + // fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 1.0))); + fragNormal = normalize(vertexNormal); // Calculate final vertex position gl_Position = mvp * vec4(vertexPosition, 1.0); - fragNormalOriginal = vertexNormal; - fragNormalView = (mvp * vec4(vertexNormal, 1.0)).xyz; - fragPositionView = gl_Position.xyz; - fragPositionOriginal = vertexPosition; } \ No newline at end of file diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index a14a320c..37562f24 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -52,9 +52,17 @@ Client* make_client() { InitWindow(client->width, client->height, "CodeBall"); - client->sphere = LoadModelFromMesh(GenMeshSphere(1.0f, 32, 32)); client->shader = LoadShader("base.vs", "fragment.fs"); + Mesh sphere = GenMeshSphere(1.0f, 32, 32); + for (int v = 0; v < sphere.vertexCount; v++) { + sphere.normals[v * 3] = v % 2 == 0 ? 1.0f : -1.0f; + sphere.normals[v * 3 + 1] = v % 3 == 0 ? 1.0f : -1.0f; + sphere.normals[v * 3 + 2] = 0.95f; + } + client->sphere = LoadModelFromMesh(sphere); + client->sphere.materials[0].shader = client->shader; + SetTargetFPS(60); return client; @@ -72,8 +80,8 @@ Vector3 cvt(Vec3D vec) { // TODO instancing... void MyDrawSphere(Client *client, Vector3 center, float radius, Color color) { - // DrawModel(client->sphere, center, radius, color); - DrawSphere(center, radius, color); + DrawModel(client->sphere, center, radius, color); + // DrawSphere(center, radius, color); } // Custom DrawPlane function for arbitrary orientation diff --git a/pufferlib/ocean/codeball/fragment.fs b/pufferlib/ocean/codeball/fragment.fs index b6b234e5..fb3f7323 100644 --- a/pufferlib/ocean/codeball/fragment.fs +++ b/pufferlib/ocean/codeball/fragment.fs @@ -3,19 +3,19 @@ // Input vertex attributes (from vertex shader) in vec2 fragTexCoord; in vec4 fragColor; -in vec3 fragPositionOriginal; -in vec3 fragPositionView; -in vec3 fragNormalOriginal; -in vec3 fragNormalView; +in vec3 fragPosition; +in vec3 fragNormal; + +uniform vec4 colDiffuse; // Output fragment color out vec4 finalColor; void main() { float height = 5.0; - float sun_intensity = clamp(fragPositionOriginal.y, 0.0, height) / height; + float sun_intensity = dot(fragNormal, vec3(0.0, 1.0, 0.0)); float spotlight_radius = 60.0; - float spotlight_distance = spotlight_radius - length(fragPositionOriginal.xz); + float spotlight_distance = spotlight_radius - length(fragPosition.xz); float spotlight_intensity = max(sqrt(max(spotlight_distance, 0.0) / spotlight_radius), 0.3); float mul = spotlight_intensity + sun_intensity * 0.4; - finalColor = fragColor * vec4(mul, mul, mul, 1.0); + finalColor = fragColor * colDiffuse * vec4(mul, mul, mul, 1.0); } \ No newline at end of file From 41ea81aa88801ddac2bed126223a963d7824a871 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 22:35:34 -0500 Subject: [PATCH 11/40] Factor out renderer --- pufferlib/ocean/codeball/codeball.c | 291 +--------------------------- pufferlib/ocean/codeball/codeball.h | 7 +- pufferlib/ocean/codeball/renderer.h | 224 +++++++++++++++++++++ 3 files changed, 230 insertions(+), 292 deletions(-) create mode 100644 pufferlib/ocean/codeball/renderer.h diff --git a/pufferlib/ocean/codeball/codeball.c b/pufferlib/ocean/codeball/codeball.c index 37562f24..a36a8909 100644 --- a/pufferlib/ocean/codeball/codeball.c +++ b/pufferlib/ocean/codeball/codeball.c @@ -1,8 +1,7 @@ +#include #include "codeball.h" #include "puffernet.h" -#include "raymath.h" -#include "rlgl.h" -#include +#include "renderer.h" #if defined(PLATFORM_DESKTOP) #define GLSL_VERSION 330 @@ -24,274 +23,12 @@ void free_allocated(CodeBall* env) { free(env->rewards); } -typedef struct Client Client; -struct Client { - float width; - float height; - Color robot_color[2]; - Color ball_color; - Color nitro_color; - Camera3D camera; - Shader shader; - Model sphere; -}; - -Client* make_client() { - Client* client = (Client*)calloc(1, sizeof(Client)); - client->width = 800; // Example width - client->height = 600; // Example height - client->robot_color[0] = RED; - client->robot_color[1] = BLUE; - client->ball_color = WHITE; - client->nitro_color = GREEN; - client->camera = - // (Camera3D){(Vector3){0.0f, 40.0f, -60.0f}, (Vector3){0.0f, 0.0f, - // 0.0f}, - (Camera3D){(Vector3){0.0f, 60.0f, -90.0f}, (Vector3){0.0f, 0.0f, 0.0f}, - (Vector3){0.0f, 1.0f, 0.0f}, 45.0f, 0}; - - InitWindow(client->width, client->height, "CodeBall"); - - client->shader = LoadShader("base.vs", "fragment.fs"); - - Mesh sphere = GenMeshSphere(1.0f, 32, 32); - for (int v = 0; v < sphere.vertexCount; v++) { - sphere.normals[v * 3] = v % 2 == 0 ? 1.0f : -1.0f; - sphere.normals[v * 3 + 1] = v % 3 == 0 ? 1.0f : -1.0f; - sphere.normals[v * 3 + 2] = 0.95f; - } - client->sphere = LoadModelFromMesh(sphere); - client->sphere.materials[0].shader = client->shader; - - SetTargetFPS(60); - - return client; -} - -void close_client(Client* client) { - CloseWindow(); - free(client); -} - -Vector3 cvt(Vec3D vec) { - return (Vector3){vec.x, vec.y, vec.z}; -} - -// TODO instancing... -void MyDrawSphere(Client *client, Vector3 center, float radius, - Color color) { - DrawModel(client->sphere, center, radius, color); - // DrawSphere(center, radius, color); -} - -// Custom DrawPlane function for arbitrary orientation -void MyDrawPlane(Vector3 center, Vector3 normal, float width, float height, - Color color) { - Vector3 u, v; - - if (fabsf(normal.y) < 0.999f) { - u = (Vector3){normal.z, 0, -normal.x}; - } else { - u = (Vector3){1, 0, 0}; - } - v = Vector3CrossProduct(normal, u); - - u = Vector3Normalize(u); - v = Vector3Normalize(v); - - Vector3 p1 = - Vector3Subtract(Vector3Subtract(center, Vector3Scale(u, width / 2)), - Vector3Scale(v, height / 2)); - Vector3 p2 = Vector3Add(Vector3Subtract(center, Vector3Scale(u, width / 2)), - Vector3Scale(v, height / 2)); - Vector3 p3 = Vector3Add(Vector3Add(center, Vector3Scale(u, width / 2)), - Vector3Scale(v, height / 2)); - Vector3 p4 = Vector3Subtract(Vector3Add(center, Vector3Scale(u, width / 2)), - Vector3Scale(v, height / 2)); - - DrawTriangleStrip3D((Vector3[]){p1, p2, p4, p3}, 4, color); -} - -void render(Client* client, CodeBall* env) { - Entity* robots = env->robots; - Entity ball = env->ball; - NitroPack* nitro_packs = env->nitro_packs; - - BeginDrawing(); - - ClearBackground(DARKGRAY); - BeginMode3D(client->camera); // Begin 3D mode - - BeginShaderMode(client->shader); - - // Arena dimensions - Vector3 arena_size = {arena.width, arena.height, - arena.depth}; - - // Draw arena (box) - // DrawCube((Vector3){0, arena.height / 2, 0}, arena_size.x, arena_size.y, - // arena_size.z, LIGHTGRAY); - // DrawCube((Vector3){0, -arena.height / 2, 0}, arena_size.x, arena_size.y, - // arena_size.z, LIGHTGRAY); - - MyDrawPlane((Vector3){0, 0, 0}, (Vector3){0, -1, 0}, arena_size.x, - arena_size.z, LIGHTGRAY); // Bottom (CORRECTED NORMAL) - MyDrawPlane((Vector3){arena_size.x / 2, arena_size.y / 2, 0}, - (Vector3){1, 0, 0}, arena_size.z, arena_size.y, - LIGHTGRAY); // Right - MyDrawPlane((Vector3){-arena_size.x / 2, arena_size.y / 2, 0}, - (Vector3){-1, 0, 0}, arena_size.z, arena_size.y, - LIGHTGRAY); // Left - - for (int fb = 0; fb < 2; fb++) { - int sign = fb == 0 ? 1 : -1; - Vector3 normal = {0, 0, sign}; - MyDrawPlane( - (Vector3){0, arena.goal_height + (arena_size.y - arena.goal_height) / 2, - arena_size.z / 2 * sign}, - normal, arena.goal_width, - (arena_size.y - arena.goal_height), - LIGHTGRAY); // Back middle - double remaining = (arena_size.x - arena.goal_width) / 2; - MyDrawPlane((Vector3){-arena.goal_width / 2 - remaining / 2, arena_size.y / 2, - arena_size.z / 2 * sign}, - normal, remaining, arena_size.y, - LIGHTGRAY); // Back right - MyDrawPlane((Vector3){arena.goal_width / 2 + remaining / 2, - arena_size.y / 2, arena_size.z / 2 * sign}, - normal, remaining, arena_size.y, - LIGHTGRAY); // Back left - } - - MyDrawPlane((Vector3){0, arena_size.y / 2, -arena_size.z / 2}, - (Vector3){0, 0, -1}, arena_size.x, arena_size.y, - LIGHTGRAY); // Front - - // Draw Goals (as boxes) - Vector3 goal_size = {arena.goal_width, arena.goal_height, arena.goal_depth}; - - Color goal_colors[2] = {GREEN, YELLOW}; - for (int gi = 0; gi < 2; gi++) { - Color goal_color = goal_colors[gi]; - int sign = gi == 0 ? 1 : -1; - MyDrawPlane( - (Vector3){0, goal_size.y / 2, sign * (arena.depth / 2 + arena.goal_depth)}, - (Vector3){0, 0, sign}, goal_size.x, goal_size.y, goal_color); // Back - MyDrawPlane((Vector3){-goal_size.x / 2, goal_size.y / 2, - sign * (arena.depth / 2 + goal_size.z / 2)}, - (Vector3){-1, 0, 0}, goal_size.z, goal_size.y, - goal_color); // Left - MyDrawPlane((Vector3){goal_size.x / 2, goal_size.y / 2, - sign * (arena.depth / 2 + goal_size.z / 2)}, - (Vector3){1, 0, 0}, goal_size.z, goal_size.y, - goal_color); // Right - MyDrawPlane( - (Vector3){0, goal_size.y, sign * (arena.depth / 2 + goal_size.z / 2)}, - (Vector3){0, 1, 0}, goal_size.x, goal_size.z, goal_color); // Top - - MyDrawPlane((Vector3){0, 0, sign * (arena.depth / 2 + goal_size.z / 2)}, - (Vector3){0, -1, 0}, goal_size.x, goal_size.z, goal_color); // Bottom - } - - // Draw robots (colored by side) - for (int i = 0; i < env->n_robots; i++) { - Color robot_color = - robots[i].side ? client->robot_color[1] : client->robot_color[0]; - // DrawCube(cvt(robots[i].position), robots[i].radius * 2, robots[i].radius * 2, - // robots[i].radius * 2, robot_color); - MyDrawSphere(client, cvt(robots[i].position), robots[i].radius, robot_color); - } - - // Draw ball (as a box) - // DrawCube(cvt(ball.position), ball.radius * 2, ball.radius * 2, ball.radius * 2, - // client->ball_color); - MyDrawSphere(client, cvt(ball.position), ball.radius, client->ball_color); - - // Draw nitro packs (as boxes) - for (int i = 0; i < env->n_nitros; i++) { - if (nitro_packs[i].alive) { - MyDrawSphere(client, cvt(nitro_packs[i].position), nitro_packs[i].radius * 2, - client->nitro_color); - } - } - - EndShaderMode(); - - EndMode3D(); - - DrawFPS(10, 10); - - EndDrawing(); - - -} - -// void render(Client* client, CodeBall* env) { -// BeginDrawing(); -// ClearBackground(DARKGRAY); - -// Entity* robots = env->robots; -// Entity ball = env->ball; -// NitroPack* nitro_packs = env->nitro_packs; - -// // Arena dimensions for scaling -// float arena_width_scaled = arena.width * client->width / arena.width; -// float arena_depth_scaled = (arena.depth + 2 * arena.goal_depth) * client->height / (arena.depth + 2 * arena.goal_depth); -// float arena_x_offset = client->width / 2.0f; -// float arena_z_offset = client->height / 2.0f; - -// // Draw background -// DrawRectangle(0, 0, client->width, client->height, DARKGRAY); - -// // Draw arena (now a separate colored rectangle) -// DrawRectangle(arena_x_offset - arena_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, arena_width_scaled, arena_depth_scaled, LIGHTGRAY); - -// // Draw goal areas (Corrected positioning and size) -// float goal_width_scaled = arena.goal_width * client->width / arena.width; -// float goal_depth_scaled = arena.goal_depth * client->height / (arena.depth + 2 * arena.goal_depth); -// float goal_height_scaled = arena.goal_height * client->height / (arena.depth + 2 * arena.goal_depth); - -// DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset + arena_depth_scaled / 2 - goal_depth_scaled, goal_width_scaled, goal_height_scaled, GREEN); // Blue goal -// DrawRectangle(arena_x_offset - goal_width_scaled / 2, arena_z_offset - arena_depth_scaled / 2, goal_width_scaled, goal_height_scaled, YELLOW); // Red goal - -// // Draw robots (Colored by side) -// for (int i = 0; i < env->n_robots; i++) { -// Color robot_color = robots[i].side ? client->robot_color[1] : client->robot_color[0]; // Right is blue, left is red -// Vector2 robot_pos = { -// arena_x_offset + robots[i].position.x * arena_width_scaled / arena.width, -// arena_z_offset - robots[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) -// }; -// DrawCircleV(robot_pos, robots[i].radius * arena_width_scaled / arena.width, robot_color); -// } - -// // Draw ball -// Vector2 ball_pos = { -// arena_x_offset + ball.position.x * arena_width_scaled / arena.width, -// arena_z_offset - ball.position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) -// }; -// DrawCircleV(ball_pos, ball.radius * arena_width_scaled / arena.width, client->ball_color); - -// // Draw nitro packs -// for (int i = 0; i < env->n_nitros; i++) { -// if (nitro_packs[i].alive) { -// Vector2 nitro_pos = { -// arena_x_offset + nitro_packs[i].position.x * arena_width_scaled / arena.width, -// arena_z_offset - nitro_packs[i].position.z * arena_depth_scaled / (arena.depth + 2 * arena.goal_depth) -// }; -// DrawCircleV(nitro_pos, nitro_packs[i].radius * arena_width_scaled / arena.width, client->nitro_color); -// } -// } - -// DrawFPS(10, 10); -// EndDrawing(); -// } - int main() { srand(time(NULL)); // Seed the random number generator Client* client = make_client(); CodeBall env; - env.n_robots = 50; + env.n_robots = 6; env.n_nitros = 4; allocate(&env); reset(&env); @@ -304,28 +41,6 @@ int main() { for (int i = 0; i < 10000; i++) { if (WindowShouldClose()) break; - // Camera controls - Vector3 addition = {0, 0, 0}; - Vector3 camera_forward = Vector3Normalize(Vector3Subtract(client->camera.target, client->camera.position)); - Vector3 camera_right = Vector3Normalize(Vector3CrossProduct(camera_forward, (Vector3){0, 1, 0})); - Vector3 camera_up = Vector3Normalize(Vector3CrossProduct(camera_right, camera_forward)); - - if (IsKeyDown(KEY_W)) addition = Vector3Add(addition, camera_forward); - if (IsKeyDown(KEY_S)) addition = Vector3Subtract(addition, camera_forward); - if (IsKeyDown(KEY_D)) addition = Vector3Add(addition, camera_right); - if (IsKeyDown(KEY_A)) addition = Vector3Subtract(addition, camera_right); - if (IsKeyDown(KEY_E)) addition = Vector3Add(addition, camera_up); - if (IsKeyDown(KEY_Q)) addition = Vector3Subtract(addition, camera_up); - addition = Vector3Scale(Vector3Normalize(addition), 0.5f); - client->camera.position = Vector3Add(client->camera.position, addition); - - // if (IsKeyDown(KEY_D)) client->camera.position.x -= 0.5f; - // if (IsKeyDown(KEY_A)) client->camera.position.x += 0.5f; - // if (IsKeyDown(KEY_E)) client->camera.position.y += 0.5f; - // if (IsKeyDown(KEY_Q)) client->camera.position.y -= 0.5f; - // if (IsKeyDown(KEY_S)) client->camera.position.z -= 0.5f; - // if (IsKeyDown(KEY_W)) client->camera.position.z += 0.5f; - for (int j = 0; j < env.n_robots; j++) { Vec3D tgt = diff --git a/pufferlib/ocean/codeball/codeball.h b/pufferlib/ocean/codeball/codeball.h index ff5cad9a..edf510a8 100644 --- a/pufferlib/ocean/codeball/codeball.h +++ b/pufferlib/ocean/codeball/codeball.h @@ -1,10 +1,9 @@ -#include +#pragma once + #include #include #include -#include #include -#include "raylib.h" // Constants (from the description) #define ROBOT_MIN_RADIUS 1.0 @@ -682,4 +681,4 @@ void step(CodeBall* env) { } } env->tick++; -} +} \ No newline at end of file diff --git a/pufferlib/ocean/codeball/renderer.h b/pufferlib/ocean/codeball/renderer.h new file mode 100644 index 00000000..603edea9 --- /dev/null +++ b/pufferlib/ocean/codeball/renderer.h @@ -0,0 +1,224 @@ +#pragma once +#include "raylib.h" +#include "codeball.h" +#include "raymath.h" +#include "rlgl.h" +#include + +typedef struct Client Client; +struct Client { + float width; + float height; + Color robot_color[2]; + Color ball_color; + Color nitro_color; + Camera3D camera; + Shader shader; + Model sphere; +}; + +Client* make_client() { + Client* client = (Client*)calloc(1, sizeof(Client)); + client->width = 800; // Example width + client->height = 600; // Example height + client->robot_color[0] = RED; + client->robot_color[1] = BLUE; + client->ball_color = WHITE; + client->nitro_color = GREEN; + client->camera = + // (Camera3D){(Vector3){0.0f, 40.0f, -60.0f}, (Vector3){0.0f, 0.0f, + // 0.0f}, + (Camera3D){(Vector3){0.0f, 60.0f, -90.0f}, (Vector3){0.0f, 0.0f, 0.0f}, + (Vector3){0.0f, 1.0f, 0.0f}, 45.0f, 0}; + + InitWindow(client->width, client->height, "CodeBall"); + + client->shader = LoadShader("base.vs", "fragment.fs"); + + Mesh sphere = GenMeshSphere(1.0f, 32, 32); + for (int v = 0; v < sphere.vertexCount; v++) { + sphere.normals[v * 3] = v % 2 == 0 ? 1.0f : -1.0f; + sphere.normals[v * 3 + 1] = v % 3 == 0 ? 1.0f : -1.0f; + sphere.normals[v * 3 + 2] = 0.95f; + } + client->sphere = LoadModelFromMesh(sphere); + client->sphere.materials[0].shader = client->shader; + + SetTargetFPS(60); + + return client; +} + +void close_client(Client* client) { + CloseWindow(); + free(client); +} + +Vector3 cvt(Vec3D vec) { + return (Vector3){vec.x, vec.y, vec.z}; +} + +// TODO instancing... +void MyDrawSphere(Client *client, Vector3 center, float radius, + Color color) { + DrawModel(client->sphere, center, radius, color); + // DrawSphere(center, radius, color); +} + +// Custom DrawPlane function for arbitrary orientation +void MyDrawPlane(Vector3 center, Vector3 normal, float width, float height, + Color color) { + Vector3 u, v; + + if (fabsf(normal.y) < 0.999f) { + u = (Vector3){normal.z, 0, -normal.x}; + } else { + u = (Vector3){1, 0, 0}; + } + v = Vector3CrossProduct(normal, u); + + u = Vector3Normalize(u); + v = Vector3Normalize(v); + + Vector3 p1 = + Vector3Subtract(Vector3Subtract(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + Vector3 p2 = Vector3Add(Vector3Subtract(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + Vector3 p3 = Vector3Add(Vector3Add(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + Vector3 p4 = Vector3Subtract(Vector3Add(center, Vector3Scale(u, width / 2)), + Vector3Scale(v, height / 2)); + + DrawTriangleStrip3D((Vector3[]){p1, p2, p4, p3}, 4, color); +} + +void render(Client* client, CodeBall* env) { + Entity* robots = env->robots; + Entity ball = env->ball; + NitroPack* nitro_packs = env->nitro_packs; + + BeginDrawing(); + + ClearBackground(DARKGRAY); + BeginMode3D(client->camera); // Begin 3D mode + + BeginShaderMode(client->shader); + + // Arena dimensions + Vector3 arena_size = {arena.width, arena.height, + arena.depth}; + + // Draw arena (box) + // DrawCube((Vector3){0, arena.height / 2, 0}, arena_size.x, arena_size.y, + // arena_size.z, LIGHTGRAY); + // DrawCube((Vector3){0, -arena.height / 2, 0}, arena_size.x, arena_size.y, + // arena_size.z, LIGHTGRAY); + + MyDrawPlane((Vector3){0, 0, 0}, (Vector3){0, -1, 0}, arena_size.x, + arena_size.z, LIGHTGRAY); // Bottom (CORRECTED NORMAL) + MyDrawPlane((Vector3){arena_size.x / 2, arena_size.y / 2, 0}, + (Vector3){1, 0, 0}, arena_size.z, arena_size.y, + LIGHTGRAY); // Right + MyDrawPlane((Vector3){-arena_size.x / 2, arena_size.y / 2, 0}, + (Vector3){-1, 0, 0}, arena_size.z, arena_size.y, + LIGHTGRAY); // Left + + for (int fb = 0; fb < 2; fb++) { + int sign = fb == 0 ? 1 : -1; + Vector3 normal = {0, 0, sign}; + MyDrawPlane( + (Vector3){0, arena.goal_height + (arena_size.y - arena.goal_height) / 2, + arena_size.z / 2 * sign}, + normal, arena.goal_width, + (arena_size.y - arena.goal_height), + LIGHTGRAY); // Back middle + double remaining = (arena_size.x - arena.goal_width) / 2; + MyDrawPlane((Vector3){-arena.goal_width / 2 - remaining / 2, arena_size.y / 2, + arena_size.z / 2 * sign}, + normal, remaining, arena_size.y, + LIGHTGRAY); // Back right + MyDrawPlane((Vector3){arena.goal_width / 2 + remaining / 2, + arena_size.y / 2, arena_size.z / 2 * sign}, + normal, remaining, arena_size.y, + LIGHTGRAY); // Back left + } + + MyDrawPlane((Vector3){0, arena_size.y / 2, -arena_size.z / 2}, + (Vector3){0, 0, -1}, arena_size.x, arena_size.y, + LIGHTGRAY); // Front + + // Draw Goals (as boxes) + Vector3 goal_size = {arena.goal_width, arena.goal_height, arena.goal_depth}; + + Color goal_colors[2] = {GREEN, YELLOW}; + for (int gi = 0; gi < 2; gi++) { + Color goal_color = goal_colors[gi]; + int sign = gi == 0 ? 1 : -1; + MyDrawPlane( + (Vector3){0, goal_size.y / 2, sign * (arena.depth / 2 + arena.goal_depth)}, + (Vector3){0, 0, sign}, goal_size.x, goal_size.y, goal_color); // Back + MyDrawPlane((Vector3){-goal_size.x / 2, goal_size.y / 2, + sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){-1, 0, 0}, goal_size.z, goal_size.y, + goal_color); // Left + MyDrawPlane((Vector3){goal_size.x / 2, goal_size.y / 2, + sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){1, 0, 0}, goal_size.z, goal_size.y, + goal_color); // Right + MyDrawPlane( + (Vector3){0, goal_size.y, sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){0, 1, 0}, goal_size.x, goal_size.z, goal_color); // Top + + MyDrawPlane((Vector3){0, 0, sign * (arena.depth / 2 + goal_size.z / 2)}, + (Vector3){0, -1, 0}, goal_size.x, goal_size.z, goal_color); // Bottom + } + + // Draw robots (colored by side) + for (int i = 0; i < env->n_robots; i++) { + Color robot_color = + robots[i].side ? client->robot_color[1] : client->robot_color[0]; + // DrawCube(cvt(robots[i].position), robots[i].radius * 2, robots[i].radius * 2, + // robots[i].radius * 2, robot_color); + MyDrawSphere(client, cvt(robots[i].position), robots[i].radius, robot_color); + } + + // Draw ball (as a box) + // DrawCube(cvt(ball.position), ball.radius * 2, ball.radius * 2, ball.radius * 2, + // client->ball_color); + MyDrawSphere(client, cvt(ball.position), ball.radius, client->ball_color); + + // Draw nitro packs (as boxes) + for (int i = 0; i < env->n_nitros; i++) { + if (nitro_packs[i].alive) { + MyDrawSphere(client, cvt(nitro_packs[i].position), nitro_packs[i].radius * 2, + client->nitro_color); + } + } + + EndShaderMode(); + + EndMode3D(); + + DrawFPS(10, 10); + + EndDrawing(); + + // Camera controls + Vector3 addition = {0, 0, 0}; + Vector3 camera_forward = Vector3Normalize( + Vector3Subtract(client->camera.target, client->camera.position)); + Vector3 camera_right = Vector3Normalize( + Vector3CrossProduct(camera_forward, (Vector3){0, 1, 0})); + Vector3 camera_up = + Vector3Normalize(Vector3CrossProduct(camera_right, camera_forward)); + + if (IsKeyDown(KEY_W)) addition = Vector3Add(addition, camera_forward); + if (IsKeyDown(KEY_S)) addition = Vector3Subtract(addition, camera_forward); + if (IsKeyDown(KEY_D)) addition = Vector3Add(addition, camera_right); + if (IsKeyDown(KEY_A)) addition = Vector3Subtract(addition, camera_right); + if (IsKeyDown(KEY_E)) addition = Vector3Add(addition, camera_up); + if (IsKeyDown(KEY_Q)) addition = Vector3Subtract(addition, camera_up); + addition = Vector3Scale(Vector3Normalize(addition), 0.5f); + client->camera.position = Vector3Add(client->camera.position, addition); +} \ No newline at end of file From 2a2c11d3c6066a05cebeb7d560da5c810a24f0e9 Mon Sep 17 00:00:00 2001 From: nev Date: Sat, 14 Dec 2024 23:07:43 -0500 Subject: [PATCH 12/40] More refactoring, add Python wrappers --- .gitignore | 3 +- codeball | Bin 0 -> 1235096 bytes pufferlib/ocean/codeball/Makefile | 3 +- pufferlib/ocean/codeball/codeball.py | 99 +++++++++++++ pufferlib/ocean/codeball/cy_codeball.pyx | 180 +++++++++++++++++++++++ pufferlib/ocean/codeball/cy_squared.pyx | 70 --------- pufferlib/ocean/codeball/renderer.h | 6 +- 7 files changed, 284 insertions(+), 77 deletions(-) create mode 100755 codeball create mode 100644 pufferlib/ocean/codeball/codeball.py create mode 100644 pufferlib/ocean/codeball/cy_codeball.pyx delete mode 100644 pufferlib/ocean/codeball/cy_squared.pyx diff --git a/.gitignore b/.gitignore index 4f9ea8bd..56c54fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,5 @@ pufferlib/puffernet.c pufferlib/extensions.c pufferlib/ocean/tactical/c_tactical.c pufferlib/ocean/grid/c_grid.c -.idea/ \ No newline at end of file +.idea/ +*.dSYM/ \ No newline at end of file diff --git a/codeball b/codeball new file mode 100755 index 0000000000000000000000000000000000000000..e21063e5831e20c030777f4c5968935b0319b92f GIT binary patch literal 1235096 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�#*e1@4@{V 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