尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Fully on-chain multiplayer Pac-Man built with BOLT ECS (MagicBlock) + Ephemeral Rollups for real-time gameplay on Solana.
• Entity — just an ID (a PDA on Solana). No data, no logic.
• Component — pure data struct (= a Solana account). Position, health, score, etc.
• System — a Solana program that contains the logic. Receives components, modifies them, returns them.
Each element in the game (player, map, monster) is an Entity with Components attached. The World program handles PDA derivation, account routing, and component lifecycle — you just declare what components your system needs.查看完整 README ↗Fully on-chain multiplayer Pac-Man built with BOLT ECS (MagicBlock) + Ephemeral Rollups for real-time gameplay on Solana.
| Layer | Tech |
|---|---|
| On-chain framework | BOLT ECS (extends Anchor) |
| Real-time engine | Ephemeral Rollups (MagicBlock) |
| Language | Rust (programs) / TypeScript (frontend) |
| Frontend | Next.js 16 + React 19 + Tailwind 4 |
Each element in the game (player, map, monster) is an Entity with Components attached. The World program handles PDA derivation, account routing, and component lifecycle — you just declare what components your system needs.
A temporary mini-Solana dedicated to the game session.
eu.magicblock.app), US (us.magicblock.app), Asia (as.magicblock.app)All 5 components use #[component(delegate)] for ER support.
1. INIT (L1)
Create World + map entity + 5 components
init-map: spawn snake at random bottom corner, 3 ghosts at other corners, set status Playing
2. DELEGATE (L1 → ER)
DelegateComponent for all 5 component PDAs (2 batches)
Accounts locked on L1, cloned into ER
3. GAMEPLAY (ER — all gasless, ~50ms)
move-player: wall check, snake body block, coin pickup, kill check, anti-teleport, spawn lock
move-monster: BFS pathfinding (snake + 3 ghosts), ghost wander, kill check
Session keys: wallet signs once, then moves auto-sign (no popup)
4. END GAME (ER → L1)
end-game system checks: timeout / all coins / no players
Commit & Undelegate → final scores permanent on Solana L1
Every frontend connects to the ER via WebSocket (onAccountChange). When any account changes (player moves, coin collected, monster moves), all connected clients receive the update instantly. No game server needed.
| Component | Fields | Description |
|---|---|---|
Position | x (i64), y (i64), spawn_x (i64), spawn_y (i64), orientation (u16), sprinting (bool) | Player/entity position + spawn point. Coords x100 (1300 = tile 13, 1350 = tile 13 right quadrant) |
PlayerState | authority (Pubkey), alive (bool), spawn_time (i64), locked (bool), lock_time (i64), coins_saved (u16), respawn_time (i64) | Player stats, life status, spawn lock, respawn timer |
CollectedCoins | bits ([u8; 421]), count (u16) | Per-player coin bitset — 4 coins per tile (1 per quadrant), 841 tiles × 4 = 3364 bits |
MapConfig | seed (u64), width (u16), height (u16), active_players (u8), total_coins (u16), start_time (i64), status (GameStatus) | Map metadata + game state (Waiting/Playing/Finished) |
PlayerRegistry | players ([[u8;32]; 10]), player_states ([[u8;32]; 10]), count (u8) | Registry of active players (Position + PlayerState PDAs). Max 10 |
Monster | x (i64), y (i64), direction (u16), body ([i64; 12]), ghosts ([i64; 6]), ghosts_dir ([u16; 3]) | Snake head (centered, +50) + 6 body segments + 3 ghost positions + ghost directions. All coords x100 |
| System | Input Components | Description |
|---|---|---|
init-map | MapConfig, Monster | Parse totalCoins from args. Snake spawns at random bottom corner (start_time % 2), 3 ghosts at other 3 corners. Init snake body. Set status to Playing |
spawn-player | Position, PlayerState, MapConfig, PlayerRegistry | Spawn player in ghost house center (10 predefined positions in x=12-16, y=12-16). Init alive=true, coins zeroed, locked=false. Register in PlayerRegistry. Increment active_players |
move-player | Position, CollectedCoins, MapConfig, Monster, PlayerState | Sub-tile movement (0.25 tile per move). Wall check. Snake body block. Per-quadrant coin pickup. Rectangle kill check (snake) + quadrant kill check (ghosts). Spawn lock (5s + coin reset) |
move-monster | Monster, MapConfig, Position, PlayerState, CollectedCoins + remaining_accounts | Snake: BFS + 25 unit/tick smooth movement, body shifts on tile change. Ghosts: BFS to tile, sub-tile chase on same tile (targets exact player position). Random wander when no target. Respawn check (revives dead players after 15s). Kill check vs all players |
end-game | MapConfig, PlayerState | Check 3 end conditions: all coins collected, no active players, timeout (50 min) |
SnakeCollision error)All monsters have a 20-tile Manhattan distance detection range. Beyond that, they wander randomly. This means you can avoid monsters by staying far enough away, but they'll chase you aggressively once you're in range.
The frontend sends a moveMonster transaction every 400ms via session key. The on-chain system moves all 4 monsters in a single transaction (400k CU budget).
When a player dies (monster kill), they are teleported to their spawn point and must wait 15 seconds before respawning. The respawn is handled by move-monster (runs every 400ms even when dead). On death, collected coins are reset (bitset cleared). coins_saved is preserved.
When a player enters the safe zone (ghost house) from outside:
coins_saved += collected_coins.count → score is banked permanentlylocked = true, lock_time = now + 5 secondsDuring the lock:
lock_time expires (on-chain Locked error)Fixed 29x29 Pac-Man layout with ghost house safe zone.
libs/map-utils/src/bsp.rs) and TypeScript (app/frontend/app/lib/map-gen.ts)All validation is on-chain — the frontend is untrusted.
Instead of failing transactions on invalid moves, move-player uses a desync flag. When a validation fails (wall, teleport, snake body), the transaction still confirms but player_state.desync = true is set and the move is rejected silently. While desync:
desync = false and the player can move againThis prevents tx failures from causing SOL loss and keeps the client synchronized.
| Check | Behavior |
|---|---|
| Anti-teleport (max 25 units walk / 50 sprint) | Sets desync = true |
| Wall collision | Sets desync = true |
| Snake body collision | Sets desync = true |
| Spawn lock (can't exit safe zone during cooldown) | Move silently ignored (no desync) |
| Game must be Playing | require! — tx fails |
| Player must be alive | require! — tx fails |
| Snake kill (centered rectangle — player inside 100x100 box centered on snake head) | Sets alive = false |
| Ghost kill (same tile + same quadrant) | Sets alive = false |
| Spawn position validation | require! — tx fails |
move-monster uses a slot-based rate limit (last_monster_slot in MapConfig). Monsters only move if current_slot >= last_slot + 3 (~150ms on ER). Multiple clients calling moveMonster in the same window only results in one move. Kill check and respawn always run regardless of rate limit.
CUBE3D/
├── programs-ecs/
│ ├── components/
│ │ ├── position/ # x, y, orientation, sprinting
│ │ ├── player-state/ # authority, alive, locked, coins_saved, respawn_time
│ │ ├── collected-coins/ # per-player coin bitset (4 coins/tile, 421 bytes)
│ │ ├── map-config/ # seed, width, height, total_coins, status
│ │ ├── player-registry/ # Position + PlayerState PDAs, max 10 players
│ │ └── monster/ # snake (head + body) + 3 ghosts (positions + directions)
│ └── systems/
│ ├── init-map/ # Spawn monsters at corners, set Playing
│ ├── spawn-player/ # Register player in ghost house
│ ├── move-player/ # Movement + coins + anti-teleport + kill check + spawn lock
│ ├── move-monster/ # BFS pathfinding + ghost wander + anti-collision + kill
│ └── end-game/ # Check win/lose/timeout conditions
├── libs/
│ └── map-utils/ # Shared map bitset, coin detection, PRNG (Rust)
├── app/
│ └── frontend/ # Next.js 16 + React 19 + Tailwind 4 (see frontend/README.md)
├── tests/ # E2E tests
├── docs/ # Architecture & decisions
├── patches/ # session-keys custom lib
└── Anchor.toml
# Terminal 1 — L1 (Surfpool)
surfpool start --rpc-url 'https://devnet.helius-rpc.com/?api-key=YOUR_KEY'
# Terminal 2 — Ephemeral Rollup
ephemeral-validator --remotes "http://localhost:8899" --remotes "ws://localhost:8900" -l "7799" --lifecycle ephemeral
bolt build
anchor deploy --provider.cluster http://localhost:8899
Important: After redeploying, restart the ER validator and delete
magicblock-test-storage/for a clean start. The ER caches program binaries.
cd app/frontend
npm install
npm run dev
Open http://localhost:3000, connect a Solana wallet (Phantom on localhost).
bolt build
anchor deploy --provider.cluster 'https://devnet.helius-rpc.com/?api-key=YOUR_KEY'
| Program | ID |
|---|---|
| position | D6arufzkdPfmne2TrUGaMSA73HEQvSMpSpW81EtrpfEg |
| player-state | 3R6wE1awZdKHDSGchGhS2xHKKB2QSR1xQfCB9GgagZah |
| map-config | 4tNjQVh1bSjqMYNRoWK8jpgvBr1suSjX7adpt6vrgsLV |
| player-registry | 3KYSbQkcqDoRdRUbyT6HGU8a67VCzuhKkfQTUHLDmKLL |
| monster | 8xPThZpo5mJf9avTuGbgDnLKojHxuGF5V53iiUUn6yT2 |
| init-map | 5CB1AR5p23rHraxA367Rt2cyFC3kLRxDga8nywY5dNEV |
| spawn-player | 62SXEXeaqYAQ4PTL3Vgu5LU1bt5iwCYwPvkjmVWUWKnE |
| move-player | GX7dFnXqthzMkBdsvEwsqtprcaPLHUVbxkC2G2j4L8Ap |
| move-monster | EJYmw1gd3eq6ig6aDMAqNfvN3ZiFqsGo5FCCWZrtCok |
| end-game | DNPfzbrCvfodqjTzqgcRYRSfxVyivvZ3BoThcNXoPXgM |
These constraints shaped the architecture:
Vec, no heap allocations. All arrays are stack-allocated ([u8; 106], [u16; MAX_TILES])execute() and passed by reference to bfs_direction(). Cannot call BFS as a function with its own stack arrays 4 timesmove-monster needs 400k CU (4 BFS pathfinding calls). Set via ComputeBudgetProgram.setComputeUnitLimit() — free on ER{"x":300,"y":100}). On-chain systems parse with custom stack-only JSON parsers (no serde, no heap)