ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
6.7 KiB
Text
165 lines
6.7 KiB
Text
# ============================================================================
|
|
# 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 ----------------------------------------
|
|
state SnakeState {
|
|
direction: int = DIR_RIGHT
|
|
length: int = 3
|
|
tick: int = 0
|
|
food_x: int = 14
|
|
food_y: int = 7
|
|
mode: int = MODE_PLAY
|
|
score: int = 0
|
|
ate: int = 0 # did the head reach the food this step?
|
|
tail_x: int = 0 # where the tail sat before it moved (grow here)
|
|
tail_y: int = 0
|
|
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(snake_st: mut SnakeState, 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 } }
|
|
snake_st.direction = DIR_RIGHT
|
|
snake_st.length = 3
|
|
snake_st.tick = 0
|
|
snake_st.mode = MODE_PLAY
|
|
snake_st.score = 0
|
|
snake_st.food_x = 14
|
|
snake_st.food_y = 7
|
|
return 0
|
|
}
|
|
|
|
handler Boot(snake_st: mut SnakeState) phase Start { reset(snake_st, 0) }
|
|
|
|
handler Input(snake_st: mut SnakeState) phase Input {
|
|
let k = Input.key()
|
|
if snake_st.mode == MODE_PLAY {
|
|
if k == 'w' { if snake_st.direction != DIR_DOWN { snake_st.direction = DIR_UP } }
|
|
if k == 's' { if snake_st.direction != DIR_UP { snake_st.direction = DIR_DOWN } }
|
|
if k == 'a' { if snake_st.direction != DIR_RIGHT { snake_st.direction = DIR_LEFT } }
|
|
if k == 'd' { if snake_st.direction != DIR_LEFT { snake_st.direction = DIR_RIGHT } }
|
|
}
|
|
if snake_st.mode == MODE_OVER {
|
|
if k == 0 { snake_st.acked = 1 }
|
|
if k != 0 { if snake_st.acked == 1 { reset(snake_st, 0) } }
|
|
}
|
|
}
|
|
|
|
handler Move(snake_st: mut SnakeState) phase Update {
|
|
if snake_st.mode == MODE_PLAY {
|
|
snake_st.tick += 1
|
|
if snake_st.tick >= 6 {
|
|
snake_st.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 == snake_st.length - 1 { snake_st.tail_x = pt.x; snake_st.tail_y = pt.y }
|
|
}
|
|
for step in 0 .. snake_st.length {
|
|
let i = snake_st.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
|
|
snake_st.ate = 0
|
|
for (ph, sh) in query [Pos, Seg] {
|
|
if sh.order == 0 {
|
|
if snake_st.direction == DIR_UP { ph.y -= 1 }
|
|
if snake_st.direction == DIR_DOWN { ph.y += 1 }
|
|
if snake_st.direction == DIR_LEFT { ph.x -= 1 }
|
|
if snake_st.direction == DIR_RIGHT { ph.x += 1 }
|
|
if ph.x < 0 { snake_st.mode = MODE_OVER }
|
|
if ph.x >= GRID_W { snake_st.mode = MODE_OVER }
|
|
if ph.y < 0 { snake_st.mode = MODE_OVER }
|
|
if ph.y >= GRID_H { snake_st.mode = MODE_OVER }
|
|
if ph.x == snake_st.food_x { if ph.y == snake_st.food_y { snake_st.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 { snake_st.mode = MODE_OVER } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if snake_st.ate == 1 {
|
|
snake_st.score += 1
|
|
spawn Body { Seg { order: snake_st.length }; Pos { x: snake_st.tail_x, y: snake_st.tail_y } }
|
|
snake_st.length += 1
|
|
snake_st.food_x = Random.range(low: 0, high: GRID_W - 1)
|
|
snake_st.food_y = Random.range(low: 0, high: GRID_H - 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
handler Draw(snake_st: SnakeState) 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: snake_st.food_x * TILE + 3, y: snake_st.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: snake_st.score, color: SHADE_SCORE, scale: 1)
|
|
if snake_st.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()
|
|
}
|
|
}
|