Skip to content

DocAtPrompt/tty-rex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tty-rex

A terminal love letter to Chrome's offline T-Rex runner — written in Rust on top of crossterm, drawn with Unicode block characters, played wherever your shell lives.

cargo run --release

Status: developed and tested on macOS only. crossterm itself supports Linux and Windows (modern Windows Terminal), and the code uses no platform-specific calls, but the rendering and key handling have not been verified there. If you run it on something other than macOS, please open an issue with what you saw.


Why this exists

I'm an aficionado of retro arcade-y things. The Chrome dino is one of those rare tiny games that's perfect on its purpose: pure timing, no scoreboard nags, no power-ups, no shop. It exists where you accidentally meet it (an offline error page) and asks nothing of you except attention.

This is my homage. Same idea, but it doesn't need your wifi to be down — just a terminal. Built mostly because I wanted to see how far you can push terminal rendering before it stops feeling like ASCII art and starts feeling like a tiny game.

A bit of trivia for the curious

The original Chrome dino was built in 2014 by Google's Chrome UX team in Mountain View — mainly Sebastien Gabriel, with Alan Bettes and Edward Jung. The internal codename was Project Bolan, after Marc Bolan, frontman of the glam-rock band T. Rex. The dinosaur itself was a wink: being offline meant living "in the stone age".

At launch the game only had cacti to dodge. Pterodactyls (which force you to duck) and the night-mode toggle (which inverts the palette after a few thousand points) came in later iterations. Google has estimated something like 270 million plays per month, mostly on mobile devices in regions with unreliable connectivity — which probably makes it one of the most-played games most people have never thought of as a "real" game.

You can still play the original at chrome://dino in any Chrome browser, online or off.

Screenshots

All three views below show the same 80×24 terminal layout. White background and dark foreground are forced by the renderer (Chrome-dino convention) so the game looks the same regardless of your terminal theme.

Title screen — current score and HI sit in the top-right corner; on first launch both are zero. The dino stands idle behind the overlay.

                                                               00000   HI 00000

       ___                                                 _____
     _(   )_                                            _(       )_
    (_______)                                          ( _________)

                █████ █████ █   █     ████  █████ █   █
                  █     █    █ █      █   █ █      █ █
                  █     █     █   ███ ████  ████    █
                  █     █     █       █  █  █      █ █
                  █     █     █       █   █ █████ █   █

                              Press SPACE to start

                  ←  RELAXED   [ STANDARD ]   BRUTAL  →

              SPACE/↑ jump   ↓ duck   ← → difficulty   Q quit
          ████
          ██▀▀
      ▄   ███▀
      ▀█████▀
        ▀  █
────────────────────────────────────────────────────────────────────────────────
   ·               ·               ·               ·               ·

In-game — the run-history column lives under HI: LAST is the most recent finished round, older runs stack chronologically below. Two pterodactyls in flight (one HIGH, one LOW), a large cactus on the ground, the dino mid-stride.

                                                               01234   HI 04567
                                                             LAST 03210
                                                                  02890
                                                                  03100
                                                                  01450
                                                                  02080
                                                                  00940

       ___
     _(   )_
    (_______)


                                          ▙  ▟
                                           ▀▀

          ████             ▄    ▄
          ██▀▀              ▀██▀                          █
      ▄   ███▀                                          █▄█▄█
      ▀█████▀                                            ▀█▀
        ▀  █                                              █
────────────────────────────────────────────────────────────────────────────────
   ·               ·               ·               ·               ·

Game over — no difficulty selector here; the locked difficulty keeps the high-score honest. The run that just ended is already recorded as LAST. Dead-dino has an x for an eye and a frozen-leg pose. The cactus that killed it sits right next to the dino's body.

                                                               03812   HI 04567
                                                             LAST 03812
                                                                  03210
                                                                  02890
                                                                  03100
                                                                  01450
                                                                  02080
              █████ █████ █   █ █████   █████ █   █ █████ ████
              █     █   █ ██ ██ █       █   █ █   █ █     █   █
              █  ██ █████ █ █ █ ████    █   █ █   █ ████  ████
              █   █ █   █ █   █ █       █   █  █ █  █     █  █
              █████ █   █ █   █ █████   █████   █   █████ █   █

                              SPACE restart   Q quit


          ████    █
          ██▀▀  █▄█▄█
      ▄   ██x▀   ▀█▀
      ▀█████▀     █
        ▀█▀█
────────────────────────────────────────────────────────────────────────────────
   ·               ·               ·               ·               ·

Install / Build

git clone https://github.com/<YOUR-USERNAME>/tty-rex
cd tty-rex
cargo build --release
./target/release/tty-rex

You need Rust (any 2021-edition-capable toolchain, tested on stable). The only runtime dependency is crossterm 0.28.

Usage

tty-rex                            # Standard difficulty
tty-rex relaxed                    # easy mode
tty-rex brutal                     # heavy gravity, fast world
tty-rex --difficulty standard      # explicit flag form
tty-rex -d brutal
tty-rex --help

Controls

Key Action
Space / jump
duck under low birds (or fast-fall while airborne)
← → change difficulty (only on the title screen)
Space / R restart after a game over
Q / Esc quit

Difficulty presets

Stat Relaxed Standard Brutal
Gravity (cells/s²) 90 130 170
Jump velocity -38 -42 -46
Initial speed 22 32 40
Acceleration 0.4 0.7 1.1
Max speed 55 80 110

