Skip to content

Repository files navigation

Oligarki

Papan permainan satir tentang politik dan ekonomi Indonesia.

Svelte 5 Phaser 3 Vite 7 Offline PWA Capacitor Android


Oligarki is a satirical Monopoly-style board game set in Indonesian politics and economics. Buy up warteg and nickel smelters, dodge the anti-corruption commission, and outlast opponents like Jenderal (Purn.) and Crazy Rich Menantu.

It runs fully offline as an installable PWA and ships as an Android app via Capacitor. All in-game text is in Bahasa Indonesia.

Features

  • 40-tile board with a fake-3D tilted view and a follow camera, rendered in Phaser
  • 32 event cards across two decks — Kartu Nasib (16) and Kartu Kebijakan (16)
  • 7 CPU personas across three difficulty tiers (santai / normal / licik)
  • 8 optional house rules — free-parking jackpot, mortgages, inflation, auctions, trading, and more
  • Two modesKlasik (last one standing) and Kilat (lap target), plus a fully custom setup
  • Fully offline — no network at runtime; audio is WebAudio-synthesised, so no audio files ship
  • Deterministic engine — seeded RNG makes every game reproducible and testable

Quick start

Requires Node.js 22+.

npm install
npm run dev          # Vite dev server with HMR on :5173

Scripts

Command What it does
npm run dev Dev server with hot reload
npm run test Full Vitest suite, once
npm run test -- tests/engine/rent.test.ts A single test file
npm run test:watch Watch mode
npm run build svelte-check typecheck + production build to dist/
npm run preview Serve the built dist/ (use this to test the offline/PWA build)
npm run icons Regenerate public/icons/* from source art

Definition of done for any change: npm run test && npm run build both clean.

Architecture

The game is a strict one-way loop. Every state change goes through the orchestrator — there is no other mutation path.

UI / AI  →  Orchestrator.dispatch(Action)  →  engine reducer
                                                   │
                          (new GameState, GameEvent[])
                                                   │
                             animator plays events → stores update → UI reacts
Directory Responsibility
src/engine/ The entire rules state machine — pure, deterministic, seeded RNG. Touches no Phaser, Svelte, or DOM.
src/orchestrator.ts The only mutation path. Owns autosave, store updates, animation queueing, and CPU scheduling.
src/scene/ Phaser presentation. animator.ts drains the GameEvent queue; iso.ts handles ring→grid→screen math.
src/ai/ decide(state, playerId) → Action, parameterised by persona and difficulty.
src/content/ All game data — board, card decks, personas, and strings.ts. Every UI string lives here.
src/ui/ Svelte 5 overlay mounted above the Phaser canvas. Screens route off screenStore.

Two invariants worth knowing before you touch the orchestrator:

  • dispatch uses a queue plus a re-entrancy guard, because CPU auto-play can trigger synchronous recursive dispatches.
  • gameStore is only published after the animator drains, so dialogs and the HUD never spoil a result before the token visually lands.

Assets

All art and audio is indirected through public/assets/asset-manifest.json (59 keys). Code references manifest keys such as tile.go or token.helm — never filenames. To swap an asset, drop the file into public/assets/ and repoint the manifest entry; no code change needed.

Testing

tests/ mirrors src/. Vitest runs in a Node environment with no DOM or Phaser — the engine's purity is what makes that possible.

npm run test              # 184 tests across 32 files
npm run test -- tests/sim # the balance regression gate

tests/sim/ is a balance regression gate, not a smoke test: it runs roughly 1,600 simulated games and asserts there are no deadlocks, that turn counts stay in band, and that difficulty win rates hold. Re-run it and check the printed median and win-rate stats whenever you change rent tables (src/engine/rent.ts), persona parameters, or level multipliers.

Gotcha: newGame(config) stores the config by reference. In tests, never mutate st.config — spread a fresh config object instead.

Android build

The web build is bundled into the APK and served from the WebView's https://localhost/ root, so the game runs entirely offline. App ID com.oligarki.game, minSdk 24, targetSdk 36.

npm run build                 # vite -> dist/
npx cap sync android          # copy dist/ into the Android project
cd android
export ANDROID_HOME=$HOME/Android/Sdk ANDROID_SDK_ROOT=$HOME/Android/Sdk
export JAVA_HOME=/path/to/jdk-21
./gradlew assembleRelease
# -> android/app/build/outputs/apk/release/app-release.apk

JDK 21 is required. Capacitor 8's Android project compiles with source release 21; building under JDK 17 fails with invalid source release: 21. The JBR bundled with Android Studio works (<android-studio>/jbr).

Signing

Release signing is read from android/keystore.properties, which is gitignored and not included in this repository — you need to supply your own. Create it with:

storeFile=keystore/your-release.jks
storePassword=…
keyAlias=…
keyPassword=…

storeFile is resolved via rootProject.file(...), so the path is relative to android/, not to the app module. Getting that wrong produces a "keystore not found" error.

Keep your keystore safe — losing it means the app can no longer be updated under the same identity.

License

ISC.

About

Oligarki is a satirical Monopoly-style board game set in Indonesian politics and economics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages