v / examples / snek / snek.v
183 lines · 159 sloc · 4.08 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1import gg
2import os
3import rand
4import time
5import math.vec { Vec2 }
6
7// constants
8const font = $embed_file('../assets/fonts/RobotoMono-Regular.ttf')
9const top_height = 100
10const canvas_size = 700
11const game_size = 17
12const tile_size = canvas_size / game_size
13const tick_rate_ms = 100
14const high_score_file_path = os.join_path(os.cache_dir(), 'v', 'examples', 'snek')
15
16// types
17type HighScore = int
18type Vec = Vec2[int]
19
20fn (mut h HighScore) save() {
21 os.mkdir_all(os.dir(high_score_file_path)) or { return }
22 os.write_file(high_score_file_path, (*h).str()) or { return }
23}
24
25fn (mut h HighScore) load() {
26 h = (os.read_file(high_score_file_path) or { '' }).int()
27}
28
29struct App {
30mut:
31 gg &gg.Context = unsafe { nil }
32 score int
33 best HighScore
34 snake []Vec
35 dir Vec
36 dir_queue []Vec
37 food Vec
38 last_tick i64
39}
40
41// utility
42fn (mut app App) reset_game() {
43 app.score = 0
44 app.snake = [Vec{3, 8}, Vec{2, 8}, Vec{1, 8}, Vec{0, 8}]
45 app.dir = Vec{1, 0}
46 app.dir_queue = []
47 app.food = Vec{10, 8}
48 app.last_tick = time.ticks()
49}
50
51fn (mut app App) move_food() {
52 for {
53 x := rand.intn(game_size) or { 0 }
54 y := rand.intn(game_size) or { 0 }
55 app.food = Vec{x, y}
56 if app.food !in app.snake {
57 return
58 }
59 }
60}
61
62fn on_frame(mut app App) {
63 // check if snake bit itself
64 if app.snake[0] in app.snake[1..] {
65 app.reset_game()
66 }
67
68 // check if snake hit a wall
69 if app.snake[0].x < 0 || app.snake[0].x >= game_size || app.snake[0].y < 0
70 || app.snake[0].y >= game_size {
71 app.reset_game()
72 }
73 progress := f32_min(1, f32(time.ticks() - app.last_tick) / f32(tick_rate_ms))
74
75 // draw everything:
76 app.gg.begin()
77 // draw food
78 app.gg.draw_rect_filled(tile_size * app.food.x, tile_size * app.food.y + top_height, tile_size,
79 tile_size, gg.red)
80
81 // draw snake
82 for pos in app.snake[..app.snake.len - 1] {
83 app.gg.draw_rect_filled(tile_size * pos.x, tile_size * pos.y + top_height, tile_size,
84 tile_size, gg.blue)
85 }
86
87 // draw partial head
88 head := app.snake[0]
89 app.gg.draw_rect_filled(tile_size * (head.x + app.dir.x * progress),
90
91 tile_size * (head.y + app.dir.y * progress) + top_height, tile_size, tile_size, gg.blue)
92
93 // draw partial tail
94 tail := app.snake.last()
95 tail_dir := app.snake[app.snake.len - 2] - tail
96 app.gg.draw_rect_filled(tile_size * (tail.x + tail_dir.x * progress),
97
98 tile_size * (tail.y + tail_dir.y * progress) + top_height, tile_size, tile_size, gg.blue)
99
100 // draw score bar
101 app.gg.draw_rect_filled(0, 0, canvas_size, top_height, gg.black)
102 app.gg.draw_text(150, top_height / 2, 'Score: ${app.score}', gg.TextCfg{
103 color: gg.white
104 align: .center
105 vertical_align: .middle
106 size: 65
107 })
108 app.gg.draw_text(canvas_size - 150, top_height / 2, 'Best: ${app.best}', gg.TextCfg{
109 color: gg.white
110 align: .center
111 vertical_align: .middle
112 size: 65
113 })
114
115 if progress == 1 {
116 // "snake" along
117 mut prev := app.snake[0]
118 app.snake[0] = app.snake[0] + app.dir
119 for i in 1 .. app.snake.len {
120 app.snake[i], prev = prev, app.snake[i]
121 }
122
123 // add tail segment if food has been eaten
124 if app.snake[0] == app.food {
125 app.score++
126 if app.score > app.best {
127 app.best = app.score
128 app.best.save()
129 }
130 app.snake << app.snake.last() + app.snake.last() - app.snake[app.snake.len - 2]
131 app.move_food()
132 }
133
134 if app.dir_queue.len > 0 {
135 dir := app.dir_queue.pop()
136 if dir.x != -app.dir.x || dir.y != -app.dir.y {
137 app.dir = dir
138 }
139 }
140
141 app.last_tick = time.ticks()
142 }
143
144 app.gg.end()
145}
146
147// events
148fn on_keydown(key gg.KeyCode, _mod gg.Modifier, mut app App) {
149 app.dir_queue << match key {
150 .w, .up {
151 Vec{0, -1}
152 }
153 .s, .down {
154 Vec{0, 1}
155 }
156 .a, .left {
157 Vec{-1, 0}
158 }
159 .d, .right {
160 Vec{1, 0}
161 }
162 else {
163 return
164 }
165 }
166}
167
168mut app := App{}
169app.reset_game()
170app.best.load()
171
172mut font_copy := font
173app.gg = gg.new_context(
174 bg_color: gg.white
175 frame_fn: on_frame
176 keydown_fn: on_keydown
177 user_data: &app
178 width: canvas_size
179 height: top_height + canvas_size
180 window_title: 'snek'
181 font_bytes_normal: unsafe { font_copy.data().vbytes(font_copy.len) }
182)
183app.gg.run()
184