1 | // import modules for use in app |
2 | import term.ui as termui |
3 | import rand |
4 | |
5 | // define some global constants |
6 | const ( |
7 | block_size = 1 |
8 | buffer = 10 |
9 | green = termui.Color{0, 255, 0} |
10 | grey = termui.Color{150, 150, 150} |
11 | white = termui.Color{255, 255, 255} |
12 | blue = termui.Color{0, 0, 255} |
13 | red = termui.Color{255, 0, 0} |
14 | black = termui.Color{0, 0, 0} |
15 | ) |
16 | |
17 | // what edge of the screen are you facing |
18 | enum Orientation { |
19 | top |
20 | right |
21 | bottom |
22 | left |
23 | } |
24 | |
25 | // what's the current state of the game |
26 | enum GameState { |
27 | pause |
28 | gameover |
29 | game |
30 | oob // snake out-of-bounds |
31 | } |
32 | |
33 | // simple 2d vector representation |
34 | struct Vec { |
35 | mut: |
36 | x int |
37 | y int |
38 | } |
39 | |
40 | // determine orientation from vector (hacky way to set facing from velocity) |
41 | fn (v Vec) facing() Orientation { |
42 | result := if v.x >= 0 { |
43 | Orientation.right |
44 | } else if v.x < 0 { |
45 | Orientation.left |
46 | } else if v.y >= 0 { |
47 | Orientation.bottom |
48 | } else { |
49 | Orientation.top |
50 | } |
51 | return result |
52 | } |
53 | |
54 | // generate a random vector with x in [min_x, max_x] and y in [min_y, max_y] |
55 | fn (mut v Vec) randomize(min_x int, min_y int, max_x int, max_y int) { |
56 | v.x = rand.int_in_range(min_x, max_x) or { min_x } |
57 | v.y = rand.int_in_range(min_y, max_y) or { min_y } |
58 | } |
59 | |
60 | // part of snake's body representation |
61 | struct BodyPart { |
62 | mut: |
63 | pos Vec = Vec{ |
64 | x: block_size |
65 | y: block_size |
66 | } |
67 | color termui.Color = green |
68 | facing Orientation = .top |
69 | } |
70 | |
71 | // snake representation |
72 | struct Snake { |
73 | mut: |
74 | app &App = unsafe { nil } |
75 | direction Orientation |
76 | body []BodyPart |
77 | velocity Vec = Vec{ |
78 | x: 0 |
79 | y: 0 |
80 | } |
81 | } |
82 | |
83 | // length returns the snake's current length |
84 | fn (s Snake) length() int { |
85 | return s.body.len |
86 | } |
87 | |
88 | // impulse provides a impulse to change the snake's direction |
89 | fn (mut s Snake) impulse(direction Orientation) { |
90 | mut vec := Vec{} |
91 | match direction { |
92 | .top { |
93 | vec.x = 0 |
94 | vec.y = -1 * block_size |
95 | } |
96 | .right { |
97 | vec.x = 2 * block_size |
98 | vec.y = 0 |
99 | } |
100 | .bottom { |
101 | vec.x = 0 |
102 | vec.y = block_size |
103 | } |
104 | .left { |
105 | vec.x = -2 * block_size |
106 | vec.y = 0 |
107 | } |
108 | } |
109 | s.direction = direction |
110 | s.velocity = vec |
111 | } |
112 | |
113 | // move performs the calculations for the snake's movements |
114 | fn (mut s Snake) move() { |
115 | mut i := s.body.len - 1 |
116 | width := s.app.width |
117 | height := s.app.height |
118 | // move the parts of the snake as appropriate |
119 | for i = s.body.len - 1; i >= 0; i-- { |
120 | mut piece := s.body[i] |
121 | if i > 0 { // just move the body of the snake up one position |
122 | piece.pos = s.body[i - 1].pos |
123 | piece.facing = s.body[i - 1].facing |
124 | } else { // verify that the move is valid and move the head if so |
125 | piece.facing = s.direction |
126 | new_x := piece.pos.x + s.velocity.x |
127 | new_y := piece.pos.y + s.velocity.y |
128 | piece.pos.x += if new_x > block_size && new_x < width - block_size { |
129 | s.velocity.x |
130 | } else { |
131 | 0 |
132 | } |
133 | piece.pos.y += if new_y > block_size && new_y < height - block_size { |
134 | s.velocity.y |
135 | } else { |
136 | 0 |
137 | } |
138 | } |
139 | s.body[i] = piece |
140 | } |
141 | } |
142 | |
143 | // grow add another part to the snake when it catches the rat |
144 | fn (mut s Snake) grow() { |
145 | head := s.get_tail() |
146 | mut pos := Vec{} |
147 | // add the segment on the opposite side of the previous tail |
148 | match head.facing { |
149 | .bottom { |
150 | pos.x = head.pos.x |
151 | pos.y = head.pos.y - block_size |
152 | } |
153 | .left { |
154 | pos.x = head.pos.x + block_size |
155 | pos.y = head.pos.y |
156 | } |
157 | .top { |
158 | pos.x = head.pos.x |
159 | pos.y = head.pos.y + block_size |
160 | } |
161 | .right { |
162 | pos.x = head.pos.x - block_size |
163 | pos.y = head.pos.y |
164 | } |
165 | } |
166 | s.body << BodyPart{ |
167 | pos: pos |
168 | facing: head.facing |
169 | } |
170 | } |
171 | |
172 | // get_body gets the parts of the snakes body |
173 | fn (s Snake) get_body() []BodyPart { |
174 | return s.body |
175 | } |
176 | |
177 | // get_head get snake's head |
178 | fn (s Snake) get_head() BodyPart { |
179 | return s.body[0] |
180 | } |
181 | |
182 | // get_tail get snake's tail |
183 | fn (s Snake) get_tail() BodyPart { |
184 | return s.body[s.body.len - 1] |
185 | } |
186 | |
187 | // randomize randomizes position and veolcity of snake |
188 | fn (mut s Snake) randomize() { |
189 | speeds := [-2, 0, 2] |
190 | mut pos := s.get_head().pos |
191 | pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer) |
192 | for pos.x % 2 != 0 || (pos.x < buffer && pos.x > s.app.width - buffer) { |
193 | pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer) |
194 | } |
195 | s.velocity.y = rand.int_in_range(-1 * block_size, block_size) or { 0 } |
196 | s.velocity.x = speeds[rand.intn(speeds.len) or { 0 }] |
197 | s.direction = s.velocity.facing() |
198 | s.body[0].pos = pos |
199 | } |
200 | |
201 | // check_overlap determine if the snake's looped onto itself |
202 | fn (s Snake) check_overlap() bool { |
203 | h := s.get_head() |
204 | head_pos := h.pos |
205 | for i in 2 .. s.length() { |
206 | piece_pos := s.body[i].pos |
207 | if head_pos.x == piece_pos.x && head_pos.y == piece_pos.y { |
208 | return true |
209 | } |
210 | } |
211 | return false |
212 | } |
213 | |
214 | fn (s Snake) check_out_of_bounds() bool { |
215 | h := s.get_head() |
216 | return h.pos.x + s.velocity.x <= block_size |
217 | || h.pos.x + s.velocity.x > s.app.width - s.velocity.x |
218 | || h.pos.y + s.velocity.y <= block_size |
219 | || h.pos.y + s.velocity.y > s.app.height - block_size - s.velocity.y |
220 | } |
221 | |
222 | // draw draws the parts of the snake |
223 | fn (s Snake) draw() { |
224 | mut a := s.app |
225 | for part in s.get_body() { |
226 | a.termui.set_bg_color(part.color) |
227 | a.termui.draw_rect(part.pos.x, part.pos.y, part.pos.x + block_size, part.pos.y + block_size) |
228 | $if verbose ? { |
229 | text := match part.facing { |
230 | .top { '^' } |
231 | .bottom { 'v' } |
232 | .right { '>' } |
233 | .left { '<' } |
234 | } |
235 | a.termui.set_color(white) |
236 | a.termui.draw_text(part.pos.x, part.pos.y, text) |
237 | } |
238 | } |
239 | } |
240 | |
241 | // rat representation |
242 | struct Rat { |
243 | mut: |
244 | pos Vec = Vec{ |
245 | x: block_size |
246 | y: block_size |
247 | } |
248 | captured bool |
249 | color termui.Color = grey |
250 | app &App = unsafe { nil } |
251 | } |
252 | |
253 | // randomize spawn the rat in a new spot within the playable field |
254 | fn (mut r Rat) randomize() { |
255 | r.pos.randomize(2 * block_size + buffer, 2 * block_size + buffer, r.app.width - block_size - buffer, |
256 | r.app.height - block_size - buffer) |
257 | } |
258 | |
259 | [heap] |
260 | struct App { |
261 | mut: |
262 | termui &termui.Context = unsafe { nil } |
263 | snake Snake |
264 | rat Rat |
265 | width int |
266 | height int |
267 | redraw bool = true |
268 | state GameState = .game |
269 | } |
270 | |
271 | // new_game setups the rat and snake for play |
272 | fn (mut a App) new_game() { |
273 | mut snake := Snake{ |
274 | body: []BodyPart{len: 1, init: BodyPart{}} |
275 | app: a |
276 | } |
277 | snake.randomize() |
278 | mut rat := Rat{ |
279 | app: a |
280 | } |
281 | rat.randomize() |
282 | a.snake = snake |
283 | a.rat = rat |
284 | a.state = .game |
285 | a.redraw = true |
286 | } |
287 | |
288 | // initialize the app and record the width and height of the window |
289 | fn init(mut app App) { |
290 | w, h := app.termui.window_width, app.termui.window_height |
291 | app.width = w |
292 | app.height = h |
293 | app.new_game() |
294 | } |
295 | |
296 | // event handles different events for the app as they occur |
297 | fn event(e &termui.Event, mut app App) { |
298 | match e.typ { |
299 | .mouse_down {} |
300 | .mouse_drag {} |
301 | .mouse_up {} |
302 | .key_down { |
303 | match e.code { |
304 | .up, .w { app.move_snake(.top) } |
305 | .down, .s { app.move_snake(.bottom) } |
306 | .left, .a { app.move_snake(.left) } |
307 | .right, .d { app.move_snake(.right) } |
308 | .r { app.new_game() } |
309 | .c {} |
310 | .p { app.state = if app.state == .game { GameState.pause } else { GameState.game } } |
311 | .escape, .q { exit(0) } |
312 | else { exit(0) } |
313 | } |
314 | if e.code == .c { |
315 | } else if e.code == .escape { |
316 | exit(0) |
317 | } |
318 | } |
319 | else {} |
320 | } |
321 | app.redraw = true |
322 | } |
323 | |
324 | // frame perform actions on every tick |
325 | fn frame(mut app App) { |
326 | app.update() |
327 | app.draw() |
328 | } |
329 | |
330 | // update perform any calculations that are needed before drawing |
331 | fn (mut a App) update() { |
332 | if a.state == .game { |
333 | a.snake.move() |
334 | if a.snake.check_out_of_bounds() { |
335 | $if verbose ? { |
336 | a.snake.body[0].color = red |
337 | } $else { |
338 | a.state = .oob |
339 | } |
340 | } |
341 | if a.snake.check_overlap() { |
342 | a.state = .gameover |
343 | return |
344 | } |
345 | if a.check_capture() { |
346 | a.rat.randomize() |
347 | a.snake.grow() |
348 | } |
349 | } |
350 | } |
351 | |
352 | // draw write to the screen |
353 | fn (mut a App) draw() { |
354 | // reset screen |
355 | a.termui.clear() |
356 | a.termui.set_bg_color(white) |
357 | a.termui.draw_empty_rect(1, 1, a.width, a.height) |
358 | // determine if a special screen needs to be draw |
359 | match a.state { |
360 | .gameover { |
361 | a.draw_gameover() |
362 | a.redraw = false |
363 | } |
364 | .pause { |
365 | a.draw_pause() |
366 | } |
367 | else { |
368 | a.redraw = true |
369 | } |
370 | } |
371 | a.termui.set_color(blue) |
372 | a.termui.set_bg_color(white) |
373 | a.termui.draw_text(3 * block_size, a.height - (2 * block_size), 'p - (un)pause r - reset q - quit') |
374 | // draw the snake, rat, and score if appropriate |
375 | if a.redraw { |
376 | a.termui.set_bg_color(black) |
377 | a.draw_gamescreen() |
378 | if a.state == .oob { |
379 | a.state = .gameover |
380 | } |
381 | } |
382 | // write to the screen |
383 | a.termui.reset_bg_color() |
384 | a.termui.flush() |
385 | } |
386 | |
387 | // move_snake move the snake in specified direction |
388 | fn (mut a App) move_snake(direction Orientation) { |
389 | a.snake.impulse(direction) |
390 | } |
391 | |
392 | // check_capture determine if the snake overlaps with the rat |
393 | fn (a App) check_capture() bool { |
394 | snake_pos := a.snake.get_head().pos |
395 | rat_pos := a.rat.pos |
396 | return snake_pos.x <= rat_pos.x + block_size && snake_pos.x + block_size >= rat_pos.x |
397 | && snake_pos.y <= rat_pos.y + block_size && snake_pos.y + block_size >= rat_pos.y |
398 | } |
399 | |
400 | fn (mut a App) draw_snake() { |
401 | a.snake.draw() |
402 | } |
403 | |
404 | fn (mut a App) draw_rat() { |
405 | a.termui.set_bg_color(a.rat.color) |
406 | a.termui.draw_rect(a.rat.pos.x, a.rat.pos.y, a.rat.pos.x + block_size, a.rat.pos.y + block_size) |
407 | } |
408 | |
409 | fn (mut a App) draw_gamescreen() { |
410 | $if verbose ? { |
411 | a.draw_debug() |
412 | } |
413 | a.draw_score() |
414 | a.draw_rat() |
415 | a.draw_snake() |
416 | } |
417 | |
418 | fn (mut a App) draw_score() { |
419 | a.termui.set_color(blue) |
420 | a.termui.set_bg_color(white) |
421 | score := a.snake.length() - 1 |
422 | a.termui.draw_text(a.width - (2 * block_size), block_size, '${score:03d}') |
423 | } |
424 | |
425 | fn (mut a App) draw_pause() { |
426 | a.termui.set_color(blue) |
427 | a.termui.draw_text((a.width / 2) - block_size, 3 * block_size, 'Paused!') |
428 | } |
429 | |
430 | fn (mut a App) draw_debug() { |
431 | a.termui.set_color(blue) |
432 | a.termui.set_bg_color(white) |
433 | snake := a.snake |
434 | a.termui.draw_text(block_size, 1 * block_size, 'Display_width: ${a.width:04d} Display_height: ${a.height:04d}') |
435 | a.termui.draw_text(block_size, 2 * block_size, 'Vx: ${snake.velocity.x:+02d} Vy: ${snake.velocity.y:+02d}') |
436 | a.termui.draw_text(block_size, 3 * block_size, 'F: ${snake.direction}') |
437 | snake_head := snake.get_head() |
438 | rat := a.rat |
439 | a.termui.draw_text(block_size, 4 * block_size, 'Sx: ${snake_head.pos.x:+03d} Sy: ${snake_head.pos.y:+03d}') |
440 | a.termui.draw_text(block_size, 5 * block_size, 'Rx: ${rat.pos.x:+03d} Ry: ${rat.pos.y:+03d}') |
441 | } |
442 | |
443 | fn (mut a App) draw_gameover() { |
444 | a.termui.set_bg_color(white) |
445 | a.termui.set_color(red) |
446 | a.rat.pos = Vec{ |
447 | x: -1 |
448 | y: -1 |
449 | } |
450 | x_offset := ' ##### '.len // take half of a line from the game over text and store the length |
451 | start_x := (a.width / 2) - x_offset |
452 | a.termui.draw_text(start_x, (a.height / 2) - 3 * block_size, ' ##### ####### ') |
453 | a.termui.draw_text(start_x, (a.height / 2) - 2 * block_size, ' # # ## # # ###### # # # # ###### ##### ') |
454 | a.termui.draw_text(start_x, (a.height / 2) - 1 * block_size, ' # # # ## ## # # # # # # # # ') |
455 | a.termui.draw_text(start_x, (a.height / 2) - 0 * block_size, ' # #### # # # ## # ##### # # # # ##### # # ') |
456 | a.termui.draw_text(start_x, (a.height / 2) + 1 * block_size, ' # # ###### # # # # # # # # ##### ') |
457 | a.termui.draw_text(start_x, (a.height / 2) + 2 * block_size, ' # # # # # # # # # # # # # # ') |
458 | a.termui.draw_text(start_x, (a.height / 2) + 3 * block_size, ' ##### # # # # ###### ####### ## ###### # # ') |
459 | } |
460 | |
461 | fn main() { |
462 | mut app := &App{} |
463 | app.termui = termui.init( |
464 | user_data: app |
465 | event_fn: event |
466 | frame_fn: frame |
467 | init_fn: init |
468 | hide_cursor: true |
469 | frame_rate: 10 |
470 | ) |
471 | app.termui.run()! |
472 | } |