1 | import os |
2 | import gg |
3 | import gx |
4 | // import sokol.sapp |
5 | import time |
6 | import rand |
7 | |
8 | // constants |
9 | const ( |
10 | top_height = 100 |
11 | canvas_size = 700 |
12 | game_size = 17 |
13 | tile_size = canvas_size / game_size |
14 | tick_rate_ms = 100 |
15 | ) |
16 | |
17 | const high_score_file_path = os.join_path(os.cache_dir(), 'v', 'examples', 'snek') |
18 | |
19 | // types |
20 | struct Pos { |
21 | x int |
22 | y int |
23 | } |
24 | |
25 | fn (a Pos) + (b Pos) Pos { |
26 | return Pos{a.x + b.x, a.y + b.y} |
27 | } |
28 | |
29 | fn (a Pos) - (b Pos) Pos { |
30 | return Pos{a.x - b.x, a.y - b.y} |
31 | } |
32 | |
33 | enum Direction { |
34 | up |
35 | down |
36 | left |
37 | right |
38 | } |
39 | |
40 | type HighScore = int |
41 | |
42 | fn (mut h HighScore) save() { |
43 | os.mkdir_all(os.dir(high_score_file_path)) or { return } |
44 | os.write_file(high_score_file_path, (*h).str()) or { return } |
45 | } |
46 | |
47 | fn (mut h HighScore) load() { |
48 | h = (os.read_file(high_score_file_path) or { '' }).int() |
49 | } |
50 | |
51 | struct App { |
52 | mut: |
53 | gg &gg.Context = unsafe { nil } |
54 | score int |
55 | best HighScore |
56 | snake []Pos |
57 | dir Direction |
58 | last_dir Direction |
59 | food Pos |
60 | start_time i64 |
61 | last_tick i64 |
62 | } |
63 | |
64 | // utility |
65 | fn (mut app App) reset_game() { |
66 | app.score = 0 |
67 | app.snake = [ |
68 | Pos{3, 8}, |
69 | Pos{2, 8}, |
70 | Pos{1, 8}, |
71 | Pos{0, 8}, |
72 | ] |
73 | app.dir = .right |
74 | app.last_dir = app.dir |
75 | app.food = Pos{10, 8} |
76 | app.start_time = time.ticks() |
77 | app.last_tick = time.ticks() |
78 | } |
79 | |
80 | fn (mut app App) move_food() { |
81 | for { |
82 | x := rand.intn(game_size) or { 0 } |
83 | y := rand.intn(game_size) or { 0 } |
84 | app.food = Pos{x, y} |
85 | |
86 | if app.food !in app.snake { |
87 | return |
88 | } |
89 | } |
90 | } |
91 | |
92 | // events |
93 | fn on_keydown(key gg.KeyCode, mod gg.Modifier, mut app App) { |
94 | match key { |
95 | .w, .up { |
96 | if app.last_dir != .down { |
97 | app.dir = .up |
98 | } |
99 | } |
100 | .s, .down { |
101 | if app.last_dir != .up { |
102 | app.dir = .down |
103 | } |
104 | } |
105 | .a, .left { |
106 | if app.last_dir != .right { |
107 | app.dir = .left |
108 | } |
109 | } |
110 | .d, .right { |
111 | if app.last_dir != .left { |
112 | app.dir = .right |
113 | } |
114 | } |
115 | else {} |
116 | } |
117 | } |
118 | |
119 | fn on_frame(mut app App) { |
120 | app.gg.begin() |
121 | |
122 | now := time.ticks() |
123 | |
124 | if now - app.last_tick >= tick_rate_ms { |
125 | app.last_tick = now |
126 | |
127 | // finding delta direction |
128 | delta_dir := match app.dir { |
129 | .up { Pos{0, -1} } |
130 | .down { Pos{0, 1} } |
131 | .left { Pos{-1, 0} } |
132 | .right { Pos{1, 0} } |
133 | } |
134 | |
135 | // "snaking" along |
136 | mut prev := app.snake[0] |
137 | app.snake[0] = app.snake[0] + delta_dir |
138 | |
139 | for i in 1 .. app.snake.len { |
140 | tmp := app.snake[i] |
141 | app.snake[i] = prev |
142 | prev = tmp |
143 | } |
144 | |
145 | // adding last segment |
146 | if app.snake[0] == app.food { |
147 | app.move_food() |
148 | app.score++ |
149 | if app.score > app.best { |
150 | app.best = app.score |
151 | app.best.save() |
152 | } |
153 | app.snake << app.snake.last() + app.snake.last() - app.snake[app.snake.len - 2] |
154 | } |
155 | |
156 | app.last_dir = app.dir |
157 | } |
158 | // drawing snake |
159 | for pos in app.snake { |
160 | app.gg.draw_rect_filled(tile_size * pos.x, tile_size * pos.y + top_height, tile_size, |
161 | tile_size, gx.blue) |
162 | } |
163 | |
164 | // drawing food |
165 | app.gg.draw_rect_filled(tile_size * app.food.x, tile_size * app.food.y + top_height, |
166 | tile_size, tile_size, gx.red) |
167 | |
168 | // drawing top |
169 | app.gg.draw_rect_filled(0, 0, canvas_size, top_height, gx.black) |
170 | app.gg.draw_text(150, top_height / 2, 'Score: ${app.score}', gx.TextCfg{ |
171 | color: gx.white |
172 | align: .center |
173 | vertical_align: .middle |
174 | size: 65 |
175 | }) |
176 | app.gg.draw_text(canvas_size - 150, top_height / 2, 'Best: ${app.best}', gx.TextCfg{ |
177 | color: gx.white |
178 | align: .center |
179 | vertical_align: .middle |
180 | size: 65 |
181 | }) |
182 | |
183 | // checking if snake bit itself |
184 | if app.snake[0] in app.snake[1..] { |
185 | app.reset_game() |
186 | } |
187 | // checking if snake hit a wall |
188 | if app.snake[0].x < 0 || app.snake[0].x >= game_size || app.snake[0].y < 0 |
189 | || app.snake[0].y >= game_size { |
190 | app.reset_game() |
191 | } |
192 | |
193 | app.gg.end() |
194 | } |
195 | |
196 | const font = $embed_file('../assets/fonts/RobotoMono-Regular.ttf') |
197 | |
198 | // setup |
199 | fn main() { |
200 | mut app := App{ |
201 | gg: 0 |
202 | } |
203 | app.reset_game() |
204 | app.best.load() |
205 | |
206 | mut font_copy := font |
207 | font_bytes := unsafe { |
208 | font_copy.data().vbytes(font_copy.len) |
209 | } |
210 | |
211 | app.gg = gg.new_context( |
212 | bg_color: gx.white |
213 | frame_fn: on_frame |
214 | keydown_fn: on_keydown |
215 | user_data: &app |
216 | width: canvas_size |
217 | height: top_height + canvas_size |
218 | create_window: true |
219 | resizable: false |
220 | window_title: 'snek' |
221 | font_bytes_normal: font_bytes |
222 | ) |
223 | |
224 | app.gg.run() |
225 | } |