Coding with AI for the First Time

Share
In this article, I'll document my experience trying Github's Copilot (model version GPT-4o) in VS Code (Visual Studio Code) to program a quick Pong game in Rust with Web Assembly in one day. Copilot is an AI assistant that can generate code for you. I've never used something like Copilot before, and I've read plenty of opinions on both sides of the spectrum: from programmers claiming AI is terrible and just generates bugs, to programmers saying it's 2025 and you HAVE to know how to use AI. Well, I don't know how to use this, so let's learn how it works. I also don't know Rust or Web Assembly, so this sounds like a great opportunity to learn all of it at once.

Summary: I managed to make the game in 8 hours, but I didn't learn Rust, and I spent a lot of time fighting Rust not working and Copilot replacing my broken Rust code with its Copilot's broken Rust code. On the plus side, it was a fun introduction to web assembly.

Play: https://www.virtualcuriosities.com/s/pong/vibe-coded/index.html

Source code: https://github.com/virtualcuriosities/pong-vibe-coded-rust-web-assembly

Second part: [Fixing Copilot's Code by Learning Rust]

Warning: the text below isn't edited. Rather than a review of Copilot it's simply my thoughts as I navigated through the project using Copilot. It's just long and shouldn't contain a lot of useful information. Sorry.

A screenshot of a game of pong with simple graphics.
The game. You just lost it.

Set up

Setting up Copilot on VS Code is very simple. All I needed to do was:

1: create a free Github account to get free access to Copilot. Copilot is an online service.

2: install the Copilot extension on VS Code. This can be found by simply searching for "Copilot" in VS Code's extensions. Since I don't know exactly what sort of information is going to be sent online, I created a separate profile on VS Code for this to separate this extension from the ones I normally use.

3: Copilot will show a welcome message telling you that you have to sign in in the main pane. Strangely there is no sign in button in this welcome message, but after you click on "Mark done" it becomes a side pane and then a link for signing in appears. The link will open in your web browser, where you can log in in your Github account and VS Code access to some information of your account. When you are finished you get a link to go back to VS Code and the process is complete.

Project Idea

The project we'll be making is just a simple Pong game. Done in Web Assembly because I want to learn how to use that. In Rust, because I also want to learn how to use that. We won't be using fancy canvas graphics because it's a pong game and we can just use CSS for the graphics.

Scaffolding

After creating a new blank project, an empty folder opened in VS Code as a workspace, with Copilot by my side the first question is: what can Copilot do?

Scaffolding is always a very boring part of any new project. Can I ask Copilot to do it? How? Thankfully Copilot developers put a "type /help to get help" right above the empty chat box so I could ask Copilot what it can do for me.

Generating a New Project with AI

After clicking on a link in the help output that read /new, I got @workspace /new added to the chat box. To be honest I didn't read what @ does. Either way, I prompted following:

@workspace /new  set up a simple rust web assembly project with a static html page

Copilot displayed the outline of a project and VS Code opened a dialog to choose a folder where to create it. That's not what I expected, to be honest, as I thought it would just create it on the empty workspace I had opened, but I can see why that wouldn't be the case. I chose the same folder I had opened and it added a rust-wasm-project to the workspace. Then I had to manually drag and drop the files out of that folder to the root of the workspace. Why can't the AI do all this manual labor for me?

An AI chat box. The user virtualcuriosities (with its avatar) says: @workspace /new set up a simple rust web assembly project with a static html page. To which "Workspace" answers: Sure, here's a proposed directory structure for a Rust WebAssembly porject with a static HTML page: a tree structure is displayed, rust-wasm-project as te root node, then a src directory with a lib.rs, and a static directory with index.html and style.css. Also on root are Cargo.toml and README.md. A button reads "Create workspace..." Below, a thumbs up and a thumbs down button, followed by a plus minus button. At the bottom, a text box reads "Ask Copilot." Under it, an @ button, a paper clip button, a microphone button, and and two dropdown list buttons, one that reads GPT-4o and another that is a rightward arrow.
Copilot's chatbox answering a query to create a new project.

Looking through the files it generated I see a README.md. How nice of the AI to write a README.md for the scaffold its mathematical models generated out of nothing and certainly haven't copy pasted from somewhere. It even has a section titled "Contributing" and a section called "License"!

Wait, License???!

## Contributing

Feel free to submit issues or pull requests if you have suggestions or improvements for the project.

## License

This project is licensed under the MIT License. See the LICENSE file for more details.

As the curious person I am, I have several questions.

Did the AI just git clone'd a random repo called rust-wasm-project? I can't seem to find it on Google. The README.md starts with "This project is a simple Rust application compiled to WebAssembly (Wasm) that can be run in a web browser" but Google can't find any webpage where this is written verbatim.

If this is generated by the AI model, what happens if it generates a license statement? Does it mean the code is licensed to me by Copilot? As far as I know, computer programs don't have rights, which means they also don't have copyright of anything they produce. If they can't own the code, they also can't license it, so that wouldn't make sense.

Let's say that there exists a rust-wasm-project repository that Google can't find with scaffold code for a web assembly project in Rust and it does have a license statement. Why, though? This has nothing to do with AI, but if I start a project using a MIT-licensed repository as starting point, at which point can I change the license and have full copyright of the project if ever? What happens if I never change the index.html? Is it just going to be MIT-licensed forever? I really don't enjoy these copyright ship of Theseus problems.

By the way, I don't have a LICENSE file. It says "See the LICENSE file" but there is no LICENSE file.

Well, I'm an AI programmer now trying to learn the ways of vibe coding, so I guess I'll just do as AI programmers do and ignore all this license stuff. As AI programmers say: "we busy building rn can't be bothered with legal".

Hello World

Okay, so I have a lib.rs, an index.html, and a cargo.toml. How do I run this. I can't just press Run -> Start Debugging or even build the project because Copilot didn't generate (copy?) a tasks.json file for VS Code. I'm pretty sure you can't just import .rs files into HTML, it needs to be compiled to whatever format web assembly requires. I don't really know any of this stuff. I'll just let the AI do it for me.

But first let me take a look at the code just to make sure this doesn't have anything that will just delete all my files when I press run.

So I have this Javascript the HTML file:

import init, { your_function_name } from '../pkg/rust_wasm_project.js';

async function run() {
    await init();
    document.getElementById('wasm-button').addEventListener('click', () => {
        const result = your_function_name();
        document.getElementById('output').innerText = result;
    });
}

run();

This doesn't sound right, for multiple reasons. First of all I'm pretty sure that your_function_name is a placeholder. Second of all I don't have a pkg folder. There is nothing in the .toml file that makes reference to this either. In the .rs I have:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    let greeting = format!("Hello, {}!", name);
    alert(&greeting);
}

I don't know if any of this is right but I already don't like it if you have to import every single browser function like that. I mean, I don't know if it's correct Rust, I'm pretty sure this is completely wrong given that greet doesn't even return anything for const result in the Javascript.

It's fine. Let's not be too harsh on the AI. This is a multi-language project, which is probably the reason for the discontinuity. After the scaffolding it's going to be mainly Rust OR Javascript so it's probably going to get things right more often.

