diff --git a/examples/term.ui/vyper.v b/examples/term.ui/vyper.v new file mode 100644 index 0000000000..810ebb06fd --- /dev/null +++ b/examples/term.ui/vyper.v @@ -0,0 +1,465 @@ +// import modules for use in app +import term.ui as termui +import rand + +// define some global constants +const ( + block_size = 1 + buffer = 10 + green = termui.Color{0, 255, 0} + grey = termui.Color{150, 150, 150} + white = termui.Color{255, 255, 255} + blue = termui.Color{0, 0, 255} + red = termui.Color{255, 0, 0} + black = termui.Color{0, 0, 0} +) + +// what edge of the screen are you facing +enum Orientation { + top + right + bottom + left +} + +// what's the current state of the game +enum GameState { + pause + gameover + game + oob // snake out-of-bounds +} + +// simple 2d vector representation +struct Vec { +mut: + x int + y int +} + +// determine orientation from vector (hacky way to set facing from velocity) +fn (v Vec) facing() Orientation { + result := if v.x >= 0 { + Orientation.right + } else if v.x < 0 { + Orientation.left + } else if v.y >= 0 { + Orientation.bottom + } else { + Orientation.top + } + return result +} + +// generate a random vector with x in [min_x, max_x] and y in [min_y, max_y] +fn (mut v Vec) randomize(min_x int, min_y int, max_x int, max_y int) { + v.x = rand.int_in_range(min_x, max_x) + v.y = rand.int_in_range(min_y, max_y) +} + +// part of snake's body representation +struct BodyPart { +mut: + pos Vec = { + x: block_size + y: block_size +} + color termui.Color = green + facing Orientation = .top +} + +// snake representation +struct Snake { +mut: + app &App + direction Orientation + body []BodyPart + velocity Vec = Vec{ + x: 0 + y: 0 +} +} + +// length returns the snake's current length +fn (s Snake) length() int { + return s.body.len +} + +// impulse provides a impulse to change the snake's direction +fn (mut s Snake) impulse(direction Orientation) { + mut vec := Vec{} + match direction { + .top { + vec.x = 0 + vec.y = -1 * block_size + } + .right { + vec.x = 2 * block_size + vec.y = 0 + } + .bottom { + vec.x = 0 + vec.y = block_size + } + .left { + vec.x = -2 * block_size + vec.y = 0 + } + } + s.direction = direction + s.velocity = vec +} + +// move performs the calculations for the snake's movements +fn (mut s Snake) move() { + mut i := s.body.len - 1 + width := s.app.width + height := s.app.height + // move the parts of the snake as appropriate + for i = s.body.len - 1; i >= 0; i-- { + mut piece := s.body[i] + if i > 0 { // just move the body of the snake up one position + piece.pos = s.body[i - 1].pos + piece.facing = s.body[i - 1].facing + } else { // verify that the move is valid and move the head if so + piece.facing = s.direction + new_x := piece.pos.x + s.velocity.x + new_y := piece.pos.y + s.velocity.y + piece.pos.x += if new_x > block_size && new_x < width - block_size { s.velocity.x } else { 0 } + piece.pos.y += if new_y > block_size && new_y < height - block_size { s.velocity.y } else { 0 } + } + s.body[i] = piece + } +} + +// grow add another part to the snake when it catches the rat +fn (mut s Snake) grow() { + head := s.get_tail() + mut pos := Vec{} + // add the segment on the opposite side of the previous tail + match head.facing { + .bottom { + pos.x = head.pos.x + pos.y = head.pos.y - block_size + } + .left { + pos.x = head.pos.x + block_size + pos.y = head.pos.y + } + .top { + pos.x = head.pos.x + pos.y = head.pos.y + block_size + } + .right { + pos.x = head.pos.x - block_size + pos.y = head.pos.y + } + } + s.body << BodyPart{ + pos: pos + facing: head.facing + } +} + +// get_body gets the parts of the snakes body +fn (s Snake) get_body() []BodyPart { + return s.body +} + +// get_head get snake's head +fn (s Snake) get_head() BodyPart { + return s.body[0] +} + +// get_tail get snake's tail +fn (s Snake) get_tail() BodyPart { + return s.body[s.body.len - 1] +} + +// randomize randomizes position and veolcity of snake +fn (mut s Snake) randomize() { + speeds := [-2, 0, 2] + mut pos := s.get_head().pos + pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer) + for pos.x % 2 != 0 || (pos.x < buffer && pos.x > s.app.width - buffer) { + pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer) + } + s.velocity.y = rand.int_in_range(-1 * block_size, block_size) + s.velocity.x = speeds[rand.intn(speeds.len)] + s.direction = s.velocity.facing() + s.body[0].pos = pos +} + +// check_overlap determine if the snake's looped onto itself +fn (s Snake) check_overlap() bool { + h := s.get_head() + head_pos := h.pos + for i in 2 .. s.length() { + piece_pos := s.body[i].pos + if head_pos.x == piece_pos.x && head_pos.y == piece_pos.y { + return true + } + } + return false +} + +fn (s Snake) check_out_of_bounds() bool { + h := s.get_head() + return h.pos.x + s.velocity.x <= block_size || + h.pos.x + s.velocity.x > s.app.width - s.velocity.x || h.pos.y + s.velocity.y <= block_size || + h.pos.y + s.velocity.y > + s.app.height - block_size - s.velocity.y +} + +// draw draws the parts of the snake +fn (s Snake) draw() { + mut a := s.app + for part in s.get_body() { + a.termui.set_bg_color(part.color) + a.termui.draw_rect(part.pos.x, part.pos.y, part.pos.x + block_size, part.pos.y + block_size) + $if verbose ? { + text := match part.facing { + .top { '^' } + .bottom { 'v' } + .right { '>' } + .left { '<' } + } + a.termui.set_color(white) + a.termui.draw_text(part.pos.x, part.pos.y, text) + } + } +} + +// rat representation +struct Rat { +mut: + pos Vec = { + x: block_size + y: block_size +} + captured bool + color termui.Color = grey + app &App +} + +// randomize spawn the rat in a new spot within the playable field +fn (mut r Rat) randomize() { + r.pos.randomize(2 * block_size + buffer, 2 * block_size + buffer, r.app.width - block_size - + buffer, r.app.height - block_size - buffer) +} + +struct App { +mut: + termui &termui.Context = 0 + snake Snake + rat Rat + width int + height int + redraw bool = true + state GameState = .game +} + +// new_game setups the rat and snake for play +fn (mut a App) new_game() { + mut snake := Snake{ + body: []BodyPart{len: 1, init: BodyPart{}} + app: a + } + snake.randomize() + mut rat := Rat{ + app: a + } + rat.randomize() + a.snake = snake + a.rat = rat + a.state = .game + a.redraw = true +} + +// initialize the app and record the width and height of the window +fn init(x voidptr) { + mut app := &App(x) + w, h := app.termui.window_width, app.termui.window_height + app.width = w + app.height = h + app.new_game() +} + +// event handles different events for the app as they occur +fn event(e &termui.Event, x voidptr) { + mut app := &App(x) + match e.typ { + .mouse_down {} + .mouse_drag {} + .mouse_up {} + .key_down { + match e.code { + .up, .w { app.move_snake(.top) } + .down, .s { app.move_snake(.bottom) } + .left, .a { app.move_snake(.left) } + .right, .d { app.move_snake(.right) } + .r { app.new_game() } + .c {} + .p { app.state = if app.state == .game { GameState.pause } else { GameState.game } } + .escape, .q { exit(0) } + else { exit(0) } + } + if e.code == .c { + } else if e.code == .escape { + exit(0) + } + } + else {} + } + app.redraw = true +} + +// frame perform actions on every tick +fn frame(x voidptr) { + mut app := &App(x) + app.update() + app.draw() +} + +// update perform any calculations that are needed before drawing +fn (mut a App) update() { + if a.state == .game { + a.snake.move() + if a.snake.check_out_of_bounds() { + $if verbose ? { + a.snake.body[0].color = red + } $else { + a.state = .oob + } + } + if a.snake.check_overlap() { + a.state = .gameover + return + } + if a.check_capture() { + a.rat.randomize() + a.snake.grow() + } + } +} + +// draw write to the screen +fn (mut a App) draw() { + // reset screen + a.termui.clear() + a.termui.set_bg_color(white) + a.termui.draw_empty_rect(1, 1, a.width, a.height) + // determine if a special screen needs to be draw + match a.state { + .gameover { + a.draw_gameover() + a.redraw = false + } + .pause { + a.draw_pause() + } + else { + a.redraw = true + } + } + a.termui.set_color(blue) + a.termui.set_bg_color(white) + a.termui.draw_text(3 * block_size, a.height - (2 * block_size), 'p - (un)pause r - reset q - quit') + // draw the snake, rat, and score if appropriate + if a.redraw { + a.termui.set_bg_color(black) + a.draw_gamescreen() + if a.state == .oob { + a.state = .gameover + } + } + // write to the screen + a.termui.reset_bg_color() + a.termui.flush() +} + +// move_snake move the snake in specified direction +fn (mut a App) move_snake(direction Orientation) { + a.snake.impulse(direction) +} + +// check_capture determine if the snake overlaps with the rat +fn (a App) check_capture() bool { + snake_pos := a.snake.get_head().pos + rat_pos := a.rat.pos + return snake_pos.x <= rat_pos.x + block_size && + snake_pos.x + block_size >= rat_pos.x && snake_pos.y <= rat_pos.y + block_size && snake_pos.y + + block_size >= rat_pos.y +} + +fn (mut a App) draw_snake() { + a.snake.draw() +} + +fn (mut a App) draw_rat() { + a.termui.set_bg_color(a.rat.color) + a.termui.draw_rect(a.rat.pos.x, a.rat.pos.y, a.rat.pos.x + block_size, a.rat.pos.y + block_size) +} + +fn (mut a App) draw_gamescreen() { + $if verbose ? { + a.draw_debug() + } + a.draw_score() + a.draw_rat() + a.draw_snake() +} + +fn (mut a App) draw_score() { + a.termui.set_color(blue) + a.termui.set_bg_color(white) + score := a.snake.length() - 1 + a.termui.draw_text(a.width - (2 * block_size), block_size, '${score:03d}') +} + +fn (mut a App) draw_pause() { + a.termui.set_color(blue) + a.termui.draw_text((a.width / 2) - block_size, 3 * block_size, 'Paused!') +} + +fn (mut a App) draw_debug() { + a.termui.set_color(blue) + a.termui.set_bg_color(white) + snake := a.snake + a.termui.draw_text(block_size, 1 * block_size, 'Display_width: ${a.width:04d} Display_height: ${a.height:04d}') + a.termui.draw_text(block_size, 2 * block_size, 'Vx: ${snake.velocity.x:+02d} Vy: ${snake.velocity.y:+02d}') + a.termui.draw_text(block_size, 3 * block_size, 'F: $snake.direction') + snake_head := snake.get_head() + rat := a.rat + a.termui.draw_text(block_size, 4 * block_size, 'Sx: ${snake_head.pos.x:+03d} Sy: ${snake_head.pos.y:+03d}') + a.termui.draw_text(block_size, 5 * block_size, 'Rx: ${rat.pos.x:+03d} Ry: ${rat.pos.y:+03d}') +} + +fn (mut a App) draw_gameover() { + a.termui.set_bg_color(white) + a.termui.set_color(red) + a.rat.pos = Vec{ + x: -1 + y: -1 + } + x_offset := ' ##### '.len // take half of a line from the game over text and store the length + start_x := (a.width / 2) - x_offset + a.termui.draw_text(start_x, (a.height / 2) - 3 * block_size, ' ##### ####### ') + a.termui.draw_text(start_x, (a.height / 2) - 2 * block_size, ' # # ## # # ###### # # # # ###### ##### ') + a.termui.draw_text(start_x, (a.height / 2) - 1 * block_size, ' # # # ## ## # # # # # # # # ') + a.termui.draw_text(start_x, (a.height / 2) - 0 * block_size, ' # #### # # # ## # ##### # # # # ##### # # ') + a.termui.draw_text(start_x, (a.height / 2) + 1 * block_size, ' # # ###### # # # # # # # # ##### ') + a.termui.draw_text(start_x, (a.height / 2) + 2 * block_size, ' # # # # # # # # # # # # # # ') + a.termui.draw_text(start_x, (a.height / 2) + 3 * block_size, ' ##### # # # # ###### ####### ## ###### # # ') +} + +mut app := &App{} +app.termui = termui.init({ + user_data: app + event_fn: event + frame_fn: frame + init_fn: init + hide_cursor: true + frame_rate: 10 +}) +app.termui.run()