Skip to content

Instantly share code, notes, and snippets.

@naranyala
Created April 11, 2025 10:12
Show Gist options
  • Save naranyala/bd0b84d44ff1d25b16f45fdb13372ef7 to your computer and use it in GitHub Desktop.
Save naranyala/bd0b84d44ff1d25b16f45fdb13372ef7 to your computer and use it in GitHub Desktop.
snake game in v (vlang)
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