Regardless, Copilot pretty much explained how web assembly is done (by finding and copying an existing repo?), and that's pretty useful already, assuming this is actually the right way to do it.

Now how do I run this?

I scroll up on Copilot's window to read the /help output.

You can ask me general programming questions, or chat with the following participants which have specialized expertise and can perform actions:

  • workspace - Ask about your workspace
    • /explain - Explain how the code in your active editor works
    • /tests - Generate unit tests for the selected code
    • /fix - Propose a fix for the problems in the selected code
    • /new - Scaffold code for a new file or project in a workspace
    • /newNotebook - Create a new Jupyter Notebook
    • /fixTestFailure - Propose a fix for the failing test
    • /setupTests - Set up tests in your project (Experimental)
  • vscode - Ask questions about VS Code
    • /search - Generate query parameters for workspace search
    • /startDebugging - Generate launch config and start debugging in VS Code (Experimental)
  • terminal - Ask how to do something in the terminal
    • /explain - Explain something in the terminal
  • github - Get answers grounded in web search, code search, and your enterprise's knowledge bases

Since I'm missing a tasks.json, I assume I need /new so it creates a new file.

@workspace /new generate code to build lib.rs as pkg/rust_wasm_project.js and run the project from static/index.html

This seems to work at first, but upon further inspection I see a button to "Create Workspace." Copilot says "Sure, here's a proposed directory structure for your Rust WebAssembly project:" I don't want to create a new project, just new files. Also no tasks.json in the proposed document outline.

I guess you use /fix to create new files without creating a whole workspace, then?

@workspace /fix generate a tasks.json to build lib.rs as pkg/rust_wasm_project.js and run the project from static/index.html

This made Copilot write the tasks.json code in the chat box. What? Am I supposed to create a new file myself and copy and paste it? That's manual labor! Why can't the AI do this.

Now I'd like to retry this with /new just to see if it works but I'm pretty sure free Copilot accounts have a limit on how many times you can use it and I'd like to see if I can complete this project before I hit the limit. In fact, this is probably a good time to check those limits.

For developers looking to get started with Copilot.

  • Includes up to 2,000 code completions in IDEs per month
  • Includes up to 50 total chat requests per month in Copilot Chat
https://docs.github.com/en/copilot/managing-copilot/managing-copilot-as-an-individual-subscriber/getting-started-with-copilot-on-your-personal-account/about-individual-copilot-plans-and-benefits (accessed 2025-04-14)

I see. I've been using AI wrong all this time. I was supposed to be completing code in the IDE, and not using the chat box that appears when you install it. Well, I have 47 chat requests left, that should be enough to get this project to build at least.

The tasks.json that Copilot generated contains two tasks:

[{
    "label": "Build Rust WebAssembly",
    "type": "shell",
    "command": "wasm-pack",
    "args": [
        "build",
        "--target",
        "web",
        "--out-dir",
        "../static/pkg"
    ],
},
{
    "label": "Serve Static Files",
    "type": "shell",
    "command": "python3",
    "args": [
        "-m",
        "http.server",
        "8080",
        "--directory",
        "static"
    ],
}]

I'm pretty sure this is wrong, after all ../static/pkg is going to be outside the workspace. Using Python for the local server is fine with me, however it would be simpler to just open the local file in the web browser directly.

I don't have a wasm-pack.

@workspace install wasm-pack

Copilot says "To install wasm-pack, run the following command in your terminal:" What, me? Why don't you do it?

Observation: every time I select a portion of the text Copilot outputs to paste here by pressing Ctrl+C it just copies the entire output of the answer.

Okay, so just cargo install wasm-pack and, wow, that's a lot of dependencies. "error: failed to compile wasm-pack v0.13.1," "package home v0.5.11 cannot be built because it requires rustc 1.81 or newer, while the currently active rustc version is 1.75.0." What is "home," why does wasm-pack depend on it, and why does it require a newer version of rustc? It says I can try rerunning cargo install with --locked, so I guess I'll just do that instead of updating.

...what is human-panic...

After installing it, I try to build again and wasm-pack is still not found. I'm pretty sure I need to add Rust's bin directory to PATH to make this work, but that sounds like a hassle.

what is the npx equivalent in rust that lets me run wasm-pack without putting it in PATH?

Copliot: "Rust does not have a direct equivalent to npx, but you can run wasm-pack without adding it to your PATH by using Cargo's install directory. When you install wasm-pack with Cargo, it is placed in the .cargo/bin directory in your home folder."

This sounds terrible, but I guess it works, so I'll change tasks.json to use ~/.cargo/bin/wasm-pack as command instead.

It runs, but it doesn't work. "Error: wasm32-unknown-unknown target not found in sysroot: "/usr"". What does this even mean.

No, seriously, what does this even mean. What could have gone wrong.

Did I install wasm-pack wrong, did I forget some dependency, do I need some extra configuration, is tasks.json written correctly, where could the mistake possibly be. I have no idea because the AI made it all up.

At this point, I feel it would be easier to just follow a tutorial on how to set up web assembly for Rust since at least that guarantees a working scaffold. In fact, I'm just going to do that.

Non-Rustup setups

wasm-pack compiles your code using the wasm32-unknown-unknown target. wasm-pack will automatically add this target for Rustup setups if you don't already have it installed by doing rustup target add wasm32-unknown-unknown. However, if you're not using Rustup, then we won't be able to do this automatically, and you'll have to do this yourself.

[...]

To manually add the wasm32-unknown-unknown target you will need to download it from the rust-lang website and put the contents in the correct folder.

https://rustwasm.github.io/docs/wasm-pack/prerequisites/non-rustup-setups.html#non-rustup-setups (accessed 2025-04-12)

It's like I'm programming C++ again. I could use rustup but I'm not sure I trust its hundreds line long curl pipe bash install script. It says I should copy a tarball to rustc --print sysroot. That's in /usr for me and /home in the tutorial. /usr/lib/rustlib seems to have similar contents (x86_64-unknown-linux-gnu) so I guess I'll just have to sudo mv this. I don't know why mine is different. Let's try building again.

"error[E0514]: found crate core compiled by an incompatible version of rustc." What? Why. Do you want x86_64-unknown-linux-gnu or wasm32-unknown-unknown? Make up your mind.

Okay, it seems the problem is that I installed rust using apt install when I should have used rustup all along. I guess I'll just apt remove rustc and reinstall the curl pipe bash way. I tried running cargo build twice and now I no longer have a /usr/bin/cargo? Reopening the terminal fixed it.

I hate dealing with build systems, why can't the AI just fix this for me so I can focus on the fun part of actually programming things?

I undo the change on tasks.json so the command is just wasm-pack again like Copilot wrote it. A new terminal can find it, but VS Code can't. Reloading the window doesn't fix it, so I guess I'll just close VS Code and reopen it. Now wasm-pack works.

"Your wasm pkg is ready to publish". Thanks, I wanted to read those words. As expected it put the output in the wrong folder outside the workspace because Copilot wrote it ../pkg. I just noticed it was ../static/pkg, actually, so more wrong than I thought.

I see this produces a .wasm file together with a .js file with 200 lines of code to load it, plus a couple of .d.ts files for Typescript. Well, static/index.html can't load this pkg/rust_wasm_project.js, and that isn't a static file, so I guess I'll just symlink static/pkg -> pkg and fix the import so I can open this in my web browser already.

