Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
Arcade tennis in the browser, on a deterministic simulation. Shares its engine with websoccer.
GamesJavaScript · CSSGitHub @markclausingOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Arcade tennis in the browser, in the spirit of the 16-bit classics: a court seen
from above, two small players, one button, and a ball that has to land in.
1 player against the CPU, 2 players on one keyboard, or online with a
four-character room code.
No dependencies, no build step. HTML, CSS and JavaScript exactly as the browser
receives them.
It is the second game built this way. The first is
websoccer, and the two share their
engine — see Shared with websoccer.
Then open http://localhost:5173/. There is no npm install; there are no
packages to install.Read the full README ↗Arcade tennis in the browser, in the spirit of the 16-bit classics: a court seen from above, two small players, one button, and a ball that has to land in. 1 player against the CPU, 2 players on one keyboard, or online with a four-character room code.
No dependencies, no build step. HTML, CSS and JavaScript exactly as the browser receives them.
It is the second game built this way. The first is websoccer, and the two share their engine — see Shared with websoccer.
git clone https://github.com/markclausing/webtennis.git
cd webtennis
npm start
Then open http://localhost:5173/. There is no npm install; there are no
packages to install.
| Player 1 (blue) | Player 2 (red) | |
|---|---|---|
| Move | W A S D | Arrow keys |
| Serve / hit | Space | Enter |
| Pause | Esc |
The court is not the real shape. A singles court is nearly three times as long as it is wide, and at that shape there is time to stroll to every ball - the width is the only thing you have to cover in a hurry and there was not enough of it. This one is about two to one: shorter, wider, and it makes you choose which ball to go for. The ball speeds came down with the length, or the same shot would simply arrive sooner in a smaller court, which is a different game again - at the old speeds one point in seven was an ace.
The rules themselves are the real ones, because they are what makes tennis tennis: the ball has to land in, it may bounce once, a serve has to find the diagonal box, and the score runs fifteen, thirty, forty, deuce, advantage. A set is won by two clear games, and the score goes up in the middle of the screen after every point, in the words the umpire uses.
Two things decide points beyond getting to the ball.
You can volley. Anything up to head height can be played out of the air, so standing at the net is a real position rather than a place to watch balls go past. The CPU answers it by lobbing you, which is what a lob is for.
Pace forces errors. The speed of the ball arriving eats into the wind-up you had, so a shot struck flat out is awkward to handle however early your opponent started - it cannot be outrun, because it is charged at the moment of contact. Measured against the CPU, feeding it the same ball at different speeds: at 262 px/s on arrival it comes back 52% of the way down the court, at 336 it comes back short and sitting up, and at 414 and above it does not come back at all.
The net opens the angles. How sharp a shot you can play depends on how far up the court you are standing: from the baseline a full sideways swing moves the ball 26% of the way to the sideline, from the service line 45%, and from the net 53% - and aftertouch adds to all of them. The price is paid automatically. Stand that close and the next ball is on you before you have finished admiring this one.
The CPU does not come to the net, so for now that half of it is yours alone.
Three difficulty settings, and what separates them is measured rather than guessed. The strongest lever is a delay before the CPU sets off after the ball — reading the shot early is most of what makes a good player look quick. The second is how far its aim strays, and that aim is allowed to stray past the lines. That last part sounds like a detail and is not: an earlier version clamped every shot into the court, which meant the CPU could not miss, made no errors at any level, and easy and hard finished level with each other. Games taken off HARD over four matches: EASY 6, NORMAL 17, HARD 22.
Points end the way they do in tennis: at HARD against itself, 93% of them are winners and 7% aces; against EASY, a quarter of the points are errors.
Eleven files are identical in both games — the input mask, the touch controls, the lockstep netcode, the relay, the Cloudflare Worker, the high score table, the three-letter name entry, the formant speech synthesiser and the maths. They are shared by being the same file in both repositories rather than by a package, because neither game has a build step and neither is going to grow one for this.
Copying is only worth anything if somebody notices when the copies part ways:
node tools/sync-shared.js # are they still the same?
node tools/sync-shared.js --pull # take websoccer's copy
node tools/sync-shared.js --push # send this one back
It runs as part of npm test, so a change on one side shows up as a failing test
on the other rather than as a mystery six months later. It expects websoccer to
be checked out beside this repository, and says so and passes if it is not.
Nothing that knows what sport it is gets shared. The simulation, the court, the
sounds and the words are each game's own — a shared file full of if (tennis)
would be worse than two files.
The score is called out loud by the same formant synthesiser websoccer uses for its commentator: a buzz through three sharp filters, which is roughly what the speech chips of the era did. The voice is shared; the words are not. Tennis is the easy case for a talking scoreboard, because the calls have not changed in a century — love, fifteen, thirty, forty, deuce, advantage, game.
npm test # the lot
npm run test:sim # whole matches headless: determinism, scoring, the rules
npm run test:shared # the shared files, against websoccer next door
The scoring is tested by driving points straight through the scorer rather than by playing, because deuce, advantage and advantage-lost take a long time to reach by playing and are exactly where a scoreboard goes subtly wrong.
Online play and the shared high score board run on a Cloudflare Worker of this game's own — the same code websoccer uses, deployed separately on purpose. The board lives in a single Durable Object under one key, so one Worker serving both games would mean one table with football and tennis results mixed into it. Two Workers, two boards. They post into the same Discord channel, which is only a webhook address and does not care which game is talking.
cd worker && npx wrangler login && npx wrangler deploy
npx wrangler secret put DISCORD_WEBHOOK # optional: announce new entries
npx wrangler secret put ADMIN_KEY # optional: lets you clean the board
Then put the address it prints in src/config.js as DEFAULT_RELAY, with
wss://.
MIT.
An original tribute to the top-down sports games of the nineties: no code, artwork or other parts of any existing game, and no affiliation with their makers or rights holders.