Last active
July 28, 2025 16:24
-
-
Save naranyala/5d50e57b64923e4eaa6834edd81a4fc5 to your computer and use it in GitHub Desktop.
2D car obstacles game using nim programming and raylib (reactive style)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import raylib as rl | |
| import random | |
| import math | |
| import asyncdispatch | |
| import strutils | |
| import sequtils | |
| # Game constants | |
| const | |
| ScreenWidth = 800 | |
| ScreenHeight = 600 | |
| PlayerWidth = 50 | |
| PlayerHeight = 80 | |
| ObstacleWidth = 50 | |
| ObstacleHeight = 50 | |
| MaxObstacles = 10 | |
| PlayerSpeed = 5 | |
| InitialObstacleSpeed = 3 | |
| FPS = 60 | |
| # Explicit types for domain concepts | |
| type | |
| GameCoordinate = distinct float # Allows negative values for y-coordinates | |
| NonNegativeInt = distinct int | |
| PositiveFloat = distinct float # For values that must remain positive (e.g., speed) | |
| Player = object | |
| x, y: GameCoordinate | |
| color: rl.Color | |
| Obstacle = object | |
| x, y: GameCoordinate | |
| active: bool | |
| color: rl.Color | |
| speed: PositiveFloat | |
| GameState = object | |
| player: Player | |
| obstacles: array[MaxObstacles, Obstacle] | |
| score: NonNegativeInt | |
| gameOver: bool | |
| obstacleSpeed: PositiveFloat | |
| GameEventKind = enum | |
| MoveLeft, MoveRight, ResetGame | |
| GameEvent = object | |
| kind: GameEventKind | |
| # Type constructors with validation | |
| proc newGameCoordinate(value: float): GameCoordinate = | |
| GameCoordinate(value) # No restriction on negative values | |
| proc newPositiveFloat(value: float): PositiveFloat = | |
| assert value >= 0, "Value must be non-negative" | |
| PositiveFloat(value) | |
| proc newNonNegativeInt(value: int): NonNegativeInt = | |
| assert value >= 0, "Value must be non-negative" | |
| NonNegativeInt(value) | |
| # Initial state | |
| proc initPlayer(): Player = | |
| Player( | |
| x: newGameCoordinate(ScreenWidth.float / 2 - PlayerWidth.float / 2), | |
| y: newGameCoordinate(ScreenHeight.float - PlayerHeight.float - 20), | |
| color: rl.SKYBLUE | |
| ) | |
| proc initObstacle(): Obstacle = | |
| Obstacle( | |
| x: newGameCoordinate(0), | |
| y: newGameCoordinate(-ObstacleHeight.float), # Allow negative y | |
| active: false, | |
| color: rl.RED, | |
| speed: newPositiveFloat(InitialObstacleSpeed) | |
| ) | |
| proc initGameState(): GameState = | |
| var obstacles: array[MaxObstacles, Obstacle] | |
| for i in 0..<MaxObstacles: | |
| obstacles[i] = initObstacle() | |
| GameState( | |
| player: initPlayer(), | |
| obstacles: obstacles, | |
| score: newNonNegativeInt(0), | |
| gameOver: false, | |
| obstacleSpeed: newPositiveFloat(InitialObstacleSpeed) | |
| ) | |
| # Simulated reactive primitives | |
| type | |
| Observable[T] = ref object | |
| subscribers: seq[proc(value: T): Future[void]] | |
| Subscriber[T] = proc(value: T): Future[void] | |
| proc subscribe[T](obs: Observable[T], subscriber: Subscriber[T]) = | |
| obs.subscribers.add(subscriber) | |
| proc next[T](obs: Observable[T], value: T) {.async.} = | |
| for subscriber in obs.subscribers: | |
| await subscriber(value) | |
| proc newObservable[T](): Observable[T] = | |
| Observable[T](subscribers: @[]) | |
| # Game event streams | |
| let | |
| inputEvents = newObservable[GameEvent]() | |
| stateEvents = newObservable[GameState]() | |
| renderEvents = newObservable[GameState]() | |
| # Game logic | |
| proc updatePlayerPosition(state: GameState, event: GameEvent): GameState = | |
| result = state | |
| if not state.gameOver: | |
| case event.kind | |
| of MoveLeft: | |
| let newX = max(0.0, state.player.x.float - PlayerSpeed) | |
| result.player.x = newGameCoordinate(newX) | |
| of MoveRight: | |
| let newX = min(ScreenWidth.float - PlayerWidth.float, state.player.x.float + PlayerSpeed) | |
| result.player.x = newGameCoordinate(newX) | |
| of ResetGame: | |
| result = initGameState() | |
| proc updateObstacles(state: GameState): GameState = | |
| result = state | |
| if not state.gameOver: | |
| # Spawn new obstacles | |
| for i in 0..<MaxObstacles: | |
| if not result.obstacles[i].active: | |
| if rand(100) < 2: | |
| result.obstacles[i].active = true | |
| result.obstacles[i].x = newGameCoordinate(rand(ScreenWidth - ObstacleWidth).float) | |
| result.obstacles[i].y = newGameCoordinate(-ObstacleHeight.float) | |
| result.obstacles[i].speed = state.obstacleSpeed | |
| break | |
| # Update obstacle positions and check collisions | |
| for i in 0..<MaxObstacles: | |
| if result.obstacles[i].active: | |
| result.obstacles[i].y = newGameCoordinate(result.obstacles[i].y.float + result.obstacles[i].speed.float) | |
| # Collision detection | |
| if rl.checkCollisionRecs( | |
| rl.Rectangle(x: state.player.x.float, y: state.player.y.float, width: PlayerWidth.float, height: PlayerHeight.float), | |
| rl.Rectangle(x: result.obstacles[i].x.float, y: result.obstacles[i].y.float, width: ObstacleWidth.float, height: ObstacleHeight.float) | |
| ): | |
| result.gameOver = true | |
| # Check if obstacle passed bottom | |
| if result.obstacles[i].y.float > ScreenHeight.float: | |
| result.obstacles[i].active = false | |
| result.score = newNonNegativeInt(result.score.int + 1) | |
| if result.score.int mod 5 == 0: | |
| result.obstacleSpeed = newPositiveFloat(result.obstacleSpeed.float + 0.5) | |
| proc render(state: GameState) {.async.} = | |
| rl.beginDrawing() | |
| rl.clearBackground(rl.RAYWHITE) | |
| # Draw road | |
| rl.drawRectangle(0, 0, ScreenWidth, ScreenHeight, rl.DARKGRAY) | |
| # Draw road markings | |
| for i in countup(0, ScreenHeight, 40): | |
| rl.drawRectangle( | |
| rl.Rectangle(x: (ScreenWidth div 2 - 5).float, y: i.float, width: 10.0, height: 20.0), | |
| rl.YELLOW | |
| ) | |
| # Draw player | |
| rl.drawRectangleRounded( | |
| rl.Rectangle(x: state.player.x.float, y: state.player.y.float, width: PlayerWidth.float, height: PlayerHeight.float), | |
| 0.2, 5, state.player.color | |
| ) | |
| # Draw obstacles | |
| for obstacle in state.obstacles: | |
| if obstacle.active: | |
| rl.drawRectangleRounded( | |
| rl.Rectangle(x: obstacle.x.float, y: obstacle.y.float, width: ObstacleWidth.float, height: ObstacleHeight.float), | |
| 0.2, 5, obstacle.color | |
| ) | |
| # Draw score | |
| rl.drawText("Score: " & $state.score.int, 10, 10, 20, rl.WHITE) | |
| if state.gameOver: | |
| rl.drawText("GAME OVER", ScreenWidth div 2 - rl.measureText("GAME OVER", 40) div 2, ScreenHeight div 2 - 50, 40, rl.RED) | |
| rl.drawText("Press SPACE to restart", ScreenWidth div 2 - rl.measureText("Press SPACE to restart", 20) div 2, ScreenHeight div 2 + 20, 20, rl.WHITE) | |
| rl.endDrawing() | |
| proc main() {.async.} = | |
| # Initialize window | |
| rl.initWindow(ScreenWidth, ScreenHeight, "Car Escape Game - Reactive") | |
| rl.setTargetFPS(FPS) | |
| # Initialize game state | |
| var currentState = initGameState() | |
| # Subscribe to state updates | |
| stateEvents.subscribe(proc(state: GameState): Future[void] = | |
| currentState = state | |
| renderEvents.next(state) | |
| ) | |
| # Subscribe to rendering | |
| renderEvents.subscribe(render) | |
| # Game loop | |
| while not rl.windowShouldClose(): | |
| # Handle input events | |
| if rl.isKeyDown(rl.KeyboardKey.LEFT): | |
| await inputEvents.next(GameEvent(kind: MoveLeft)) | |
| if rl.isKeyDown(rl.KeyboardKey.RIGHT): | |
| await inputEvents.next(GameEvent(kind: MoveRight)) | |
| if rl.isKeyPressed(rl.KeyboardKey.SPACE) and currentState.gameOver: | |
| await inputEvents.next(GameEvent(kind: ResetGame)) | |
| # Update game state reactively | |
| var newState = currentState | |
| for event in @[GameEvent(kind: MoveLeft), GameEvent(kind: MoveRight), GameEvent(kind: ResetGame)]: | |
| if rl.isKeyDown(rl.KeyboardKey.LEFT) and event.kind == MoveLeft or | |
| rl.isKeyDown(rl.KeyboardKey.RIGHT) and event.kind == MoveRight or | |
| rl.isKeyPressed(rl.KeyboardKey.SPACE) and currentState.gameOver and event.kind == ResetGame: | |
| newState = updatePlayerPosition(newState, event) | |
| newState = updateObstacles(newState) | |
| await stateEvents.next(newState) | |
| await sleepAsync(1000 div FPS) | |
| rl.closeWindow() | |
| # Run the game | |
| randomize() | |
| waitFor main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment