Coverage Summary for Class: ContractPickupModel (core.pipeline)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ContractPickupModel |
100%
(1/1)
|
91.7%
(22/24)
|
100%
(70/70)
|
99.3%
(281/283)
|
| ContractPickupModel$computePickups$$inlined$sortedBy$1 |
0%
(0/1)
|
|
| Total |
50%
(1/2)
|
91.7%
(22/24)
|
100%
(70/70)
|
99.3%
(281/283)
|
// FILE: core/src/main/kotlin/core/pipeline/ContractPickupModel.kt
package core.pipeline
import core.BalanceSettings
import core.evaluateContractForHero
import core.primitives.ActiveStatus
import core.primitives.ActiveContractId
import core.primitives.BoardStatus
import core.primitives.ContractId
import core.primitives.HeroId
import core.primitives.HeroStatus
import core.state.*
/**
* Contract pickup decision model.
*
* ## Semantic Ownership
* Answers: **Which contract should a hero pick?**
*
* ## Contract
* - Banned heroes (effective status BANNED) are skipped during pickup.
* - Warned heroes can still take contracts.
* - Only heroes with status AVAILABLE (and not banned) participate in pickup.
*
* ## Stability Gradient
* STABLE: Pure decision logic with explicit rules.
*
* ## Determinism
* - No RNG usage. All inputs are explicit.
* - Iteration order is deterministic (sorted by ID).
*
* ## Boundary Rules
* - Must NOT emit events.
* - Must NOT mutate state directly.
*/
object ContractPickupModel {
/**
* Computes contract pickup decisions for arriving heroes.
*
* ## Contract
* - Heroes with effective BANNED status are skipped (no decision emitted).
* - Uses [FraudInvestigationModel.isBanActive] to check ban status.
*
* @param arrivingHeroIds Heroes arriving today (sorted by ID).
* @param roster Current hero roster.
* @param board Current board contracts.
* @param currentDay Current day index.
* @param startingActiveContractId Starting ID for new active contracts.
* @return [PickupResult] with all pickup decisions.
*/
fun computePickups(
arrivingHeroIds: List<HeroId>,
roster: List<Hero>,
board: List<BoardContract>,
currentDay: Int,
startingActiveContractId: Int
): PickupResult {
val sortedHeroIds = arrivingHeroIds.sortedBy { it.value }
var nextActiveContractId = startingActiveContractId
val rosterIndexById = HashMap<Int, Int>(roster.size * 2)
for (i in roster.indices) rosterIndexById[roster[i].id.value] = i
val mutableBoard = board.toMutableList()
val mutableRoster = roster.toMutableList()
val newActives = ArrayList<ActiveContract>()
val pickupDecisions = ArrayList<PickupDecision>()
// Track open contract indices
val openIdx = IntArray(board.size)
var openCount = 0
for (i in board.indices) {
if (board[i].status == BoardStatus.OPEN) openIdx[openCount++] = i
}
for (heroId in sortedHeroIds) {
val heroIndex = rosterIndexById[heroId.value] ?: continue
val hero = mutableRoster[heroIndex]
// Skip banned heroes - they cannot take contracts
if (FraudInvestigationModel.isBanActive(hero, currentDay)) {
// No decision emitted for banned heroes (they're not participating)
continue
}
var bestBoardIndex = -1
var bestScore = Int.MIN_VALUE
var bestBoard: BoardContract? = null
for (k in 0 until openCount) {
val bi = openIdx[k]
val b = mutableBoard[bi]
if (b.status != BoardStatus.OPEN) continue
val score = evaluateContractForHero(hero, b, b.baseDifficulty).score
if (score > bestScore) {
bestScore = score
bestBoardIndex = bi
bestBoard = b
}
}
if (bestBoardIndex < 0) {
pickupDecisions.add(
PickupDecision(
heroId = heroId,
decision = PickupDecisionType.NO_CONTRACT,
boardContractId = null,
activeContractId = null,
declineReason = null
)
)
continue
}
val chosen = bestBoard!!
if (bestScore < 0) {
val reason = if (bestScore < BalanceSettings.DECLINE_HARD_THRESHOLD) "unprofitable" else "too_risky"
pickupDecisions.add(
PickupDecision(
heroId = heroId,
decision = PickupDecisionType.DECLINED,
boardContractId = chosen.id,
activeContractId = null,
declineReason = reason
)
)
continue
}
val activeId = nextActiveContractId++
newActives.add(
ActiveContract(
id = ActiveContractId(activeId),
boardContractId = chosen.id,
takenDay = currentDay,
daysRemaining = BalanceSettings.DAYS_REMAINING_INIT,
heroIds = listOf(heroId),
status = ActiveStatus.WIP
)
)
mutableBoard[bestBoardIndex] = chosen.copy(status = BoardStatus.LOCKED)
mutableRoster[heroIndex] = hero.copy(status = HeroStatus.ON_MISSION)
pickupDecisions.add(
PickupDecision(
heroId = heroId,
decision = PickupDecisionType.ACCEPTED,
boardContractId = chosen.id,
activeContractId = ActiveContractId(activeId),
declineReason = null
)
)
}
return PickupResult(
decisions = pickupDecisions,
newActives = newActives,
updatedBoard = mutableBoard,
updatedRoster = mutableRoster,
nextActiveContractId = nextActiveContractId
)
}
}
/**
* Type of pickup decision.
*/
enum class PickupDecisionType {
ACCEPTED,
DECLINED,
NO_CONTRACT
}
/**
* Single hero's pickup decision.
*/
data class PickupDecision(
val heroId: HeroId,
val decision: PickupDecisionType,
val boardContractId: ContractId?,
val activeContractId: ActiveContractId?,
val declineReason: String?
)
/**
* Result of all pickup decisions for the day.
*/
data class PickupResult(
val decisions: List<PickupDecision>,
val newActives: List<ActiveContract>,
val updatedBoard: List<BoardContract>,
val updatedRoster: List<Hero>,
val nextActiveContractId: Int
)