Skip to content
Journey to Code

Part 16 of 16

Rustbound

The Tutorial Is Not the Game.

Make Your First Open-Source Contribution to Rustbound Idle

· By · 12 min read

View as Markdown

Cover reading "The Tutorial Is Not the Game." with article_15 frozen on the left and the community main branch flowing through discussion, issue, and pull request on the right

Up until now, I knew what came next.

I knew we needed damage before enemies. Enemies needed a player. The player needed a game to own the fight, and the game needed time before it could keep moving while we were away. A save had to become text before it could cross into a browser. The browser had to be told, firmly, not to get the rules.

That worked because this was a tutorial. Somebody had to choose the path.

The path is finished.

So I do not want to keep making every decision myself. Rustbound Idle is now an open-source community project. If you followed this series, you already understand enough of it to take part.

This is the final Rustbound article. It has no new game code. It is about how a finished tutorial becomes a project other people can change—and how your first change can be small enough to finish this week.

Where the game stands#

Count what we built, one article at a time:

text
small pure functions → composition → Player + Enemy → borrowing
→ Game state → gold, XP, and levels → upgrades and named outcomes
→ tick(seconds) → a headless library → saves as text
→ WebAssembly → a Web Component → a public URL
→ an installable offline app → saves that do not get the final word

It is a small game. Click a slime, earn gold, buy an upgrade, close the tab, and return to progress. It is deployed, installable, and tested in Rust and a real browser.

It is also unfinished on purpose. There are no bosses. There is one upgrade. The interface is plain. Every gap is something a reader of this series can fill because every gap lives in a layer you have already seen.

You do not have to be a Rust developer#

Open source on Rustbound does not mean everybody disappears into game.rs.

Part of the projectWays to help
Rust enginerules, combat, progression, saves
WebAssembly borderwhat JavaScript may ask Rust to do
Web Componentthe screen, browser clock, and keyboard
Design and accessibilityhow the game looks, feels, and communicates
PWAinstallation, caching, and updates
TestsRust behavior and real-browser checks
Documentationsetup, architecture, and clearer explanations
Communitytriage, review, and welcoming the next person

If the Rust lessons were your favorite, there is plenty of Rust. If you liked the browser lessons more than the ownership ones, improve the layout or reduced-motion behavior. If you do not want to write code yet, play the game and explain what feels wrong.

None of those is a lesser contribution. A project is bigger than the language in its engine.

Which contribution requires writing Rust?

Give each GitHub tool one job#

Repositories offer several places to talk. Mixing them together makes a project feel louder than it is.

ToolThe question it answers
Discussions“I have an idea. What should this become?”
Issues“We understand a specific piece of work. Who wants it?”
Pull requests“Here is a change we can review.”
Versioned docs“What must stay true as this code changes?”

An idea can begin half formed in a Discussion. Once its behavior and boundaries are clear, it becomes an Issue. Somebody builds that issue on a branch and opens a pull request. Architecture that must change with the code lives in docs/, reviewed in the same pull request.

A project board will help when enough work is active that Issues no longer answer “what matters now?” A wiki will help when community decisions and meeting notes need a durable home. Creating either on day one would give us another empty place to maintain, so we are waiting until the project earns them.

That is part of open source too: infrastructure should solve a problem the community actually has.

A public repository still has to say yes#

Public code does not automatically welcome contributions. A newcomer needs to know where to begin, how to run the project, and what the maintainers will protect.

Rustbound now has that path:

text
README.md                 what this project is
CONTRIBUTING.md           how to take part
CODE_OF_CONDUCT.md        how we treat one another
SECURITY.md               what to report privately
SUPPORT.md                where questions belong
CHANGELOG.md              what changed
docs/ARCHITECTURE.md      which layer owns what

.github/
├── ISSUE_TEMPLATE/       useful bug and feature reports
├── PULL_REQUEST_TEMPLATE.md
└── workflows/ci.yml      checks every proposed change

The contribution guide says one thing before it says anything about tools:

You do not need to write Rust to contribute. Ideas, testing, documentation, design, accessibility feedback, and welcoming other contributors all count.

Then it protects the three boundaries we spent this series building:

  1. The Rust engine never learns about the browser.
  2. The browser never calculates a game rule.
  3. The service worker makes application files available offline; it owns no game state.

Those are not arbitrary style preferences. They keep one change from quietly putting reward math in JavaScript or saves in the cache.

The repository also has pull-request CI now. Every proposal runs formatting, Clippy, and the Rust test suite before a person reviews it. Automation cannot decide whether an idea is good. It can save reviewers from repeatedly finding the same mechanical mistakes.

