From f44853a87f97697ec5f827ec1ffc0191651bebd5 Mon Sep 17 00:00:00 2001 From: Larpon Date: Tue, 17 Nov 2020 15:25:41 +0100 Subject: [PATCH] examples: term.ui - add a pong clone (#6857) --- examples/term.ui/pong.v | 494 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 494 insertions(+) create mode 100644 examples/term.ui/pong.v diff --git a/examples/term.ui/pong.v b/examples/term.ui/pong.v new file mode 100644 index 0000000000..af2f14c8b1 --- /dev/null +++ b/examples/term.ui/pong.v @@ -0,0 +1,494 @@ +import term +import term.ui +import time + +enum Mode { + menu + game +} + +const ( + player_one = 1 // Human control this racket + player_two = 0 // Take over this AI controller + white = ui.Color{255, 255, 255} + orange = ui.Color{255, 140, 0} +) + +struct App { +mut: + tui &ui.Context = 0 + mode Mode = Mode.menu + width int + height int + game &Game = 0 + dt f32 + ticks i64 +} + +fn (mut a App) init() { + a.game = &Game{ + app: a + } + w, h := a.tui.window_width, a.tui.window_height + a.width = w + a.height = h + term.erase_del_clear() + term.set_cursor_position({ + x: 0 + y: 0 + }) +} + +fn (mut a App) start_game() { + if a.mode != .game { + a.mode = .game + } + a.game.init() +} + +fn (mut a App) frame() { + ticks := time.ticks() + a.dt = f32(ticks - a.ticks) / 1000.0 + a.width, a.height = a.tui.window_width, a.tui.window_height + if a.mode == .game { + a.game.update() + } + a.tui.clear() + a.render() + a.tui.flush() + a.ticks = ticks +} + +fn (mut a App) quit() { + if a.mode != .menu { + a.game.quit() + return + } + term.set_cursor_position({ + x: 0 + y: 0 + }) + exit(0) +} + +fn (mut a App) event(e &ui.Event) { + match e.typ { + .mouse_move { + if a.mode != .game { + return + } + // TODO mouse movement for real Pong sharks + // a.game.move_player(player_one, 0, -1) + } + .key_down { + match e.code { + .escape, .q { + a.quit() + } + .w { + if a.mode != .game { + return + } + a.game.move_player(player_one, 0, -1) + } + .a { + if a.mode != .game { + return + } + a.game.move_player(player_one, 0, -1) + } + .s { + if a.mode != .game { + return + } + a.game.move_player(player_one, 0, 1) + } + .d { + if a.mode != .game { + return + } + a.game.move_player(player_one, 0, 1) + } + .left { + if a.mode != .game { + return + } + a.game.move_player(player_two, 0, -1) + } + .right { + if a.mode != .game { + return + } + a.game.move_player(player_two, 0, 1) + } + .up { + if a.mode != .game { + return + } + a.game.move_player(player_two, 0, -1) + } + .down { + if a.mode != .game { + return + } + a.game.move_player(player_two, 0, 1) + } + .enter, .space { + if a.mode == .menu { + a.start_game() + } + } + else {} + } + } + else {} + } +} + +fn (mut a App) free() { + unsafe { + a.game.free() + free(a.game) + } +} + +fn (mut a App) render() { + match a.mode { + .menu { a.draw_menu() } + else { a.draw_game() } + } +} + +fn (mut a App) draw_menu() { + cx := int(f32(a.width) * 0.5) + y025 := int(f32(a.height) * 0.25) + y075 := int(f32(a.height) * 0.75) + cy := int(f32(a.height) * 0.5) + // + a.tui.set_color(white) + a.tui.bold() + a.tui.draw_text(cx - 2, y025, 'VONG') + a.tui.reset() + a.tui.draw_text(cx - 13, y025 + 1, '(A game of Pong written in V)') + // + a.tui.set_color(white) + a.tui.bold() + a.tui.draw_text(cx - 3, cy + 1, 'START') + a.tui.reset() + // + a.tui.draw_text(cx - 9, y075 + 1, 'Press SPACE to start') + a.tui.reset() + a.tui.draw_text(cx - 5, y075 + 3, 'ESC to Quit') + a.tui.reset() +} + +fn (mut a App) draw_game() { + a.game.draw() +} + +struct Player { +mut: + game &Game + pos Vec + racket_size int = 4 + score int + ai bool +} + +fn (mut p Player) move(x f32, y f32) { + p.pos.x += x + p.pos.y += y +} + +fn (mut p Player) update() { + if !p.ai { + return + } + if isnil(p.game) { + return + } + // dt := p.game.app.dt + ball := &p.game.ball + // Evil AI that eventually will take over the world + p.pos.y = ball.pos.y - int(f32(p.racket_size) * 0.5) +} + +struct Vec { +mut: + x f32 + y f32 +} + +fn (mut v Vec) set(x f32, y f32) { + v.x = x + v.y = y +} + +struct Ball { +mut: + pos Vec + vel Vec + acc Vec +} + +fn (mut b Ball) update(dt f32) { + b.pos.x += b.vel.x * b.acc.x * dt + b.pos.y += b.vel.y * b.acc.y * dt +} + +struct Game { +mut: + app &App = 0 + players []Player + ball Ball +} + +fn (mut g Game) move_player(id int, x int, y int) { + mut p := &g.players[id] + if p.ai { // disable AI when moved + p.ai = false + } + p.move(x, y) +} + +fn (mut g Game) init() { + if g.players.len == 0 { + g.players = []Player{len: 2, init: Player{ // <- BUG omitting the init will result in smaller racket sizes??? + game: g + }} + } + g.reset() +} + +fn (mut g Game) reset() { + mut i := 0 + for mut p in g.players { + p.score = 0 + if i != player_one { + p.ai = true + } + i++ + } + g.new_round() +} + +fn (mut g Game) new_round() { + mut i := 0 + for mut p in g.players { + p.pos.x = if i == 0 { 3 } else { g.app.width - 2 } + p.pos.y = f32(g.app.height) * 0.5 - f32(p.racket_size) * 0.5 + i++ + } + g.ball.pos.set(f32(g.app.width) * 0.5, f32(g.app.height) * 0.5) + g.ball.vel.set(-8, -15) + g.ball.acc.set(2.0, 1.0) +} + +fn (mut g Game) update() { + dt := g.app.dt + mut b := &g.ball + for mut p in g.players { + p.update() + // Keep rackets within the game area + if p.pos.y <= 0 { + p.pos.y = 1 + } + if p.pos.y + p.racket_size >= g.app.height { + p.pos.y = g.app.height - p.racket_size - 1 + } + // Check ball collision + // Player left side + if p.pos.x < f32(g.app.width) * 0.5 { + // Racket collision + if b.pos.x <= p.pos.x + 1 { + if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size { + b.vel.x *= -1 + } + } + // Behind racket + if b.pos.x < p.pos.x { + g.players[1].score++ + g.new_round() + } + } else { + // Player right side + if b.pos.x >= p.pos.x - 1 { + if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size { + b.vel.x *= -1 + } + } + if b.pos.x > p.pos.x { + g.players[0].score++ + g.new_round() + } + } + } + if b.pos.x <= 1 || b.pos.x >= g.app.width { + b.vel.x *= -1 + } + if b.pos.y <= 2 || b.pos.y >= g.app.height { + b.vel.y *= -1 + } + b.update(dt) +} + +fn (mut g Game) quit() { + if g.app.mode != .game { + return + } + g.app.mode = .menu +} + +fn (mut g Game) draw_big_digit(px f32, py f32, digit int) { + // TODO use draw_line or draw_point to fix tearing with non-monospaced terminal fonts + mut gfx := g.app.tui + x, y := int(px), int(py) + match digit { + 0 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, '█ █') + gfx.draw_text(x, y + 2, '█ █') + gfx.draw_text(x, y + 3, '█ █') + gfx.draw_text(x, y + 4, '█████') + } + 1 { + gfx.draw_text(x + 3, y + 0, '█') + gfx.draw_text(x + 3, y + 1, '█') + gfx.draw_text(x + 3, y + 2, '█') + gfx.draw_text(x + 3, y + 3, '█') + gfx.draw_text(x + 3, y + 4, '█') + } + 2 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, ' █') + gfx.draw_text(x, y + 2, '█████') + gfx.draw_text(x, y + 3, '█') + gfx.draw_text(x, y + 4, '█████') + } + 3 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, ' ██') + gfx.draw_text(x, y + 2, ' ████') + gfx.draw_text(x, y + 3, ' ██') + gfx.draw_text(x, y + 4, '█████') + } + 4 { + gfx.draw_text(x, y + 0, '█ █') + gfx.draw_text(x, y + 1, '█ █') + gfx.draw_text(x, y + 2, '█████') + gfx.draw_text(x, y + 3, ' █') + gfx.draw_text(x, y + 4, ' █') + } + 5 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, '█') + gfx.draw_text(x, y + 2, '█████') + gfx.draw_text(x, y + 3, ' █') + gfx.draw_text(x, y + 4, '█████') + } + 6 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, '█') + gfx.draw_text(x, y + 2, '█████') + gfx.draw_text(x, y + 3, '█ █') + gfx.draw_text(x, y + 4, '█████') + } + 7 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, ' █') + gfx.draw_text(x, y + 2, ' █') + gfx.draw_text(x, y + 3, ' █') + gfx.draw_text(x, y + 4, ' █') + } + 8 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, '█ █') + gfx.draw_text(x, y + 2, '█████') + gfx.draw_text(x, y + 3, '█ █') + gfx.draw_text(x, y + 4, '█████') + } + 9 { + gfx.draw_text(x, y + 0, '█████') + gfx.draw_text(x, y + 1, '█ █') + gfx.draw_text(x, y + 2, '█████') + gfx.draw_text(x, y + 3, ' █') + gfx.draw_text(x, y + 4, '█████') + } + else {} + } +} + +fn (mut g Game) draw() { + mut gfx := g.app.tui + gfx.set_bg_color(white) + // Border + gfx.draw_empty_rect(1, 1, g.app.width, g.app.height) + // Center line + gfx.draw_dashed_line(int(f32(g.app.width) * 0.5), 0, int(f32(g.app.width) * 0.5), + int(g.app.height)) + border := 1 + mut y, mut x := 0, 0 + for p in g.players { + x = int(p.pos.x) + y = int(p.pos.y) + gfx.reset_bg_color() + gfx.set_color(white) + if x < f32(g.app.width) * 0.5 { + g.draw_big_digit(f32(g.app.width) * 0.25, 3, p.score) + } else { + g.draw_big_digit(f32(g.app.width) * 0.75, 3, p.score) + } + gfx.reset_color() + gfx.set_bg_color(white) + // Racket + gfx.draw_line(x, y + border, x, y + p.racket_size) + } + // Ball + gfx.draw_point(int(g.ball.pos.x), int(g.ball.pos.y)) + // gfx.draw_text(22,2,'$g.ball.pos') + gfx.reset_bg_color() +} + +fn (mut g Game) free() { + g.players.clear() +} + +// TODO Remove these wrapper functions when we can assign methods as callbacks +fn init(x voidptr) { + mut app := &App(x) + app.init() +} + +fn frame(x voidptr) { + mut app := &App(x) + app.frame() +} + +fn cleanup(x voidptr) { + mut app := &App(x) + app.free() +} + +fn fail(error string) { + eprintln(error) +} + +fn event(e &ui.Event, x voidptr) { + mut app := &App(x) + app.event(e) +} + +// main +mut app := &App{} +app.tui = ui.init({ + user_data: app + init_fn: init + frame_fn: frame + cleanup_fn: cleanup + event_fn: event + fail_fn: fail + capture_events: true + hide_cursor: true + frame_rate: 60 +}) +app.tui.run()