Why do I get CORS errors for importing from './pkg/rust_wasm_project.js'? It's in a subdirectory, for Christ's sake. I guess I'll have to launch the local server as well.

"Uncaught SyntaxError: The requested module './pkg/rust_wasm_project.js' does not provide an export named 'your_function_name'" Makes sense. What functions does it provide, though? Can I just import greet instead?

import init, { greet } from './pkg/rust_wasm_project.js';

async function run() {
    await init();
    document.getElementById('wasm-button').addEventListener('click', () => {
        greet("Hello World!");
    });
}

run();

Okay, this works. This actually works. Finally. I have Rust code, I have Javascript code, I can actually start coding stuff now.

This sounds like a good place to set up a save point, I mean, a git commit.

Git Commit Messages???

While I was writing my second initial git commit message in VS Code because I forgot to add the symlink in the first commit I pressed Tab and Enter and that didn't press the large commit button under the text box but a barely visible icon on its right that was for generating commit messages with AI.

It turned my "add pkg symlink" into "add symlink for pkg directory in static". I wonder if this counted as a chat or as a code completion.

Declaring Classes

The first thing we need in our little pong game is a class to hold the data of the ball. I don't know how to create classes in Rust, so I'm going to ask Copilot to handle this for me.

@workspace /new create a ball.rs file with a class for a ball in a pong game with x and an y member fields.

This time it actually gives me a button that says "Create file" instead of workspace. But that button still opens a file dialog for me to choose where to save ball.rs. Manual labor. I wish it would just put it in src/ automatically.

pub struct Ball {
    pub x: f32,
    pub y: f32,
    pub dx: f32,
    pub dy: f32,
}

impl Ball {
    pub fn new(x: f32, y: f32, dx: f32, dy: f32) -> Self {
        Ball { x, y, dx, dy }
    }

    pub fn update_position(&mut self) {
        self.x += self.dx;
        self.y += self.dy;
    }

    pub fn reset_position(&mut self, x: f32, y: f32) {
        self.x = x;
        self.y = y;
    }
}

So this is Rust, I... think? I didn't ask for movement vectors but I'll probably need it anyway so thanks, I guess. Normally I'd try compiling this but we busy building rn can't be bothered with that. What's the other thing called in pong? Racket? Pad?

@workspace /new create a src/pad.rs file with a class for a pad in a pong game with x and y member fields.

Copilot: "Sorry, I can’t assist with that." What do you mean??? You just did it. Just do it again!

I feel like if I knew how to write these classes myself it would be faster to do it than to ask Copilot to do it so I'm just going to copy and paste the code.

@workspace /fix add a Pong struct to src/ball.rs with left, top, bottom, and right fields.

Copilot just gave me the code. I have to copy and paste it. Manual labor. Why not button to just put it in the file with a single click? I feel like I'm using this wrong.

Wait, there is actually a button to "apply in editor" but it ONLY APPEARS when you hover over the code snippet. I wonder if the person who decided to put the "type /help for help" explicitly above the chat box is the same person who decide to just hide buttons like this.

Pressing Apply in Editor asks you to choose which open editor to apply it to, then displays a scanning animation, and then you can see the changes and press a Keep button to keep them or Undo to remove them. Looks cool but seems to take too long for simple additions. Fortunately there is also a Insert at Cursor button that should quickly insert the code at the cursor.

...does analyzing the file count as a code completion...?

pub struct Pong {
    pub left: f32,
    pub top: f32,
    pub bottom: f32,
    pub right: f32,
}

impl Pong {
    pub fn new(left: f32, top: f32, bottom: f32, right: f32) -> Self {
        Pong { left, top, bottom, right }
    }
}

After 20 years we're still inventing new ways to not have to manually program a boilerplate constructor instead of just getting rid of it at language level. I guess some things never change.

Importing Classes

My classes are in ball.rs, how do I get them to lib.rs where my other code is?

@workspace /fix import ball, pad, and pong from ball.rs into lib.rs

Pressing Apply to Editor didn't prompt me to select an editor this time. It added two lines of code:

mod ball;

use ball::{Ball, Pad, Pong};

I'm going to assume you just need both mod and use in Rust for some reason and Copilot got it right.

Displaying Graphics

The next thing we have to do is very difficult. We need to somehow display a square on the screen. Can we do this from web assembly?

@workspace /fix create a function in lib.rs to add a div to <body> and give it the class "ball"

Copilot:

use wasm_bindgen::prelude::*;
use web_sys::{window, Document, Element};

#[wasm_bindgen]
pub fn add_ball_div() {
    // Get the window and document objects
    let window = window().expect("should have a window");
    let document = window.document().expect("should have a document");

    // Create a new div element
    let div = document.create_element("div").expect("should create a div");
    div.set_class_name("ball");

    // Append the div to the body
    let body = document.body().expect("should have a body");
    body.append_child(&div).expect("should append the div");
}

What in the world is expect and why is there an expect after every single statement in Rust? This feels like it should be called assert. I mean I can tell what it is doing but that's such a weird name.

Why is everything snake cased now. set_class_name? Why is it coming from web_sys now? Didn't I need an extern to get alert into Rust before? I mean this is better but I don't really understand the difference. Does it have to be different? Did Copilot just happen to generate a terrible way to use the browser API that one time by chance? I don't know.

I'll need to set the left and top style using a Ball's fields and then call this on greet just to make sure ball.rs gets compiled. I don't know how instantiate classes in Rust, so I'll ask Copilot to do it for me.

I clicked on the code pane this time and there is an autocompletion of a comment for setting the initial position of the div. What a silly concept, why would I need to autocomplete a comment? Wait... what if...

// Instantiate a Ball object
let ball = Ball::new(0.0, 0.0, 1.0, 1.0);

Amazing. If I type // Instantiate a Ball object the next line Copilot can guess what it's supposed to do and I can just press Tab to make it appear. So this is what a completion is. Which means 2000 code completions actually means 2000 lines of code at minimum?

An animated image displaying an example of Copilot's code completion in action. After // Instantiate a ball object is typed, Copilot guesses what is the next line of code that should be written: let ball = ball::Ball::new..., and adding this line takes only a single key press.
A recording of Copilot's code completion in action. After manually typing a code comment, Copilot guesses what the next line of code should be an displays it in the editor. Pressing the Tab key inserts the Copilot's code.
// Set the initial position of the div
div.set_attribute("style", &format!("left: {}px; top: {}px;", ball.x, ball.y))
        .expect("should set style");

Perfect, I'm not even going to read this. It's too verbose.

Okay, fine I'll read it. Because I'm learning Rust. Why &? Does Rust have variadic arguments or is this a template? Why !? I'm guessing this is a template since it's called format it should accept any type.

Alright, let's see if this compiles.

It doesn't compile. Unused imports? Really? That is a warning, not an error. Wait, never mind the error is "use of unresolved module or unlinked crate web_sys".

Copilot, help me here, it's your code.

@workspace /fix use of unresolved module or unlinked crate web_sys

