Specimen · snake.ptl
Snake
A complete, playable game in one Petal file. Arrow keys to steer.
compiling petal…
this sketch is interactive — click the canvas and use the mouse / arrow keys to play.
Source
1// Snake Game2// Arrow keys to move. Eat food to grow. Don't hit walls or yourself.3 4let grid_size = 205let cols = screen_width() / grid_size6let rows = screen_height() / grid_size7 8// Game state9state dir_x = 110state dir_y = 011state next_dir_x = 112state next_dir_y = 013state snake = [{x: 10, y: 10}, {x: 9, y: 10}, {x: 8, y: 10}]14state food_x = 1515state food_y = 1016state score = 017state game_over = false18state move_timer = 0.019state frame = 020 21let move_interval = 0.122 23let _draw_line = draw_line24let _draw_text = draw_text25fn draw_line(x1, y1, x2, y2, r, g, b)26 _draw_line(x1, y1, x2, y2, r, g, b)27end28fn draw_line(a, b, color)29 _draw_line(a.x, a.y, b.x, b.y, color.r, color.g, color.b)30end31fn draw_text(text, x, y, size, r, g, b)32 _draw_text(text, x, y, size, r, g, b)33end34fn draw_text(text, pos, size, color)35 _draw_text(text, pos.x, pos.y, size, color.r, color.g, color.b)36end37 38let score_color = {r: 200, g: 255, b: 220}39 40// Input handling — buffer direction so we don't reverse into ourselves41if key_pressed("up") && dir_y != 1 then42 next_dir_x = 043 next_dir_y = -144end45if key_pressed("down") && dir_y != -1 then46 next_dir_x = 047 next_dir_y = 148end49if key_pressed("left") && dir_x != 1 then50 next_dir_x = -151 next_dir_y = 052end53if key_pressed("right") && dir_x != -1 then54 next_dir_x = 155 next_dir_y = 056end57 58// Restart on space after game over59if game_over && key_pressed("space") then60 game_over = false61 snake = [{x: 10, y: 10}, {x: 9, y: 10}, {x: 8, y: 10}]62 dir_x = 163 dir_y = 064 next_dir_x = 165 next_dir_y = 066 score = 067 food_x = int(random(2.0, float(cols) - 2.0))68 food_y = int(random(2.0, float(rows) - 2.0))69end70 71// Update logic72if !game_over then73 move_timer += dt()74 75 if move_timer >= move_interval then76 move_timer = move_timer - move_interval77 78 // Apply buffered direction79 dir_x = next_dir_x80 dir_y = next_dir_y81 82 // Calculate new head position83 let head = snake[0]84 let new_x = head.x + dir_x85 let new_y = head.y + dir_y86 87 // Check wall collision (inside the border)88 if new_x < 1 || new_x >= cols - 1 || new_y < 1 || new_y >= rows - 1 then89 game_over = true90 end91 92 // Check self collision93 if !game_over then94 for i in range(0, len(snake)) do95 let seg = snake[i]96 if seg.x == new_x && seg.y == new_y then97 game_over = true98 end99 end100 end101 102 if !game_over then103 // Build new snake with new head104 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_y107 108 // Keep length: if ate food, keep all segments; otherwise drop last109 let keep_count = len(snake)110 if !ate_food then111 keep_count = len(snake) - 1112 end113 for i in range(0, keep_count) do114 push(new_snake, snake[i])115 end116 snake = new_snake117 118 if ate_food then119 score += 1120 // Spawn new food not on snake, inside border121 food_x = int(random(2.0, float(cols) - 2.0))122 food_y = int(random(2.0, float(rows) - 2.0))123 end124 end125 end126end127 128frame += 1129 130// ============================================================131// Drawing132// ============================================================133 134// Dark background135clear(15, 12, 30)136 137// Subtle grid pattern138for gx in range(1, cols - 1) do139 for gy in range(1, rows - 1) do140 if (gx + gy) % 2 == 0 then141 draw_rect(gx * grid_size, gy * grid_size, grid_size, grid_size, 18, 15, 35)142 end143 end144end145 146// Border walls - thick neon border147let border_r = 40148let border_g = 30149let border_b = 80150// Top wall151for bx in range(0, cols) do152 draw_rect(bx * grid_size, 0, grid_size, grid_size, border_r, border_g, border_b)153end154// Bottom wall155for bx in range(0, cols) do156 draw_rect(bx * grid_size, (rows - 1) * grid_size, grid_size, grid_size, border_r, border_g, border_b)157end158// Left wall159for by in range(0, rows) do160 draw_rect(0, by * grid_size, grid_size, grid_size, border_r, border_g, border_b)161end162// Right wall163for by in range(0, rows) do164 draw_rect((cols - 1) * grid_size, by * grid_size, grid_size, grid_size, border_r, border_g, border_b)165end166// Inner border glow line167draw_rect_outline(grid_size + 1, grid_size + 1, (cols - 2) * grid_size - 2, (rows - 2) * grid_size - 2, 80, 60, 160)168// Outer border highlight169draw_rect_outline(0, 0, cols * grid_size, rows * grid_size, 60, 50, 120)170 171// ---- Draw food with pulsing glow effect ----172let pulse = frame % 40173let pulse_size = 0174if pulse < 20 then175 pulse_size = pulse / 4176else177 pulse_size = (40 - pulse) / 4178end179 180// Outer glow181let fx = food_x * grid_size + grid_size / 2182let fy = food_y * grid_size + grid_size / 2183draw_circle(fx, fy, 12 + pulse_size, 80, 10, 10)184draw_circle(fx, fy, 10 + pulse_size, 140, 20, 20)185// Main food body186draw_circle(fx, fy, 8, 255, 60, 80)187// Highlight188draw_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) do193 let seg = snake[i]194 let sx = seg.x * grid_size195 let sy = seg.y * grid_size196 let pad = 1197 198 // Gradient: head is bright cyan-green, fades to deep teal at tail199 let t = 0.0200 if snake_len > 1 then201 t = float(i) / float(snake_len - 1)202 end203 // 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 then209 // Head: draw slightly larger with brighter color210 draw_rect(sx + pad, sy + pad, grid_size - pad * 2, grid_size - pad * 2, 100, 255, 200)211 // Inner highlight on head212 draw_rect(sx + 4, sy + 4, grid_size - 8, grid_size - 8, 160, 255, 220)213 214 // Eyes215 let eye_size = 3216 if dir_x == 1 then217 // Facing right218 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 end223 if dir_x == -1 then224 // Facing left225 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 end230 if dir_y == -1 then231 // Facing up232 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 end237 if dir_y == 1 then238 // Facing down239 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 end244 else245 // Body segment with gradient and padding for "segmented" look246 draw_rect(sx + pad, sy + pad, grid_size - pad * 2, grid_size - pad * 2, sr, sg, sb)247 // Subtle inner highlight248 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 end250end251 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 then259 // 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 panel263 let panel_x = screen_width() / 2 - 160264 let panel_y = screen_height() / 2 - 60265 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 prompt272 if frame % 60 < 40 then273 draw_text("Press SPACE to restart", {x: screen_width() / 2 - 130, y: screen_height() / 2 + 35}, 22, {r: 180, g: 160, b: 220})274 end275endThe 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.