| 1 | // import modules for use in app |
| 2 | import term.ui as termui |
| 3 | import rand |
| 4 | import math.vec |
| 5 | |
| 6 | // define some global constants |
| 7 | const block_size = 1 |
| 8 | const buffer = 10 |
| 9 | const green = termui.Color{0, 255, 0} |
| 10 | const grey = termui.Color{150, 150, 150} |
| 11 | const white = termui.Color{255, 255, 255} |
| 12 | const blue = termui.Color{0, 0, 255} |
| 13 | const red = termui.Color{255, 0, 0} |
| 14 | const black = termui.Color{0, 0, 0} |
| 15 | |
| 16 | // what edge of the screen are you facing |
| 17 | enum Orientation { |
| 18 | top |
| 19 | right |
| 20 | bottom |
| 21 | left |
| 22 | } |
| 23 | |
| 24 | // what's the current state of the game |
| 25 | enum GameState { |
| 26 | pause |
| 27 | gameover |
| 28 | game |
| 29 | oob // snake out-of-bounds |
| 30 | } |
| 31 | |
| 32 | // simple 2d vector representation |
| 33 | type Vec = vec.Vec2[int] |
| 34 | |
| 35 | // determine orientation from vector (hacky way to set facing from velocity) |
| 36 | fn (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] |
| 50 | fn (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 |
| 56 | struct BodyPart { |
| 57 | mut: |
| 58 | pos Vec = Vec{block_size, block_size} |
| 59 | color termui.Color = green |
| 60 | facing Orientation = .top |
| 61 | } |
| 62 | |
| 63 | // snake representation |
| 64 | struct Snake { |
| 65 | mut: |
| 66 | app &App = unsafe { nil } |
| 67 | direction Orientation |
| 68 | body []BodyPart |
| 69 | velocity Vec |
| 70 | } |
| 71 | |
| 72 | // length returns the snake's current length |
| 73 | fn (s Snake) length() int { |
| 74 | return s.body.len |
| 75 | } |
| 76 | |
| 77 | // impulse provides a impulse to change the snake's direction |
| 78 | fn (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 |
| 104 | fn (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 |
| 134 | fn (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 |
| 164 | fn (s Snake) get_body() []BodyPart { |
| 165 | return s.body |
| 166 | } |
| 167 | |
| 168 | // get_head get snake's head |
| 169 | fn (s Snake) get_head() BodyPart { |
| 170 | return s.body[0] |
| 171 | } |
| 172 | |
| 173 | // get_tail get snake's tail |
| 174 | fn (s Snake) get_tail() BodyPart { |
| 175 | return s.body[s.body.len - 1] |
| 176 | } |
| 177 | |
| 178 | // randomize randomizes position and veolcity of snake |
| 179 | fn (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 |
| 193 | fn (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 | |
| 205 | fn (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 |
| 214 | fn (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 |
| 234 | struct Rat { |
| 235 | mut: |
| 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 |
| 243 | fn (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] |
| 249 | struct App { |
| 250 | mut: |
| 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 |
| 261 | fn (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 |
| 278 | fn 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 |
| 286 | fn 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 |
| 316 | fn frame(mut app App) { |
| 317 | app.update() |
| 318 | app.draw() |
| 319 | } |
| 320 | |
| 321 | // update perform any calculations that are needed before drawing |
| 322 | fn (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 |
| 344 | fn (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 |
| 381 | fn (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 |
| 386 | fn (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 | |
| 393 | fn (mut a App) draw_snake() { |
| 394 | a.snake.draw() |
| 395 | } |
| 396 | |
| 397 | fn (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 | |
| 402 | fn (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 | |
| 411 | fn (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 | |
| 418 | fn (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 | |
| 423 | fn (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 | |
| 439 | fn (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 | |
| 461 | type InitFn = fn (voidptr) |
| 462 | |
| 463 | type EventFn = fn (&termui.Event, voidptr) |
| 464 | |
| 465 | type FrameFn = fn (voidptr) |
| 466 | |
| 467 | fn 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 | |