Copilot: "The error indicates that the web_sys crate is not included in your project dependencies. To resolve this issue, you need to add the web-sys crate to your Cargo.toml file and enable the necessary features. Steps to Fix: Add web-sys to Cargo.toml: Open your Cargo.toml file and add the following: [...]"

[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement"] }

Come on, you know where cargo.toml is. It's your code.

Can I press Apply in Editor while lib.rs is open? Nope. It doesn't work. It will just add the code to lib.rs and that kind of looks like it would work considering the syntax is very similar to Rust but then it won't compile. I have to open cargo.toml. Manually.

I hate dealing with dependencies, why can't the AI just fix this for me so I can focus on the fun part of actually programming things?

After fixing the dependencies and recompile the project, I can see in developer tools in Chrome that I do in fact have a ball div now. Hurray!

Good spot to commit. What was I even doing again?

Copilot: "update dependencies and add ball module for Pong game functionality"

Perfect. There is something fundamentally off with the way this sounds and is phrased, but instead of writing a commit message I just click on a button now, so that works for me. What am I going to do with all this time I saved from not having to write single line commit messages?

Displaying Graphics but For Real This Time

The next things we need to do are very simple and straightforward:

  • Remove the scaffold code Copilot generated we won't use.
  • Move the Rust entry point from a button event handler to when the web page loads.
  • Create divs for the pads and the field.
  • Reparent the pads and ball div to the field div.
  • Add width and height fields to Pad and Ball and set the dimensions of the divs.
  • Add a CSS background and position: absolute and relative to actually display everything on the screen.

Naturally this should be a single commit like "fix code" or something. We already know how to do all of this in Rust from the code that Copilot generated so there shouldn't be any need to rely on the AI.

While editing the CSS, I added a .ball class that Copilot autocompleted to have a bouncing animated, 50px width and height, a border radius, and a red background. I changed the color to white and the size to 0px. When adding a .field class I thought the autocompletions were a little distracting. I was thinking what was I supposed to do next and suddenly this grey text appear on the screen inducing me to read it. My brain can't read text and think at the same time so I'd rather not have these interruptions constantly. When I made the .pad class, it autocompleted 0px width and height and white background. Since normally you would never use 0px for both width and height I assume this is the LLM autocompleting from context.

I just noticed I can't rename things in Rust in VS Code. I installed rust-analyzer to fix this problem. It added the type of everything after the name of the variables. I hate this. But at least I know what type things are now.

After writing the code for field_div and ball_div, I'm pleased to say I just pressed tab 6 times for these:

// Create pad divs
let left_pad_div = document.create_element("div").expect("should create a div");
left_pad_div.set_class_name("pad");
field_div.append_child(&left_pad_div).expect("should append the div");

let right_pad_div = document.create_element("div").expect("should create a div");
right_pad_div.set_class_name("pad");
field_div.append_child(&right_pad_div).expect("should append the div");

The only thing I had to type was let left_pad_div = and let right_pad_div =. Clearly this should be a function instead but why write a function to reuse code so I don't have to write code when Copilot can just write the code for me so I don't have to write code?

I tried to refactor the structs to use a Point struct and then a Box struct but apparently I can't use pub box: Box? I guess I'll just name it Rect.

While changing the constructor of Pong I got my largest autocompletion yet. Everything between Pong { and the last } was guessed by Copilot:

Pong {
    field: Rect {
        top_left: Point { x: left, y: top },
        size: Point {
            x: right - left,
            y: bottom - top,
        },
    },
    ball: Ball {
        rect: Rect {
            top_left: Point { x: 0.0, y: 0.0 },
            size: Point { x: 10.0, y: 10.0 },
        },
        speed: Point { x: 1.0, y: 1.0 },
    },
    left_pad: Pad {
        rect: Rect {
            top_left: Point { x: left, y: (top + bottom) / 2.0 },
            size: Point { x: 10.0, y: 50.0 },
        },
    },
    right_pad: Pad {
        rect: Rect {
            top_left: Point { x: right - 10.0, y: (top + bottom) / 2.0 },
            size: Point { x: 10.0, y: 50.0 },
        },
    },
}

This is a bit wrong, of course. It's impressive that it got some parts right, like aligning the y of pads to the mid point of top and bottom and putting the left pad on the left and the right pad on the right, but it kept the ball at the origin position (top-left corner) for some reason.

It may sound not that bad because it's only "a bit" wrong, but the problem is that I don't know what bit is wrong unless I read it. At least I now know that I can just type field:, ball:, etc., in Rust to define a struct.

I tried to rewrite using a constant let center = ... at the start but I'm getting an error when I try to use center.y after setting top_left: center for the ball? "use of moved value: center"? It's a struct. Just pass it around by value. I'm not even calling a constructor here. Am I? I'm going to guess I need & here? That didn't work so I guess it's time for the AI.

I just noticed when I type /fix in the chat box it says "Propose a fix for the problems in the selection" so I guess I'll just put the text cursor on the error and type just /fix instead of explaining it on the chat box.

@workspace /fix

Copilot: "The issue arises because the center value is moved when it is used in the ball initialization. To fix this, you can clone the center value when it is used multiple times. Here's the corrected code:"

Wow, that actually worked. I thought I'd have to actually select the text, but simply placing the text cursor in the middle of a word with a squiggly line seems to be enough.

impl Pong {
    pub fn new(left: f32, top: f32, bottom: f32, right: f32) -> Self {
        let pad_margin: f32 = 10.0;
        let center = Point { x: (right - left) / 2.0, y: (bottom - top) / 2.0 };

        Pong {
            field: Rect {
                top_left: Point { x: left, y: top },
                size: Point {
                    x: right - left,
                    y: bottom - top,
                },
            },
            ball: Ball {
                rect: Rect {
                    top_left: center.clone(), // Clone center for reuse
                    size: Point { x: 10.0, y: 10.0 },
                },
                speed: Point { x: 10.0, y: 10.0 },
            },
            left_pad: Pad {
                rect: Rect {
                    top_left: Point { x: left + pad_margin, y: center.y }, // Use center.y directly
                    size: Point { x: 10.0, y: 50.0 },
                },
            },
            right_pad: Pad {
                rect: Rect {
                    top_left: Point { x: right - pad_margin, y: center.y }, // Use center.y directly
                    size: Point { x: 10.0, y: 50.0 },
                },
            },
        }
    }
}

Okay, so I get I need to type center.clone() instead of just center, but Copilot added comments to the lines that weren't bugged for some reason, so when I click on Apply in Editor those working lines will be modified. Oh well, that's a small price to pay for the convenience of AI.

That should work, so why do I have a new squiggly line? "no method named clone found for struct Point in the current scope". Copilot?

"The issue arises because the Point struct does not implement the Clone trait, which is required to use the .clone() method. To fix this, you need to derive the Clone trait for the Point struct. Here's the corrected code:"

#[derive(Clone)] // Derive the Clone trait for Point
pub struct Point {
    pub x: f32,
    pub y: f32,
}

Okay, this sound excessive just to pass a struct by value, but I guess that's how you do things in Rust.

No more squiggly lines. Perfect.

This is great. I can just write wrong code and tell Copilot to fix it. self.rect.top_left += self.speed. Squiggly line? /fix. I don't even need to read the explanation because I can just click Apply in Editor and then check the diff. If it looks to me I keep it. It's worked so far, except for the times it was wrong.

"cannot move out of self.speed which is behind a mutable reference. move occurs because self.speed has type Point, which does not implement the Copy trait. ball.rs(76, 41): consider cloning the value if the performance cost is acceptable: .clone()"

What do you mean .clone() has a performance cost? I'm programming Rust for the performance. This isn't Javascript. Copilot, /fix this right now.

For some reason the code snippet is the entire file even when there is only on line changed. It doesn't matter either way since I'm not going to read it, I'll just click Apply in Editor.

impl Ball {
    pub fn update_position(&mut self) {
        self.rect.top_left += self.speed.clone(); // Clone speed to avoid moving it
    }
}

This isn't how I wanted to fix it. I guess that's on me for not writing anything after /fix. I started with 50 chat requests and I just spent one to add 8 characters of text to the source code. AI is a powerful drug. I wonder how many I have left.

Premature optimization is the root of all evil so I guess I'll just wait until next month when the quota resets to optimize this with Copilot.

Autocompletion is very good at simple yet repetitive tasks.

let pong = Pong::new(0, 0, 640, 480);
let ball = pong.ball;

// I just tabbed the rest

let left_pad = pong.left_pad;
let right_pad = pong.right_pad;
let field = pong.field;

It's really good at guessing what I want.

pub fn set_pos (tab!) ition(element: &Element, x: f32, y: f32) {
    element.set_attribute("style", &format!("left: {}px; top: {}px;", x, y))
        .expect("should set style");
}

I'd have to look up what type should element be if it weren't for Copilot. Now I'm going to assume it's correct until an error shows up. Nevertheless I keep having to read the code that Copilot writes to make sure it makes sense. I don't like having to do that.

Is that what "vibe coding" is? Just assume it works by default and check later? I'll see if I can get more in the vibes. That sounds magnificient.

pub fn set_size(element: &Element, width: f32, height: f32) {
    element.set_attribute("style", &format!("width: {}px; height: {}px;", width, height))
        .expect("should set style");
}

I'm not reading the code above. I just typed set_size and the rest appeared like magic. Let's hope it works like magic as well!

I notice now that this used two f32 instead of my Point struct, but it's fine because Copilot autocompletes the y field after I type the x field in an argument.

set_position(ball_div, ball.rect.top_left.x, ball.rect.top_left.y);
set_size(ball_div, ball.rect.size.x, ball.rect.size.y);

"expected &Element, found Element". Okay, I don't need AI for this. I'll just add the & myself. What does it do? Pass by reference? Pass it mutable? I don't know and I'm probably not going to find out before I ship this.

"expected f32, found integer". Seriously? Okay, 0, 0, 640, 480 to 0.0, 0.0, 640.0, 480.0. I don't need AI for this. I've deal with C++ before.

Okay, it compiles. Lots of unused stuff but it compiles. And I have a left pad. In Rust. But no right pad for some reason. And no ball. And it's at the top of the screen instead of in the middle. Whose fault is this? Is my code buggy or Copilot's?

I can't tell Copilot to /fix what I'm seeing in the web browser. An LLM had no way to figure out what I'm talking about. It has no concept of graphics, so there is no way it can figure out what part of the source code, no matter how simple, fails to place the right pad on the screen.

But I'm going to try anyway just to see what happens..

@workspace /fix my code was supposed to display a left pad, a right pad, and a ball in the web browser but only the left pad appears. Please fix it so the right pad and the ball also appear. Thanks.

...why did I write please and thanks...?

Copilot: "The issue lies in the set_position and set_size functions. Both functions overwrite the style attribute of the elements, which causes only the last style (position or size) to take effect. To fix this, you need to combine the left, top, width, and height styles into a single style attribute. Here’s the corrected code:"

Oh, yeah, that makes sense. In Javascript you would just access the style property directly but in Rust you have to (do you really have to?) overwrite the style property every time which complicates things.

pub fn set_position_and_size(element: &Element, x: f32, y: f32, width: f32, height: f32) {
    element
        .set_attribute(
            "style",
            &format!(
                "position: absolute; left: {}px; top: {}px; width: {}px; height: {}px;",
                x, y, width, height
            ),
        )
        .expect("should set style");
}

I have a feeling deep in the bottom of my heart—or is it my stomach?—that this is definitely not the right way to do this, but I only need to call this in 2 places, tops, so I'll just take it. Apply!

Rebuild, refresh, and there it is, it actually works.

It's not perfect because I made some mistakes, but apart from that it's already looking like Pong except nothing moves yet.

Also I noticed that I had removed "create left pad" and "create right pad" comments to turn them into a single comment "create pads" but Copilot removed my comment and re-added the ones I had removed. I guess Copilot writes the comments for my code now.

"cannot divide f32 by {integer}". Again? I'm just going to add a zero to it. It's a literal. Come on! Divide by 2, divide by 2.0, it's the same thing!

Now that it looks perfect we can commit it. Copilot, what have you done?

Copilot: "refactor Pong game structure and enhance ball and pad functionality"

Exactly.

Game Controls

Next I want to be able to move the player pad. For this I just want it to go toward the mouse pointer at a constant speed. We could do this on mouse move events, but we'll use a main game loop to do this because we'll need it later for physics and collision checking.

In Javascript this can be done simply with setTimeout. It's not high precision but this is a pong game. I wonder if I can listen to events from web assembly?

// add a setTimeout event handler

That should do it. Now we wait.

let closure = Closure::wrap(Box::new(move || {
    alert("Hello from Rust!");
}) as Box<dyn Fn()>);

What? No. That's not even close to what I said. What kind of autocompletion is that?

It turns out if I accept this autocompletion it gives the rest in the next two lines as separate autocompletions. I'm going to assume this all counts separately for my quota. Also is the autocompletion when it displays the autocompletion or when I take it?

window
    .set_timeout_with_callback_and_timeout_and_arguments_0(
        closure.as_ref().unchecked_ref(),
        1000,
    )
    .expect("should register timeout");
closure.forget(); // Prevent the closure from being dropped

Looks good to me. No idea what a Box is or what move || is doing, or why is this so verbose compared to Javascript, but so long as it works. Wait, set_timeout_with_callback_and_timeout_and_arguments_0? Is that a real function? That zero doesn't sound good. Does Rust not have function overload? It's a typed language?

// set pad position from mouse position
let mouse_x = window
    .event()
    .expect("should have event")
    .client_x()
    .expect("should have client_x");

This is definitely wrong. I'm not really sure if you need to capture the pointer first with an event but I guess I'm about to find out.

Never mind, squiggly line.

"no method named expect found for struct JsValue in the current scope".

Copilot?: "The issue is that the window.event() method is not available in modern browsers and is not part of the web_sys crate. Instead, you should use an event listener to capture mouse events and update the position of the left pad accordingly."

So why did you add it. I keep having to tell Copilot to /fix its own mistakes.

The suggested fix is the entire file again so I just have to keep apply. If they charge separately for that it means when you ask Copilot to fix something you get charged 4 times because you're guaranteed to have to ask it to fix its own code. That sounds like a great business model. Sell the solution to the problem you have sold.

// Add mousemove event listener to update left pad position
let left_pad_div = left_pad_div.dyn_into::<HtmlElement>().expect("should be an HtmlElement");
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
    let mouse_y = event.client_y() as f32;
    left_pad.rect.top_left.y = mouse_y - left_pad.rect.size.y / 2.0;
    set_position_and_size(
        &left_pad_div,
        left_pad.rect.top_left.x,
        left_pad.rect.top_left.y,
        left_pad.rect.size.x,
        left_pad.rect.size.y,
    );
}) as Box<dyn FnMut(_)>);