The guide also says what to do when an AI tool helps. Say that it helped, and roughly how. Keep the commit authored by the person doing the work. A tool can be credited with a Co-authored-by: trailer, but it should not replace the contributor's name with its own.

That sounds like a small distinction. Our first pull request showed why it matters.

A proposed offline-progress cap changes how much progress a player earns. Where should the design conversation begin?

Someone got here first. Then somebody did it again.#

I had a walkthrough ready for this section. It added two assertions to src/rules.rs: one checking that level 3 costs 50 XP, and one checking that the largest possible stage cannot overflow an enemy's health. I filed it as issue #1, added the good first issue label, and planned to publish this article a week later.

The repository was public. The issue was open. Before the article went out, somebody found it.

@ShahadatTest opened pull request #8. They put both assertions inside the existing tests, explained what saturating_* protects us from, disclosed that a Cursor agent helped, and ran the checks. The pull request changed three lines in one file. It was merged the same day.

So I rewrote the walkthrough.

The next exercise tested click_upgrade_bonus, the one rule in rules.rs with no test. It is the small function from Article 7 that turns a count of upgrades into bonus damage. I filed issue #9 and left one decision for the contributor: work out what click_upgrade_bonus(u32::MAX) should return before running the test.

Ten minutes later, @dannyward630 opened pull request #10. It has three assertions, a comment explaining the maximum-value boundary, every local check from CONTRIBUTING.md, and a description that names the Codex assistance. The commit is authored by the contributor.

The first contribution landed, and the next proposal arrived, before Rustbound Idle had officially invited anyone to contribute.

There was one wrinkle in the first pull request. Its commit was authored by cursoragent, so GitHub credited the tool account instead of the person who found the issue and carried the work through review. That does not erase the contribution. It did reveal a gap in our instructions, so CONTRIBUTING.md now says plainly that the person doing the work should author the commit.

The second pull request followed that guidance. The guide changed, and the next contributor used it. That is a rather efficient documentation test.

Then came the third, and it taught a different lesson. Pull request #11 took a help wanted issue from Article 14: make sure the service worker's list of cached files cannot drift away from what the release build actually produces. The first version worked. Every check passed. It was also 971 lines long, most of them a small shell interpreter written to guard a ten-entry list, and each round of automated review had added more code to review.

I asked one question in my own words: what does this test do the first time somebody adds a line to build.sh that it does not recognize, and is that what you want? The answer came back clear—it would silently skip the line, and no, that is not what a guard should do—and the next version was less than half the size. It accepts only the exact command shapes the build script uses today and fails loudly on anything else. That is the version on main.

A test can pass and still be too big. Review is where that gets noticed, and the contributor who takes that feedback well is the one you want back.

I stopped rewriting the exercise after that. A specific issue can close between writing a paragraph and checking the paragraph. The useful part is not the issue number. It is the loop.

An AI assistant helped write a change. Who should be the commit author?

Read the tests before you touch the code#

Every contributor to Rustbound does the same first thing, whether they know it or not: they read a test to learn what a rule is supposed to do. The tests in src/rules.rs are the project's specification, one assertion at a time.

Try it in reverse. Here is the test for one rule from Article 6, and the function body is missing. Rebuild the function from its tests, then press Check my code.

Rebuild the rule from its tests
/// How much XP a player needs to move past `level`.
///
/// The tests below are the specification. Level 1 costs 20 XP, and
/// every level after that costs 15 more than the one before it.
pub fn xp_to_next_level(level: u32) -> u32 {
    // TODO: level 1 → 20, level 2 → 35, level 3 → 50.
    // Use saturating arithmetic so a huge level cannot wrap around.
    0
}

That is the whole skill. A contributor who can read a test and say what it protects can review a pull request, write the next test, and explain a change in their own words.

A first contribution, all the way through#

Start with the live good first issue list. A useful first issue has a boundary, a definition of done, and at least one decision left for you to make.

Before writing code, open the issue and check whether somebody has commented or linked a pull request. Leave a short comment saying you want to work on it. That gives the maintainer a chance to confirm the scope, and it keeps two people from solving the same small problem at once.

Once you have something scoped, fork the repository on GitHub, clone your fork, and make a branch named for the change. This example names a documentation branch. Use the words from your issue for yours:

Terminal
git clone https://github.com/YOUR-USERNAME/rustbound-idle.git
cd rustbound-idle
git switch -c docs-clarify-setup
cargo test -j 2 --all-targets

