# ============================================================================ # snake.ludic — a small, complete game in Ludic. No sprites or assets: every # pixel is drawn from primitives. Build: bin/ludic build examples/games/snake.ludic # # It shows the everyday shape of a Ludic game: an ECS for the moving parts (one # entity per snake segment), named program state for the rest, and a Render # handler that paints the frame with the Screen API and named colors. # ============================================================================ program Snake { property Pos { x: int = 0, y: int = 0 } property Seg { order: int = 0 } # 0 = head, 1.. = body toward the tail const GRID_W: int = 20 # playfield, in tiles const GRID_H: int = 15 const TILE: int = 16 # pixels per tile const DIR_UP: int = 0 const DIR_DOWN: int = 1 const DIR_LEFT: int = 2 const DIR_RIGHT: int = 3 const MODE_PLAY: int = 0 const MODE_OVER: int = 1 # ---- the game's whole state, named ---------------------------------------- var direction: int = DIR_RIGHT var length: int = 3 var tick: int = 0 var food_x: int = 14 var food_y: int = 7 var mode: int = MODE_PLAY var score: int = 0 var ate: int = 0 # did the head reach the food this step? var tail_x: int = 0 # where the tail sat before it moved (grow here) var tail_y: int = 0 var acked: int = 0 # game-over key must be released before restart # ---- custom shades this game uses (values the named palette doesn't carry) - const SHADE_BG: int = 0x0d1020 # backdrop behind the board const SHADE_TILE_A: int = 0x141a2e # checkerboard, light square const SHADE_TILE_B: int = 0x10162a # checkerboard, dark square const SHADE_FOOD: int = 0xe0405a const SHADE_BODY: int = 0x40c060 const SHADE_HEAD: int = 0x9dffb0 const SHADE_SCORE: int = 0xffe060 const SHADE_OVER: int = 0xff5555 function reset(unused: int) -> int { for (p, s) in query [Pos, Seg] { despawn self() } spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } } spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } } spawn S2 { Seg { order: 2 }; Pos { x: 8, y: 7 } } direction = DIR_RIGHT length = 3 tick = 0 mode = MODE_PLAY score = 0 food_x = 14 food_y = 7 return 0 } handler Boot phase Start { reset(0) } handler Input phase Input { let k = Input.key() if mode == MODE_PLAY { if k == 'w' { if direction != DIR_DOWN { direction = DIR_UP } } if k == 's' { if direction != DIR_UP { direction = DIR_DOWN } } if k == 'a' { if direction != DIR_RIGHT { direction = DIR_LEFT } } if k == 'd' { if direction != DIR_LEFT { direction = DIR_RIGHT } } } if mode == MODE_OVER { if k == 0 { acked = 1 } if k != 0 { if acked == 1 { reset(0) } } } } handler Move phase Update { if mode == MODE_PLAY { tick += 1 if tick >= 6 { tick = 0 # remember the tail cell, then drag every segment onto the one ahead of it for (pt, stg) in query [Pos, Seg] { if stg.order == length - 1 { tail_x = pt.x; tail_y = pt.y } } for step in 0 .. length { let i = length - 1 - step if i >= 1 { for (pa, sa) in query [Pos, Seg] { if sa.order == i { for (pb, sb) in query [Pos, Seg] { if sb.order == i - 1 { pa.x = pb.x; pa.y = pb.y } } } } } } # move the head, check the walls, check the food ate = 0 for (ph, sh) in query [Pos, Seg] { if sh.order == 0 { if direction == DIR_UP { ph.y -= 1 } if direction == DIR_DOWN { ph.y += 1 } if direction == DIR_LEFT { ph.x -= 1 } if direction == DIR_RIGHT { ph.x += 1 } if ph.x < 0 { mode = MODE_OVER } if ph.x >= GRID_W { mode = MODE_OVER } if ph.y < 0 { mode = MODE_OVER } if ph.y >= GRID_H { mode = MODE_OVER } if ph.x == food_x { if ph.y == food_y { ate = 1 } } } } # self-collision: head onto any body segment for (ph, sh) in query [Pos, Seg] { if sh.order == 0 { for (pb, sb) in query [Pos, Seg] { if sb.order > 0 { if pb.x == ph.x { if pb.y == ph.y { mode = MODE_OVER } } } } } } if ate == 1 { score += 1 spawn Body { Seg { order: length }; Pos { x: tail_x, y: tail_y } } length += 1 food_x = Random.range(low: 0, high: GRID_W - 1) food_y = Random.range(low: 0, high: GRID_H - 1) } } } } handler Draw phase Render { Screen.clear(SHADE_BG) # checkerboard field for gy in 0 .. GRID_H { for gx in 0 .. GRID_W { var tile_color = SHADE_TILE_A if (gx + gy) % 2 == 0 { tile_color = SHADE_TILE_B } Screen.fill_rectangle(x: gx * TILE, y: gy * TILE, width: TILE, height: TILE, color: tile_color) } } # food, inset a few pixels inside its tile Screen.fill_rectangle(x: food_x * TILE + 3, y: food_y * TILE + 3, width: TILE - 6, height: TILE - 6, color: SHADE_FOOD) # every snake segment, straight from the ECS for (p, s) in query [Pos, Seg] { var seg_color = SHADE_BODY if s.order == 0 { seg_color = SHADE_HEAD } # the head is brighter Screen.fill_rectangle(x: p.x * TILE + 1, y: p.y * TILE + 1, width: TILE - 2, height: TILE - 2, color: seg_color) } Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) Screen.draw_number(x: 52, y: 4, value: score, color: SHADE_SCORE, scale: 1) if mode == MODE_OVER { Screen.draw_text(x: 112, y: 104, text: "GAME OVER", color: SHADE_OVER, scale: 2) Screen.draw_text(x: 84, y: 140, text: "PRESS ANY KEY", color: Color.White, scale: 1) } Screen.show() } }