window
    .add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())
    .expect("should add mousemove event listener");
closure.forget(); // Prevent the closure from being dropped

I clicked Keep without checking. It just replaced all the code. Now I don't know how to set the timeout! Good thing I wrote it all down on this blog post.

By the way what is let left_pad_div = left_pad_div doing. Why can I redeclare variables in Rust? Are you telling me I MUST type 2.0 but reusing the same name for different things is alright? Wow. Okay. I'll just never use this. Rename to html_left_pad_div. Wait, that redeclaration isn't even used by the code Copilot generated?!

"unresolved import web_sys::MouseEvent. no MouseEvent in the root". I see there is a link to fix using copilot when you hover over a squiggly line so you don't even need to type /fix. That should help speed things up.

Copilot: "The issue is that MouseEvent is not part of web_sys; instead, it should be imported from wasm_bindgen::JsCast."

You know, I've always felt it shouldn't be that hard to just index every single function and class name so you can always quickly import something even if you don't know the specific package or module it came from, but for some reason every language server I've ever deal with in VS Code fails to do that. I see we need AI to solve this issue?

use web_sys::{window, Document, Element, HtmlElement};
use wasm_bindgen::JsCast; // Import JsCast for MouseEvent casting

This doesn't even work.

"the name JsCast is defined multiple times". IT WAS ALREADY IMPORTED???

