Coverage Summary for Class: CanonicalJsonKt (core.serde)

Class Class, % Method, % Branch, % Line, % Instruction, %
CanonicalJsonKt 100% (1/1) 100% (28/28) 50% (2/4) 99.5% (190/191) 99.1% (763/770)


 package core.serde
 
 import core.primitives.*
 import core.state.*
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
 
 /**
  * Canonical JSON serialization for GameState.
  * - Compact (no pretty print)
  * - Deterministic (stable field ordering)
  * - Value classes as raw Int
  * - arrivalsToday dropped on save, empty on load
  */
 
 @OptIn(ExperimentalSerializationApi::class)
 private val json = Json {
     encodeDefaults = true
     explicitNulls = false
     prettyPrint = false
 }
 
 private const val SUPPORTED_SAVE_VERSION = 1
 
 /**
  * Serialize a [GameState] into canonical JSON.
  *
  * @param state State to serialize.
  * @return Canonical JSON string (deterministic).
  */
 fun serialize(state: GameState): String {
     val dto = toDto(state)
     return json.encodeToString(dto)
 }
 
 /**
  * Deserialize canonical JSON into a [GameState].
  *
  * @param jsonString Canonical JSON produced by [serialize].
  * @return Deserialized [GameState].
  * @throws IllegalArgumentException if the save version is unsupported.
  */
 fun deserialize(jsonString: String): GameState {
     val dto = json.decodeFromString<GameStateDto>(jsonString)
 
     // Validate save version
     require(dto.meta.saveVersion == SUPPORTED_SAVE_VERSION) {
         "Unsupported saveVersion: ${dto.meta.saveVersion}"
     }
 
     return fromDto(dto)
 }
 
 // --- toDto mappings ---
 
 /**
  * Map domain [GameState] to its serializable DTO.
  *
  * @param state Domain state.
  * @return DTO suitable for serialization.
  */
 fun toDto(state: GameState): GameStateDto {
     return GameStateDto(
         meta = toDto(state.meta),
         guild = toDto(state.guild),
         region = toDto(state.region),
         economy = toDto(state.economy),
         contracts = toDto(state.contracts),
         heroes = toDto(state.heroes)
     )
 }
 
 /**
  * Map domain [MetaState] to DTO.
  *
  * @param meta Domain meta state.
  */
 fun toDto(meta: MetaState): MetaStateDto {
     return MetaStateDto(
         saveVersion = meta.saveVersion,
         seed = meta.seed,
         dayIndex = meta.dayIndex,
         revision = meta.revision,
         ids = IdCountersDto(
             nextContractId = meta.ids.nextContractId,
             nextHeroId = meta.ids.nextHeroId,
             nextActiveContractId = meta.ids.nextActiveContractId
         ),
 
         taxDueDay = meta.taxDueDay,
         taxAmountDue = meta.taxAmountDue,
         taxPenalty = meta.taxPenalty,
         taxMissedCount = meta.taxMissedCount
     )
 }
 
 /**
  * Map domain [GuildState] to DTO.
  *
  * @param guild Domain guild state.
  */
 fun toDto(guild: GuildState): GuildStateDto {
     return GuildStateDto(
         guildRank = guild.guildRank,
         reputation = guild.reputation,
         completedContractsTotal = guild.completedContractsTotal,
         contractsForNextRank = guild.contractsForNextRank,
         proofPolicy = guild.proofPolicy.name
     )
 }
 
 /**
  * Map domain [RegionState] to DTO.
  *
  * @param region Domain region state.
  */
 fun toDto(region: RegionState): RegionStateDto {
     return RegionStateDto(
         stability = region.stability
     )
 }
 
 /**
  * Map domain [EconomyState] to DTO.
  *
  * @param economy Domain economy state.
  */
 fun toDto(economy: EconomyState): EconomyStateDto {
     return EconomyStateDto(
         moneyCopper = economy.moneyCopper,
         trophiesStock = economy.trophiesStock,
         reservedCopper = economy.reservedCopper
     )
 }
 
 /**
  * Map domain [ContractState] to DTO.
  *
  * @param contracts Domain contract state.
  */
 fun toDto(contracts: ContractState): ContractStateDto {
     return ContractStateDto(
         inbox = contracts.inbox.map { toDto(it) },
         board = contracts.board.map { toDto(it) },
         archive = contracts.archive.map { toDto(it) },
         active = contracts.active.map { toDto(it) },
         returns = contracts.returns.map { toDto(it) }
     )
 }
 
 /**
  * Map domain [ContractDraft] to DTO.
  *
  * @param draft Domain draft.
  */
 fun toDto(draft: ContractDraft): ContractDraftDto {
     return ContractDraftDto(
         id = draft.id.value,
         createdDay = draft.createdDay,
         nextAutoResolveDay = draft.nextAutoResolveDay,
         title = draft.title,
         rankSuggested = draft.rankSuggested.name,
         feeOffered = draft.feeOffered,
         salvage = draft.salvage.name,
         baseDifficulty = draft.baseDifficulty,
         proofHint = draft.proofHint,
         clientDeposit = draft.clientDeposit
     )
 }
 
 /**
  * Map domain [BoardContract] to DTO.
  *
  * @param board Domain board contract.
  */
 fun toDto(board: BoardContract): BoardContractDto {
     return BoardContractDto(
         id = board.id.value,
         postedDay = board.postedDay,
         title = board.title,
         rank = board.rank.name,
         fee = board.fee,
         salvage = board.salvage.name,
         baseDifficulty = board.baseDifficulty,
         status = board.status.name,
         clientDeposit = board.clientDeposit
     )
 }
 
 /**
  * Map domain [ActiveContract] to DTO.
  *
  * @param active Domain active contract.
  */
 fun toDto(active: ActiveContract): ActiveContractDto {
     return ActiveContractDto(
         id = active.id.value,
         boardContractId = active.boardContractId.value,
         takenDay = active.takenDay,
         daysRemaining = active.daysRemaining,
         heroIds = active.heroIds.map { it.value },
         status = active.status.name
     )
 }
 
 /**
  * Map domain [ReturnPacket] to DTO.
  *
  * @param packet Domain return packet.
  */
 fun toDto(packet: ReturnPacket): ReturnPacketDto {
     return ReturnPacketDto(
         activeContractId = packet.activeContractId.value,
         boardContractId = packet.boardContractId.value,
         heroIds = packet.heroIds.map { it.value },
         resolvedDay = packet.resolvedDay,
         outcome = packet.outcome.name,
         trophiesCount = packet.trophiesCount,
         trophiesQuality = packet.trophiesQuality.name,
         reasonTags = packet.reasonTags,
         requiresPlayerClose = packet.requiresPlayerClose,
         suspectedTheft = packet.suspectedTheft
     )
 }
 
 /**
  * Map domain [HeroState] to DTO.
  *
  * @param heroes Domain hero state.
  */
 fun toDto(heroes: HeroState): HeroStateDto {
     return HeroStateDto(
         roster = heroes.roster.map { toDto(it) }
         // arrivalsToday is NOT serialized
     )
 }
 
 /**
  * Map domain [Hero] to DTO.
  *
  * @param hero Domain hero.
  */
 fun toDto(hero: Hero): HeroDto {
     return HeroDto(
         id = hero.id.value,
         name = hero.name,
         rank = hero.rank.name,
         klass = hero.klass.name,
         traits = TraitsDto(
             greed = hero.traits.greed,
             honesty = hero.traits.honesty,
             courage = hero.traits.courage
         ),
         status = hero.status.name,
         historyCompleted = hero.historyCompleted
     )
 }
 
 // --- fromDto mappings ---
 
 /**
  * Map DTO [GameStateDto] back into domain [GameState].
  *
  * @param dto DTO produced by decoding JSON.
  */
 fun fromDto(dto: GameStateDto): GameState {
     return GameState(
         meta = fromDtoMeta(dto.meta),
         guild = fromDto(dto.guild),
         region = fromDto(dto.region),
         economy = fromDto(dto.economy),
         contracts = fromDto(dto.contracts),
         heroes = fromDto(dto.heroes)
     )
 }
 
 /**
  * Map DTO [MetaStateDto] into domain [MetaState].
  *
  * @param dto Meta DTO.
  */
 fun fromDtoMeta(dto: MetaStateDto): MetaState {
     return MetaState(
         saveVersion = dto.saveVersion,
         seed = dto.seed,
         dayIndex = dto.dayIndex,
         revision = dto.revision,
         ids = IdCounters(
             nextContractId = dto.ids.nextContractId,
             nextHeroId = dto.ids.nextHeroId,
             nextActiveContractId = dto.ids.nextActiveContractId
         ),
 
         taxDueDay = dto.taxDueDay,
         taxAmountDue = dto.taxAmountDue,
         taxPenalty = dto.taxPenalty,
         taxMissedCount = dto.taxMissedCount
     )
 }
 
 /**
  * Map DTO [GuildStateDto] into domain [GuildState].
  *
  * @param dto Guild DTO.
  */
 fun fromDto(dto: GuildStateDto): GuildState {
     return GuildState(
         guildRank = dto.guildRank,
         reputation = dto.reputation,
         completedContractsTotal = dto.completedContractsTotal,
         contractsForNextRank = dto.contractsForNextRank,
         proofPolicy = ProofPolicy.valueOf(dto.proofPolicy)
     )
 }
 
 /**
  * Map DTO [RegionStateDto] into domain [RegionState].
  *
  * @param dto Region DTO.
  */
 fun fromDto(dto: RegionStateDto): RegionState {
     return RegionState(
         stability = dto.stability
     )
 }
 
 /**
  * Map DTO [EconomyStateDto] into domain [EconomyState].
  *
  * @param dto Economy DTO.
  */
 fun fromDto(dto: EconomyStateDto): EconomyState {
     return EconomyState(
         moneyCopper = dto.moneyCopper,
         trophiesStock = dto.trophiesStock,
         reservedCopper = dto.reservedCopper
     )
 }
 
 /**
  * Map DTO [ContractStateDto] into domain [ContractState].
  *
  * @param dto Contract state DTO.
  */
 fun fromDto(dto: ContractStateDto): ContractState {
     return ContractState(
         inbox = dto.inbox.map { fromDtoDraft(it) },
         board = dto.board.map { fromDtoBoard(it) },
         archive = dto.archive.map { fromDtoBoard(it) },
         active = dto.active.map { fromDtoActive(it) },
         returns = dto.returns.map { fromDtoReturn(it) }
     )
 }
 
 /**
  * Map DTO [ContractDraftDto] into domain [ContractDraft].
  *
  * @param dto Draft DTO.
  */
 fun fromDtoDraft(dto: ContractDraftDto): ContractDraft {
     return ContractDraft(
         id = ContractId(dto.id),
         createdDay = dto.createdDay,
         nextAutoResolveDay = dto.nextAutoResolveDay,
         title = dto.title,
         rankSuggested = Rank.valueOf(dto.rankSuggested),
         feeOffered = dto.feeOffered,
         salvage = SalvagePolicy.valueOf(dto.salvage),
         baseDifficulty = dto.baseDifficulty,
         proofHint = dto.proofHint,
         clientDeposit = dto.clientDeposit
     )
 }
 
 /**
  * Map DTO [BoardContractDto] into domain [BoardContract].
  *
  * @param dto Board DTO.
  */
 fun fromDtoBoard(dto: BoardContractDto): BoardContract {
     return BoardContract(
         id = ContractId(dto.id),
         postedDay = dto.postedDay,
         title = dto.title,
         rank = Rank.valueOf(dto.rank),
         fee = dto.fee,
         salvage = SalvagePolicy.valueOf(dto.salvage),
         baseDifficulty = dto.baseDifficulty,
         status = BoardStatus.valueOf(dto.status),
         clientDeposit = dto.clientDeposit
     )
 }
 
 /**
  * Map DTO [ActiveContractDto] into domain [ActiveContract].
  *
  * @param dto Active DTO.
  */
 fun fromDtoActive(dto: ActiveContractDto): ActiveContract {
     return ActiveContract(
         id = ActiveContractId(dto.id),
         boardContractId = ContractId(dto.boardContractId),
         takenDay = dto.takenDay,
         daysRemaining = dto.daysRemaining,
         heroIds = dto.heroIds.map { HeroId(it) },
         status = ActiveStatus.valueOf(dto.status)
     )
 }
 
 /**
  * Map DTO [ReturnPacketDto] into domain [ReturnPacket].
  *
  * @param dto Return DTO.
  */
 fun fromDtoReturn(dto: ReturnPacketDto): ReturnPacket {
     return ReturnPacket(
         activeContractId = ActiveContractId(dto.activeContractId),
         boardContractId = ContractId(dto.boardContractId),
         heroIds = dto.heroIds.map { HeroId(it) },
         resolvedDay = dto.resolvedDay,
         outcome = Outcome.valueOf(dto.outcome),
         trophiesCount = dto.trophiesCount,
         trophiesQuality = Quality.valueOf(dto.trophiesQuality),
         reasonTags = dto.reasonTags,
         requiresPlayerClose = dto.requiresPlayerClose,
         suspectedTheft = dto.suspectedTheft
     )
 }
 
 /**
  * Map DTO [HeroStateDto] into domain [HeroState].
  *
  * @param dto Hero state DTO.
  */
 fun fromDto(dto: HeroStateDto): HeroState {
     return HeroState(
         roster = dto.roster.map { fromDtoHero(it) },
         arrivalsToday = emptyList() // K11 decision: reset to empty on load
     )
 }
 
 /**
  * Map DTO [HeroDto] into domain [Hero].
  *
  * @param dto Hero DTO.
  */
 fun fromDtoHero(dto: HeroDto): Hero {
     return Hero(
         id = HeroId(dto.id),
         name = dto.name,
         rank = Rank.valueOf(dto.rank),
         klass = HeroClass.valueOf(dto.klass),
         traits = Traits(
             greed = dto.traits.greed,
             honesty = dto.traits.honesty,
             courage = dto.traits.courage
         ),
         status = HeroStatus.valueOf(dto.status),
         historyCompleted = dto.historyCompleted
     )
 }