license: public domain CC0
Git-Game MMO on a Temporal Effect Runtime
Replayable mechanics, versioned rules, and task-centric debugging
1. Motivation
Git‑game as an MMO is:
- a long‑lived, evolving world,
- driven by player actions that look like Git/CI/infra operations,
- full of edge cases, exploits, and emergent behavior,
- season‑based, with changing rules and late‑game weirdness.
We want:
- “Regular programming” for v1 of the game,
- Replayable mechanics for debugging and lore,
- Versioned rules for balance patches and seasons,
- Shadow testing and percent rollout for new mechanics,
- Task‑centric debugging for hard‑to‑reproduce bugs.
The temporal effect + versioned function system is a natural fit.
2. Problem space
2.1 MMO-specific challenges
- Evolving rules: merge, rebase, conflict resolution, CI behavior all change over time.
- Exploits: players will find degenerate strategies and bugs.
- Scale: many concurrent players, shards, and background jobs.
- Lore: past events should be inspectable, replayable, and narrativized.
2.2 What we need from the runtime
- Replayable effects for key decisions (not everything).
- Versioned mechanics with type‑safe evolution.
- Shadow testing of new rules on live traffic.
- Per‑task replay for debugging and simulation.
- Config‑driven routing between versions (no call‑site changes).
3. Abstract goals for git-game MMO
- Model core mechanics as pure(ish) functions in a game monad.
- Use a modern effect system (
effectful/polysemystyle) for IO, DB, time, RNG, etc. - Introduce a Replay effect for opt‑in checkpoints and replays.
- Wrap core mechanics as Versioned functions with schemas/codecs.
- Use routing policies (primary/shadow/percent) to control which version runs.
- Store replay logs for selected tasks (incidents, famous events, player sessions).
- Expose tools for:
- replaying tasks,
- comparing versions,
- debugging specific players/actions,
- running offline simulations.
4. High-level architecture
4.1 Game monad and effects
We define a game monad with a small, explicit effect stack:
type GameM = Eff
'[ Http -- for external services, webhooks, etc.
, DB -- game state persistence
, Clock -- time
, RNG -- randomness
, Replay -- checkpoints & replay
, Log -- structured logging
]
Core effects:
data DB :: Effect where
LoadWorld :: ShardId -> DB m WorldState
SaveWorld :: ShardId -> WorldState -> DB m ()
LoadPlayer :: PlayerId -> DB m PlayerState
SavePlayer :: PlayerId -> PlayerState -> DB m ()
data Clock :: Effect where
Now :: Clock m UTCTime
data RNG :: Effect where
RandomInt :: (Int, Int) -> RNG m Int
data Replay :: Effect where
Checkpoint :: Serializable a => Text -> a -> Replay m ()
Restore :: Serializable a => Text -> Replay m (Maybe a)
Helper:
replayable :: Serializable a => Text -> GameM a -> GameM a
replayable key action = do
m <- Restore key
case m of
Just v -> pure v
Nothing -> do
v <- action
Checkpoint key v
pure v
4.2 Core game mechanics as functions
Examples:
data Action
= Commit CommitInput
| Merge MergeInput
| Rebase RebaseInput
| OpenPR PROpenInput
| RunCI CIInput
-- ...
handleAction :: PlayerId -> Action -> GameM [WorldDelta]
handleAction pid action =
case action of
Commit ci -> handleCommit pid ci
Merge mi -> handleMerge pid mi
Rebase ri -> handleRebase pid ri
OpenPR pi -> handleOpenPR pid pi
RunCI ci -> handleRunCI pid ci
Key decision points are wrapped in replayable:
handleMerge :: PlayerId -> MergeInput -> GameM [WorldDelta]
handleMerge pid mi = do
world <- LoadWorld (mergeShard mi)
result <- replayable "resolveMerge" $
resolveMerge world pid mi
applyMergeResult world result
4.3 Versioned mechanics
We wrap core mechanics in Versioned:
data Version = Version
{ major :: Int
, minor :: Int
} deriving (Eq, Ord, Show)
data Versioned i o = Versioned
{ version :: Version
, runVersioned :: i -> GameM o
, inputCodec :: Codec i
, outputCodec :: Codec o
}
-- Example: merge resolution
data MergeInput = MergeInput { ... }
data MergeResult = MergeResult { ... }
resolveMerge_v1 :: Versioned MergeInput MergeResult
resolveMerge_v1 = Versioned
{ version = Version 1 0
, runVersioned = resolveMergeImplV1
, inputCodec = mergeInputCodec
, outputCodec = mergeResultCodec
}
resolveMerge_v2 :: Versioned MergeInput MergeResult
resolveMerge_v2 = Versioned
{ version = Version 2 0
, runVersioned = resolveMergeImplV2
, inputCodec = mergeInputCodec
, outputCodec = mergeResultCodec
}
We group versions into families:
data VersionedFamily where
Family :: (Typeable i, Typeable o)
=> Map Version (Versioned i o)
-> VersionedFamily
newtype Registry = Registry (Map Text VersionedFamily)
Registering:
registry :: Registry
registry = Registry $ fromList
[ ("resolveMerge", Family (fromList
[ (Version 1 0, resolveMerge_v1)
, (Version 2 0, resolveMerge_v2)
]))
-- other mechanics...
]
4.4 Routing policies (primary, shadow, percent)
data RoutingMode
= Primary Version
| Shadow Version Version
| Percent Version Version Int -- candidate percent
data FunctionRoute = FunctionRoute
{ name :: Text
, family :: VersionedFamily
, mode :: RoutingMode
}
type RoutingTable = Map Text FunctionRoute
Example config (YAML-ish):
functions:
resolveMerge:
routing:
mode: shadow
primary: "1.0"
shadow: "2.0"
resolveConflict:
routing:
mode: percent
primary: "1.0"
candidate: "2.0"
percent: 10
Engine logic (sketch):
runVersionedWithRouting
:: RoutingTable
-> Text -- function name
-> i
-> GameM o
runVersionedWithRouting table fname input = do
let FunctionRoute{family, mode} = table ! fname
case family of
Family versions ->
case mode of
Primary v ->
runVersioned (versions ! v) input
Shadow primaryV shadowV -> do
let primaryFn = versions ! primaryV
shadowFn = versions ! shadowV
primaryOut <- runVersioned primaryFn input
-- run shadow in background, log divergence
_ <- forkShadow shadowFn input primaryOut
pure primaryOut
Percent primaryV candidateV pct -> do
r <- RandomInt (1, 100)
let chosenV = if r <= pct then candidateV else primaryV
runVersioned (versions ! chosenV) input
5. Development & deployment flow
5.1 Version 1: “regular programming”
- You write
GameMfunctions likehandleAction,resolveMergeImplV1, etc. - You register v1 versions in the registry.
- You run the server with a simple routing table (all
Primary v1).
main :: IO ()
main = runGameServer defaultConfig registryV1
This feels like a normal Haskell service.
5.2 Adding version 2 of a mechanic
You implement:
resolveMergeImplV2 :: MergeInput -> GameM MergeResult
resolveMergeImplV2 = -- new rules, maybe more logging, etc.
Wrap it as resolveMerge_v2, add to the family, and update routing config to:
- shadow v2 against v1, or
- send 5–10% of traffic to v2.
No call‑site changes.
5.3 Debugging a hard-to-reproduce bug
A weird merge result occurs for player X on shard Y.
The engine has stored a replay log for that task (because
resolveMergewasreplayable).You load the log and replay:
debugMerge :: ReplayLog -> IO () debugMerge log = runDebug $ replay log $ do result <- resolveMergeImplV2 (loggedInput log) Log.info ("Replay result: " <> show result)If needed, you create a debug variant:
resolveMergeDebug :: MergeInput -> GameM MergeResult resolveMergeDebug mi = do Log.info ("Debug merge input: " <> show mi) r <- resolveMergeImplV2 mi Log.info ("Debug merge result: " <> show r) pure rYou temporarily route only this task or this player to
resolveMergeDebugvia tooling.
5.4 MMO-scale operations
- Shards: each shard runs its own
GameMserver with its own world state. - Routing per shard: you can roll out v2 of a mechanic to a subset of shards.
- Cohorts: you can route new players to experimental mechanics.
- Seasonal rules: you can define “Season 3” as a routing table that prefers v3 of certain mechanics.
6. Player-facing features powered by replay
Because we store replay logs for selected tasks:
- Time-travel rooms: in-game museums where players can “watch” famous merges, outages, or wars—driven by actual replay logs.
- Alternate timelines: “What if this PR had been merged under Season 1 rules?” → run both v1 and v3, show divergent outcomes.
- Personal training sims: late‑game ability where players can replay their own last N actions in a sandbox.
These are not bolted‑on features—they fall out of the runtime’s core capabilities.
7. Concrete sketch: a small end-to-end flow
7.1 Types
data MergeInput = MergeInput
{ shardId :: ShardId
, base :: CommitId
, head :: CommitId
, playerId :: PlayerId
}
data MergeResult = MergeResult
{ success :: Bool
, conflicts :: [Conflict]
, newCommit :: Maybe CommitId
}
7.2 v1 implementation
resolveMergeImplV1 :: MergeInput -> GameM MergeResult
resolveMergeImplV1 mi = do
world <- LoadWorld (shardId mi)
-- simple, maybe naive rules
pure (naiveMerge world mi)
7.3 v2 implementation
resolveMergeImplV2 :: MergeInput -> GameM MergeResult
resolveMergeImplV2 mi = do
world <- LoadWorld (shardId mi)
Log.info ("resolveMerge v2 for " <> show (playerId mi))
-- smarter conflict resolution, new rules
pure (smarterMerge world mi)
7.4 Versioned wrappers
resolveMerge_v1 :: Versioned MergeInput MergeResult
resolveMerge_v1 = Versioned
{ version = Version 1 0
, runVersioned = resolveMergeImplV1
, inputCodec = mergeInputCodec
, outputCodec = mergeResultCodec
}
resolveMerge_v2 :: Versioned MergeInput MergeResult
resolveMerge_v2 = Versioned
{ version = Version 2 0
, runVersioned = resolveMergeImplV2
, inputCodec = mergeInputCodec
, outputCodec = mergeResultCodec
}
7.5 Using it in handleMerge
handleMerge :: PlayerId -> MergeInput -> GameM [WorldDelta]
handleMerge pid mi = do
-- replayable decision point
result <- replayable "resolveMerge" $
runVersionedWithRouting routingTable "resolveMerge" mi
applyMergeResult (shardId mi) result
8. Game plan
Phase 1: Core GameM + Effects
- Define
GameM,DB,Clock,RNG,Replay,Log. - Implement interpreters for production and test/replay.
- Define
Phase 2: Versioned Mechanics
- Introduce
Version,Versioned i o,VersionedFamily,Registry. - Wrap key mechanics: merge, rebase, conflict resolution, CI.
- Introduce
Phase 3: Routing & Shadow Testing
- Implement
RoutingMode,FunctionRoute,RoutingTable. - Add shadow and percent rollout for selected mechanics.
- Implement
Phase 4: Replay Logs & Tools
- Implement replay log storage for selected tasks.
- Build CLI/UI for:
- listing tasks,
- replaying tasks,
- flipping routing for specific tasks/players.
Phase 5: MMO Integration
- Shard management (one
GameMserver per shard). - Cohort/season routing configs.
- Player‑facing features (time‑travel rooms, alternate timelines).
- Shard management (one
Phase 6: Iterate
- Add more effects (metrics, tracing).
- Add more versioned mechanics.
- Explore multi‑process orchestration later if needed.
This gives you a path where:
- v1 of git‑game MMO feels like “just write Haskell game logic,”
- the engine’s powers (replay, versioning, shadowing) are opt‑in and policy‑driven,
- and the late‑game fantasy—time‑bending, lore‑driven, rules‑shifting MMO—is directly powered by the same temporal effect system.
No comments:
Post a Comment