Summary
Build the Gameplay bounded context to orchestrate game flow, including nation definitions, turn management, and integration with Rondel and Accounting domains.
Motivation
Currently:
- Rondel operates in isolation without game context
- Nations are free-form strings with no validation
- No turn order or game progression
- No connection between starting a game and initializing rondel/accounting
The Gameplay domain provides:
- Canonical nation definitions matching Imperial rules
- Game lifecycle management (create, start, end)
- Turn order tracking
- Orchestration of cross-BC initialization
- Foundation for victory conditions
Game Flow Overview
┌─────────────────────────────────────────┐
│ Gameplay Domain │
│ │
[CreateGame] ─────►│ GameState │
│ • GameId │
[StartGame] ──────►│ • Players (investor assignments) │
│ • Turn order │
[EndTurn] ────────►│ • Current nation │
│ • Game phase │
└──────────────┬──────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────┐ ┌─────────────────┐
│ Rondel │ │Accounting │ │ (Future: Map, │
│ │ │ │ │ Combat, etc.) │
└─────────────┘ └───────────┘ └─────────────────┘
Implementation Plan
1. Nation Types (Gameplay.fsi)
Define the six nations from Imperial:
// src/Imperium/Gameplay.fsi
namespace Imperium
module Gameplay =
// ──────────────────────────────────────────────────────────────────────────
// Value Types
// ──────────────────────────────────────────────────────────────────────────
/// The six great powers in Imperial.
[<RequireQualifiedAccess>]
type NationId =
| AustriaHungary
| Italy
| France
| GreatBritain
| Germany
| Russia
module NationId =
/// All six nations in bond order (turn order for rondel).
val all: NationId list
/// Convert to display string.
val toString: NationId -> string
/// Parse from string (case-insensitive).
val tryParse: string -> Result<NationId, string>
/// Starting treasury for each nation (in millions).
val startingTreasury: NationId -> int
// ──────────────────────────────────────────────────────────────────────────
// Domain State
// ──────────────────────────────────────────────────────────────────────────
/// Game phase indicating current state.
[<RequireQualifiedAccess>]
type GamePhase =
| Setup // Players joining, assigning bonds
| InProgress // Active gameplay
| Finished // Game complete
/// A player in the game (human or AI).
type Player = {
PlayerId: Primitives.Id
Name: string
}
/// Assignment of initial bonds to players.
type BondAssignment = {
Nation: NationId
Player: Player
BondValue: int // 2, 4, 6, 9, 12, 16, 20, 25, 30
}
/// Persistent game state.
type GameState = {
GameId: Primitives.Id
Phase: GamePhase
Players: Player list
/// Initial bond assignments (determines investor relationships)
BondAssignments: BondAssignment list
/// Nations in turn order
TurnOrder: NationId list
/// Index into TurnOrder for current nation
CurrentTurnIndex: int
/// Number of completed rounds
RoundNumber: int
}
// ──────────────────────────────────────────────────────────────────────────
// Commands
// ──────────────────────────────────────────────────────────────────────────
/// Create a new game (enters Setup phase).
type CreateGameCommand = {
GameId: Primitives.Id
/// Players joining the game
Players: Player list
}
/// Start the game after setup (enters InProgress phase).
/// Triggers initialization of Rondel and Accounting.
type StartGameCommand = {
GameId: Primitives.Id
/// Bond assignments must cover all nations
BondAssignments: BondAssignment list
}
/// Advance to next nation's turn.
type EndTurnCommand = {
GameId: Primitives.Id
}
/// Union of all gameplay commands.
type GameplayCommand =
| CreateGame of CreateGameCommand
| StartGame of StartGameCommand
| EndTurn of EndTurnCommand
// ──────────────────────────────────────────────────────────────────────────
// Events
// ──────────────────────────────────────────────────────────────────────────
/// Game was created and is in setup phase.
type GameCreatedEvent = {
GameId: Primitives.Id
Players: Player list
}
/// Game started, all nations initialized.
type GameStartedEvent = {
GameId: Primitives.Id
TurnOrder: NationId list
/// First nation to act
FirstNation: NationId
}
/// Turn advanced to next nation.
type TurnAdvancedEvent = {
GameId: Primitives.Id
PreviousNation: NationId
CurrentNation: NationId
RoundNumber: int
}
/// Game ended (victory condition met).
type GameEndedEvent = {
GameId: Primitives.Id
/// Final scores per player
FinalScores: Map<Primitives.Id, int>
}
/// Union of all gameplay events.
type GameplayEvent =
| GameCreated of GameCreatedEvent
| GameStarted of GameStartedEvent
| TurnAdvanced of TurnAdvancedEvent
| GameEnded of GameEndedEvent
// ──────────────────────────────────────────────────────────────────────────
// Outbound Commands (to other BCs)
// ──────────────────────────────────────────────────────────────────────────
/// Initialize rondel for a new game.
type InitializeRondelOutboundCommand = {
GameId: Primitives.Id
Nations: Set<string>
}
/// Initialize accounting balances for a new game.
type InitializeAccountingOutboundCommand = {
GameId: Primitives.Id
InitialBalances: Map<string, int>
}
/// Union of outbound commands.
type GameplayOutboundCommand =
| InitializeRondel of InitializeRondelOutboundCommand
| InitializeAccounting of InitializeAccountingOutboundCommand
// ──────────────────────────────────────────────────────────────────────────
// Dependencies
// ──────────────────────────────────────────────────────────────────────────
/// Load game state.
type LoadGameState = Primitives.Id -> Async<GameState option>
/// Save game state.
type SaveGameState = GameState -> Async<Result<unit, string>>
/// Publish gameplay events.
type PublishGameplayEvent = GameplayEvent -> Async<unit>
/// Dispatch commands to other bounded contexts.
type DispatchOutboundCommand = GameplayOutboundCommand -> Async<Result<unit, string>>
/// Unified dependencies for Gameplay handlers.
type GameplayDependencies = {
Load: LoadGameState
Save: SaveGameState
Publish: PublishGameplayEvent
Dispatch: DispatchOutboundCommand
}
// ──────────────────────────────────────────────────────────────────────────
// Queries
// ──────────────────────────────────────────────────────────────────────────
/// Query for current game state.
type GetGameStateQuery = { GameId: Primitives.Id }
/// Current game status view.
type GameStateView = {
GameId: Primitives.Id
Phase: GamePhase
CurrentNation: NationId option
RoundNumber: int
Players: Player list
}
/// Query dependencies.
type GameplayQueryDependencies = {
Load: LoadGameState
}
/// Get current game state.
val getGameState: GameplayQueryDependencies -> GetGameStateQuery -> Async<GameStateView option>
// ──────────────────────────────────────────────────────────────────────────
// Handlers
// ──────────────────────────────────────────────────────────────────────────
/// Execute a gameplay command.
val execute: GameplayDependencies -> GameplayCommand -> Async<unit>
2. Implementation (Gameplay.fs)
// src/Imperium/Gameplay.fs
namespace Imperium
open Imperium.Primitives
module Gameplay =
// ──────────────────────────────────────────────────────────────────────────
// Value Types
// ──────────────────────────────────────────────────────────────────────────
[<RequireQualifiedAccess>]
type NationId =
| AustriaHungary
| Italy
| France
| GreatBritain
| Germany
| Russia
module NationId =
/// Nations in bond/turn order (per Imperial rules)
let all = [
NationId.AustriaHungary
NationId.Italy
NationId.France
NationId.GreatBritain
NationId.Germany
NationId.Russia
]
let toString = function
| NationId.AustriaHungary -> "Austria-Hungary"
| NationId.Italy -> "Italy"
| NationId.France -> "France"
| NationId.GreatBritain -> "Great Britain"
| NationId.Germany -> "Germany"
| NationId.Russia -> "Russia"
let tryParse (raw: string) : Result<NationId, string> =
if String.IsNullOrWhiteSpace raw then
Error "Nation cannot be empty"
else
let normalized = raw.Trim().ToLowerInvariant().Replace("-", "").Replace(" ", "")
match normalized with
| "austriahungary" | "austria" -> Ok NationId.AustriaHungary
| "italy" -> Ok NationId.Italy
| "france" -> Ok NationId.France
| "greatbritain" | "britain" | "gb" | "uk" -> Ok NationId.GreatBritain
| "germany" -> Ok NationId.Germany
| "russia" -> Ok NationId.Russia
| _ ->
let valid = all |> List.map toString |> String.concat ", "
Error $"Unknown nation '{raw}'. Valid nations: {valid}"
/// Starting treasury per Imperial rules
let startingTreasury = function
| NationId.AustriaHungary -> 2
| NationId.Italy -> 6
| NationId.France -> 8
| NationId.GreatBritain -> 6
| NationId.Germany -> 10
| NationId.Russia -> 4
// ──────────────────────────────────────────────────────────────────────────
// Domain State
// ──────────────────────────────────────────────────────────────────────────
[<RequireQualifiedAccess>]
type GamePhase =
| Setup
| InProgress
| Finished
type Player = {
PlayerId: Id
Name: string
}
type BondAssignment = {
Nation: NationId
Player: Player
BondValue: int
}
type GameState = {
GameId: Id
Phase: GamePhase
Players: Player list
BondAssignments: BondAssignment list
TurnOrder: NationId list
CurrentTurnIndex: int
RoundNumber: int
}
// ──────────────────────────────────────────────────────────────────────────
// Commands
// ──────────────────────────────────────────────────────────────────────────
type CreateGameCommand = {
GameId: Id
Players: Player list
}
type StartGameCommand = {
GameId: Id
BondAssignments: BondAssignment list
}
type EndTurnCommand = {
GameId: Id
}
type GameplayCommand =
| CreateGame of CreateGameCommand
| StartGame of StartGameCommand
| EndTurn of EndTurnCommand
// ──────────────────────────────────────────────────────────────────────────
// Events
// ──────────────────────────────────────────────────────────────────────────
type GameCreatedEvent = {
GameId: Id
Players: Player list
}
type GameStartedEvent = {
GameId: Id
TurnOrder: NationId list
FirstNation: NationId
}
type TurnAdvancedEvent = {
GameId: Id
PreviousNation: NationId
CurrentNation: NationId
RoundNumber: int
}
type GameEndedEvent = {
GameId: Id
FinalScores: Map<Id, int>
}
type GameplayEvent =
| GameCreated of GameCreatedEvent
| GameStarted of GameStartedEvent
| TurnAdvanced of TurnAdvancedEvent
| GameEnded of GameEndedEvent
// ──────────────────────────────────────────────────────────────────────────
// Outbound Commands
// ──────────────────────────────────────────────────────────────────────────
type InitializeRondelOutboundCommand = {
GameId: Id
Nations: Set<string>
}
type InitializeAccountingOutboundCommand = {
GameId: Id
InitialBalances: Map<string, int>
}
type GameplayOutboundCommand =
| InitializeRondel of InitializeRondelOutboundCommand
| InitializeAccounting of InitializeAccountingOutboundCommand
// ──────────────────────────────────────────────────────────────────────────
// Dependencies
// ──────────────────────────────────────────────────────────────────────────
type LoadGameState = Id -> Async<GameState option>
type SaveGameState = GameState -> Async<Result<unit, string>>
type PublishGameplayEvent = GameplayEvent -> Async<unit>
type DispatchOutboundCommand = GameplayOutboundCommand -> Async<Result<unit, string>>
type GameplayDependencies = {
Load: LoadGameState
Save: SaveGameState
Publish: PublishGameplayEvent
Dispatch: DispatchOutboundCommand
}
// ──────────────────────────────────────────────────────────────────────────
// Queries
// ──────────────────────────────────────────────────────────────────────────
type GetGameStateQuery = { GameId: Id }
type GameStateView = {
GameId: Id
Phase: GamePhase
CurrentNation: NationId option
RoundNumber: int
Players: Player list
}
type GameplayQueryDependencies = {
Load: LoadGameState
}
let getGameState (deps: GameplayQueryDependencies) (q: GetGameStateQuery) : Async<GameStateView option> =
async {
let! stateOpt = deps.Load q.GameId
return stateOpt |> Option.map (fun s ->
let currentNation =
if s.Phase = GamePhase.InProgress && s.TurnOrder.Length > 0 then
Some s.TurnOrder.[s.CurrentTurnIndex]
else
None
{ GameId = s.GameId
Phase = s.Phase
CurrentNation = currentNation
RoundNumber = s.RoundNumber
Players = s.Players })
}
// ──────────────────────────────────────────────────────────────────────────
// Internal Handlers
// ──────────────────────────────────────────────────────────────────────────
/// Create a new game in Setup phase.
let internal createGame (deps: GameplayDependencies) (cmd: CreateGameCommand) : Async<unit> =
async {
let! existing = deps.Load cmd.GameId
match existing with
| Some _ ->
// Idempotent: already exists
()
| None ->
if List.isEmpty cmd.Players then
failwith "Game must have at least one player"
let state: GameState = {
GameId = cmd.GameId
Phase = GamePhase.Setup
Players = cmd.Players
BondAssignments = []
TurnOrder = NationId.all
CurrentTurnIndex = 0
RoundNumber = 0
}
let! saveResult = deps.Save state
match saveResult with
| Ok () ->
let event: GameCreatedEvent = {
GameId = cmd.GameId
Players = cmd.Players
}
do! deps.Publish (GameCreated event)
| Error msg ->
failwith $"Failed to save game state: {msg}"
}
/// Start the game, initializing Rondel and Accounting.
let internal startGame (deps: GameplayDependencies) (cmd: StartGameCommand) : Async<unit> =
async {
let! stateOpt = deps.Load cmd.GameId
match stateOpt with
| None ->
failwith $"Game {Id.toString cmd.GameId} not found"
| Some state ->
if state.Phase <> GamePhase.Setup then
failwith "Game can only be started from Setup phase"
// Validate bond assignments cover all nations
let assignedNations =
cmd.BondAssignments
|> List.map (fun a -> a.Nation)
|> Set.ofList
let allNations = NationId.all |> Set.ofList
if assignedNations <> allNations then
let missing = Set.difference allNations assignedNations |> Set.map NationId.toString
failwith $"Bond assignments missing nations: {missing}"
// Update state
let newState = {
state with
Phase = GamePhase.InProgress
BondAssignments = cmd.BondAssignments
RoundNumber = 1
}
let! saveResult = deps.Save newState
match saveResult with
| Error msg -> failwith $"Failed to save game state: {msg}"
| Ok () ->
// Initialize Rondel
let nationNames = NationId.all |> List.map NationId.toString |> Set.ofList
let rondelCmd: InitializeRondelOutboundCommand = {
GameId = cmd.GameId
Nations = nationNames
}
let! rondelResult = deps.Dispatch (InitializeRondel rondelCmd)
match rondelResult with
| Error msg -> failwith $"Failed to initialize Rondel: {msg}"
| Ok () -> ()
// Initialize Accounting with starting treasuries
let balances =
NationId.all
|> List.map (fun n -> NationId.toString n, NationId.startingTreasury n)
|> Map.ofList
let accountingCmd: InitializeAccountingOutboundCommand = {
GameId = cmd.GameId
InitialBalances = balances
}
let! accountingResult = deps.Dispatch (InitializeAccounting accountingCmd)
match accountingResult with
| Error msg -> failwith $"Failed to initialize Accounting: {msg}"
| Ok () -> ()
// Publish game started event
let event: GameStartedEvent = {
GameId = cmd.GameId
TurnOrder = newState.TurnOrder
FirstNation = newState.TurnOrder.[0]
}
do! deps.Publish (GameStarted event)
}
/// End current nation's turn, advance to next.
let internal endTurn (deps: GameplayDependencies) (cmd: EndTurnCommand) : Async<unit> =
async {
let! stateOpt = deps.Load cmd.GameId
match stateOpt with
| None ->
failwith $"Game {Id.toString cmd.GameId} not found"
| Some state ->
if state.Phase <> GamePhase.InProgress then
failwith "Can only end turn during InProgress phase"
let previousNation = state.TurnOrder.[state.CurrentTurnIndex]
let nextIndex = (state.CurrentTurnIndex + 1) % state.TurnOrder.Length
let isNewRound = nextIndex = 0
let newState = {
state with
CurrentTurnIndex = nextIndex
RoundNumber = if isNewRound then state.RoundNumber + 1 else state.RoundNumber
}
let! saveResult = deps.Save newState
match saveResult with
| Error msg -> failwith $"Failed to save game state: {msg}"
| Ok () ->
let event: TurnAdvancedEvent = {
GameId = cmd.GameId
PreviousNation = previousNation
CurrentNation = newState.TurnOrder.[nextIndex]
RoundNumber = newState.RoundNumber
}
do! deps.Publish (TurnAdvanced event)
}
// ──────────────────────────────────────────────────────────────────────────
// Public Router
// ──────────────────────────────────────────────────────────────────────────
let execute (deps: GameplayDependencies) (cmd: GameplayCommand) : Async<unit> =
match cmd with
| CreateGame c -> createGame deps c
| StartGame c -> startGame deps c
| EndTurn c -> endTurn deps c
3. Contract Types (Contract.Gameplay.fs)
// src/Imperium/Contract.Gameplay.fs
namespace Imperium.Contract
module Gameplay =
/// Serializable player for persistence/API.
type Player = {
PlayerId: System.Guid
Name: string
}
/// Serializable bond assignment.
type BondAssignment = {
Nation: string
PlayerId: System.Guid
BondValue: int
}
/// Serializable game state.
type GameState = {
GameId: System.Guid
Phase: string // "Setup", "InProgress", "Finished"
Players: Player list
BondAssignments: BondAssignment list
TurnOrder: string list
CurrentTurnIndex: int
RoundNumber: int
}
/// Create game command contract.
type CreateGameCommand = {
GameId: System.Guid
Players: Player list
}
/// Start game command contract.
type StartGameCommand = {
GameId: System.Guid
BondAssignments: BondAssignment list
}
/// End turn command contract.
type EndTurnCommand = {
GameId: System.Guid
}
/// Game created event contract.
type GameCreatedEvent = {
GameId: System.Guid
Players: Player list
}
/// Game started event contract.
type GameStartedEvent = {
GameId: System.Guid
TurnOrder: string list
FirstNation: string
}
/// Turn advanced event contract.
type TurnAdvancedEvent = {
GameId: System.Guid
PreviousNation: string
CurrentNation: string
RoundNumber: int
}
4. Terminal Host (Imperium.Terminal)
// src/Imperium.Terminal/Gameplay/Host.fs
namespace Imperium.Terminal.Gameplay
open Imperium.Primitives
open Imperium.Gameplay
open Imperium.Terminal
/// In-memory gameplay store.
type GameplayStore = {
Load: Id -> Async<GameState option>
Save: GameState -> Async<Result<unit, string>>
}
module InMemoryGameplayStore =
open System.Collections.Concurrent
let create () : GameplayStore =
let states = ConcurrentDictionary<Id, GameState>()
{ Load = fun id -> async {
return match states.TryGetValue(id) with
| true, s -> Some s
| false, _ -> None }
Save = fun s -> async {
states.[s.GameId] <- s
return Ok () } }
/// Gameplay host record.
type GameplayHost = {
Execute: GameplayCommand -> unit
Query: GetGameStateQuery -> GameStateView option
}
module GameplayHost =
let create
(store: GameplayStore)
(bus: IBus)
(dispatchToRondel: unit -> (Rondel.RondelCommand -> Async<unit>))
(dispatchToAccounting: unit -> (Accounting.AccountingCommand -> Async<unit>))
: GameplayHost =
let deps: GameplayDependencies = {
Load = store.Load
Save = store.Save
Publish = fun event ->
async {
do! bus.Publish event
}
Dispatch = fun cmd ->
async {
match cmd with
| InitializeRondel c ->
let rondelCmd: Rondel.SetToStartingPositionsCommand = {
GameId = c.GameId
Nations = c.Nations
}
do! dispatchToRondel () (Rondel.SetToStartingPositions rondelCmd)
return Ok ()
| InitializeAccounting c ->
let accountingCmd: Accounting.InitializeNationBalancesCommand = {
GameId = c.GameId
InitialBalances = c.InitialBalances |> Map.map (fun _ v -> Amount.create v |> Result.defaultValue Unchecked.defaultof<_>)
}
do! dispatchToAccounting () (Accounting.InitializeNationBalances accountingCmd)
return Ok ()
}
}
let queryDeps: GameplayQueryDependencies = { Load = store.Load }
{ Execute = fun cmd ->
Gameplay.execute deps cmd |> Async.RunSynchronously
Query = fun q ->
Gameplay.getGameState queryDeps q |> Async.RunSynchronously }
Test Plan
Unit Tests (GameplayTests.fs)
[<Tests>]
let gameplayTests =
testList "Gameplay" [
testList "NationId" [
testCase "all contains six nations" <| fun _ ->
Expect.equal (List.length NationId.all) 6 "Should have 6 nations"
testCase "toString returns display names" <| fun _ ->
Expect.equal (NationId.toString NationId.AustriaHungary) "Austria-Hungary" ""
Expect.equal (NationId.toString NationId.GreatBritain) "Great Britain" ""
testCase "tryParse is case insensitive" <| fun _ ->
Expect.isOk (NationId.tryParse "france") ""
Expect.isOk (NationId.tryParse "FRANCE") ""
Expect.isOk (NationId.tryParse "France") ""
testCase "tryParse handles aliases" <| fun _ ->
Expect.isOk (NationId.tryParse "austria") ""
Expect.isOk (NationId.tryParse "britain") ""
Expect.isOk (NationId.tryParse "gb") ""
testCase "tryParse rejects unknown nation" <| fun _ ->
Expect.isError (NationId.tryParse "spain") ""
testCase "startingTreasury matches Imperial rules" <| fun _ ->
Expect.equal (NationId.startingTreasury NationId.Germany) 10 "Germany starts with 10M"
Expect.equal (NationId.startingTreasury NationId.France) 8 "France starts with 8M"
Expect.equal (NationId.startingTreasury NationId.AustriaHungary) 2 "Austria-Hungary starts with 2M"
]
testList "createGame" [
testCase "creates game in Setup phase" <| fun _ ->
let gameplay, events = createGameplay ()
let gameId = Id.newId ()
let player = { PlayerId = Id.newId (); Name = "Alice" }
gameplay.Execute <| CreateGame { GameId = gameId; Players = [player] }
let state = gameplay.Query { GameId = gameId }
Expect.isSome state ""
Expect.equal state.Value.Phase GamePhase.Setup ""
testCase "publishes GameCreated event" <| fun _ ->
let gameplay, events = createGameplay ()
let gameId = Id.newId ()
gameplay.Execute <| CreateGame { GameId = gameId; Players = [{ PlayerId = Id.newId (); Name = "Bob" }] }
Expect.equal events.Count 1 ""
match events.[0] with
| GameCreated e -> Expect.equal e.GameId gameId ""
| _ -> failtest "Expected GameCreated"
testCase "rejects empty player list" <| fun _ ->
let gameplay, _ = createGameplay ()
Expect.throws (fun () ->
gameplay.Execute <| CreateGame { GameId = Id.newId (); Players = [] }
) ""
]
testList "startGame" [
testCase "transitions to InProgress phase" <| fun _ ->
// Setup
let gameplay, _ = createGameplay ()
let gameId = Id.newId ()
let player = { PlayerId = Id.newId (); Name = "Alice" }
gameplay.Execute <| CreateGame { GameId = gameId; Players = [player] }
// Create bond assignments for all nations
let bonds = NationId.all |> List.map (fun n -> { Nation = n; Player = player; BondValue = 9 })
gameplay.Execute <| StartGame { GameId = gameId; BondAssignments = bonds }
let state = gameplay.Query { GameId = gameId }
Expect.equal state.Value.Phase GamePhase.InProgress ""
testCase "initializes Rondel via dispatch" <| fun _ ->
// Verify SetToStartingPositions command is dispatched
()
testCase "initializes Accounting via dispatch" <| fun _ ->
// Verify InitializeNationBalances command is dispatched
()
testCase "rejects incomplete bond assignments" <| fun _ ->
let gameplay, _ = createGameplay ()
let gameId = Id.newId ()
let player = { PlayerId = Id.newId (); Name = "Alice" }
gameplay.Execute <| CreateGame { GameId = gameId; Players = [player] }
// Only assign 3 nations
let bonds = NationId.all |> List.take 3 |> List.map (fun n -> { Nation = n; Player = player; BondValue = 9 })
Expect.throws (fun () ->
gameplay.Execute <| StartGame { GameId = gameId; BondAssignments = bonds }
) ""
]
testList "endTurn" [
testCase "advances to next nation" <| fun _ ->
// Start game, end first turn, verify Austria-Hungary -> Italy
()
testCase "wraps around after last nation" <| fun _ ->
// End 6 turns, verify back to Austria-Hungary with RoundNumber = 2
()
testCase "publishes TurnAdvanced event" <| fun _ ->
()
]
]
Acceptance Criteria
Dependencies
Files to Create/Modify
| File |
Action |
src/Imperium/Contract.Gameplay.fs |
Create (new file) |
src/Imperium/Gameplay.fsi |
Replace placeholder |
src/Imperium/Gameplay.fs |
Replace placeholder |
src/Imperium/Imperium.fsproj |
Add Contract.Gameplay.fs to build order |
src/Imperium.Terminal/Gameplay/Store.fs |
Create (new file) |
src/Imperium.Terminal/Gameplay/Host.fs |
Create (new file) |
src/Imperium.Terminal/Program.fs |
Wire up Gameplay host |
tests/Imperium.UnitTests/GameplayTests.fs |
Expand from placeholder |
Notes
- Nation turn order follows Imperial bond order: AH, IT, FR, GB, DE, RU
- Starting treasuries from Imperial rules (varies by nation)
- Bond values: 2, 4, 6, 9, 12, 16, 20, 25, 30 (not implemented in this issue)
- Victory conditions (25 power points) deferred to future issue
Summary
Build the Gameplay bounded context to orchestrate game flow, including nation definitions, turn management, and integration with Rondel and Accounting domains.
Motivation
Currently:
The Gameplay domain provides:
Game Flow Overview
Implementation Plan
1. Nation Types (Gameplay.fsi)
Define the six nations from Imperial:
2. Implementation (Gameplay.fs)
3. Contract Types (Contract.Gameplay.fs)
4. Terminal Host (Imperium.Terminal)
Test Plan
Unit Tests (GameplayTests.fs)
Acceptance Criteria
NationIdtype with all six nationsNationId.all,toString,tryParse,startingTreasuryfunctionsGamePhaseenum (Setup, InProgress, Finished)GameStatewith turn trackingCreateGamecommand creates game in Setup phaseStartGamecommand validates bond assignments and dispatches to Rondel/AccountingEndTurncommand advances turn orderGameplayEventtypes published correctlyDependencies
InitializeNationBalancescommandFiles to Create/Modify
src/Imperium/Contract.Gameplay.fssrc/Imperium/Gameplay.fsisrc/Imperium/Gameplay.fssrc/Imperium/Imperium.fsprojsrc/Imperium.Terminal/Gameplay/Store.fssrc/Imperium.Terminal/Gameplay/Host.fssrc/Imperium.Terminal/Program.fstests/Imperium.UnitTests/GameplayTests.fsNotes