The chosen difficulty is locked once the first game starts, so the high score stays comparable across rounds in the same session. To switch later, restart the binary.

Obstacles

  • Small cactus (3 cells tall) — jump.
  • Large cactus (4 cells tall, with arms) — jump higher / earlier.
  • Low pterodactyl (dark blue, head height) — duck. Unlocks after score 50.
  • High pterodactyl (dark magenta, above the dino) — don't jump under it. Mid-flight body is high enough to clip your jump arc.

Birds bob up and down on a sine wave; the visual offset is render-only, the hitbox stays put.

Implementation notes

A single-file Rust program, ~1300 lines including 16 unit tests. Architecture is intentionally flat — Game struct holds all mutable state, render and update are methods on it, no event bus, no ECS, no abstraction layers. For a game this size that's the right call; the moment you'd add a second screen or a multiplayer mode it'd start hurting.

Things that took surprisingly long to get right

Collision math. Started with hand-tuned AABB insets ("shrink the bbox by 1 column on each side for fairness"). It worked until it didn't: the large cactus's arms sit on the outermost columns of the sprite, and the inset silently excluded them. Players could clip through cactus arms at a precise frame timing and not die. Fix: throw away the heuristics and compute a tight bounding box from the actual non-whitespace cells of the current sprite. Now the bbox is the visible silhouette. See tight_bbox() and the regression test large_cactus_arm_at_dino_right_edge_collides.

Duck-end snap. When the duck timer expires, the dino has to "stand up" — its dino_y jumps from ground_y - 3 (duck height) up to ground_y - 5 (run height) in a single frame. A naive snap (if dino_y >= ground_top { snap }) also fired when the player jumped out of a duck pose, because the negative vy of the jump impulse ran through the else branch, the snap saw a dino_y "below" the running ground top, and zeroed vy. Effect: the jump silently failed. Fix: gate the snap on vy >= 0.0 so it only triggers when the dino is descending or at rest.

Key event kinds across platforms. crossterm normalises key events into Press, Repeat, and Release variants, but what your terminal actually emits varies a lot:

  • macOS / Linux without kitty-keyboard-protocol enhancements: only Press. Holding a key produces a stream of Press events at the terminal's auto-repeat rate.
  • Windows console (modern Windows Terminal): Press AND Release for every key, by default. A naïve handler that reacts to "any key event" would call jump() twice per tap.

The input loop guards against both with a single match arm:

Event::Key(KeyEvent { code, modifiers, kind, .. })
    if matches!(kind, KeyEventKind::Press | KeyEventKind::Repeat) => { ... }

Release events are filtered out by the guard, so each player keypress drives exactly one game action regardless of platform.

Ducking without release events. Since Release isn't reliable everywhere, ducking is timer-based instead of edge-triggered. Each Down keypress (auto-repeated by the terminal while held) refreshes a duck_until 0.6 s into the future; when the player releases, no more events arrive, the timer expires, and ducking ends. Brittle on terminals with very slow auto-repeat, but pragmatic.

Terminal background. Originally I let the user's terminal theme show through. Then somebody with a white terminal could not read the white-on-white score. Fix: force a white background and use only dark ANSI foreground colors for everything in the play field. Looks identical to Chrome dino regardless of the user's profile. ANSI colors only — Color::Rgb would have looked nicer but support varies wildly across emulators.

What I'd do differently next time

  • Per-cell rendering with a backbuffer. Right now every render frame queues MoveTo+Print calls directly. A 2D char+attr buffer with a flush step would make sprite layering trivial and enable diff-based output. For this game's complexity it would have been overkill, but it's the natural next step if you want particle effects, day-night transitions, or a second player.
  • Tests from day one. I added unit tests for check_collision only after shipping bugs. Should have started with them — collision math is the kind of code where table-driven tests are nearly free and catch every regression instantly.
  • Sprite definitions as &[(char, fg)] instead of &str arrays. Would let me color the dino's eye separately from its body, mark "transparent" cells explicitly instead of hacking with spaces, and skip the runtime tight-bbox calculation.

Pitfalls if you fork it

  • Unicode width assumptions. Block elements (, , , , ) and arrows (, , , ) are all assumed to be exactly one terminal cell wide. They are in every modern monospace font I tested, but a font without them substituted via fallback can break alignment.
  • Frame pacing. A simple sleep(remaining) works on local TTYs but stutters in some hosted terminal multiplexers. If you see jitter, look at the FRAME_DURATION loop in run().
  • Resize during a game. Handled, but the cacti currently in flight don't reposition relative to the new ground line — they stay glued to the old ground_y until they scroll off. Fine in practice, surprising in code review.

Project layout

.
├── Cargo.toml
├── Cargo.lock
├── README.md
└── src/
    └── main.rs   ← everything lives here

License

No license file yet — add one before publishing if you intend others to fork or redistribute.

Acknowledgements

  • Original Chrome dino: Sebastien Gabriel, Alan Bettes, Edward Jung at Google.
  • Marc Bolan, in absentia, for the codename.
  • crossterm — the cross-platform terminal-manipulation crate that does all the heavy lifting (raw mode, alternate screen, ANSI sequences, key events, the lot).

Built with ♥, supported by Claude.

About

Terminal homage to Chrome's T-Rex runner — Rust + crossterm

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages