Skip to content

Instantly share code, notes, and snippets.

@naranyala
Last active July 28, 2025 16:24
Show Gist options
  • Select an option

  • Save naranyala/5d50e57b64923e4eaa6834edd81a4fc5 to your computer and use it in GitHub Desktop.

Select an option

Save naranyala/5d50e57b64923e4eaa6834edd81a4fc5 to your computer and use it in GitHub Desktop.
2D car obstacles game using nim programming and raylib (reactive style)
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