3D multiplayer archery on Monad. Stake MON, take three shots, winner takes the pot. Built at Monad Blitz Jogja, 25 April 2026.
| Web app | https://manah-hudas-projects-a8e7f558.vercel.app (stable alias โ always points to latest production, public access) |
| Submission writeup | SUBMISSION.md |
| Smart contract (Monad Testnet) | 0x6d77b08139d9d37a2067f086cc6f7359821326cc |
| Source | https://github.com/PugarHuda/manah-blitz |
Manah is a multiplayer turn-based archery game on Monad. Log in with Gmail (Privy embedded wallet) or connect any wallet, stake MON, then take 3 shots each at a 10-ring target. Highest aggregate score wins the pot.
/practiceโ solo, no stake. Pure-TS physics that mirrorsManah.sol1:1, so practice scores โ real-game scores./playโ create or join room โ multiplayer, on-chain stake + scoring. Eachshoot()call emits 50TickComputedevents that flash on MonadVision in real time.
Rendering (Three.js), simulation (delta-time physics), game logic (FSM + scoring), and networking (Socket.IO event-only) are kept in separate modules under web/src/game/. Each can be replaced or hosted differently without touching the others. The on-chain layer (contracts/src/Manah.sol) is the trust anchor โ turn order, stakes, payouts, and scoring all happen there, regardless of what the visual layer is doing.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Manah Stack โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ RENDERING LAYER web/src/game/rendering-layer.ts โ
โ Three.js scene, lights, target, first-person bow (WebXR roadmap) โ
โ โ
โ SIMULATION LAYER web/src/game/simulation.ts โ
โ Arrow physics (delta-time), gravity, collision (target + ground), โ
โ per-difficulty config (gravity / hit radius / distance) โ
โ โ
โ GAME LOGIC LAYER web/src/game/game-manager.ts โ
โ FSM: aiming โ shooting โ impact โ replay โ next โ
โ 30 s turn timer, 2 pause tokens, WA/FITA scoring (1โ10) โ
โ โ
โ NETWORKING LAYER web/src/game/network-manager.ts โ
โ Socket.IO client (event-only โ no position sync). Server-authoritativeโ
โ timer + turn lock. Falls back to fully-local play when no server URL. โ
โ โ
โ ON-CHAIN LAYER contracts/src/Manah.sol โ
โ Room lifecycle, stake escrow, on-chain trajectory (50 ticks/shot), โ
โ bullseye-lock + linear-falloff scoring, AFK skip, settle-to-winner. โ
โ โ
โ AUTH LAYER web/src/components/providers.tsx โ
โ Privy embedded wallet (Gmail) + external wallet via @privy-io/wagmi โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The four runtime layers are modular โ none mutate another's state directly. State flows: Input (Gesture) โ GameManager โ Simulation โ Rendering, with the optional Networking layer broadcasting events between clients.
Monad's pitch is "products the EVM has never seen before." Manah is exactly that:
| Chain | Cost / round | Feel | Verdict |
|---|---|---|---|
| Ethereum L1 | ~$300 | unplayable | gas-prohibitive |
| Polygon ยท Base ยท Arbitrum | ~$0.05 | 2โ3s lag | latency kills the gesture |
| Monad โญ | $0.003 | 800ms finality | parallel + cheap + EVM-native |
- Room-isolated storage (
mapping(roomId => Room)) โ Monad's parallel scheduler runs concurrent rooms on different threads. - 50
TickComputedevents pershoot()โ turn throughput into the demo. Watch them flash on MonadVision. - In-house fixed-point trig (
contracts/src/Trig.sol) โ 19-entry sine LUT @ 5ยฐ + linear interp, ~200 gas/call. No PRBMath dep.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. LOGIN โ
โ Continue with Gmail โ Privy creates an embedded wallet โ
โ OR Connect wallet โ MetaMask / Rainbow / etc. โ
โ โ
โ 2. PICK DIFFICULTY โ
โ easy 3 m ยท 8 m/sยฒ gravity ยท 0.7 m hit radius โ
โ medium 5 m ยท 9.8 m/sยฒ ยท 0.6 m โ default โ
โ hard 8 m ยท 11 m/sยฒ ยท 0.5 m โ
โ โ
โ 3. CREATE OR JOIN ROOM โ
โ Set max players (2โ8) and stake. Share the room link. โ
โ All players lock the same MON amount. โ
โ โ
โ 4. THREE SHOTS EACH โ
โ 30 s aiming clock โ press ยท drag (power) ยท release โ
โ Arrow physics integrate over 50 ticks (40 ms each) โ
โ WA/FITA scoring (1โ10) by distance from bullseye โ
โ Two pause tokens per player โ
โ โ
โ 5. WINNER TAKES POT โ
โ Highest aggregate score wins. Smart contract โ
โ auto-transfers the full stake pool. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Next.js 16 (App Router, Turbopack default) + React 19
- Tailwind v4 (CSS-first
@themeโ notailwind.config.ts) - Three.js for the rendering layer (3D scene; WebXR is a roadmap item)
- Socket.IO client for multiplayer event sync
- Zustand for client UI state, wagmi v3 + viem for chain calls
- Privy for auth โ embedded wallet on Gmail login, external wallets via
@privy-io/wagmi
- Foundry 1.5.1, Solidity 0.8.26 with
via_ir = true - No external deps beyond
forge-std(vendored) - 23 unit tests, all green
- Deployed to Monad Testnet at
0x6d77b08139d9d37a2067f086cc6f7359821326cc
- Node.js + Socket.IO (event-driven, server-authoritative)
- Runs locally for hackathon demo:
node server/index.js - Production hosting: TBD (Render / Fly / Railway)
# 1. Clone
git clone https://github.com/PugarHuda/manah-blitz.git
cd manah-blitz
# 2. Web
cd web
cp .env.example .env.local # fill NEXT_PUBLIC_PRIVY_APP_ID and (optional) NEXT_PUBLIC_SOCKET_URL
npm install
npm run dev # http://localhost:3000
# 3. Contracts (separate terminal, optional)
cd contracts
cp .env.example .env # fill PRIVATE_KEY
forge test -vv # 23/23 green
forge script script/Deploy.s.sol --rpc-url monad_testnet --broadcast \
--private-key $PRIVATE_KEY # deploys + writes broadcast/
# 4. Multiplayer server (optional โ single-player works without it)
cd server
npm install
npm start # ws://localhost:3001
# Then set NEXT_PUBLIC_SOCKET_URL=http://localhost:3001 in web/.env.local| Network | Monad Testnet |
| Chain ID | 10143 |
| RPC | https://testnet-rpc.monad.xyz |
| Currency | MON |
| Explorer | https://testnet.monadexplorer.com |
| Faucet | https://testnet.monad.xyz/ |
function createRoom(uint8 maxPlayers, uint128 stake) external payable returns (uint256 roomId);
function joinRoom(uint256 roomId) external payable;
function startGame(uint256 roomId) external; // host or auto on full
function shoot(uint256 roomId, int256 angle, int256 power) external;
function skipTurn(uint256 roomId, address player) external;
function settleGame(uint256 roomId) external; // payout winner, CEI
function getRoom(uint256 roomId) external view returns (...);
function previewScore(uint256 distMm) external pure returns (uint16);Encoding: angle โ [0, 9000] centidegrees, power โ [1, 10000] basis points. UI converts radians/percent in web/src/lib/manah.ts.
event RoomCreated(uint256 indexed roomId, address indexed host, uint8 maxPlayers, uint128 stake);
event PlayerJoined(uint256 indexed roomId, address indexed player, uint8 numPlayers);
event GameStarted(uint256 indexed roomId, bytes32 targetSeed, int128 targetY, uint128 pot);
event TickComputed(uint256 indexed roomId, uint8 tickIndex, int256 x, int256 y); // 50 per shot
event ArrowLanded(uint256 indexed roomId, address indexed player, uint8 arrowIndex, bool hit, uint256 distMm, uint16 points, uint16 newScore);
event TurnSkipped(uint256 indexed roomId, address indexed player, uint8 arrowsUsed);
event GameSettled(uint256 indexed roomId, address indexed winner, uint128 payout, uint16 winningScore);Per-shot gas: ~170k (โ $0.003 on Monad testnet).
- Next.js 16 web app on Vercel with Tailwind v4 brand system, Git-linked auto-deploy
- Public access โ Vercel SSO Protection disabled, anyone with the URL plays without team auth
- Privy auth โ three flows tested:
- Continue with Gmail โ embedded wallet auto-creates on Monad testnet
- Connect wallet โ external wallet (MetaMask/Rainbow) via SIWE
- Live wallet pill (address + MON balance, refresh every 8 s) with empty-state faucet CTA
- On-chain
Manah.soldeployed to Monad Testnet at0x6d77b08โฆ - FE wired to contract via wagmi hooks (
useCreateRoom,useJoinRoom,useShoot,useRoom,useLeaderboard,useSettleGame) - Race-safe Privy โ wagmi connector bridge โ every write call routes through
ensureConnector()which awaitssetActiveWallet(wallets[0])before submitting, eliminating the "Connector not connected" error that hit users when they clicked join immediately after sign-in - Difficulty selector (easy / medium / hard) โ forwarded via URL through lobby โ room โ game
- Practice mode (
/practice) โ solo HTML+SVG 2.5D scene, dashed yellow trajectory preview, animated nocked arrow, score popups, pause menu, 60 s round timer - Multiplayer game (
/room/[id]/game) โ same mechanic, each release submitsshoot(roomId, angle, power)on-chain; status cycles "Confirm in walletโฆ" โ "Streaming 50 ticks on-chainโฆ" โ MonadVision tx deep-link; arrowsLeft + score read fromusePlayer()for chain-authoritative truth, with optimistic local sim driving the arrow animation - Real-time on-chain leaderboard during the match โ
useLeaderboard()does a wagmi multicall over every player address, refresh every 2 s, sorted by score with the local player highlighted; live opponent points visible while shots are mid-flight - Auto-settle on game over โ when every player burns their three arrows, the lowest-address player automatically dispatches
settleGame(). Other clients' calls revert (status flips to Settled on first); the contract_recordScore+_payoutflow transfers the entire stake pool to the highest scorer - Winner banner with payout MON amount + MonadVision deep-link to the settle tx, and "You won the pot" / "Match settled" copy depending on local-player perspective
- Vercel production deploy with
vercel.jsonbuild env vars (Privy app id + contract address) - Public commit history with co-authored agentic-pair commits
- Three.js 3D scene with first-person bow + animated arrow flight + trajectory preview line โ modules under
web/src/game/(BowSystem, CameraManager, ArcheryGame) wired but the canvas play loop is being polished; multiplayer FE currently uses the simpler HTML/SVG 2.5D renderer for parity with Practice - Authoritative Socket.IO multiplayer server (
server/index.jsruns locally; needs Render/Fly hosting) - Pause / resume on-chain (contract has
skipTurnfor AFK; UI button โ tx not wired)
- AR mode (WebXR) โ plane detection + world-anchored target placement (
enterAR()entry point inarchery-game.tsexists; no UI button yet) - PWA manifest + service worker for installable mobile shell
- Replace
prevrandaowind seed with Pyth Entropy for verifiable randomness - Pyth Price Feeds โ live MON/USD on stake UI and payout
- Envio HyperIndex โ sub-200ms leaderboard subscription instead of polling
- Magma โ auto-stake winnings to gMON
- Tournament mode (bracket elimination across rooms)
- Apply to Monad Momentum
.
โโโ web/ Next.js 16 frontend (deployed to Vercel)
โ โโโ src/app/ App Router routes (/, /play, /practice, /room/[id], /room/[id]/game)
โ โโโ src/components/ LoginButton, Providers (Privy + Wagmi)
โ โโโ src/game/ 4-layer engine: rendering / simulation / manager / network
โ โโโ src/lib/ chains, abi, hooks, manah encoders, physics (TS port), zustand store
โโโ contracts/ Foundry project (Manah.sol + Trig.sol + 23 tests)
โ โโโ src/ Manah.sol, Trig.sol
โ โโโ test/ Manah.t.sol
โ โโโ script/Deploy.s.sol Single-shot deploy script
โ โโโ broadcast/ Deployment artifacts (proof of testnet deploy)
โโโ server/ Socket.IO multiplayer server (run with `npm start`)
Practice mode runs the trajectory in TypeScript (web/src/lib/physics.ts), bit-equivalent to Manah.sol's _integrateAndEmit + _scoreShot. This means a player who tunes their aim in practice carries that intuition into staked play โ practice scores โ real scores. If Manah.sol constants change, both must update; the source of truth is the contract.
Server-signed scoring works but shifts trust to a backend. On-chain physics gives:
- Verifiability โ any observer can re-run the trajectory from
TickComputedevents. - Trustlessness โ no backend key to compromise.
- Demo value โ 50 events per shot is a visible feature on the explorer. Monad's throughput becomes the spectacle.
We send { direction, power } over Socket.IO; clients re-simulate locally. No 60Hz position broadcasting, no interpolation drift. The server owns the FSM (turn, score, timer); clients are just renderers + input.
| Name | Focus |
|---|---|
| Pugar Huda Mantoro (@PugarHuda) | Frontend, Privy/wagmi integration, contract wiring, Vercel deploy, repo |
| Muhamad Azis (@mazis9651) | Three.js scene, gesture system, Socket.IO multiplayer server |
Built with Claude Opus 4.7 (1M context) as agentic pair-programmer โ see commit co-authors for the conversation trail.
MIT
Manah only exists because Monad exists. On other chains it's gas-prohibitive or laggy. On Monad, it's playable.