This API requires the following crate features to be activated: MouseEvent

https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.MouseEvent.html (accessed 2025-04-12)

Alright, so I was missing a dependency in Cargo.toml. Seriously, why no AI for this?

"cannot assign to left_pad.rect.top_left.y, as it is not declared as mutable". What does this mean? Are fields constant by default in Rust? That doesn't make sense.

Copilot: "The problem is that left_pad is not declared as mutable, so you need to add the mut keyword to its declaration to allow modification."

What.

let mut left_pad = pong.left_pad;

What. That's definitely not what I want to do. Or is it? I'm not even sure.

Okay, I'll just use pong.left_pad directly in the closure and then I'll have to OH MY GOD.

pong.left_pad.rect.top_left.y = mouse_y - pong.left_pad.rect.size.y / 2.0;
let left_pad = pong.left_pad;
set_position_and_size(
    &left_pad_div,
    left_pad.rect.top_left.x,
    left_pad.rect.top_left.y,
    left_pad.rect.size.x,
    left_pad.rect.size.y,
);

Noooooooooo. Why?

And I get an error?

"expected a closure that implements the FnMut trait, but this closure only implements FnOnce". I absolutely can't understand what this means. I mean I can understand it but I have no idea what it expects me to do. "closure is FnOnce because it moves the variable pong.left_pad out of its environment".

I just want to pass pong by reference into the closure.

@workspace /fix pass pong as a reference into the closure

Copilot: "To pass pong as a reference into the closure, you can wrap it in an Rc<RefCell<T>> to allow shared ownership and interior mutability. This is necessary because closures in Rust require captured variables to implement the FnMut trait, and Rc<RefCell<T>> provides a way to mutate shared data safely."

Wow, this makes C++ closures look elegant by comparison. So in Rust I need to define the closure, drop the closure, and wrap the values in not one but two templates? I can't believe I'm saying this but I miss C++. I wonder if you can compile C++ to web assembly.

let pong = Rc::new(RefCell::new(Pong::new(0.0, 0.0, 640.0, 480.0)));
let ball = pong.borrow().ball.clone();
let left_pad = pong.borrow().left_pad.clone();
let right_pad = pong.borrow().right_pad.clone();
let field = pong.borrow().field.clone();

// ...

let pong_clone = Rc::clone(&pong);
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
    let mouse_y = event.client_y() as f32;
    let mut pong = pong_clone.borrow_mut();

This can't be the right way to do this.

"no method named clone found for struct Ball in the current scope". Oh dear Lord, the performance. And I'm not supposed to clone this because I'll set the position. Okay, I guess I'll just remove these aliases then.

Okay. It compiled. I genuinely have no idea what I was even doing. The left pad moves with the mouse. The right pad is in the wrong position now and I didn't even touch so I have no idea why.

One one hand, Copilot isn't really helping me learn Rust since I still have no idea what I'm doing. On the other hand, it's helping me program in Rust even if I have no idea what I'm doing. Look at my beautiful AI-generated code:

// clamp the left pad position to the field
if pong.left_pad.rect.top_left.y  < pong.field.top_left.y{
    pong.left_pad.rect.top_left.y = pong.field.top_left.y;
}

if pong.left_pad.rect.top_left.y  > pong.field.top_left.y + pong.field.size.y - pong.left_pad.rect.size.y {
    pong.left_pad.rect.top_left.y = pong.field.top_left.y + pong.field.size.y - pong.left_pad.rect.size.y;
}

I definitely wouldn't have written this without AI.

Okay, it seems I had swapped x and y for the right pad at some point. Bug fixed.

I forgot about it but the set_position is changing my relative field to absolute. It needs to be relative so I can center it. Bug fixed.

Fixing this bug created a bug: the mouse pointer coordinates aren't offset my the field's div position. This should also be easy to fix. Wait, where is offsetTop on Rust? Why does it have client_top but not offset?

Okay, it seems I can get offset_top() if I use dyn_into to cast field to HTMLElement, but then the closure doesn't work anymore, so I guess I'll have to do it where I define field since I have no idea what I'm supposed to do.

let field_div = document.create_element("div").expect("should create a div").dyn_into::<HtmlElement>().expect("should cast to HtmlElement");

It compiles. It works. Perfect. Time to commit.

Copilot: "enhance Pong game with mouse control for left pad and add Clone trait to structures"

I completely forgot I had added the clone traits since I ended up not using them, but I'm afraid I might need them in the future so I'll just leave it as is.

Moving the Ball

This should be easy. I'll need the set timeout code that Copilot created and subsequently deleted but that thankfully I have saved in this very blog post draft, and just some basic math. It can't go wrong.

if pong.ball.speed.y < 0.0 {
    if pong.ball.rect.top_left.y < 0 {
        pong.ball.rect.top_left.y = 0.0;
        pong.ball.speed.y *= -1.0;
    }
}

Copilot can really autocomplete code like speed.y *= -1.0 just from the condition. That's pretty amazing, to be honest.

