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