From 21cc0af5d8ab22f66ad43b5548aa280bb3020c44 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Fri, 6 Feb 2026 15:51:02 +0200 Subject: [PATCH] examples: add examples/gg/pong/pong.v (created by using Google gemini cli `Auto (Gemini 3)`) (#26528) --- examples/gg/pong/README.md | 44 +++++ examples/gg/pong/pong.v | 346 +++++++++++++++++++++++++++++++++++++ 2 files changed, 390 insertions(+) create mode 100644 examples/gg/pong/README.md create mode 100644 examples/gg/pong/pong.v diff --git a/examples/gg/pong/README.md b/examples/gg/pong/README.md new file mode 100644 index 000000000..d49b40ba0 --- /dev/null +++ b/examples/gg/pong/README.md @@ -0,0 +1,44 @@ +A small Pong game written in V using the `gg` and `sokol.audio` modules. + +## Features +- **Proportional Resizing**: The game scales ball, paddles, and speeds based on the window size. +- **Audio Effects**: Synthesized sounds for wall bounces, paddle hits, and scoring. +- **Modern-Retro UI**: Deep blue background with vibrant accent colors. +- **Real-time Metrics**: Displays ball speed in m/s (assuming a 10m wide playfield). + +## AI use note: +This example was created by Google gemini-cli `Auto (Gemini 3)`, using the following prompts: + +1. create a small Pong game using gg in examples/gg/pong.v . +2. the game should start not paused. Show the current score for each player on the corresponding + side of the screen. Show the current vertical position for each player below the score too. + Show a green "PAUSED, press space to unpause" label in the bottom middle of the screen, + when the game is paused. + Show a yellow label "Press space - pause; W/S - left player; Up/Down - right player." in the + bottom middle of the screen, when the game is not paused. +3. make the score and position labels green +4. make the ball be a filled white circle +5. make the paddles be slightly rounded +6. make the pause key work when a key_down event is received; it is not like the other keys +7. make the background very dark blue +8. be a good game designer, and tweak the colors and positions of all the screen elements, + if they do not look good. +9. move the bottom status text even more down, so that the last stripe of the center line does not + intersect it. Move the paused label in the center of the screen and make it bigger +10. move the paused text about 80 pixels to the top; make the bottom status text slightly bigger +11. move the paused text about 30 pixels down +12. use the `sokol.audio` module and add simple sound effects to the game: when the ball bounces + on a wall, it should produce a ping sound; when the ball bounces on a paddle it should produce + a pong sound (slightly lower pitched); when the ball escapes behind a paddle, and the game + resets, produce a low pitched buzzing sound +13. the sounds work fine, but there is a very slight click at the end, that is especially + pronounced at the end of the buzz sound +14. the buzz sound still has a slight click at the end +15. make the left paddle bight red, and the right paddle bright blue +16. assume that the whole playfield is 10 meters wide (5 on the left and 5 on the right side). + Calculate the ball speed. Show the ball speed in the top center of the screen + (for example `Ball: 5.2 m/s`, note it should be always rounded at + the first digit after the dot (i.e. show 5 as `Ball: 5.0 m/s`). +17. make sure to resize the ball, paddles and playfield proportionally, when the screen + is maximized +18. append all my current user prompts to examples/gg/pong/README.md diff --git a/examples/gg/pong/pong.v b/examples/gg/pong/pong.v new file mode 100644 index 000000000..140e786fa --- /dev/null +++ b/examples/gg/pong/pong.v @@ -0,0 +1,346 @@ +// vtest build: !openbsd +module main + +import gg +import rand +import sokol.audio +import math + +const paddle_speed_base = f32(400.0) +const ball_speed_base = f32(300.0) + +// Colors +const color_bg = gg.Color{10, 15, 30, 255} +const color_foreground = gg.Color{230, 230, 230, 255} +const color_accent = gg.Color{0, 255, 100, 255} +const color_secondary = gg.Color{0, 200, 80, 255} +const color_text_dim = gg.Color{150, 150, 150, 200} +const color_line = gg.Color{100, 100, 100, 150} +const color_p1 = gg.Color{255, 50, 50, 255} +const color_p2 = gg.Color{50, 100, 255, 255} + +struct SoundManager { +mut: + ping []f32 + pong []f32 + buzz []f32 +} + +fn (mut sm SoundManager) init() { + sample_rate := f32(audio.sample_rate()) + + // Ping (Wall) + ping_duration := f32(0.1) + ping_frames := int(sample_rate * ping_duration) + for i in 0 .. ping_frames { + t := f32(i) / sample_rate + // Use a steeper exponential-like decay for a cleaner "pluck" + env := f32(math.pow(f32(1.0) - t / ping_duration, 3)) + sm.ping << f32(0.3) * math.sinf(t * 800.0 * 2 * math.pi) * env + } + // Absolute silence buffer to prevent clicks + for _ in 0 .. 100 { + sm.ping << 0 + } + + // Pong (Paddle) + pong_duration := f32(0.1) + pong_frames := int(sample_rate * pong_duration) + for i in 0 .. pong_frames { + t := f32(i) / sample_rate + env := f32(math.pow(f32(1.0) - t / pong_duration, 3)) + sm.pong << f32(0.3) * math.sinf(t * 400.0 * 2 * math.pi) * env + } + for _ in 0 .. 100 { + sm.pong << 0 + } + + // Buzz (Reset) + buzz_duration := f32(0.3) + buzz_frames := int(sample_rate * buzz_duration) + for i in 0 .. buzz_frames { + t := f32(i) / sample_rate + env := f32(math.pow(f32(1.0) - t / buzz_duration, 2)) + // Add a harmonic for a more "buzzy" feel + val := f32(0.25) * math.sinf(t * 100.0 * 2 * math.pi) + + f32(0.1) * math.sinf(t * 200.0 * 2 * math.pi) + sm.buzz << val * env + } + for _ in 0 .. 200 { + sm.buzz << 0 + } +} + +struct App { +mut: + ctx &gg.Context = unsafe { nil } + width int = 800 + height int = 600 + paddle_width f32 + paddle_height f32 + ball_size f32 + p1_y f32 + p2_y f32 + ball_x f32 + ball_y f32 + ball_vx f32 + ball_vy f32 + p1_score int + p2_score int + paused bool + sounds SoundManager +} + +fn (mut app App) update_sizes() { + size := app.ctx.window_size() + app.width = size.width + app.height = size.height + + // Base scales on 800x600 + scale_x := f32(app.width) / 800.0 + scale_y := f32(app.height) / 600.0 + + app.paddle_width = 15.0 * scale_x + app.paddle_height = 80.0 * scale_y + app.ball_size = 15.0 * scale_x // Keep it circular based on width scale +} + +fn (mut app App) reset_ball() { + app.ball_x = f32(app.width) / 2 - app.ball_size / 2 + app.ball_y = f32(app.height) / 2 - app.ball_size / 2 + + scale_x := f32(app.width) / 800.0 + mut vx := ball_speed_base * scale_x + if rand.intn(2) or { 0 } == 0 { + vx = -vx + } + app.ball_vx = vx + app.ball_vy = (f32(rand.intn(301) or { 150 } - 150)) * (f32(app.height) / 600.0) +} + +fn (mut app App) reset_game() { + app.p1_score = 0 + app.p2_score = 0 + app.p1_y = f32(app.height) / 2 - app.paddle_height / 2 + app.p2_y = f32(app.height) / 2 - app.paddle_height / 2 + app.reset_ball() + app.paused = false +} + +fn on_frame(data voidptr) { + mut app := unsafe { &App(data) } + app.ctx.begin() + + // Draw dashed center line + line_segments := 20 + segment_height := f32(app.height) / line_segments + for i in 0 .. line_segments { + if i % 2 == 0 { + app.ctx.draw_rect_filled(f32(app.width) / 2 - 1, i * segment_height, 2, segment_height, + color_line) + } + } + + // Draw paddles + app.ctx.draw_rounded_rect_filled(20, app.p1_y, app.paddle_width, app.paddle_height, + 5, color_p1) + app.ctx.draw_rounded_rect_filled(f32(app.width) - 20 - app.paddle_width, app.p2_y, + app.paddle_width, app.paddle_height, 5, color_p2) + + // Draw ball + app.ctx.draw_circle_filled(app.ball_x + app.ball_size / 2, app.ball_y + app.ball_size / 2, + app.ball_size / 2, color_foreground) + + // Draw ball speed (10m = 800px => 1m = 80px) + // We scale the meter definition with width + meter_px := 80.0 * (f32(app.width) / 800.0) + speed_px := math.sqrt(app.ball_vx * app.ball_vx + app.ball_vy * app.ball_vy) + speed_ms := speed_px / meter_px + app.ctx.draw_text(app.width / 2, 20, 'Ball: ${speed_ms:.1f} m/s', + size: 20 + color: color_foreground + align: .center + ) + + // Draw scores and positions + // Moved up to be less obtrusive + app.ctx.draw_text(app.width / 4, 30, app.p1_score.str(), + size: 40 + color: color_accent + align: .center + ) + app.ctx.draw_text(app.width / 4, 80, 'Y: ${int(app.p1_y)}', + size: 16 + color: color_secondary + align: .center + ) + + app.ctx.draw_text(3 * app.width / 4, 30, app.p2_score.str(), + size: 40 + color: color_accent + align: .center + ) + app.ctx.draw_text(3 * app.width / 4, 80, 'Y: ${int(app.p2_y)}', + size: 16 + color: color_secondary + align: .center + ) + + if app.paused { + app.ctx.draw_text(app.width / 2, app.height / 2 - 50, 'PAUSED', + size: 64 + color: color_accent + align: .center + ) + app.ctx.draw_text(app.width / 2, app.height / 2 + 10, 'Press SPACE to Resume', + size: 20 + color: color_accent + align: .center + ) + } else { + app.ctx.draw_text(app.width / 2, app.height - 25, 'SPACE: Pause | W/S: P1 | UP/DOWN: P2 | R: Reset', + size: 16 + color: color_text_dim + align: .center + ) + } + + app.ctx.end() +} + +fn on_event(e &gg.Event, data voidptr) { + mut app := unsafe { &App(data) } + if e.typ == .resized || e.typ == .restored { + app.update_sizes() + } + if e.typ == .key_down { + match e.key_code { + .space { app.paused = !app.paused } + .r { app.reset_game() } + else {} + } + } +} + +fn on_update(dt f32, data voidptr) { + mut app := unsafe { &App(data) } + + if app.paused { + return + } + + scale_y := f32(app.height) / 600.0 + paddle_speed := paddle_speed_base * scale_y + + // Paddle 1 movement (W/S) + if app.ctx.pressed_keys[gg.KeyCode.w] { + app.p1_y -= paddle_speed * dt + } + if app.ctx.pressed_keys[gg.KeyCode.s] { + app.p1_y += paddle_speed * dt + } + + // Paddle 2 movement (Up/Down) + if app.ctx.pressed_keys[gg.KeyCode.up] { + app.p2_y -= paddle_speed * dt + } + if app.ctx.pressed_keys[gg.KeyCode.down] { + app.p2_y += paddle_speed * dt + } + + // Constrain paddles + if app.p1_y < 0 { + app.p1_y = 0 + } + if app.p1_y > f32(app.height) - app.paddle_height { + app.p1_y = f32(app.height) - app.paddle_height + } + if app.p2_y < 0 { + app.p2_y = 0 + } + if app.p2_y > f32(app.height) - app.paddle_height { + app.p2_y = f32(app.height) - app.paddle_height + } + + // Ball movement + app.ball_x += app.ball_vx * dt + app.ball_y += app.ball_vy * dt + + // Ball wall collision (Top/Bottom) + if app.ball_y <= 0 { + app.ball_y = 0 + app.ball_vy = -app.ball_vy + audio.push(app.sounds.ping.data, app.sounds.ping.len) + } else if app.ball_y >= f32(app.height) - app.ball_size { + app.ball_y = f32(app.height) - app.ball_size + app.ball_vy = -app.ball_vy + audio.push(app.sounds.ping.data, app.sounds.ping.len) + } + + // Ball paddle collision + // P1 + if app.ball_vx < 0 && app.ball_x <= 20 + app.paddle_width && app.ball_x >= 20 { + if app.ball_y + app.ball_size >= app.p1_y && app.ball_y <= app.p1_y + app.paddle_height { + app.ball_x = 20 + app.paddle_width + app.ball_vx = -app.ball_vx * 1.05 // Slightly speed up + // Add some vertical velocity based on where it hit the paddle + hit_pos := (app.ball_y + app.ball_size / 2) - (app.p1_y + app.paddle_height / 2) + app.ball_vy += hit_pos * 5 + audio.push(app.sounds.pong.data, app.sounds.pong.len) + } + } + + // P2 + if app.ball_vx > 0 && app.ball_x + app.ball_size >= f32(app.width) - 20 - app.paddle_width + && app.ball_x + app.ball_size <= f32(app.width) - 20 { + if app.ball_y + app.ball_size >= app.p2_y && app.ball_y <= app.p2_y + app.paddle_height { + app.ball_x = f32(app.width) - 20 - app.paddle_width - app.ball_size + app.ball_vx = -app.ball_vx * 1.05 // Slightly speed up + // Add some vertical velocity based on where it hit the paddle + hit_pos := (app.ball_y + app.ball_size / 2) - (app.p2_y + app.paddle_height / 2) + app.ball_vy += hit_pos * 5 + audio.push(app.sounds.pong.data, app.sounds.pong.len) + } + } + + // Score + if app.ball_x < 0 { + app.p2_score++ + app.reset_ball() + audio.push(app.sounds.buzz.data, app.sounds.buzz.len) + } else if app.ball_x > f32(app.width) { + app.p1_score++ + app.reset_ball() + audio.push(app.sounds.buzz.data, app.sounds.buzz.len) + } +} + +fn main() { + mut app := &App{} + app.width = 800 + app.height = 600 + app.paddle_width = 15.0 + app.paddle_height = 80.0 + app.ball_size = 15.0 + app.p1_y = f32(app.height) / 2 - app.paddle_height / 2 + app.p2_y = f32(app.height) / 2 - app.paddle_height / 2 + app.reset_ball() + + audio.setup(buffer_frames: 512) + app.sounds.init() + + app.ctx = gg.new_context( + width: app.width + height: app.height + window_title: 'V Pong' + user_data: app + frame_fn: on_frame + event_fn: on_event + update_fn: on_update + bg_color: color_bg + resizable: true + create_window: true + ) + + app.ctx.run() +} -- 2.39.5