// Copyright (c) 2020 Lars Pontoppidan. All rights reserved. // Use of this source code is governed by the MIT license distributed with this software. 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} ) [heap] struct App { mut: tui &ui.Context = unsafe { nil } mode Mode = Mode.menu width int height int game &Game = unsafe { nil } 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 = unsafe { nil } 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 := unsafe { &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 } [heap] struct Game { mut: app &App = unsafe { nil } players []Player ball Ball } fn (mut g Game) move_player(id int, x int, y int) { mut p := unsafe { &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 := unsafe { &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(mut app App) { app.init() } fn frame(mut app App) { app.frame() } fn cleanup(mut app App) { unsafe { app.free() } } fn fail(error string) { eprintln(error) } fn event(e &ui.Event, mut app App) { app.event(e) } fn 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()! }