petal

Specimen · snake.ptl

Snake

A complete, playable game in one Petal file. Arrow keys to steer.

gameinteractive
snake.ptl
compiling petal…

this sketch is interactive — click the canvas and use the mouse / arrow keys to play.

snake.ptl
1// Snake Game
2// Arrow keys to move. Eat food to grow. Don't hit walls or yourself.
3 
4let grid_size = 20
5let cols = screen_width() / grid_size
6let rows = screen_height() / grid_size
7 
8// Game state
9state dir_x = 1
10state dir_y = 0
11state next_dir_x = 1
12state next_dir_y = 0
13state snake = [{x: 10, y: 10}, {x: 9, y: 10}, {x: 8, y: 10}]
14state food_x = 15
15state food_y = 10
16state score = 0
17state game_over = false
18state move_timer = 0.0
19state frame = 0
20 
21let move_interval = 0.1
22 
23let _draw_line = draw_line
24let _draw_text = draw_text
25fn draw_line(x1, y1, x2, y2, r, g, b)
26 _draw_line(x1, y1, x2, y2, r, g, b)
27end
28fn draw_line(a, b, color)
29 _draw_line(a.x, a.y, b.x, b.y, color.r, color.g, color.b)
30end
31fn draw_text(text, x, y, size, r, g, b)
32 _draw_text(text, x, y, size, r, g, b)
33end
34fn draw_text(text, pos, size, color)
35 _draw_text(text, pos.x, pos.y, size, color.r, color.g, color.b)
36end
37 
38let score_color = {r: 200, g: 255, b: 220}
39 
40// Input handling — buffer direction so we don't reverse into ourselves
41if key_pressed("up") && dir_y != 1 then
42 next_dir_x = 0
43 next_dir_y = -1
44end
45if key_pressed("down") && dir_y != -1 then
46 next_dir_x = 0
47 next_dir_y = 1
48end
49if key_pressed("left") && dir_x != 1 then
50 next_dir_x = -1
51 next_dir_y = 0
52end
53if key_pressed("right") && dir_x != -1 then
54 next_dir_x = 1
55 next_dir_y = 0
56end
57 
58// Restart on space after game over
59if game_over && key_pressed("space") then
60 game_over = false
61 snake = [{x: 10, y: 10}, {x: 9, y: 10}, {x: 8, y: 10}]
62 dir_x = 1
63 dir_y = 0
64 next_dir_x = 1
65 next_dir_y = 0
66 score = 0
67 food_x = int(random(2.0, float(cols) - 2.0))
68 food_y = int(random(2.0, float(rows) - 2.0))
69end
70 
71// Update logic
72if !game_over then
73 move_timer += dt()
74 
75 if move_timer >= move_interval then
76 move_timer = move_timer - move_interval
77 
78 // Apply buffered direction
79 dir_x = next_dir_x
80 dir_y = next_dir_y
81 
82 // Calculate new head position
83 let head = snake[0]
84 let new_x = head.x + dir_x
85 let new_y = head.y + dir_y
86 
87 // Check wall collision (inside the border)
88 if new_x < 1 || new_x >= cols - 1 || new_y < 1 || new_y >= rows - 1 then
89 game_over = true
90 end
91 
92 // Check self collision
93 if !game_over then
94 for i in range(0, len(snake)) do
95 let seg = snake[i]
96 if seg.x == new_x && seg.y == new_y then
97 game_over = true
98 end
99 end
100 end
101 
102 if !game_over then
103 // Build new snake with new head
104 let new_head = {x: new_x, y: new_y}
105 let new_snake = [new_head]
106 let ate_food = new_x == food_x && new_y == food_y
107 
108 // Keep length: if ate food, keep all segments; otherwise drop last
109 let keep_count = len(snake)
110 if !ate_food then
111 keep_count = len(snake) - 1
112 end
113 for i in range(0, keep_count) do
114 push(new_snake, snake[i])
115 end
116 snake = new_snake
117 
118 if ate_food then
119 score += 1
120 // Spawn new food not on snake, inside border
121 food_x = int(random(2.0, float(cols) - 2.0))
122 food_y = int(random(2.0, float(rows) - 2.0))
123 end
124 end
125 end
126end
127 
128frame += 1
129 
130// ============================================================
131// Drawing
132// ============================================================
133 
134// Dark background
135clear(15, 12, 30)
136 
137// Subtle grid pattern
138for gx in range(1, cols - 1) do
139 for gy in range(1, rows - 1) do
140 if (gx + gy) % 2 == 0 then
141 draw_rect(gx * grid_size, gy * grid_size, grid_size, grid_size, 18, 15, 35)
142 end
143 end
144end
145 
146// Border walls - thick neon border
147let border_r = 40
148let border_g = 30
149let border_b = 80
150// Top wall
151for bx in range(0, cols) do
152 draw_rect(bx * grid_size, 0, grid_size, grid_size, border_r, border_g, border_b)
153end
154// Bottom wall
155for bx in range(0, cols) do
156 draw_rect(bx * grid_size, (rows - 1) * grid_size, grid_size, grid_size, border_r, border_g, border_b)
157end
158// Left wall
159for by in range(0, rows) do
160 draw_rect(0, by * grid_size, grid_size, grid_size, border_r, border_g, border_b)
161end
162// Right wall
163for by in range(0, rows) do
164 draw_rect((cols - 1) * grid_size, by * grid_size, grid_size, grid_size, border_r, border_g, border_b)
165end
166// Inner border glow line
167draw_rect_outline(grid_size + 1, grid_size + 1, (cols - 2) * grid_size - 2, (rows - 2) * grid_size - 2, 80, 60, 160)
168// Outer border highlight
169draw_rect_outline(0, 0, cols * grid_size, rows * grid_size, 60, 50, 120)
170 
171// ---- Draw food with pulsing glow effect ----
172let pulse = frame % 40
173let pulse_size = 0
174if pulse < 20 then
175 pulse_size = pulse / 4
176else
177 pulse_size = (40 - pulse) / 4
178end
179 
180// Outer glow
181let fx = food_x * grid_size + grid_size / 2
182let fy = food_y * grid_size + grid_size / 2
183draw_circle(fx, fy, 12 + pulse_size, 80, 10, 10)
184draw_circle(fx, fy, 10 + pulse_size, 140, 20, 20)
185// Main food body
186draw_circle(fx, fy, 8, 255, 60, 80)
187// Highlight
188draw_circle(fx - 2, fy - 2, 3, 255, 160, 160)
189 
190// ---- Draw snake body with gradient ----
191let snake_len = len(snake)
192for i in range(0, snake_len) do
193 let seg = snake[i]
194 let sx = seg.x * grid_size
195 let sy = seg.y * grid_size
196 let pad = 1
197 
198 // Gradient: head is bright cyan-green, fades to deep teal at tail
199 let t = 0.0
200 if snake_len > 1 then
201 t = float(i) / float(snake_len - 1)
202 end
203 // Color interpolation: head (100, 255, 200) -> tail (40, 160, 110)
204 let sr = int(100.0 - 60.0 * t)
205 let sg = int(255.0 - 95.0 * t)
206 let sb = int(200.0 - 90.0 * t)
207 
208 if i == 0 then
209 // Head: draw slightly larger with brighter color
210 draw_rect(sx + pad, sy + pad, grid_size - pad * 2, grid_size - pad * 2, 100, 255, 200)
211 // Inner highlight on head
212 draw_rect(sx + 4, sy + 4, grid_size - 8, grid_size - 8, 160, 255, 220)
213 
214 // Eyes
215 let eye_size = 3
216 if dir_x == 1 then
217 // Facing right
218 draw_circle(sx + 14, sy + 6, eye_size, 255, 255, 255)
219 draw_circle(sx + 14, sy + 14, eye_size, 255, 255, 255)
220 draw_circle(sx + 15, sy + 6, 1, 20, 20, 40)
221 draw_circle(sx + 15, sy + 14, 1, 20, 20, 40)
222 end
223 if dir_x == -1 then
224 // Facing left
225 draw_circle(sx + 6, sy + 6, eye_size, 255, 255, 255)
226 draw_circle(sx + 6, sy + 14, eye_size, 255, 255, 255)
227 draw_circle(sx + 5, sy + 6, 1, 20, 20, 40)
228 draw_circle(sx + 5, sy + 14, 1, 20, 20, 40)
229 end
230 if dir_y == -1 then
231 // Facing up
232 draw_circle(sx + 6, sy + 6, eye_size, 255, 255, 255)
233 draw_circle(sx + 14, sy + 6, eye_size, 255, 255, 255)
234 draw_circle(sx + 6, sy + 5, 1, 20, 20, 40)
235 draw_circle(sx + 14, sy + 5, 1, 20, 20, 40)
236 end
237 if dir_y == 1 then
238 // Facing down
239 draw_circle(sx + 6, sy + 14, eye_size, 255, 255, 255)
240 draw_circle(sx + 14, sy + 14, eye_size, 255, 255, 255)
241 draw_circle(sx + 6, sy + 15, 1, 20, 20, 40)
242 draw_circle(sx + 14, sy + 15, 1, 20, 20, 40)
243 end
244 else
245 // Body segment with gradient and padding for "segmented" look
246 draw_rect(sx + pad, sy + pad, grid_size - pad * 2, grid_size - pad * 2, sr, sg, sb)
247 // Subtle inner highlight
248 draw_rect(sx + 4, sy + 4, grid_size - 8, grid_size - 8, min(sr + 30, 255), min(sg + 20, 255), min(sb + 20, 255))
249 end
250end
251 
252// ---- Score display with background panel ----
253draw_rect(10, 8, 160, 32, 20, 15, 40)
254draw_rect_outline(10, 8, 160, 32, 80, 60, 160)
255draw_text("Score: " ++ str(score), {x: 18, y: 12}, 24, score_color)
256 
257// ---- Game over screen ----
258if game_over then
259 // Dim overlay (draw a large semi-transparent-ish dark rect)
260 draw_rect(grid_size, grid_size, (cols - 2) * grid_size, (rows - 2) * grid_size, 10, 8, 20)
261 
262 // Game over panel
263 let panel_x = screen_width() / 2 - 160
264 let panel_y = screen_height() / 2 - 60
265 draw_rect(panel_x, panel_y, 320, 120, 30, 20, 50)
266 draw_rect_outline(panel_x, panel_y, 320, 120, 120, 80, 200)
267 
268 draw_text("GAME OVER", {x: screen_width() / 2 - 100, y: screen_height() / 2 - 40}, 40, {r: 255, g: 80, b: 100})
269 draw_text("Score: " ++ str(score), {x: screen_width() / 2 - 50, y: screen_height() / 2}, 24, score_color)
270 
271 // Blinking restart prompt
272 if frame % 60 < 40 then
273 draw_text("Press SPACE to restart", {x: screen_width() / 2 - 130, y: screen_height() / 2 + 35}, 22, {r: 180, g: 160, b: 220})
274 end
275end

The whole program is the source above — there is no hidden runtime. Petal re-runs it every frame: state values persist between frames, draw calls paint to the canvas, and edits take effect live. Open it in the playground to poke at the numbers and watch the picture change.