The existing tests should pass before you touch anything. If they do not, stop and describe what happened in the issue. That is useful information, not a failed attempt.

Make the change the issue asks for. Keep it focused. A test belongs in the existing tests module of the file it checks. A game rule belongs in rules.rs as a small pure function. Browser work still respects the three boundaries: Rust owns the rules, the browser owns browser facts, and the service worker owns cached application files.

Run the checks the project asks for:

Terminal
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test -j 2 --all-targets

Then stage the pieces you intend to commit, review them, commit, and push the branch you are on:

Terminal
git add -p
git diff --staged
git commit -m "Clarify the local setup instructions"
git push -u origin HEAD

Open the pull request GitHub offers. The template asks what changed, why it helps, and how you checked it. Answer those in your own words. That is the part that shows the reviewer you understand the change instead of only copying its syntax.

A reviewer may request a change. That is not a polite rejection; review is part of building the thing together.

If the branch-and-merge part of that loop is new to you, practice it here first. Type each command yourself; the terminal tells you what Git would print.

Practice the branch-and-merge loop

Starting state: main has a few commits, and a change is already staged.

  1. Create a branch for the work and switch to it.
  2. Commit the staged change with a message in quotes.
  3. Go back to main.
  4. Merge the branch, then delete it.

What to notice: main does not move until the merge. On GitHub, the pull request, its conversation, and the automated checks all happen between steps 2 and 4.

Find the next door#

The issue list will change. That is a sign that the project is moving, not that the links in this article failed.

  • Browse good first issue for small prepared work.
  • Browse all open issues by the part of the project you want to learn.
  • Start in Discussions when the behavior or architectural owner is not settled.
  • Read pull request #8, #10 and #11 to compare how three proposals explain their changes, and how one of them got smaller in review.

Labels help you choose by interest: rust, frontend, pwa, testing, accessibility, and design. good first issue means the task is scoped, not that the contribution matters less.

“Improve accessibility” is a wish. “Test with a keyboard and screen reader, record the setup, then fix one focused group of problems” is work somebody can finish.

What makes a good first issue good?

The first disagreement is already open#

Our first design Discussion asks: where should the offline-progress cap live?

The browser owns the wall clock. Rust owns gameplay rules. “Only eight hours away should count” touches both truths.

Should the engine own the cap so every future interface behaves the same? Should the browser clamp elapsed time because it measured that time? Should the browser provide friendly behavior while Rust enforces the invariant? Or is unlimited offline progress part of the game's identity?

There is no implementation issue yet. That is deliberate. We should agree on the rule and its owner before asking somebody to code it.

The welcome Discussion is also live. Tell us what you want to learn, test, or change. We will begin asynchronously. A recurring live meeting should solve a need for real contributors, not create a calendar obligation before the community exists.

Maintainers still have a job#

Community-driven does not mean directionless.

Maintainers still protect code quality, architectural boundaries, security, accessibility, and how people treat one another. They keep issues ready, review pull requests, and say no when a change would harm the project.

But maintaining is different from owning every idea. If somebody proposes a mechanic I never would have designed, that is not a reason to refuse it. The useful question is:

Does this make Rustbound better, and can we build it without breaking the boundaries that are helping us?

I want this project to be able to surprise me.

It already has, three times, before the invitation even went out.

The tutorial stays still#

One practical promise: the lesson checkpoints are frozen.

text
article_1  article_2  ...  article_13  article_14  article_15

Matching tags, article-01 through article-15, make those snapshots permanent. The main branch is where the community keeps building.

There is no article_16 code branch because this lesson does not change the game. Its artifact is the community around it.

Somebody reading Article 4 next year should not compare beginner code with Rustbound v3.7, an inventory system, and fifteen contributors. The tutorial stays reproducible while the game grows past it.

From your first function to your first contribution#

We began with this:

Rust
pub fn click_damage(base: u32, bonus: u32) -> u32 {
    base.saturating_add(bonus)
}

That function became part of a player. The player fought enemies. Enemies paid gold and XP. Gold needed somewhere to go, and time became part of the game. A growing program became modules, then a library, then a saved file, then a WebAssembly module, then a screen, then a public URL, then an app that works with no signal.

Through all of it, that first function never changed.

Up until this point, I knew what came next.

I do not anymore.

That is your part.

Open a Discussion. Pick up an issue. Add one Rust test. Fix something that is hard to use with a keyboard. Explain a confusing setup step. Propose something ridiculous, then help us find the useful version hiding inside it.

We finished building my example game.

Now let us see what we can build.

Small function. Bigger game.

Keep going#

Comments require JavaScript.