From 626e8ebd90c3e1cc123edc19057c54ffbd2edf3c Mon Sep 17 00:00:00 2001 From: Oliver Dressler Date: Thu, 28 Aug 2025 09:42:00 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + Engine.lean | 197 ++++++++++++++++++++++++++++++++++++ Main.lean | 4 + README.md | 21 ++++ SDL.lean | 53 ++++++++++ c/sdl.c | 102 +++++++++++++++++++ lake-manifest.json | 5 + lakefile.lean | 30 ++++++ lean-toolchain | 1 + screenshots/screenshot1.png | Bin 0 -> 11358 bytes 10 files changed, 414 insertions(+) create mode 100644 .gitignore create mode 100644 Engine.lean create mode 100644 Main.lean create mode 100644 README.md create mode 100644 SDL.lean create mode 100644 c/sdl.c create mode 100644 lake-manifest.json create mode 100644 lakefile.lean create mode 100644 lean-toolchain create mode 100644 screenshots/screenshot1.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1cc6956 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.lake/ \ No newline at end of file diff --git a/Engine.lean b/Engine.lean new file mode 100644 index 0000000..bef93e4 --- /dev/null +++ b/Engine.lean @@ -0,0 +1,197 @@ +import SDL + +namespace Engine + +structure Color where + r : UInt8 + g : UInt8 + b : UInt8 + a : UInt8 := 255 + +structure Camera where + x : Float + y : Float + angle : Float + speed : Float := 3.0 + turnSpeed : Float := 2.0 + +abbrev Map := Array (Array UInt8) + +structure EngineState where + deltaTime : Float + lastTime : UInt32 + running : Bool + camera : Camera + gameMap : Map + +def SCREEN_WIDTH : Int32 := 1280 +def SCREEN_HEIGHT : Int32 := 720 +def FOV : Float := 1.047 -- ~60 degrees in radians + +def sampleMap : Map := #[ + #[1,1,1,1,1,1,1,1,1,1], + #[1,0,0,0,0,0,0,0,0,1], + #[1,0,1,0,0,0,0,1,0,1], + #[1,0,0,0,0,0,0,0,0,1], + #[1,0,0,0,1,1,0,0,0,1], + #[1,0,0,0,1,1,0,0,0,1], + #[1,0,0,0,0,0,0,0,0,1], + #[1,0,1,0,0,0,0,1,0,1], + #[1,0,0,0,0,0,0,0,0,1], + #[1,1,1,1,1,1,1,1,1,1] +] + +inductive Key where + | W | A | S | D | Left | Right | Space | Escape + +def keyToScancode : Key → UInt32 + | .W => SDL.SDL_SCANCODE_W | .A => SDL.SDL_SCANCODE_A | .S => SDL.SDL_SCANCODE_S + | .D => SDL.SDL_SCANCODE_D | .Left => SDL.SDL_SCANCODE_LEFT | .Right => SDL.SDL_SCANCODE_RIGHT + | .Space => SDL.SDL_SCANCODE_SPACE | .Escape => SDL.SDL_SCANCODE_ESCAPE + +def isKeyDown (key : Key) : IO Bool := SDL.getKeyState (keyToScancode key) + +def isWall (mapp : Map) (x y : Float) : Bool := + if x < 0.0 || y < 0.0 then true else + let mapX := x.floor.toUInt32.toNat + let mapY := y.floor.toUInt32.toNat + mapY >= mapp.size || mapX >= mapp[mapY]!.size || mapp[mapY]![mapX]! == 1 + +def castRay (map : Map) (startX startY angle : Float) : Float := Id.run do + let rayDirX := Float.cos angle + let rayDirY := Float.sin angle + let mut mapX := startX.floor + let mut mapY := startY.floor + + let deltaDistX := if rayDirX == 0.0 then 1e30 else Float.abs (1.0 / rayDirX) + let deltaDistY := if rayDirY == 0.0 then 1e30 else Float.abs (1.0 / rayDirY) + + let stepX := if rayDirX < 0.0 then -1 else 1 + let mut sideDistX := if rayDirX < 0.0 then (startX - mapX) * deltaDistX else (mapX + 1.0 - startX) * deltaDistX + let stepY := if rayDirY < 0.0 then -1 else 1 + let mut sideDistY := if rayDirY < 0.0 then (startY - mapY) * deltaDistY else (mapY + 1.0 - startY) * deltaDistY + + let mut hit := false + let mut side := 0 + + for _ in [0:25] do + if hit then break + if sideDistX < sideDistY then + sideDistX := sideDistX + deltaDistX + mapX := mapX + Float.ofInt stepX + side := 0 + else + sideDistY := sideDistY + deltaDistY + mapY := mapY + Float.ofInt stepY + side := 1 + + hit := isWall map mapX mapY + + if side == 0 + then (mapX - startX + (1.0 - Float.ofInt stepX) / 2.0) / rayDirX + else (mapY - startY + (1.0 - Float.ofInt stepY) / 2.0) / rayDirY + +def updateCamera (camera : Camera) (deltaTime : Float) : IO Camera := do + let moveSpeed := camera.speed * deltaTime + let mut newX := camera.x + let mut newY := camera.y + let mut newAngle := camera.angle + + if ← isKeyDown .W then + newX := newX + Float.cos camera.angle * moveSpeed + newY := newY + Float.sin camera.angle * moveSpeed + if ← isKeyDown .S then + newX := newX - Float.cos camera.angle * moveSpeed + newY := newY - Float.sin camera.angle * moveSpeed + if ← isKeyDown .A then newAngle := newAngle - camera.turnSpeed * deltaTime + if ← isKeyDown .D then newAngle := newAngle + camera.turnSpeed * deltaTime + + pure { camera with x := newX, y := newY, angle := newAngle } + +def setColor (color : Color) : IO Unit := + SDL.setRenderDrawColor color.r color.g color.b color.a *> pure () + +def fillRect (x y w h : Int32) : IO Unit := + SDL.renderFillRect x y w h *> pure () + +def renderScene (state : EngineState) : IO Unit := do + setColor { r := 87, g := 127, b := 137 } + let _ ← SDL.renderClear + + let camera := state.camera + let rayAngleStep := FOV / SCREEN_WIDTH.toFloat + + for column in [0:SCREEN_WIDTH.toNatClampNeg] do + let rayAngle := camera.angle - FOV/2 + column.toFloat * rayAngleStep + let distance := max 0.1 (castRay state.gameMap camera.x camera.y rayAngle) + let wallHeight := (SCREEN_HEIGHT.toFloat / distance) * 1.5 + + let wallStart := max 0 ((SCREEN_HEIGHT.toFloat - wallHeight) / 2).toInt32 + let wallEnd := min (SCREEN_HEIGHT - 1) (wallStart + wallHeight.toInt32) + let xPos := column.toInt32 + + if wallStart > 0 then + setColor { r := 135, g := 206, b := 235 } + fillRect xPos 0 1 wallStart + + if wallStart < wallEnd then + let lightIntensity := max 0.3 (1.0 - distance / 8.0) + let col := (200.0 * lightIntensity).toUInt8 + setColor { r := col, g := col, b := col + 20 } + fillRect xPos wallStart 1 (wallEnd - wallStart) + + if wallEnd < SCREEN_HEIGHT - 1 then + let floorShade := max 20 (60 - distance * 5).toUInt8 + setColor { r := floorShade, g := floorShade + 30, b := floorShade } + fillRect xPos wallEnd 1 (SCREEN_HEIGHT - 1 - wallEnd) + +private def updateEngineState (engineState : IO.Ref EngineState) : IO Unit := do + let state ← engineState.get + let currentTime ← SDL.getTicks + let deltaTime := (currentTime - state.lastTime).toFloat / 1000.0 + let newCamera ← updateCamera state.camera deltaTime + engineState.set { state with deltaTime, lastTime := currentTime, camera := newCamera } + +partial def gameLoop (engineState : IO.Ref EngineState) : IO Unit := do + updateEngineState engineState + + let eventType ← SDL.pollEvent + if eventType == SDL.SDL_QUIT || (← isKeyDown .Escape) then + engineState.modify (fun s => { s with running := false }) + + let state ← engineState.get + if state.running then + renderScene state + SDL.renderPresent + gameLoop engineState + +partial def run : IO Unit := do + unless (← SDL.init SDL.SDL_INIT_VIDEO) == 0 do + IO.println "Failed to initialize SDL" + return + + unless (← SDL.createWindow "LeanDoomed" 100 100 SCREEN_WIDTH SCREEN_HEIGHT SDL.SDL_WINDOW_SHOWN) != 0 do + IO.println "Failed to create window" + SDL.quit + return + + unless (← SDL.createRenderer 4294967295 SDL.SDL_RENDERER_ACCELERATED) != 0 do + IO.println "Failed to create renderer" + SDL.quit + return + + let initialState : EngineState := { + deltaTime := 0.0, lastTime := 0, running := true, + camera := { x := 1.5, y := 1.5, angle := 0.0 }, + gameMap := sampleMap + } + + let engineState ← IO.mkRef initialState + IO.println "Use WASD to move, A/D to turn, ESC to quit" + gameLoop engineState + SDL.quit + +def EngineState.setRunning (state : EngineState) (running : Bool) : EngineState := + { state with running } + +end Engine diff --git a/Main.lean b/Main.lean new file mode 100644 index 0000000..66d0bc9 --- /dev/null +++ b/Main.lean @@ -0,0 +1,4 @@ +import Engine + +def main : IO Unit := do + Engine.run diff --git a/README.md b/README.md new file mode 100644 index 0000000..baa8e56 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Lean SDL2 Bindings Example + +Playing around with SDL2 bindings in Lean4 to learn about the FFI. + +Simple real-time Doom-style raycasting engine in Lean4: + +![Screenshot](screenshots/screenshot1.png) + +## Run + +This is just an experiment and the build is currently specific to my system (Ubuntu 24.04.2). + +**You need to install dependencies and adjust paths in lakefile.lean!** + +```bash +lake exe LeanDoomed +``` + +## License + +MIT \ No newline at end of file diff --git a/SDL.lean b/SDL.lean new file mode 100644 index 0000000..6586c22 --- /dev/null +++ b/SDL.lean @@ -0,0 +1,53 @@ +namespace SDL + +def SDL_INIT_VIDEO : UInt32 := 0x00000020 +def SDL_WINDOW_SHOWN : UInt32 := 0x00000004 +def SDL_RENDERER_ACCELERATED : UInt32 := 0x00000002 +def SDL_QUIT : UInt32 := 0x100 + +def SDL_SCANCODE_W : UInt32 := 26 +def SDL_SCANCODE_A : UInt32 := 4 +def SDL_SCANCODE_S : UInt32 := 22 +def SDL_SCANCODE_D : UInt32 := 7 +def SDL_SCANCODE_LEFT : UInt32 := 80 +def SDL_SCANCODE_RIGHT : UInt32 := 79 +def SDL_SCANCODE_SPACE : UInt32 := 44 +def SDL_SCANCODE_ESCAPE : UInt32 := 41 + +@[extern "sdl_init"] +opaque init : UInt32 → IO UInt32 + +@[extern "sdl_quit"] +opaque quit : IO Unit + +@[extern "sdl_create_window"] +opaque createWindow : String → Int32 → Int32 → Int32 → Int32 → UInt32 → IO UInt32 + +@[extern "sdl_create_renderer"] +opaque createRenderer : UInt32 → UInt32 → IO UInt32 + +@[extern "sdl_set_render_draw_color"] +opaque setRenderDrawColor : UInt8 → UInt8 → UInt8 → UInt8 → IO Int32 + +@[extern "sdl_render_clear"] +opaque renderClear : IO Int32 + +@[extern "sdl_render_present"] +opaque renderPresent : IO Unit + +@[extern "sdl_render_fill_rect"] +opaque renderFillRect : Int32 → Int32 → Int32 → Int32 → IO Int32 + +@[extern "sdl_delay"] +opaque delay : UInt32 → IO Unit + +@[extern "sdl_poll_event"] +opaque pollEvent : IO UInt32 + +@[extern "sdl_get_ticks"] +opaque getTicks : IO UInt32 + +@[extern "sdl_get_key_state"] +opaque getKeyState : UInt32 → IO Bool + +end SDL diff --git a/c/sdl.c b/c/sdl.c new file mode 100644 index 0000000..3c84573 --- /dev/null +++ b/c/sdl.c @@ -0,0 +1,102 @@ +#include +#include +#include +#include + +static SDL_Window* g_window = NULL; +static SDL_Renderer* g_renderer = NULL; + +uint32_t sdl_get_version(void) { + SDL_version compiled; + SDL_VERSION(&compiled); + return compiled.major * 100 + compiled.minor * 10 + compiled.patch; +} + +lean_obj_res sdl_init(uint32_t flags, lean_obj_arg w) { + int32_t result = SDL_Init(flags); + return lean_io_result_mk_ok(lean_box_uint32(result)); +} + +lean_obj_res sdl_quit(lean_obj_arg w) { + if (g_renderer) { + SDL_DestroyRenderer(g_renderer); + g_renderer = NULL; + } + if (g_window) { + SDL_DestroyWindow(g_window); + g_window = NULL; + } + SDL_Quit(); + return lean_io_result_mk_ok(lean_box(0)); +} + +lean_obj_res sdl_create_window(lean_obj_arg title, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t flags, lean_obj_arg world) { + const char* title_str = lean_string_cstr(title); + g_window = SDL_CreateWindow(title_str, (int)x, (int)y, (int)w, (int)h, flags); + if (g_window == NULL) { + return lean_io_result_mk_ok(lean_box(0)); + } + return lean_io_result_mk_ok(lean_box(1)); +} + +lean_obj_res sdl_create_renderer(uint32_t index_unsigned, uint32_t flags, lean_obj_arg w) { + if (g_window == NULL) { + printf("C: No window available for renderer creation\n"); + return lean_io_result_mk_ok(lean_box(0)); + } + int32_t index = (int32_t)index_unsigned; + g_renderer = SDL_CreateRenderer(g_window, index, flags); + if (g_renderer == NULL) { + const char* error = SDL_GetError(); + printf("C: SDL_CreateRenderer failed: %s\n", error); + return lean_io_result_mk_ok(lean_box(0)); + } + return lean_io_result_mk_ok(lean_box(1)); +} + +lean_obj_res sdl_set_render_draw_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a, lean_obj_arg w) { + if (g_renderer == NULL) return lean_io_result_mk_ok(lean_box_uint32(-1)); + int32_t result = SDL_SetRenderDrawColor(g_renderer, r, g, b, a); + return lean_io_result_mk_ok(lean_box_uint32(result)); +} + +lean_obj_res sdl_render_clear(lean_obj_arg w) { + if (g_renderer == NULL) return lean_io_result_mk_ok(lean_box_uint32(-1)); + int32_t result = SDL_RenderClear(g_renderer); + return lean_io_result_mk_ok(lean_box_uint32(result)); +} + +lean_obj_res sdl_render_present(lean_obj_arg w) { + if (g_renderer == NULL) return lean_io_result_mk_ok(lean_box(0)); + SDL_RenderPresent(g_renderer); + return lean_io_result_mk_ok(lean_box(0)); +} + +lean_obj_res sdl_render_fill_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, lean_obj_arg world) { + if (g_renderer == NULL) return lean_io_result_mk_ok(lean_box_uint32(-1)); + SDL_Rect rect = {(int)x, (int)y, (int)w, (int)h}; + int32_t result = SDL_RenderFillRect(g_renderer, &rect); + return lean_io_result_mk_ok(lean_box_uint32(result)); +} + +lean_obj_res sdl_delay(uint32_t ms, lean_obj_arg w) { + SDL_Delay(ms); + return lean_io_result_mk_ok(lean_box(0)); +} + +lean_obj_res sdl_poll_event(lean_obj_arg w) { + SDL_Event event; + int has_event = SDL_PollEvent(&event); + return lean_io_result_mk_ok(lean_box_uint32(has_event ? event.type : 0)); +} + +lean_obj_res sdl_get_ticks(lean_obj_arg w) { + uint32_t ticks = SDL_GetTicks(); + return lean_io_result_mk_ok(lean_box_uint32(ticks)); +} + +lean_obj_res sdl_get_key_state(uint32_t scancode, lean_obj_arg w) { + const uint8_t* state = SDL_GetKeyboardState(NULL); + uint8_t pressed = state[scancode]; + return lean_io_result_mk_ok(lean_box(pressed)); +} \ No newline at end of file diff --git a/lake-manifest.json b/lake-manifest.json new file mode 100644 index 0000000..2f53fda --- /dev/null +++ b/lake-manifest.json @@ -0,0 +1,5 @@ +{"version": "1.1.0", + "packagesDir": ".lake/packages", + "packages": [], + "name": "LeanDoomed", + "lakeDir": ".lake"} diff --git a/lakefile.lean b/lakefile.lean new file mode 100644 index 0000000..69cb7dd --- /dev/null +++ b/lakefile.lean @@ -0,0 +1,30 @@ +import Lake +open System Lake DSL + +package LeanDoomed + +input_file sdl.c where + path := "c" / "sdl.c" + text := true + +target sdl.o pkg : FilePath := do + let srcJob ← sdl.c.fetch + let oFile := pkg.buildDir / "c" / "sdl.o" + let leanInclude := "/home/ooo/.elan/toolchains/leanprover--lean4---v4.22.0/include" + buildO oFile srcJob #[] #["-fPIC", "-I/usr/include/SDL2", "-D_REENTRANT", s!"-I{leanInclude}"] "cc" + +target libleansdl pkg : FilePath := do + let sdlO ← sdl.o.fetch + let name := nameToStaticLib "leansdl" + buildStaticLib (pkg.staticLibDir / name) #[sdlO] + +lean_lib SDL where + moreLinkObjs := #[libleansdl] + moreLinkArgs := #["-lSDL2", "-lSDL2_image"] + +lean_lib Engine + +@[default_target] +lean_exe LeanDoomed where + root := `Main + moreLinkArgs := #["/usr/lib/x86_64-linux-gnu/libSDL2.so", "/usr/lib/x86_64-linux-gnu/libSDL2_image.so"] diff --git a/lean-toolchain b/lean-toolchain new file mode 100644 index 0000000..1f2f20a --- /dev/null +++ b/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.22.0 \ No newline at end of file diff --git a/screenshots/screenshot1.png b/screenshots/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..ce1796cc1989937e00a58d3387733bded83aa21a GIT binary patch literal 11358 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 literal 0 HcmV?d00001