尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證https://beggarsrole.vercel.app/
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗SPARE CHANGE is a browser game about living on the street. Every new game rolls you into a random life: a person with a name, an age, a backstory and a few personality traits, dropped into one of 15 real cities — Bangkok, Mumbai, Lagos, Tokyo, New York, Paris and others. You have no home and almost no money. Your job is to get through each day, and, if you can, find a way out.
The game is played in Thai by default, with a full English translation one click away. It runs entirely in the browser with no login; your life autosaves as you play.
New players get short first-time tips the first time something comes up: the first night, hunger or thirst running low, the police starting to notice you, the way off the street. Each tip shows once and can point you to the right tab; all of them can be turned off. The ? button on the play screen opens a short how-to-play page that explains the day, every stat and the way out.
On a computer the game can be played from the keyboard: 1–9 takes the numbered action in the open tab (or picks a spot when asked where to go), ← → or Q W E R switch tabs, and during an event 1–9 picks a choice while Enter skips the text and continues. Keys are read by position, so they work on a Thai keyboard layout too.
A settings page (from the title screen) holds the language, sound, how fast event text types out (slow to instant), motion (follow the device, reduce, or full; reduce turns off every animation) and the first-time tips, which can be switched off or shown again from the start.
Players can report a bug or suggest an idea from the speech-bubble button on the play screen, or from the link at the bottom of the title and end screens. A report sent during a life carries its city, day and seed so the problem can be found again; a contact (such as a Facebook name) is optional. Only the admin page shows these messages.
When a life ends you get a summary: your timeline, the numbers of your life, a share card, and for lives that made it out, a keepsake poster you can save as an image.
?tab=daily links straight to one.npm install
npm run dev # http://localhost:3000
| Script | What it does |
|---|---|
npm run build / npm start | Production build / serve |
npm run validate | Checks every JSON data file and its cross-references (items, actions, traits, flags, translation parity) |
npm run simulate -- 300 balanced | Plays N lives with the auto-pilot (safe / balanced / risky) and prints how they ended — use after changing data |
npx tsx scripts/check-paths.ts 40 | Plays lives that commit to one way of living and counts how often each reaches its own ending — the auto-pilot never takes those paths |
npm run verify-replay -- 100 | Plays N lives through step() like the UI does, then checks the server replay reproduces each one — run after changing the engine |
npm run check-achievements -- 150 | Plays N lives into one career and reports how often each achievement is earned |
app/ page.tsx (title) · play/ (the game) · end/ (life summary) · endings/ (album) · lives/
achievements/ · leaderboard/ · admin/
api/: og (share card) · track · runs · records · leaderboard · traces · feedback · stats · daily · profile · admin
components/ game UI: StatBar, ChoiceCard, EventModal, PixelScene, CharacterSprite, Timeline,
InventoryPanel, SpotPicker, SkipPanel, RunPanel, SurvivorPoster, FeedbackModal, Flag, ...
data/ cities, origins, actions, events, endings, traits, items (JSON)
lib/ engine.ts (pure game logic) · rng.ts (seeded) · conditions.ts · data.ts (load + validate)
score.ts · achievements.ts · replay.ts · storage.ts · online.ts · db.ts · i18n.tsx
i18n/ th.json · en.json (interface text)
types/game.ts shared types
scripts/ validate-data · simulate · check-paths · verify-replay · check-achievements · test-online
The engine is pure. lib/engine.ts exposes step(state, action) => newState with no React and
no Math.random. The random-number state lives inside GameState, so the same seed and the same
choices always produce the same life. That is what makes replays, the daily challenge and
server-side score verification possible. Malformed data entries are skipped with a console warning
instead of crashing the game.
Almost everything in the game is data. Money in data files is written in meal units
(cash: 2 means the price of two cheap meals in whatever city you are in), so one event works in
every currency. Stats run from 0 to 100.
Used by actions, events, choices and endings to decide when they apply:
minDay maxDay minAge maxAge ageGroups cities notCities climates spots scenes weather phases months festival festivals traits anyTraits notTraits flags anyFlags notFlags items notItems origins min{} max{} counters{} maxCounters{} companion hasFamily sheltered shelter inSeason afterActions chance
min / max take any stat plus cash, stash and wealth (cash + stash), in meal units.
health hunger thirst hygiene hope energy rep heat cash take a number or a [min, max] range.
Also: cashPct stashPct items{} flags[] unflags[] counters{} addTraits[] companion sheltered cause.
cause (cold | heat | illness | hunger | storm | injury) decides which death ending you get if
that damage is fatal.
Counters the engine and UI read: friends mealsGiven helped viral idProgress ngoVisits charityVisits familyCalls laborDays busks taught nightsShelter gambleWins.
Append to data/events.json:
{
"id": "street_poet",
"icon": "📝",
"weight": 2,
"trigger": "any",
"tags": ["kindness"],
"cooldown": 30,
"conditions": { "minDay": 5, "cities": ["paris"] },
"title_th": "กวีข้างถนน", "title_en": "The Street Poet",
"text_th": "…", "text_en": "…",
"choices": [
{ "label_th": "ฟัง", "label_en": "Listen", "effects": { "hope": 8 },
"text_th": "…", "text_en": "…" },
{ "label_th": "ขอเขียนด้วย", "label_en": "Write one too", "chance": 0.5,
"success": { "text_th": "…", "text_en": "…", "effects": { "rep": 5, "flags": ["poet"] } },
"fail": { "text_th": "…", "text_en": "…" } }
]
}
trigger: day (start of a day), action (after an action — combine with afterActions),
any, or birthday.once: true fires only once per life; forced: true fires automatically the first time its
conditions are met.major: true puts the choice on the life timeline and pauses the auto-pilot.{name}, {city}, {dog}, {charity} and {spot}.conditions.flags.Then run npm run validate and npm run simulate to see it fire.
An action with "kind": "activity" needs no engine code: its effects (stats, counters, flags),
an optional pay range in meal units, and the events that read its counters do all the work.
The version in the top-right corner and the update bar on the title screen come from
lib/changelog.ts. Every shipped update adds an entry at the top and bumps the last number
(v1.0 was the first playable build).
Append to data/endings.json with id, title_th/en, rarity (common to legendary), icon,
priority (higher is checked first), conditions, epitaph_th/en and line_th/en (the quote on
the share card). Death endings add "death": ["cold", ...]. Secret ones get "hidden": true.
Usually an event sets a flag and the ending requires it.
Append to data/cities.json: names, flag, weight, currency, meal (local price of a cheap
meal), dailyIncome, milestoneCash, seasons (hotMonths / wetMonths / coldMonths / snow),
shelter, difficulty, charity_th/en, five spots (each with scene, income, risk,
police, tourists, optional minRep), two or three festivals and six names. Then add an SVG
flag for it in components/Flag.tsx, because flag emoji do not render on Windows.
Add an entry to ACHIEVEMENTS in lib/achievements.ts with a test that receives the finished
life, the career totals and the set of endings found. Add a progress function for long-term goals
so the achievements page can show a progress bar, then run npm run check-achievements to make sure
it is reachable and not trivially easy.
Everything online is optional. Without a database the game plays exactly the same, and the online panels say they are unavailable.
/api/track, /api/stats): lives started and finished, unique players
(an anonymous random id kept in the browser), players today, ending and city counts./api/records, page /leaderboard): every finished life, top 100 by score,
days or money in baht (rough fixed rates in lib/format.ts), plus totals and record holders.
Money, end city, character and origin are stored per run from the server replay; runs from
before that have them empty. names holds one row per player name with its start count
(/api/track adds one per life); /api/stats counts players as names./api/runs, /api/leaderboard): today (every run
finished since midnight Bangkok time), daily challenge, this week, all time and per starting
city, top 50 each. A nickname counts as one person: each exact name appears once per board, with
its best score, so a new record moves that name up instead of adding a second row. Nicknames go
through a Thai and English word filter (lib/moderation.ts)./api/daily): one seed per day on Bangkok time, derived from
DAILY_SECRET; only a player's first daily run each day counts./api/profile): the endings collection is synced to the server, and a recovery
code moves it to another device./api/traces, panel under the scene on the play page): graves and escapes written
by /api/runs, plus player notes (2–80 characters, no links, same word filter) and one candle
per player per grave. Traces never touch the engine, so replays are unaffected./api/feedback): bug reports and ideas (5–1000 characters) with the page,
language, user agent and, during a life, its city, day and seed. Six per IP per hour./api/admin, page /admin): a 30-day player chart, how long lives last, player
feedback with a done/reopen toggle, and hiding or restoring leaderboard names and street notes. Needs ADMIN_TOKEN (12+ characters).Anti-cheat. The play page records every action that changed the state. When a life ends the
client sends only the seed and that action list; /api/runs replays it with the same pure engine
(lib/replay.ts) and computes the score itself (lib/score.ts). Identical runs are de-duplicated
and every endpoint is rate-limited per IP. If you change the engine in a way that changes outcomes,
bump SAVE_VERSION — lives already in progress can then no longer be verified.
Score (lib/score.ts) = (survival 1200 · ln(1 + days/60) + escape bonus by ending rarity
[2000–9000, none for death endings] + speed bonus 3000 · (1 − days/730) for escapes + kindness
[friends × 20 + helped × 15 + meals shared × 5, at most 1500]) × starting-city difficulty (easy 1,
normal 1.25, hard 1.5, brutal 2). The log curve makes escaping beat farming days; check with
npm run simulate -- 300 safe x farm.
Database. Postgres on Neon through @neondatabase/serverless (the free
tier is plenty). Tables are created automatically on first use (lib/db.ts).
| Variable | |
|---|---|
DATABASE_URL | Neon connection string (required for online features) |
ADMIN_TOKEN | password for /admin |
DAILY_SECRET | any random string — keeps future daily seeds secret |
For local development put them in .env.local (gitignored). npm run test:online runs an
end-to-end check against the dev server and the database and deletes its test data afterwards.
Page views come from Vercel Web Analytics (<Analytics /> in app/layout.tsx); enable it in the
project's Analytics tab.
DATABASE_URL, ADMIN_TOKEN and DAILY_SECRET,
then redeploy so they take effect./api/og) runs on the Edge runtime and loads the Kanit font from Google Fonts
when it renders; if that fails it falls back to the default font.From the command line: npx vercel for a preview, npx vercel --prod for production.
next/og) cannot stack Thai tone marks above upper vowels, so
app/api/og/route.tsx positions them by hand to keep share cards readable.