Created
April 11, 2025 10:12
-
-
Save naranyala/bd0b84d44ff1d25b16f45fdb13372ef7 to your computer and use it in GitHub Desktop.
snake game in v (vlang)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gg | |
import gx | |
import math | |
import rand | |
import sokol.audio | |
import os.asset | |
import sokol.sgl | |
const designed_width = 600 | |
const designed_height = 800 | |
const grid_size = 20 // Size of each grid cell | |
const snake_initial_length = 3 | |
const highlight_color = gx.rgba(255, 255, 255, 65) | |
const shade_color = gx.rgba(0, 0, 0, 65) | |
enum Direction { | |
up | |
right | |
down | |
left | |
} | |
struct SnakeSegment { | |
mut: // Make fields mutable | |
x int | |
y int | |
} | |
struct Food { | |
mut: | |
x int | |
y int | |
color gg.Color = gx.red | |
} | |
struct Game { | |
mut: | |
width int = designed_width | |
height int = designed_height | |
snake []SnakeSegment | |
direction Direction = .right | |
next_direction Direction = .right | |
food Food | |
score int | |
game_over bool | |
timer f32 | |
move_delay f32 = 0.12 // Snake movement speed (seconds) | |
sound SoundManager | |
ctx &gg.Context = unsafe { nil } | |
} | |
fn Game.new() &Game { | |
mut g := &Game{} | |
g.init_game() | |
return g | |
} | |
enum SoundKind { | |
eat | |
crash | |
move | |
} | |
struct SoundManager { | |
mut: | |
sounds [3][]f32 // Array of sounds | |
initialised bool | |
} | |
fn (mut sm SoundManager) init() { | |
all_kinds := [SoundKind.eat, .crash, .move]! | |
sample_rate := f32(audio.sample_rate()) | |
duration, volume := 0.09, f32(.25) | |
nframes := int(sample_rate * duration) | |
for i in 0 .. nframes { | |
t := f32(i) / sample_rate | |
sm.sounds[int(SoundKind.eat)] << volume * math.sinf(t * 432.0 * 2 * math.pi) | |
sm.sounds[int(SoundKind.crash)] << volume * math.sinf(t * 123.0 * 2 * math.pi) | |
sm.sounds[int(SoundKind.move)] << volume * math.sinf(t * 174.0 * 2 * math.pi) | |
} | |
border_samples := 2000 | |
for k, s := f32(0), 0; s <= border_samples; k, s = k + 1.0 / f32(border_samples), s + 1 { | |
rk := f32(1) - k | |
rs := nframes - border_samples - 1 + s | |
for kind in all_kinds { | |
sm.sounds[int(kind)][s] *= k | |
sm.sounds[int(kind)][rs] *= rk | |
} | |
} | |
sm.initialised = true | |
} | |
fn (mut g Game) play(k SoundKind) { | |
if g.sound.initialised { | |
s := g.sound.sounds[int(k)] | |
audio.push(s.data, s.len) | |
} | |
} | |
fn (mut g Game) init_game() { | |
// Initialize snake in the middle of the screen | |
g.snake = []SnakeSegment{} | |
start_x := (g.width / grid_size) / 2 | |
start_y := (g.height / grid_size) / 2 | |
for i in 0 .. snake_initial_length { | |
g.snake << SnakeSegment{ | |
x: start_x - i | |
y: start_y | |
} | |
} | |
g.direction = .right | |
g.next_direction = .right | |
g.score = 0 | |
g.game_over = false | |
g.spawn_food() | |
} | |
fn (mut g Game) spawn_food() { | |
max_x := g.width / grid_size - 1 | |
max_y := g.height / grid_size - 1 | |
for { | |
// Generate random position for food | |
g.food.x = rand.intn(max_x) or { 10 } | |
g.food.y = rand.intn(max_y) or { 10 } | |
// Check if food overlaps with snake | |
mut overlap := false | |
for segment in g.snake { | |
if segment.x == g.food.x && segment.y == g.food.y { | |
overlap = true | |
break | |
} | |
} | |
if !overlap { | |
break | |
} | |
} | |
} | |
fn (mut g Game) update(dt f32) { | |
if g.game_over { | |
return | |
} | |
g.timer += dt | |
if g.timer < g.move_delay { | |
return | |
} | |
g.timer = 0 | |
g.direction = g.next_direction | |
// Get current head position | |
head := g.snake[0] | |
mut new_head := SnakeSegment{ | |
x: head.x | |
y: head.y | |
} | |
// Calculate new head position based on direction | |
match g.direction { | |
.up { new_head.y-- } | |
.right { new_head.x++ } | |
.down { new_head.y++ } | |
.left { new_head.x-- } | |
} | |
// Check wall collision | |
if new_head.x < 0 || new_head.x >= g.width / grid_size || | |
new_head.y < 0 || new_head.y >= g.height / grid_size { | |
g.game_over = true | |
g.play(.crash) | |
return | |
} | |
// Check self collision | |
for i := 0; i < g.snake.len; i++ { | |
segment := g.snake[i] | |
if new_head.x == segment.x && new_head.y == segment.y { | |
g.game_over = true | |
g.play(.crash) | |
return | |
} | |
} | |
// Check food collision | |
if new_head.x == g.food.x && new_head.y == g.food.y { | |
g.play(.eat) | |
g.score++ | |
g.spawn_food() | |
// Don't remove tail to grow the snake | |
} else { | |
// Remove tail for normal movement | |
g.snake = g.snake[0..g.snake.len-1] | |
g.play(.move) | |
} | |
// Add new head - fixed to use array prepend | |
mut new_snake := []SnakeSegment{cap: g.snake.len + 1} | |
new_snake << new_head | |
new_snake << g.snake | |
g.snake = new_snake | |
} | |
fn (g &Game) draw() { | |
ws := gg.window_size() | |
g.ctx.begin() | |
sgl.push_matrix() | |
sgl.scale(f32(ws.width) / f32(designed_width), f32(ws.height) / f32(designed_height), 0) | |
// Draw background | |
g.ctx.draw_rect_filled(0, 0, g.width, g.height, gx.rgb(0, 0, 0)) | |
// Draw grid (optional) | |
for x := 0; x < g.width; x += grid_size { | |
for y := 0; y < g.height; y += grid_size { | |
g.ctx.draw_rect_empty(x, y, grid_size, grid_size, gx.rgba(50, 50, 50, 100)) | |
} | |
} | |
// Draw food | |
food_x := g.food.x * grid_size | |
food_y := g.food.y * grid_size | |
g.ctx.draw_rect_filled(food_x, food_y, grid_size, grid_size, g.food.color) | |
g.ctx.draw_rect_filled(food_x, food_y, grid_size, grid_size / 5, highlight_color) | |
// Draw snake | |
for i, segment in g.snake { | |
snake_x := segment.x * grid_size | |
snake_y := segment.y * grid_size | |
color := if i == 0 { gx.rgb(0, 255, 0) } else { gx.rgb(0, 200, 0) } | |
g.ctx.draw_rect_filled(snake_x, snake_y, grid_size, grid_size, color) | |
// Add highlight to the top and left edges | |
g.ctx.draw_rect_filled(snake_x, snake_y, grid_size, grid_size / 5, highlight_color) | |
g.ctx.draw_rect_filled(snake_x, snake_y, grid_size / 5, grid_size, highlight_color) | |
// Add shade to the bottom and right edges | |
g.ctx.draw_rect_filled(snake_x, snake_y + grid_size - grid_size / 5, grid_size, grid_size / 5, shade_color) | |
g.ctx.draw_rect_filled(snake_x + grid_size - grid_size / 5, snake_y, grid_size / 5, grid_size, shade_color) | |
} | |
// Draw score | |
score_text := 'Score: ${g.score}' | |
g.ctx.draw_text(10, 10, score_text, size: 24, color: gx.white) | |
// Draw game over message | |
if g.game_over { | |
g.ctx.draw_text(g.width / 2 - 100, g.height / 2 - 20, 'GAME OVER', size: 36, color: gx.red) | |
g.ctx.draw_text(g.width / 2 - 150, g.height / 2 + 20, 'Press R to restart', size: 24, color: gx.white) | |
} | |
sgl.pop_matrix() | |
g.ctx.end() | |
} | |
fn (mut g Game) handle_key_press(key gg.KeyCode) { | |
match key { | |
.up, .w { | |
if g.direction != .down { | |
g.next_direction = .up | |
} | |
} | |
.right, .d { | |
if g.direction != .left { | |
g.next_direction = .right | |
} | |
} | |
.down, .s { | |
if g.direction != .up { | |
g.next_direction = .down | |
} | |
} | |
.left, .a { | |
if g.direction != .right { | |
g.next_direction = .left | |
} | |
} | |
.r { | |
g.init_game() | |
} | |
.escape { | |
exit(0) | |
} | |
else {} | |
} | |
} | |
fn (mut g Game) touch_event(touch_point gg.TouchPoint) { | |
ws := gg.window_size() | |
tx := touch_point.pos_x * f32(designed_width) / f32(ws.width) | |
ty := touch_point.pos_y * f32(designed_height) / f32(ws.height) | |
center_x := f32(g.width) / 2 | |
center_y := f32(g.height) / 2 | |
dx := tx - center_x | |
dy := ty - center_y | |
// Determine direction based on which component (x or y) has greater magnitude | |
if math.abs(dx) > math.abs(dy) { | |
if dx > 0 && g.direction != .left { | |
g.next_direction = .right | |
} else if dx < 0 && g.direction != .right { | |
g.next_direction = .left | |
} | |
} else { | |
if dy > 0 && g.direction != .up { | |
g.next_direction = .down | |
} else if dy < 0 && g.direction != .down { | |
g.next_direction = .up | |
} | |
} | |
} | |
fn (mut g Game) handle_event() { | |
$if wasm32_emscripten { | |
audio.setup(buffer_frames: 1024) | |
g.sound.init() | |
} | |
} | |
fn main() { | |
mut g := Game.new() | |
mut fpath := asset.get_path('../assets', 'fonts/RobotoMono-Regular.ttf') | |
$if !wasm32_emscripten { | |
audio.setup(buffer_frames: 512) | |
g.sound.init() | |
fpath = '' | |
} | |
g.ctx = gg.new_context( | |
width: g.width | |
height: g.height | |
window_title: 'V Snake' | |
frame_fn: fn (mut g Game) { | |
// Fix type error: Cast f64 to f32 | |
dt := f32(g.ctx.timer.elapsed().seconds()) | |
g.update(dt) | |
g.ctx.timer.restart() | |
g.draw() | |
} | |
keydown_fn: fn (key gg.KeyCode, _ gg.Modifier, mut g Game) { | |
g.handle_event() | |
g.handle_key_press(key) | |
} | |
event_fn: fn (e &gg.Event, mut g Game) { | |
g.handle_event() | |
if e.typ == .touches_began || e.typ == .touches_moved { | |
if e.num_touches > 0 { | |
touch_point := e.touches[0] | |
g.touch_event(touch_point) | |
} | |
} | |
} | |
user_data: g | |
font_path: fpath | |
) | |
g.ctx.run() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment