A fully onchain asynchronous battle royale with fog of war, built with Cairo and Dojo
Hex'd is a multiplayer strategy game where players navigate a hexagonal grid shrouded in fog of war. Every step to an empty tile triggers a random encounter — a gift that strengthens you or a curse that may kill you. Walk into another player's tile and combat is resolved automatically based on XP. You can ambush offline players, and every decision is a gamble between exploration and survival.
All game logic, state, and randomness live fully onchain on Starknet via the Dojo game engine. The frontend is a fully playable 3D experience built with React and Three.js.
- 21x21 Hex Grid: Axial coordinate system with bounds checking
- 3D Frontend: Playable Three.js hex grid with camera controls, fog of war, and mobile support
- Fog of War: 6-bit neighbor bitmask reveals adjacent occupancy after each move
- Asynchronous PvP Combat: XP-based resolution, attack offline players
- Gift/Curse Encounter System: 6 distinct outcomes on every empty-tile move (50% gift, 50% curse)
- Deterministic RNG: Poseidon hash from game state, fully verifiable onchain
- Leaderboard: Highest score tracking with onchain registration
- Game Counter: Tracks active concurrent games (max 350)
- Event-Driven Architecture: 7 event types for frontend reactivity
- Wallet Integration: Cartridge Controller for seamless onchain interaction
- Idle Attack Detection: Polling detects combat while player is away
cd contracts
sozo build # Compile Cairo contracts
sozo test # Run all 75 tests# Terminal 1: Local Starknet node
katana --disable-fee
# Terminal 2: Deploy contracts
cd contracts
sozo migrate
# Terminal 3: Start indexer
torii --world <WORLD_ADDRESS> --http.cors_origins "*"
# Terminal 4: Frontend
cd client
pnpm install
pnpm run devcd contracts
scarb run migrate-dev # Full build + test + deploy (local)
scarb run migrate # Full build + test + deploy (Sepolia)
scarb run spawn-dev # Execute spawn on local
scarb run move-dev # Execute move on local- Connect Wallet: Open the game and connect your Starknet wallet via Cartridge Controller.
- Start Game: Click "Start Game" to spawn on the hex grid. You get placed at a random position with 100 HP, 110 max HP, and 0 XP.
- Navigate: Click an adjacent hex tile to select your direction, then click again to confirm. You can move in 6 directions: East, NorthEast, NorthWest, West, SouthWest, SouthEast.
- Empty tile: You earn +10 XP for exploring, then an encounter triggers automatically. You receive a gift (50% chance) or curse (50% chance) that modifies your HP or XP.
- Occupied tile: Combat resolves automatically. The player with higher XP wins. Equal XP favors the attacker.
| Outcome | Attacker | Defender |
|---|---|---|
| Attacker wins | +30 XP, +10 HP, moves to defender's tile | -10 HP, pushed to attacker's old tile |
| Defender wins | -10 HP, stays put | -5 HP (retaliation), stays put |
If either player's HP reaches 0, they die. Their game session ends and their tile is cleared.
When you move to an empty tile, a deterministic encounter is rolled based on your position and the block timestamp.
| Type | Outcome | Effect | Chance |
|---|---|---|---|
| Gift (50%) | Heal | +10 HP (capped at max) | 30% of gifts |
| Empower | +20 XP | 45% of gifts | |
| Blessing | +5 HP, +10 XP | 25% of gifts | |
| Curse (50%) | Poison | -15 HP (can kill) | 40% of curses |
| Drain | -10 XP (floor: 0) | 25% of curses | |
| Hex | -10 HP, -10 XP (can kill) | 35% of curses |
Max HP is fixed at 110 and never changes. Exploration awards +10 XP per move regardless of the encounter outcome.
After each move or spawn, you receive a bitmask showing which of your 6 neighboring tiles are occupied. Occupied neighbors appear red on the grid — plan your route accordingly.
When your HP reaches 0 (from combat or a deadly encounter), your run ends. You can then:
- Register your score to the onchain leaderboard (records your final XP)
- Return to the lobby and start a new game
- Red neighbors mean danger (or opportunity) — an occupied tile means combat.
- Exploring builds XP, which determines combat outcomes. Move often.
- Curses can kill you. If your HP is low, every move is a risk.
- You can attack offline players. They can attack you while you're away too — check the toast notifications for ambush alerts.
contracts/src/
models.cairo # Models + constants + enums
lib.cairo # Module tree
constants/constants.cairo # Grid bounds, namespace
systems/game/
contracts.cairo # IGameSystems (spawn, move, register_score, get_game_state, get_highest_score)
tests.cairo # 37 integration tests
helpers/
combat.cairo # XP-based combat resolution + death handling
encounter.cairo # Gift/curse system (28 unit tests)
movement.cairo # Position + tile occupancy updates
spawn.cairo # Random spawn position
utils/
hex.cairo # Axial hex math (neighbors, bounds, occupancy) (7 unit tests)
setup.cairo # Test world deployment helper
client/ # React + TypeScript + Three.js frontend
src/
components/ # HexGrid, Header, MyGames, DeathPage, HowToPlayModal, etc.
contexts/ # GameDirector, Sound, Controller
dojo/ # useGameActions, useSystemCalls, useEntitySync
pages/ # StartPage, GamePage
stores/ # Zustand game state + UI state
three/ # Three.js hex geometry, constants, coordinate utils
types/ # TypeScript game interfaces
utils/ # Event parsing, coordinate mapping, network config
| Model | Key | Purpose |
|---|---|---|
GameSession |
game_id |
Maps game to player address, tracks active/inactive |
PlayerState |
game_id |
Position (Vec2), last direction, movement flag |
PlayerStats |
game_id |
HP, max HP, XP |
TileOccupant |
(x, y) |
Which game_id occupies a tile (0 = empty) |
GameCounter |
singleton (0) | Tracks number of active concurrent games |
HighestScore |
singleton (0) | Leaderboard: player address, username, highest XP |
| Function | Description |
|---|---|
spawn() |
Creates new game session, generates random position, initializes models, reveals neighbors |
move(game_id, direction) |
Validates ownership/bounds, resolves combat OR movement+encounter, emits events |
get_game_state(game_id) |
Read-only view returning full game state for a session |
register_score(player, username, xp) |
Updates highest score if new XP exceeds current record |
get_highest_score() |
Returns (player, username, xp) of the current leaderboard leader |
| Event | Emitted When |
|---|---|
Spawned |
New game session created |
Moved |
Player moved to an empty tile |
CombatResult |
PvP combat resolved |
PlayerDied |
Player HP reached 0 (PvP or encounter) |
NeighborsRevealed |
Fog of war update after move/spawn |
EncounterOccurred |
Gift or curse applied after move |
HighestScoreUpdated |
New highest score registered |
XP-based combat: No stats to distribute. Your XP (earned from exploration and winning fights) determines combat outcomes. Higher XP wins; ties favor the attacker.
Deterministic encounters: Outcomes are derived from poseidon_hash(game_id, x, y, block_timestamp). Same inputs always produce the same result, making the game fully verifiable.
ECS separation: Spatial state (PlayerState) and combat state (PlayerStats) are separate models, following Dojo ECS best practices. TileOccupant provides efficient reverse lookups for collision and combat detection.
Game capacity: GameCounter enforces a maximum of 350 concurrent games (~80% of the 441-tile grid), ensuring the grid never becomes fully saturated.
STARTING_HP = 100, MAX_HP = 110 (fixed)
COMBAT_DAMAGE = 10, COMBAT_RETALIATION_DAMAGE = 5
COMBAT_XP_REWARD = 30, COMBAT_HP_REWARD = 10
EXPLORE_XP_REWARD = 10
GIFT_THRESHOLD = 50 (50% gift, 50% curse)
HEAL = +10 HP, EMPOWER = +20 XP, BLESSING = +5 HP / +10 XP
POISON = -15 HP, DRAIN = -10 XP, HEX = -10 HP / -10 XP
MIN_MAX_HP = 10, MAX_CONCURRENT_GAMES = 350
Grid: GRID_MIN = -10, GRID_MAX = 10 (21x21)
Uses axial coordinates (q, r) with pointy-top hexagons on a 21x21 grid (range [-10, 10]).
(0,-1) (1,-1)
NW NE
\ /
\ /
(-1,0) W --(0,0)-- E (1,0)
/ \
/ \
SW SE
(-1,1) (0,1)
Direction vectors: E (+1,0), NE (+1,-1), NW (0,-1), W (-1,0), SW (-1,+1), SE (0,+1)
The client is a fully playable 3D game built with React, Three.js, and the Dojo SDK.
- React 18 + TypeScript + Vite
- Three.js via @react-three/fiber for 3D hex grid rendering
- Dojo SDK + Starknet.js for contract interaction
- Cartridge Controller for wallet integration
- Zustand for client state management
- Material UI + Tailwind CSS for UI components
- 3D Hex Grid: Instanced mesh rendering with procedural biome coloring and fog-of-war opacity
- Camera Controls: Orbit controls with smooth tracking to player position
- Move Confirmation: Click-to-select, click-again-to-confirm pattern prevents misclicks
- Combat Indicators: Occupied neighbor tiles shown in red
- Toast Notifications: Real-time feedback for moves, encounters, combat, and ambush alerts
- Death Screen: Game over page with final XP score and leaderboard registration
- Game Persistence: Active game saved to localStorage for resume across sessions
- Idle Attack Detection: Polling detects HP/position changes while player is away
- Mobile Support: Responsive camera and touch-friendly double-tap movement
- Background Music: Toggle-able ambient soundtrack
75 tests total, all passing:
- 3 unit tests in
models.cairo: Vec2 equality, zero check, HP constant validation - 7 unit tests in
utils/hex.cairo: All 6 movement directions + boundary validation - 28 unit tests in
helpers/encounter.cairo: Outcome determination, encounter application for all 6 outcomes, boundary values, death conditions, stat invariants - 37 integration tests in
systems/game/tests.cairo: Full world deployment tests covering spawn, movement, combat, encounter integration, player death, tile occupancy, neighbor revelation, game counter, and edge cases
cd contracts
sozo test # Runs all 75 tests- Core models (GameSession, PlayerState, PlayerStats, TileOccupant, GameCounter, HighestScore)
- Hex grid movement with bounds checking
- XP-based asynchronous combat system
- Gift/curse encounter system (6 outcomes)
- Fog of war (neighbor occupancy bitmask)
- Deterministic Poseidon-based RNG
- Event-driven architecture (7 event types)
- Comprehensive test suite (75 tests)
- Highest score leaderboard
- Game Design Document
- 3D frontend with Three.js hex grid
- Wallet integration (Cartridge Controller)
- Sepolia testnet deployment
- Idle attack detection and notifications
- Mobile-responsive design
- Full leaderboard with multiple entries and seasons
- Stat distribution (STR/DEX/VIT/LUK)
- Flee mechanic in combat
- Scoring system with multi-factor formula
- Balance tuning and gas optimization
- Mainnet deployment
- Game Design Document: Full aspirational design including planned features
- CLAUDE.md: Technical reference for AI-assisted development
- Dojo Documentation: Dojo engine reference
- Cairo Documentation: Cairo language reference
Feedback and suggestions are welcome!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Dojo Engine: Provable game engine for onchain games
- Death Mountain: Inspiration for encounter system design
- Eternum: Inspiration for hex grid mechanics
- Red Blob Games: Hex grid algorithms reference
Developer: [Femi Oje] Twitter: @0xjinius GitHub: @FemiOje Email: 0xjinius@gmail.com
Project Link: https://github.com/FemiOje/hexed
Built on Starknet with Dojo