BeamNG.Earth
BeamNG.drive용 1:1 비율 실시간 지구, 실제 고도·위성 이미지·부동 원점·보조 서버에서 LOD 스트리밍
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A browser implementation of Hong Kong style Big Two (鋤大弟): you against three AI opponents, 13 cards each, first player to shed every card wins the round. Built as a static Next.js app, deployable to Vercel with no backend. Node 22 or newer is required — the tests run TypeScript directly through Node's built-in type stripping. Live at, built by the pipeline at. The project is a stock Next.js App Router app, so no configuration is needed:README 전문 보기 ↗
A browser implementation of Hong Kong style Big Two (鋤大弟): you against three AI opponents, 13 cards each, first player to shed every card wins the round. Built as a static Next.js app, deployable to Vercel with no backend.
npm install
npm run dev # http://localhost:3000
npm run build # production build
npm test # engine + AI unit tests (node:test, no extra dependencies)
npm run typecheck
npm run bench # self-play tournament between the opponent styles
Node 22 or newer is required — the tests run TypeScript directly through Node's built-in type stripping.
Live at https://clem-big-two.vercel.app/, built by the pipeline at https://vercel.com/clem21/clem-big-two.
The project is a stock Next.js App Router app, so no configuration is needed:
npx vercel # preview deployment
npx vercel --prod # production deployment
Or import the repository at https://vercel.com/new; Vercel detects Next.js,
runs next build, and serves the single route as static content. The whole game
runs client side, so there are no environment variables or server functions.
| Element | Rule |
|---|---|
| Rank order | 3 4 5 6 7 8 9 10 J Q K A 2 (2 is highest) |
| Suit order | ♦ < ♣ < ♥ < ♠ |
| Opening | Holder of 3♦ leads, and the opening play must contain 3♦ |
| Shapes | 1, 2, 3 or 5 cards — four-card bombs are not a shape |
| Five-card order | straight < flush < full house < four of a kind + kicker < straight flush |
| Straight ties | Highest card, suit breaks the tie |
| Flush ties | Suit first, then card ranks from the top down (Hong Kong convention) |
| Full house ties | Rank of the triple |
| Quad ties | Rank of the quad |
| Straights | Run over 3…A,2 with no wrap-around: 3-4-5-6-7 lowest, J-Q-K-A-2 highest. A-2-3-4-5 is not a straight |
| Passing | Legal at any time except when leading; once everyone else passes, the table clears and the pile owner leads |
| Scoring | 1 chip per card left, ×2 at 8–9 cards, ×3 at 10–12, ×4 at 13. The winner collects the pot, so each round is zero-sum |
Rule variants differ between houses. Two choices worth flagging because other
tables play them differently: flushes are compared by suit before rank, and
straights do not wrap around the ace. Both are isolated in lib/combos.ts if you
want the alternative.
All three styles share one rule: they never pass while they hold a legal play. Selectable mid-match from the header.
| Style | Behavior |
|---|---|
| Lowest legal play (default) | Plays the lowest-quality legal combination available: smallest shape, lowest category, lowest value. Dribbles low singles and hoards big cards. |
| Random legal play | Picks uniformly at random among every legal play. |
| Competitive | Plans the hand into the fewest possible plays and protects that plan. |
The competitive tier works from an exact hand decomposition (lib/strategy.ts).
"How many turns do I still need?" is an exact-cover problem over the
combinations a hand contains, and at 13 cards it is small enough to solve
exactly with a bitmask DP rather than a greedy pass:
best[mask] = 1 + min over combos c ⊆ mask of best[mask \ c]
Pinning each step to the lowest remaining card removes duplicate permutations of the same partition, which keeps a full 13-card plan at roughly 4 ms.
On top of that plan the policy is:
npm run bench runs a self-play tournament with the seat assignment rotating by
seed so the 3♦ lead averages out. Against two lowest-legal opponents and one
random opponent over 400 rounds:
| Style | Win rate | Chips per round |
|---|---|---|
| Competitive | 48.3% | +8.20 |
| Lowest legal play | 22.8% | −3.59 |
| Random legal play | 6.3% | −4.60 |
The baseline for four players is 25%.
Strategic passing — declining to break up a group when nothing efficient is available — was implemented and measured rather than assumed. Over 1000 rounds head to head it wins more rounds (26.6% against 23.4%) but loses on chips (−0.70 a round against +0.70): the hands it holds on to get caught by the penalty multipliers. Hong Kong scoring counts chips, so the shipped policy contests instead.
app/ Next.js App Router entry, global stylesheet, icon
app/api/ Room routes (create, join, state, version, move)
components/ TableView (shared layout), GameTable (single player), OnlineTable,
PocketView (phone), TableDisplay + TableSeatGate (tablet),
RoomLobby, CreateRoom, CardView, CardFace (SVG deck), RulesPanel,
Modal, BuildFooter
lib/cards.ts Deck, rank/suit ordering, seeded shuffle and deal
lib/combos.ts Combination detection, comparison, legal move generation
lib/engine.ts Round state machine: play, pass, trick clearing, round end
lib/scoring.ts Hong Kong penalty multipliers
lib/ai.ts Opponent policies
lib/strategy.ts Exact minimum-plays hand decomposition
lib/sound.ts Web Audio sound effects
lib/room.ts Multiplayer room model: seats, intents, redaction
lib/server/ api (every endpoint), room storage (Upstash REST + memory),
crypto, rate limiting, HTTP helpers
public/ updates.txt (changelog)
bench/ Self-play tournament (npm run bench)
test/ node:test unit tests: rules, engine, AI, room model, HTTP layer,
and 450 simulated self-play rounds
The game engine is pure: startRound, applyPlay and applyPass return new
state objects and never mutate their input, and dealing runs off a seeded PRNG
(mulberry32) so any round can be replayed by seed. The React layer holds one
GameState and re-renders from it.
Four humans, any mix of humans and AI, no accounts. /play starts a table with
a password and hands back a six-character room code; everyone else opens
/room/<code>, clicks a seat and enters the password. Seats nobody takes are
played by the AI, so a round works with one human or four.
The password gets you into the room. It does not identify you — on its own, anyone who knows it could submit moves as any seat. Claiming a seat returns a random 32-byte seat token, stored as an httpOnly cookie, and the server keeps only its SHA-256 hash. Every move is authorised by that token, not the password. Passwords are stored as PBKDF2-SHA256 (100k iterations) with a per-room salt.
A seat silent for ten minutes is treated as away: the AI plays its turns and someone else may claim it. Presence is refreshed by the version poll, not only by a state fetch — a quiet table used to age everyone out while they sat watching it, and a phone that locks its screen stops polling entirely, so the window is deliberately generous.
The server is the only authority, and two rules follow:
mulberry32(seed) reproduces every
hand, so publishing the seed publishes the whole deal.Move legality is re-checked server side with the same validatePlay the client
uses — the client check is only there for a fast error message. Cards are matched
against the seat's actual hand, so a forged card id is rejected rather than
played.
Turn-based polling, not sockets. Clients poll GET /api/rooms/:id/version,
which is a ~200 byte payload, and fetch the full state only when the version
moves. Polling pauses when the tab is hidden and resumes on focus.
| Endpoint | Purpose |
|---|---|
POST /api/rooms | Start a table (password, AI style for empty seats) |
POST /api/rooms/:id/join | Claim a seat, receive the seat cookie |
DELETE /api/rooms/:id/join | Give the seat back to the AI |
GET /api/rooms/:id/version | Cheap poll: version, turn, seat status |
GET /api/rooms/:id/state | Full state, redacted for the caller's seat |
POST /api/rooms/:id/move | Play, pass or start the next round |
POST /api/rooms/:id/name | Rename the seat you hold |
POST /api/rooms/:id/table | Claim the shared table display |
DELETE /api/rooms/:id/table | Release it |
POST /api/rooms/:id/control | Table only: next round, restart match, adjust a score |
Every endpoint is a plain Request → Response function in lib/server/api.ts;
app/api/** only adapts Next's route signature onto it. That keeps the rules
about who may act and what is redacted in one place, and lets the whole HTTP
layer be tested by calling a function — no server, no Playwright.
The password endpoints are throttled, because PBKDF2 makes each guess cost the server ~50ms and an unthrottled guesser burns function time as well as getting unlimited attempts. Limits are fixed-window counters in the same store:
| Rule | Limit |
|---|---|
| Password attempts per caller, per room | 10 per 10 minutes |
| Password attempts per room, all callers | 40 per 10 minutes |
| Room creation per caller | 20 per hour |
Rotating IPs still runs into the room-wide ceiling. Exhausted callers get a 429
with Retry-After.
Writes are compare-and-set on the room version. Serverless instances share no memory and requests interleave, so "read, decide, write" without a version check would silently drop one of two moves submitted at the same moment. On Upstash that is a small Lua script; the version lives in its own key so the check needs no JSON parsing inside Redis.
Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (see .env.example).
Provision Upstash Redis from the Vercel Marketplace — Vercel's own KV product was
retired in December 2024. There is no SDK dependency; the REST protocol is one
JSON array per command.
Without those variables the app falls back to an in-process Map. That is fine for
npm run dev and for the tests, and wrong for a deployment: serverless instances
do not share memory. POST /api/rooms reports which store is in use, and the
server logs a warning in production.
A room is about 2 KB and expires after a week of silence. Four players polling the version endpoint every 3 seconds is roughly 80 requests a minute for the table.
One device can act as the table itself — a tablet lying in the middle of the
real one. Open /room/<code>/table on it and enter the same table password.
It shows:
It is also the only client that can run the match: deal the next round, restart the match with the chips back to zero, and adjust any seat's score by ±1 or ±5 when something needs settling by hand. A phone calling those endpoints gets a 403; the table holds its own token, exactly as a seat does.
The table never receives anybody's cards. It is a screen the whole room can
see, so publicRoom() withholds every hand from it, and a test asserts that.
With a table display active, the shared state is already in front of everyone, so the player view drops to what a phone actually needs: your hand, your buttons, and one line naming what is on the table. The seats, scoreboard, pile and log all disappear. Release the table and the phones return to the full layout on their next poll.
Cards are buttons, so Tab and Space work without help. On top of that:
| Key | Action |
|---|---|
| ← / → | Move along your hand |
| Space | Select or deselect the focused card |
| Enter | Play the selection |
| P | Pass |
| H | Hint |
| Esc | Clear the selection |
Shortcuts are ignored while you are typing in a field, so the lobby still works normally.
When your hand holds no legal reply there is no decision to make, so the game passes for you after a beat rather than asking you to confirm the inevitable.
The three plays before the current one sit to the left of the pile, oldest first, and the strip slides across when a new play lands and the old pile joins it. The record outlives the trick: when the table is swept the pile goes but the plays stay, faded back, so you can still see what has already been spent. Entries linger for a beat as they drop off the end so they collapse out rather than blinking away. Hidden on phones, which have no room for it — and unnecessary when a tablet is acting as the table.
A round opens with cards flying out from the middle of the table to the four seats before the hand fans in.
touch-action: manipulation removes the double-tap-to-zoom
delay that fought tap-twice-to-play, tap highlights and long-press selection
are off, and overscroll-behavior stops a downward swipe pulling the page into
a refresh mid-game.@media (hover: hover). On a touchscreen :hover
latches after a tap, so a card you merely touched used to stay lifted as though
chosen.A new room opens on Competitive opponents at a 1.5 second pace: they play one seat at a time, each landing as its own version bump so every client gets to watch it arrive. The table display can change both — the pace runs from instant through 1, 1.5, 2, 3 and 5 seconds.
Instant is still there and still resolves every waiting seat inside the request that triggered it, which is fast but means a whole row of opponents can come and go between two polls with nothing to see.
There is no background job behind this: tickAi runs from the version endpoint,
so whoever is polling carries the clock forward. That is consistent with the
rest of the room, which already refuses to advance at all unless a human is
still at the table.
Single player has no room to pace, so it uses its own timer: 1125ms between AI turns, up from 750ms for the same reason.
Tap your seat name in the phone header. The lobby is behind you once you are seated, and the name is what everyone else reads on the table, so it needs to be reachable from the one screen a player keeps. Renaming needs your seat cookie, not the password, and leaves the seat token, invite code and turn order alone.
A play does not vanish when the next one lands, and a swept table does not simply empty: the cards travel to the history strip over about a second, slowly enough to follow from across a table. The distance is measured at run time rather than guessed, because the strip sits somewhere different in the edges and corners layouts, and again in portrait.
In portrait there is no width for a trick strip beside the pile, so the table keeps the hand to beat and the single play before it.
When it becomes your turn the phone chimes and vibrates, since a phone spends the game face down when a table display is running. The table display sounds its own cue and names the seat to play.
The footer carries the version, build stamp and short commit SHA, followed by the most recent
changelog entry. Clicking that entry opens a dialog with the full history, newest first. Entries live
in public/updates.txt, one per line, appended oldest first:
2026-09-03T18:10:00Z - Initial release: ...
2026-09-03T19:05:00Z - Added a build footer with the changelog: ...
Lines are displayed verbatim, blank lines are ignored, and a missing or unreadable file leaves that
part of the footer empty rather than raising an error. Add a line whenever you ship a user-visible
change. Version and build stamp are inlined at build time by next.config.mjs; on Vercel the commit
SHA comes from VERCEL_GIT_COMMIT_SHA.
--card-w / --card-h and
--hand-overlap on :root, so a context can resize the deck by setting three
variables. The overlap tightens on narrow screens so all 13 cards still fit a
390px phone without scrolling.components/CardFace.tsx — no image
assets, crisp at any size, and the standard French-deck arrangement including rotated bottom-half
pips and mirrored corner indices.prefers-reduced-motion: reduce.lib/sound.ts) — noise bursts for card
flicks, short tones for selection, passing and the end-of-round fanfares. Nothing is downloaded.
Browsers require a user gesture before audio starts, so the context is created on first
interaction. The speaker button mutes, and the preference is stored in localStorage.