I did not see your answer
The user is saying they didn't see my answer. I got stuck in a loop of thinking and never actually delivered my response. Let me provide a concise, clear answer now.Apologies for the delay. Let me give you the answer directly.
Turn-based games are a natural fit for Elmish — arguably better than real-time games. The MVU pattern is fundamentally a state machine, which is exactly what turn-based games are.
Model your game phases as a DU in the model, then pattern-match on (phase, message) in update:
type Phase =
| PlayerTurn
| EnemyTurn
| Animating
| GameOver
type Msg =
| SelectHex of int * int
| MoveUnit of UnitId * int * int
| Attack of UnitId * UnitId
| EndTurn
| AIResolved of EnemyAction
| Tick of GameTime
| AnimationComplete
let update msg model =
match model.Phase, msg with
| PlayerTurn, SelectHex(col, row) -> ...
| PlayerTurn, MoveUnit(id, col, row) -> ...
| PlayerTurn, EndTurn -> { model with Phase = EnemyTurn }, computeAI model
| EnemyTurn, AIResolved action -> applyEnemyAction action model
| Animating, Tick dt -> progressAnimation dt model
| Animating, AnimationComplete -> advancePhase model
| _ -> model, Cmd.none // invalid action for current phase → ignoredThe compiler enforces that every valid (phase, msg) pair is handled. Invalid combos silently no-op.
| Feature | Turn-based use |
|---|---|
| Model as state | Phase, units, grid, history — all in one immutable record |
Cmd.ofAsync |
Offload heavy AI computation without blocking the frame |
Cmd.deferNextFrame |
Chain multi-step resolution (attack → damage → death → next unit) |
withTick |
Drive smooth animations between turns (unit sliding, attacks, etc.) |
DispatchMode.FrameBounded |
Prevent re-entrant dispatch during complex turn resolution |
The enemy turn dispatches a command that computes the AI action and sends the result back:
let computeAI (model: Model) : Cmd<Msg> =
Cmd.ofAsync
(async { return AI.findBestMove model.Grid model.Units })
AIResolved
(fun _ -> AIResolved EnemyPass) // fallback on errorUse an Animating phase with Tick-driven progress. The model holds animation state:
type AnimState = {
StartPos: Vector2
EndPos: Vector2
Elapsed: float32
Duration: float32
}
// In update:
| Animating, Tick dt ->
let anim = { model.Anim with Elapsed = model.Anim.Elapsed + float32 dt.TotalSeconds }
if anim.Elapsed >= anim.Duration then
{ model with Phase = nextPhase model; Anim = None }, Cmd.none
else
{ model with Anim = Some anim }, Cmd.noneSince every state transition is a pure function of (msg, model) -> model', you can maintain a history stack:
type Model = {
// ...
History: Model list
Future: Model list // for redo
}
// Push current model to history before applying a change
| PlayerTurn, MoveUnit(id, col, row) ->
let newModel = applyMove id col row model
{ newModel with History = model :: model.History; Future = [] }, Cmd.none
| _, Undo ->
match model.History with
| prev :: rest -> { prev with History = rest; Future = model :: model.Future }
| [] -> model, Cmd.noneNo special framework support is needed. The Elmish architecture handles turn-based games through its core mechanics: a state machine model, pure update function, and commands for side effects. The phase-gated pattern match gives you compile-time safety that the right actions are only available at the right time.