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
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A single-page card-table simulator. baize.html is the whole app: open it in a browser, or publish it as is. It plays Klondike, FreeCell, Golf, Pyramid, Spider (one, two or four suits; two decks) and Forty Thieves (two decks), Yukon and TriPeaks, and the engine is written so that other games (Hanabi, for one) can be added as rulesets.
'Baize' is the green woollen cloth that covers a card table.
baize.html| Script id | Contents |
|---|---|
engine | Piles, actions, Session (replayable, with undo), movableCards, and the rulesets. No DOM access. |
solver-wasm | The Forty Thieves and Spider searches in WebAssembly (tools/wasm, base64 between the wasm:start and wasm:end markers); registers itself in CardSim.fast, which search() tries first. |
deck-art | The 52 card faces as SVG strings (generated, about 440 kB). |
deck | A deck: face(card), back(), label(card). |
app | Table drawing, pointer and keyboard input, saves, page controls. |
A ruleset's contract is described in the comment above the engine script.
A game is stored as (ruleset, options, seed, actions) and the position is replayed from that, so a saved game cannot hold an illegal position.
npm test
Runs the engine, rulesets and deck art in Node (about 30 seconds, mostly random playouts and solver runs). There are no dependencies to install.
python3 test/ui.test.py [check_name ...]
Drives the page in Chromium with Python Playwright (naming checks on the command line runs only those): the game menu and saves, highlighting with Shift and rank keys, the Show moves toggle, Golf taps, FreeCell drag and double-tap, Pyramid pairs and kings, Spider and Forty Thieves taps and deals, and the rules card (opening, closing, stacking order). Deals are fixed by seed. One check fails if cards ever get a CSS filter (or a transition on one) again, which is what made highlighting slow. It also checks that a new deal clears the table before dealing, and that winnable deals can be won. It also checks the options menu, the search dialogue and its Cancel, that the page never scrolls or zooms on any screen size, and that a tapped card flies above the others.
There is no build step: baize.html is already an installable app. Beside it are manifest.webmanifest, sw.js (a service worker that serves the game from its cache, so it works offline, and refreshes the cache in the background) and icons/ (drawn for this page, CC0; tools/pwa/make-icons.py redraws them from tools/pwa/icon.svg). Serve the folder over HTTPS, for example with GitHub Pages, and open baize.html (the bare address forwards to it). On an iPad use Safari, then Share and Add to Home Screen; Chrome and Edge offer Install. python3 test/pwa.test.py serves the folder locally and checks the manifest, icons, service worker and offline play.
Because the cache is refreshed in the background, an edited page appears the second time the app is opened. The web fonts come from Google Fonts and are cached after the first online visit; without them the page uses system fonts.
Klondike, FreeCell, Golf, Pyramid, TriPeaks, Yukon, Forty Thieves and Spider with one or two suits offer Deals: Winnable. There is one search, search() in the engine, for deals and for hints: best first by the ruleset's progress() score (every ruleset that can be solved must have one), over the ruleset's own moves(), with DEPTH_COST weighing depth against score, a position key to skip repeats, and a tidy-up that cuts loops out of a line it returns. Deals and hints differ only in their parameters: winOnly (the deal search wants proofs, not good guesses), and the budget, which for a hint is never less than the ruleset's searchNodes, so a hint can always re-prove a deal that the deal search proved. seedHunt() runs search() on one seed after another. A solution that is found is real, since the tests replay it, but a search that gives up proves nothing. The Winnable setting is a preference and the app keeps it: when it is set, every deal, including a game's first and the first after switching game, is searched for until a win is found, with no time limit. The dialogue with its spinner has a Cancel button, which leaves the deal random (Deals Random); the player who finds this too slow can switch to Random. The status line says what the deal in front of you is: Deals Winnable only when the search proved it (the app keeps that as proved with the saved game, and as session.proved while it is open), Deals Random otherwise. A game saved before the app recorded this (no proved in the save) is checked once when it is opened with Winnable preferred: a hint-style search from its start marks it winnable if it finds a win; if it does not and no card has moved the deal is replaced by one that is winnable; if the player had begun it is kept, and called random. A deal that is proved is never called hopeless by a hint from its start; if the player has since moved off every winning line, the hint says that the deal was winnable and to undo. Spider has a key of its own (columns in any order, finished runs only counted, two cards of one suit and rank alike) and a progress that also counts a smaller card buried under a bigger one. With those the JavaScript search wins almost every one-suit deal and about one in five two-suit deals within 3000 positions; with four suits it wins none. Four suits are possible only in WebAssembly, by restarting a weighted search (below and docs/spider-four-suits.md), so Spider says fastOnly for four suits: where WebAssembly is unavailable the app does not hunt for ever but deals a random game and says why. Forty Thieves is helped twice: its ruleset names positions up to symmetry (key: columns and foundations sorted, the two copies of a card alike), and its search runs in WebAssembly (below), which is about fifty times faster, so a winnable deal takes a fraction of a second (it finds a win for about one seed in twelve).
WebAssembly. tools/wasm is the Forty Thieves and Spider searches again, in C with no libraries. core.h is the search, once: best first on a score, over a game's own moves, with a table of positions seen, optional restarts, and memory kept small by storing a position as a record of how it was reached and rebuilding it from its parent when it is expanded. forty.h and spider.h hold each game's rules (the same moves, score and position name as the rulesets), and solver.c is the module's interface. build.sh compiles it with clang's wasm32 target and wasm-ld (nothing else is needed; the module is about 40 kB and asks for 32 MB of memory), and embed.py writes it into baize.html as base64, so the app is still one file. search() asks CardSim.fast[rules.id] first, with any position (the page turns it into a byte image: CardSim.fast.spider.image and the like), so hints from the middle of a game use it too, and the C budget is the ruleset's fastNodes if that is larger than the JavaScript budget. It answers only where WebAssembly can be had (a page's own thread may not compile a module this size, but a worker may, and the search runs in a worker), and every line it returns is replayed through the rules before it is trusted, so anything else falls back to the one JavaScript search. WebAssembly is missing or refused where Lockdown Mode is on in iOS or macOS (it turns off WebKit's JIT and with it WebAssembly), where a host page's Content-Security-Policy lacks wasm-unsafe-eval (possibly an artifact viewer's sandbox), and on some locked-down enterprise browsers; all current browsers and installed PWAs otherwise support it. The C is a duplicate of the rules, kept as an accelerator only (the JavaScript search needs about ten seconds where Forty Thieves needs a fifth of one) and never trusted. On the development machine it expands 120,000 to 340,000 positions a second against about 5,000 for JavaScript.
Spider in four suits needs more than speed: a plain best-first search wins 1 of 60 held-out deals within 150,000 positions. Spider's C search charges a cost for each move of depth, breaks ties at random, and restarts every 30,000 positions with another depth cost and seed (knobs in tuning, defaults in spider.h), which wins 43 of 60 (72%), 1 of 60 with neither, in about 1.5 s a winnable deal; one and two suits win 60 and 59 of 60 in a fraction of that. The method, the measurements and what is not known are written up in docs/spider-four-suits.md; node tools/wasm/measure.js reproduces the figures.
The C is held to the JavaScript rules. Because it repeats them, the module exports functions that take any position (load_position, position_moves, position_step, position_score, position_key_*), and test/engine.test.js compares them with each ruleset's moves, progress and key at every position of random playouts (Forty Thieves; Spider with one, two and four suits and with dealing into gaps on and off): the same moves in the same order, a score a whole number of times the JavaScript one, one position name for exactly the positions the JavaScript names alike, columns in any order, and every move leading where reduce leads. The test sends positions through the page's own image builder. Changing the rules in one place and not the other fails that test. It also checks that the lines win, that a hint from twelve moves into a deal still finds the win, and that the JavaScript search still works without the C.
The faces are Adrian Kennard's playing cards (https://www.me.uk/cards/), dedicated to the public domain under CC0 1.0, taken from @letele/playing-cards 0.1.0 (CC0-1.0). The card back is drawn for this page and is also CC0. To regenerate the embedded faces, run tools/art/build.sh.
The Card faces setting (in the menu) swaps the standard faces for "jumbo" ones drawn for this page: a white card in a thick black border, the rank and suit as large as the strip of a fanned column allows (26% of a card's height; FAN_STEP), once at the top left and once turned round at the bottom right, and the rank again, huge, in the middle; red and black. tools/jumbo-deck/build.py draws them (the ranks are paths made from the outlines of DejaVu Sans Condensed Bold, so every device draws the same shapes and no font is loaded; the four suits are drawn in the script) and writes them into baize.html between the contrast-art markers, about 90 KB. The faces are 5:7 like the table's cards, so nothing is stretched. useHighContrast on the deck switches sets and refreshFaces redraws the cards on the table. To change the look, edit the constants at the top of the script and run it again.
Until the player has chosen card faces, the page starts with the jumbo ones if the device reports prefers-contrast: more. That is an accessibility setting, not a screen size: on an iPad or iPhone it is Settings, Accessibility, Display & Text Size, Increase Contrast (on a Mac, Increase contrast; on Windows, a contrast theme). Tapping the title five times opens a diagnostics panel whose contrast line shows what the device reports and which faces are in use.
The icons are Google's Material Symbols (outlined, weight 500), licensed under the Apache License 2.0, copied unchanged from @material-symbols/svg-500 0.48.1 into an inline SVG sprite near the top of baize.html. No font is loaded, so there is nothing to fetch and no fallback glyph. To change the set, edit the list in tools/icons/embed.mjs, then run npm install && node embed.mjs in tools/icons. NOTICE and LICENSE-material-symbols.txt carry the attribution and the licence text.
Commit each change on its own, run npm test first, and keep diffs small. The history before the first test commit was rebuilt from saved snapshots of each published version, so some early commits bundle several changes.
The page never scrolls or zooms; the cards shrink to fit (down to 22 px wide). Only the rules card scrolls. Browser zoom with Ctrl and +/- is left alone for accessibility.
The service worker answers from its cache and checks the server in the background (past the browser's own cache). When baize.html has changed, open pages show "A new version is ready" with a Reload button; the app also checks each time it comes back to the front. An installed iPad app is suspended rather than closed, so without this it would keep the old version indefinitely.
The Statistics button (in the menu) shows, for each game, deals played and won, win rate, fewest moves in a win and the winning streak (current and best), kept in the browser's local storage. A deal counts when it is won, or when it is given up (New deal, or a change of options) after at least one move; a restart never counts, and a deal is never counted twice, however often it is undone, redone or reloaded. The bookkeeping is recordResult in the engine, tested in Node.
Hint (its button in the header, or the H key) searches from the current position on a worker, 12,000 positions at most (more where a ruleset's searchNodes is larger), with a Cancel button if it takes a while (the dialogue appears after 350 ms and, once up, stays for at least 500 ms, so it never just flashes). The search sees the face-down cards too, so it is not a fair hint, only a useful one. It is best-first, by each ruleset's progress score, and has two possible results (search() in the engine):
If neither exists, the hint turns the stock where there is one, or says there is nothing to gain. The first move of the ruleset's own move list is never offered as a plain "plausible move": that is what once made hints shuffle runs between equal columns without end.
Hints do not change their minds. HintBook remembers every line found, for the position it came from and for each position along it, so following the hints follows one plan and needs no further search; asking twice at the same position, or asking at the first move of a deal chosen by the winnable search (which already found a line), searches nothing. A line that only gains ground is used only if its first move reaches a position not yet seen in this deal. To keep lines short, the search remembers every move it tried and takes the shortest route through them; because a position key may treat positions as equal that are not identical (FreeCell's cells), that route is replayed and the search's own route is used if it fails. Tested in Node: hints followed for 50 moves in Klondike never send a card straight back, and every line found in every game plays out on the real position.
The message stays until the player moves, undoes, deals or presses its × button. The lit cards keep their place in the stack: a hint does not lift them above their neighbours. Games whose ruleset has no moves() have no Hint (every game has one now).
Nothing is nested: the menu button (the hamburger) opens one flat panel, and no button in it opens another menu inside it. The header is one row: the title plate (Baize, with the current game as a small subtitle inside it) at the left, then Show moves (a toggle, the touch screen's answer to holding Shift), Hint, Undo, Auto-finish (always there, faded when there is nothing to finish), New deal (red, like every destructive action) and the menu button at the far right. Below 780 px wide the buttons keep only their icons; below 560 px the title plate takes a row of its own, spanning the width with the game name at its right; below 380 px New deal is an icon too. The menu holds everything else: Rules and Statistics (these open their own panel in place of the menu), the game picker, the game's own options (changing one deals again), Restart this deal (red; it discards progress, so it is out of reach of a stray tap), and the appearance choices.
Sending cards home. A double tap sends a card home (onActivate). A card dragged and let go within one and a half card widths of its foundation also goes home if nothing nearer took it (homeAction in a ruleset, NEAR_HOME in the script; Klondike, FreeCell, Yukon and Forty Thieves). The Safe cards setting (menu, Preferences; shown only for games whose ruleset has safeAction: Klondike and FreeCell) plays by itself, after each move, every card that can never be needed on the tableau: aces and twos, and any other card once both cards of the opposite colour one rank lower are home (isSafeHome). It is off by default. The cards it plays are recorded with auto: true, so Undo takes the whole run back together with the move that began it.
Statistics, Settings and the Rules card are managed together (PANELS, showPanel, closePanel and togglePanel in the page script), so at most one is open: opening one closes another, and pressing a button whose panel is open closes it. The modal ones (Statistics, Cleared, the search dialogue) are fixed to the window, not placed inside the table, so a small screen does not squeeze them. Every panel has the same × button (.icon-close), the same title size and the same button padding (--btn-pad). The win screen ("Cleared") is the Statistics panel opened in its won mode, with the results of the deal just played; it has no copy of its own. While it is open the other panels' buttons are disabled, and it closes with ×, Escape or any move that undoes the win. Below 560 px wide the header buttons show only their icons (the words stay as accessible names).
The Style setting offers Steampunk (the walnut-and-brass original and the base of all the CSS), Cyberpunk (black glass with neon edges, monospaced capitals), Sci-fi (flat rounded console panels in an LCARS-like palette, black ground) and Retro (a 1990s desktop: teal ground, grey bevelled windows, navy title bars, Tahoma). The script writes the choice to data-theme on the page, and each non-default style is one block at the end of the stylesheet that replaces the colour and font tokens and the few rules that carry the look: the desk (--desk), the title plate, buttons, segmented choices, rail, table, slots and panels. Table colour and card back are independent of the style. Scrollbars follow the style through --scroll-thumb and --scroll-track (scrollbar-color); Retro draws a chequered trough and bevelled thumb where the browser allows. A panel's side padding belongs to its title and its .plaque-body, not to the plaque, so a scrollbar sits at the panel's edge; check_scrollbars_sit_at_the_panel_edge measures this (with classic scrollbars) at three widths in every style. To add a style, add its value to SETTINGS in the engine, a colour for it in THEME_COLOURS (the browser's bar), and a block in the stylesheet; check_styles covers every choice.
The hamburger button in the header opens one panel: Rules and Statistics, the game picker, the current game's own options (Draw, Deals, ...), Restart this deal, and the appearance choices: style, table colour, card back colour and animation. Animation is Device (follow the system's Reduce Motion), Normal, Fast (half the time) or Off; the script writes the result to data-motion on the page, which the CSS keys on, so choosing Normal also overrides a system that reports Reduce Motion wrongly (as an installed iPad app can). The choices are kept in local storage under baize.settings. SETTINGS and readSettings in the engine define them and drop any stored value that is not one of the choices; the colours themselves are html[data-felt=...] and html[data-back=...] rules in the CSS. The panel is a popover (the HTML popover attribute, in the browser's top layer so nothing clips it) hung from the menu button and placed by placeMenu in the script, rather than a dialogue, so the new colours can be seen behind it; it closes with its × button or Escape. Changing a game option, or the game, leaves the panel open, so another can be tried.
No popup extends past the window. The rules card hangs under the header, so fitRules sizes it to the space left below it (on show and on resize) and only its text scrolls; the statistics dialogue is fixed to the window, and the hint message is position: fixed rather than inside the table, which can be taller than a small landscape window. check_popups_fit_the_window checks all of these in every theme at four phone sizes.