How to Put a Game on Your Portfolio
I built a typing game for my portfolio site. Words fall from the top of the screen, you type them before they reach your ship, and every few waves a blog post title shows up as a boss fight. There's a leaderboard where the top 50 scores earn a dofollow backlink.
This is how it works and how you could build your own.
The Core Loop
A canvas fills the viewport. A requestAnimationFrame loop runs at 60fps. Every frame: update positions, check collisions, render everything. That's the entire architecture.
input → engine.tick(dt) → engine.render() → next frame
The engine is a single class. It owns all game state: enemies, bullets, particles, score, wave progression. React sits on top for the menu, HUD, and game-over screen — but gameplay is pure canvas.
Why canvas instead of DOM elements? Performance. You're rendering 10+ moving words, bullets with trails, particle explosions, and a parallax starfield every frame. DOM layout would choke. Canvas gives you direct pixel control at 60fps.
Entity System
Everything on screen is a plain object in a typed array.
Enemy words have a position, speed, the word string, and a typed counter tracking how many characters the player has entered. When typed >= word.length, the word is destroyed.
interface EnemyWord {
id: number;
word: string;
x: number; y: number;
speed: number;
typed: number;
targeted: boolean;
wordType: "normal" | "gold" | "speed" | "boss";
}
Bullets are projectiles that fly from the ship to the character position. They're purely visual — the damage happens instantly when you type. But seeing a bullet streak from ship to word makes every keystroke feel like a shot.
Particles are character fragments from destroyed words. One particle per letter, random velocity, gravity pulls them down. Capped at a few hundred to avoid memory pressure.
No entity component system. No physics engine. Plain arrays, plain loops, Array.filter() to remove dead entities. It's enough.
Input and Targeting
The player types letters. The engine matches keystrokes to words.
If you're already targeting a word, the next letter must match that word's next untyped character. Hit = advance. Miss = penalty.
If you're not targeting anything, the engine scans all on-screen words for one whose next untyped character matches your keystroke. First match becomes your target.
Space deselects your current target — lets you abandon a long word and switch to an urgent threat. This one mechanic adds real strategy: triage.
For mobile, tapping a word targets it and advances one character. Same logic, different input surface.
Wave Progression
Difficulty ramps through wave config:
function getWaveConfig(wave: number) {
return {
wordCount: 6 + wave * 2,
maxOnScreen: Math.min(3 + Math.floor(wave / 2), 10),
spawnInterval: Math.max(2200 - wave * 130, 550),
baseSpeed: Math.min(32 + wave * 4, 85),
};
}
More words, faster spawn, higher fall speed — all controlled by one function. The numbers matter more than the code. Playtest obsessively. If wave 3 feels impossible, your base speed is too high. If wave 8 feels easy, your max-on-screen cap is too low.
Word tiers control vocabulary complexity. Early waves get 3-4 letter words. Later waves introduce 8-12 letter words. Mix in special types for variety: gold words for bonus points, speed words that fall faster, reverse words displayed backwards.
Boss Fights = Blog Posts
Every third wave, one of your blog post titles becomes a boss.
The title is split into 2-3 segments. Each segment is a separate enemy word that hovers at the top of the screen instead of falling to the bottom. Between segment kills, the boss "attacks" — spawning a burst of fast minion words.
const isBossWave = wave % 3 === 0 && blogPosts.length > 0;
if (isBossWave) {
const post = blogPosts[bossPostIndex % blogPosts.length];
const segments = splitBossTitle(post.title);
// segments become boss-type enemies
}
When you defeat the full boss, the blog post title and description display as a celebration screen. The game-over screen links to every blog post you fought. Gameplay becomes content discovery.
This is the part that's specific to a portfolio. Your bosses could be anything — project names, resume bullet points, tweets. Whatever you want people to see.
The Backlink Leaderboard
Top 50 scores can submit a name and website URL. Every entry becomes a dofollow link on the leaderboard page.
The mechanic is simple: Firestore collection, sorted by score descending, capped at 50 entries. A minimum qualifying score prevents spam. The leaderboard page is statically generated with ISR so it stays fast.
Why this works: the leaderboard turns players into promoters. They share their score to brag, and the share URL includes a challenge parameter (?c=42000) that shows the score to beat. The backlink gives SEO-aware visitors a tangible reason to play seriously.
Audio Without Files
Every sound is synthesized at runtime with the Web Audio API. No audio files to load, no network requests.
A "correct keystroke" is two detuned square wave oscillators sweeping from 880Hz to 440Hz over 40ms with a noise transient click. A "word destroyed" is a filtered noise burst layered with a descending sine sweep and a sub-bass thump. The pitch scales with your combo level — higher combo, higher pitch.
type(): void {
this.tone("square", 880, 440, 0.04, 0.2, 0, 3000);
this.tone("sine", 660, 330, 0.05, 0.12);
this.noise(0.01, 0.15, 4000, "highpass");
}
Layering 2-3 oscillators with different waveforms and a noise transient makes sounds feel physical. A single oscillator sounds like a ringtone. Two detuned oscillators sound like a laser.
What You'd Change
This is a typing game because I like typing games. Yours doesn't have to be.
The architecture works for anything where entities move on a canvas and the player interacts through input: tower defense where you type to build, rhythm game where words arrive on beat, puzzle game where you type to rotate pieces.
The boss-fights-as-content pattern works with any content you want to surface. The backlink leaderboard works on any game with a score.
Keep the engine. Change the skin. Make it yours.
Numbers That Matter
Every constant in the game has been tuned through playtesting. Here are the ones that matter most:
- Base fall speed: Start low (30-35 px/s). Players need time to read the words.
- Spawn interval: Start high (2000ms+). Feeling rushed before wave 3 means your interval is too aggressive.
- Max on screen: Start at 3, cap at 8-10. More than 10 is visual noise.
- Lives: 3 feels punishing for casual players. 4 gives breathing room without making it easy.
- Boss speed: Much slower than normal words. Bosses are endurance tests, not reflex tests.
If you ship a game that's too hard, nobody plays past wave 2. If it's too easy, nobody shares their score. Find the line where players die on wave 4-6 on their first try and wave 8+ once they've practiced.
Build Yours: The Prompt
Copy this into Claude Code, Cursor, or any AI coding tool while in your project directory.
The hard part isn't the code — it's picking a game that works at the intersection of "fun enough to share a score" + "surfaces your content" + "feels native to a portfolio." There are maybe five ideas that actually work:
- Typing arcade — words fall, you type to destroy them. Boss fights are blog titles. Scores are shareable. (This is what dryp.dev/play uses.)
- Terminal adventure — fake terminal where you
cdinto projects,catblog posts, debug "system errors." The game IS the portfolio. Harder to make score-shareable. - Tower defense — bugs march toward your codebase. Type tech words to place defenses. Your stack becomes your arsenal.
- Rhythm typing — words scroll on beat lanes. Type in time with synthesized music. Blog titles are guitar solos.
- Roguelike dungeon — navigate rooms by typing commands. Each room reveals a project. Text-adventure meets portfolio.
Pick one before you start. The prompt below handles everything else.
You're helping me build an interactive game for my personal portfolio/website. The game should be built into the site — not a separate app, not an iframe. Canvas-rendered, Web Audio API for synthesized sound, zero external game dependencies.
I want to build: [PICK ONE: typing arcade / terminal adventure / tower defense / rhythm typing / roguelike dungeon / or describe your own]
First, scan my codebase to understand:
- What framework and language I'm using
- What content I have (blog posts, projects, case studies, etc.)
- The visual style and color palette of my site
- How routing and pages are structured
Present what you found and how it maps to the game:
"Here's what I gathered: [framework], [content types], [design vibe]. For your [chosen game type], I'd use [content] as [bosses/levels/rooms/enemies]. Here's my plan."
Ask if I want to adjust any of those assumptions. Then ask only what you can't infer from the code:
- What should a high score earn? (Backlink from your site, shoutout, bragging rights?)
- Anything you definitely want or definitely don't want?
Then build step by step:
Phase 1: Canvas setup + game loop (requestAnimationFrame, delta time)
Phase 2: Entity system (plain typed arrays, no game engine)
Phase 3: Input handling (keyboard + touch)
Phase 4: Progression system (waves, levels, or rooms — drawn from my content)
Phase 5: Scoring + combo/multiplier system
Phase 6: Content integration (bosses, rooms, or encounters tied to my blog/projects)
Phase 7: Audio synthesis (Web Audio API, layered oscillators, no files)
Phase 8: Menu, game over, HUD — styled to match my site
Phase 9: Leaderboard (if I want one)
Phase 10: Polish (particles, screen shake, effects)
For each phase, write the code, explain key decisions, and check if I want to adjust before moving on. Use my existing stack and conventions.
Reference: https://dryp.dev/play
The agent scans your codebase and builds. You pick the game type and make the creative calls.