v / examples / term.ui
Raw file | 472 loc (434 sloc) | 11.15 KB | Latest commit hash 868908b80
1// import modules for use in app
2import term.ui as termui
3import rand
4
5// define some global constants
6const (
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
18enum Orientation {
19 top
20 right
21 bottom
22 left
23}
24
25// what's the current state of the game
26enum GameState {
27 pause
28 gameover
29 game
30 oob // snake out-of-bounds
31}
32
33// simple 2d vector representation
34struct Vec {
35mut:
36 x int
37 y int
38}
39
40// determine orientation from vector (hacky way to set facing from velocity)
41fn (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]
55fn (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
61struct BodyPart {
62mut:
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
72struct Snake {
73mut:
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
84fn (s Snake) length() int {
85 return s.body.len
86}
87
88// impulse provides a impulse to change the snake's direction
89fn (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
114fn (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
144fn (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
173fn (s Snake) get_body() []BodyPart {
174 return s.body
175}
176
177// get_head get snake's head
178fn (s Snake) get_head() BodyPart {
179 return s.body[0]
180}
181
182// get_tail get snake's tail
183fn (s Snake) get_tail() BodyPart {
184 return s.body[s.body.len - 1]
185}
186
187// randomize randomizes position and veolcity of snake
188fn (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
202fn (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
214fn (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
223fn (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
242struct Rat {
243mut:
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
254fn (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]
260struct App {
261mut:
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
272fn (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
289fn 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
297fn 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
325fn frame(mut app App) {
326 app.update()
327 app.draw()
328}
329
330// update perform any calculations that are needed before drawing
331fn (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
353fn (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
388fn (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
393fn (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
400fn (mut a App) draw_snake() {
401 a.snake.draw()
402}
403
404fn (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
409fn (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
418fn (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
425fn (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
430fn (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
443fn (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
461fn 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}