const FRAME_RATE_IN_MS: i32 = 50;
let pong_clone = Rc::clone(&pong);
let closure = Closure::wrap(Box::new(move || {
    let mut pong = pong_clone.borrow_mut();
    const DELTA: f32 = 1.0 / FRAME_RATE_IN_MS as f32;
    pong.ball.rect.top_left += Point {
        x: pong.ball.speed.x * DELTA,
        y: pong.ball.speed.y * DELTA
    };

"cannot borrow pong as immutable because it is also borrowed as mutable". Why not? Copilot, guide me through these confusing times. I don't know what a local storing is.

Copilot: "The issue arises because pong is borrowed mutably for pong.ball.rect.top_left and then immutably for pong.ball.speed. To fix this, store pong.ball.speed in a temporary variable before the mutable borrow."

let ball_speed = pong.ball.speed;
pong.ball.rect.top_left += Point {
    x: ball_speed.x * DELTA,
    y: ball_speed.y * DELTA
};

By the way rust-analyzer doesn't seem to show errors until I save, so every time Copilot suggests me some OBVIOUSLY BROKEN CODE I have no idea it's OBVIOUSLY BROKEN CODE until after I accept and save the file. I don't know what's obviously broken about this because I'm no Rust expert. To me it sounds fine. But the squiggly lines won't stop coming! And then I have to rely on Copilot again.

"cannot move out of dereference of std::cell::RefMut<'_, Pong>. move occurs because value has type Point, which does not implement the Copy trait". I have clone now, isn't that enough? I'll just add .clone() to it.

That actually fixed it. I'm a genius.

My code works now. Once. I can see the ball move from 315px to 315.2px, which means the speed is only being applied once because I forgot setTimeout doesn't repeat automatically. I needed setInterval instead. Or set_interval_with_callback_and_timeout_and_arguments_0 as Rust calls it.

It works! The pong is ponging. Now I just need to set the speed to -300, -300 and make it collide with the pads.

if pong.ball.rect.top_left.y >= pong.left_pad.rect.top_left.y && pong.ball.rect.top_left.y + pong.ball.rect.size.y <= pong.left_pad.rect.top_left.y + pong.left_pad.rect.size.y {
    if pong.ball.rect.top_left.x + pong.ball.rect.size.x >= pong.left_pad.rect.top_left.x && pong.ball.rect.top_left.x <= pong.left_pad.rect.top_left.x + pong.left_pad.rect.size.x {
        pong.ball.speed.x *= -1.0;
    }
}

You know, I was going to try to make a pool / golf game in Godot, but the physics engine was kind of random, which is why I thought about doing it in Rust with web assembly. But since that's too complicated I'd like to start small, with something like Arkanoid. That's why I'm doing Pong now, because I can just write the collision code myself. Except I'm not the one writing it and I hate how it's looking like with every fiber of my being.

But it runs.

What did I enhance this time?

Copilot: "update ball speed and implement collision detection for Pong game"

You know I'm actually pretty amazed Copilot can discern that the code I was implementing was for collision detection considering I don't remember mentioning it in any comment.

Implementing AI with AI

Heheheheh.

It was going to be a single pad but I made it two for the sake of this joke.

// compute the right pad enemy AI

What does this give me?

let mut right_pad = pong.right_pad.clone();
right_pad.rect.top_left.y = pong.ball.rect.top_left.y - right_pad.rect.size.y / 2.0;
// clamp the right pad position to the field
if right_pad.rect.top_left.y < pong.field.top_left.y {
    right_pad.rect.top_left.y = pong.field.top_left.y;
}
if right_pad.rect.top_left.y > pong.field.top_left.y + pong.field.size.y - right_pad.rect.size.y {
    right_pad.rect.top_left.y = pong.field.top_left.y + pong.field.size.y - right_pad.rect.size.y;
}
set_position_and_size(
    &right_pad_div,
    right_pad.rect.top_left.x,
    right_pad.rect.top_left.y,
    right_pad.rect.size.x,
    right_pad.rect.size.y,
);

Yeah, this is worthless. I'll just code it myself.

Where is min in Rust? I'll have to Google this because I can't figure out how to make Copilot autocomplete it for me.

"the trait bound f32: Ord is not satisfied". It's min. I have two numbers. What did I do wrong?

Copilot: "The issue is that std::cmp::min requires types implementing the Ord trait, which f32 does not; you can use f32::min instead to compare floating-point numbers."

if pad_center < ball_center {
    pong.right_pad.rect.top_left.y += f32::min(AI_SPEED * DELTA, ball_center - pad_center);
} else {
    pong.right_pad.rect.top_left.y -= f32::min(AI_SPEED * DELTA, pad_center - ball_center);
}

I miss Javascript.

Copilot: "implement AI for right pad movement and add clamping to field boundaries in Pong game"

Yes.

Score

Right now the game is done. There is no score and you can't beat the AI, but it's definitely a pong. It's just that my pong is brutal and unforgiving, like Rust, like Copilot. But I guess we can add a score really quick so it actually looks like a game.

To do this all I need to do is add a couple of divs to display the score, keep track of it in some variable, increase the ball speed with time so the AI can't keep up with it, and reset the pad and ball positions every time the ball hits one of the sides. There are all other sorts of cool things we could do but we aren't going to do because I started this today and I want to finish it today.

left_score_div.set_text_content("0");

"try wrapping the expression in Some" What. Why? It's a string literal. I don't understand. But okay I'll do that.

left_score_div.set_text_content(Some("0"));

This works? I don't know why but it does.

Okay, now how do I convert a number to a string...

left_score_div (tab!) .set_text_content(Some(&pong.left_score.to_string()));

Thanks, Copilot.

I still have no idea what & does but the game is practically done already so knowing wasn't that important after all.

Copilot: "add scoring system and speed counter to Pong game"

Correct.

Fixing Collision Detection

Turns out at 20 FPS the ball goes right through the pads when you increase the speed so I'll need an actual while loop in Rust to process the collision at smaller discrete steps instead.

I actually had the same issue with Godot's engine even after using the setting to cast the collision, besides collision between two objects not transferring the energy correctly sometimes. At least in Rust I can fix it myself.

Essentially what we need to do is take this code:

pong.ball.rect.top_left += Point {
    x: ball_speed.x * DELTA * speed_boost,
    y: ball_speed.y * DELTA * speed_boost
};

// do all collision and AI here

And slice ball_speed * DELTA * speed_boost in smaller, 1 pixel long steps. We can do this by iterating the length of the vector. We can calculate the length as the hypotenuse of a right triangle whose other sides have the lengths x and y. If we divide x and y by the length, we have a normalized vector (a vector of length 1) that we can multiply by the remainder when we get to the end of the iteration. Like this:

let ball_speed = pong.ball.speed.clone();
let speed = f32::sqrt(ball_speed.x * ball_speed.x + ball_speed.y * ball_speed.y);
let normalized = Point {
    x: ball_speed.x / speed,
    y: ball_speed.y / speed
};

let mut i = 0;
while (i as f32) < speed {
    let mut iteration = normalized.clone();
    if (i as f32 + 1.0) > speed  {
        let iteration_ratio = speed - i as f32;
        iteration = Point { 
            x: iteration.x * iteration_ratio,
            y: iteration.y * iteration_ratio
        };
    }

    pong.ball.rect.top_left += Point {
        x: iteration.x * DELTA * speed_boost,
        y: iteration.y * DELTA * speed_boost
    };

Now we have pixel perfect collision in our pong game. It sure doesn't feel like it because I probably got some hitbox condition wrong but at least it's done in small steps now the ball can't just get through the pads anymore.

I've also added some simple code to make the AI faster if you win a few times.

let player_skill = pong.right_score as i32 - pong.left_score as i32;
let mut rubberbanded_ai_speed = AI_SPEED;
if player_skill > 0 {
    rubberbanded_ai_speed *= 1.0 + 0.25 * player_skill as f32;
} else if player_skill < 0 {
    rubberbanded_ai_speed /= 1.0 + 0.25 * player_skill as f32 * -1.0;
}

The last thing to do is change the initial ball speed from -300, -300 to 300, -300 so it starts facing the AI. The AI will surely catch it the first time which would explain pong to anyone who has never played and also ensure the player can't just lose instantly on high speed pong stages.

Copilot: "refactor ball movement and AI behavior in Pong game; adjust speed dynamics and enhance collision detection"

Yes, that's exactly what we did. Adjust ball movement and AI behavior in Pong game; enhance speed dynamics and refactor collision detection.

Publishing

The last thing to do is rewrite the README.md and post it on Github.

Copilot: "add LICENSE file and update README for project clarity and structure"

By the way you can get a copy of the MIT license if your Copilot fails to generate it for you here: https://opensource.org/license/MIT.

Conclusions

I found AI to be very useful in coding. It made the frustrating experience of learning Rust less frustrating because I didn't have to learn Rust.

I'm serious. I don't think I have learned Rust at all. Copilot couldn't figure out how to do things right and for the sake of the experiment I chose not to just read the book or a tutorial like you would normally do, so in the end I barely learned anything. I let Copilot create a patchwork of code at every step I encountered a syntax bug. First Copilot fixes my code, then it fixes its own code.

It's clear to me now that AI autocompletion is very powerful, but it's also very annoying. Perhaps if I could setup it to only work when I invoke it it would be more useful. But it's simply everywhere in the VS Code UI after installing it. There is a constant Copilot sidebar. The AI-fied "sparkle" icon appears on every with keyboard focus where the lightbulb icon used to be. Hovering over an error gives you a link to fix with Copilot. Even if you don't do anything, Copilot just starts showing autocompletions, forcing its way into your code.

The pattern-matching of the AI was very good at times. Pretty much every time I needed to repeat the same code for "left" and then "right" or for "x" and then "y," the AI helped me be slightly more productive than just copying and pasting everything. Yet I still found myself copying and pasting sections of code even though I could just start typing and let the AI do it for me. Perhaps it's because I don't have confidence that the AI will actually autocomplete the code I'm thinking about at the moment that I didn't try to do it through the AI every time. I felt it would be faster to just select 3 lines of code and Ctrl+C, Ctrl+V than to type something, wait a few milliseconds, and read what Copilot gave me.

When I tried "vibe coding" and not reading the code the AI generated, that quickly turned into confusing bugs afterwards. For example, Copilot managed to quickly figure out why my set_position function to the top and left of divs wasn't working, but its fixed coded added a position: absolute; that overrode the CSS I had written. The solution provided by Copilot may have worked in some contexts, but introduced bugs in the context of my project, despite Copilot having access to my @workspace.

Ironically, I don't think I can "vibe code" because the constant interruptions of code completion suddenly appearing on my screen just ruins all my coding vibes. We don't have infinite attention. The less things that change, flash, pop up, move, or slide on the screen the better.

My biggest criticism of Copilot is that it simply goes against the DRY principle. Copilot helped me a lot in Rust particularly because I'm so bad at Rust I'm not even sure how to create a function that would have worked with passing by reference. If I had spent 1 hour learning how to program Rust I wouldn't have spent 2 hours copying and pasting the same code over and over again. I'd have just created a function and called it with several different arguments. In this sense I see that it's very easy for AI to produce a mountain of non-reusable code very fast. In fact, most of the code I've written in this experiment I'd rather not keep.

On the other hand, it's at least a starting point. I built pong in a day, in a language I didn't know, using a technology I had never used before. I couldn't have done it without Copilot. At least not as quickly.

Yes, the scaffolding was broken, and it was there and it was fixable.

The chat requests were powerful, but often gave me broken code, requiring an additional request to fix.

The code completion was amazing, but it often interrupted my thought process and I had to spend time reading Copilot's code to make sure it was correct either way.

The AI-generated commit messages were just bad. I don't think they're redeemable at all. If you can't write a single line commit message you're just lazy, and if you force other people to read AI-generated commit messages you're just a terrible person. I mean "enhance ball and pad functionality"? What does that even mean.

Statistics

Chat messages: "6%" of 50 (3???).

Code completions: "13%" of 2000 (260).

Copilot's usage stats can be found by clicking on a button on the status bar on VS Code. They sound wrong, but I'm not complaining.

The whole project appears to have taken around 8 hours to be coded from the first commit to the last. Before the first commit I spent a long time trying to get Rust to compile to Web Assembly, so it could have taken 9 or 10 hours.

Thoughts on Rust

It's clear to me that Rust works in a way fundamentally different from other languages I've tried, including novel modern languages like Zig. Things that I'd have expected to just work on any language appear to require extra syntax. I don't think it's possible for anyone to learn Rust just by using Copilot.

On the other hand, I'm pretty sure you could learn Python with Copilot because the only problem you could run into Python is the use of a nonlocal variable in a nested function and I don't think anybody is ever going to run into that.

I'm happy that there is at least a clear way to work with Javascript events by wrapping functions into a closures, and the language having traits and variadic template functions feels like it can do about anything. I'll be fixing the vibe-coded project I created while trying to learn how the language works from the book instead of just asking some LLM about it.

Thoughts on Web Assembly for Game Development

Web assembly is very powerful because it allows you to not use Javascript. Javascript's fundamental design makes compromises that make the language much slower than a program you can code in Rust or other low-level languages. Although I'm not sure if web assembly can deliver the same performance, I'm pretty sure anything with structures you define in terms of bytes is going to be faster than Javascript.

On the other hand, it became clear from this short project that you shouldn't just use web assembly for everything. As you would expect, every time you change some code in Rust, you need to recompile the code. Meanwhile changing code in Javascript you can just press F5 to reload the page.

Just as you have low-level game engines that allow scripting in Lua, and Python scripts for games that use a C++ game engine underneath, I believe it makes more sense to write performance intensive low-level logic in web assembly and keep Javascript for adjusting values.

For example, the code for detecting collisions should be web assembly, but the code to handle mouse events and update positions could be in Javascript. In fact, even the function to update the game world every X milliseconds could be called from the Javascript side instead of adding its event handler to the browser on initialization. Things like configuring the speed of objects and setting the positions when the game ends could all be done in Javascript since they are not resource intensive.

Keeping the web assembly program mostly low level will also make it write automated tests for it. Although I didn't write any tests with AI because I couldn't generate anything that was testable in the first place. For example, it's very easy to test the collision code if we properly organized it.

Written by Noel Santos.

About the Author

I'm a self-taught Brazilian programmer graduated in IT from a FATEC. In a world of increasingly complex and essential computers, I decided to use my technical expertise in hardware, desktop applications, and web technologies to create an informative resource to make PC's easier to understand.

View Comments