DefaultBattleEngineService.kt

package io.github.lishangbu.avalon.game.battle.engine.application

import io.github.lishangbu.avalon.game.battle.engine.application.gateway.BattleSessionGateway
import io.github.lishangbu.avalon.game.battle.engine.core.model.BattleLifecycle
import io.github.lishangbu.avalon.game.battle.engine.core.model.BattleType
import io.github.lishangbu.avalon.game.battle.engine.core.session.BattleSessionQuery
import io.github.lishangbu.avalon.game.battle.engine.core.session.BattleSessionTurnResult
import io.github.lishangbu.avalon.game.battle.engine.core.session.CaptureChoice
import io.github.lishangbu.avalon.game.battle.engine.core.session.RunChoice
import io.github.lishangbu.avalon.game.battle.engine.core.session.SwitchChoice
import io.github.lishangbu.avalon.game.battle.engine.core.session.target.BattleSessionTargetQuery
import io.github.lishangbu.avalon.game.battle.engine.spi.ai.BattleAiChoiceProvider

/**
 * battle-engine 默认应用门面。
 *
 * 设计意图:
 * - 对外提供稳定的程序化入口,把建局、智能 choice 解析、回合推进与结算计划生成统一收口;
 * - 内部仍然复用 `BattleSessionGateway` 这类更细粒度的基础设施,不把底层细节暴露给 game;
 * - 为 controller、game.init、game.settlement 提供明确的调用边界。
 */
class DefaultBattleEngineService(
    private val battleSessionGateway: BattleSessionGateway,
    private val battleChoiceResolver: BattleChoiceResolver,
    private val battleAiChoiceProviders: List<BattleAiChoiceProvider>,
    private val battleSettlementPlanners: List<BattleSettlementPlanner>,
) : BattleEngineService {
    override fun initializeSession(command: InitializeBattleCommand): BattleSessionQuery {
        require(command.sides.isNotEmpty()) { "At least one side must be provided." }
        battleSessionGateway.createSession(command.sessionId, command.formatId)
        battleSessionGateway.configureSession(
            sessionId = command.sessionId,
            battleKind = command.battleKind,
            capturableSideId = command.capturableSideId,
        )

        command.sides.forEach { side ->
            battleSessionGateway.registerSide(command.sessionId, side.sideId)
            val normalizedActiveIds = normalizeActiveUnitIds(side)
            side.units.forEach { unit ->
                battleSessionGateway.registerUnit(
                    sessionId = command.sessionId,
                    sideId = side.sideId,
                    unit = unit,
                    active = unit.id in normalizedActiveIds,
                )
            }
        }

        return if (command.autoStart) {
            battleSessionGateway.startSession(command.sessionId)
        } else {
            battleSessionGateway.querySession(command.sessionId)
        }
    }

    override fun startSession(sessionId: String): BattleSessionQuery = battleSessionGateway.startSession(sessionId)

    override fun querySession(sessionId: String): BattleSessionQuery = battleSessionGateway.querySession(sessionId)

    override fun queryTargets(
        sessionId: String,
        effectId: String,
        actorUnitId: String,
    ): BattleSessionTargetQuery =
        battleChoiceResolver.queryTargets(
            session = battleSessionGateway.querySession(sessionId),
            effectId = effectId,
            actorUnitId = actorUnitId,
        )

    override fun submitMoveChoice(
        sessionId: String,
        command: SubmitMoveChoiceCommand,
    ): BattleSessionQuery {
        val session = battleSessionGateway.querySession(sessionId)
        val choice = battleChoiceResolver.createMoveChoice(session, command)
        return battleSessionGateway.submitChoice(sessionId, choice)
    }

    override fun submitItemChoice(
        sessionId: String,
        command: SubmitItemChoiceCommand,
    ): BattleSessionQuery {
        val session = battleSessionGateway.querySession(sessionId)
        val choice = battleChoiceResolver.createItemChoice(session, command)
        return battleSessionGateway.submitChoice(sessionId, choice)
    }

    override fun submitCaptureChoice(
        sessionId: String,
        command: SubmitCaptureChoiceCommand,
    ): BattleSessionQuery {
        val session = battleSessionGateway.querySession(sessionId)
        val resolvedSourceUnitId = resolveCaptureSourceUnitId(session, command.sourceUnitId, command.targetId)
        val speed =
            command.speed ?: (
                session.snapshot.units[resolvedSourceUnitId]
                    ?.stats
                    ?.get("speed") ?: 0
            )
        return battleSessionGateway.submitChoice(
            sessionId,
            CaptureChoice(
                playerId = command.playerId,
                ballItemId = command.ballItemId,
                sourceUnitId = resolvedSourceUnitId,
                targetId = command.targetId,
                priority = command.priority ?: 0,
                speed = speed,
            ),
        )
    }

    override fun submitSwitchChoice(
        sessionId: String,
        command: SubmitSwitchChoiceCommand,
    ): BattleSessionQuery =
        battleSessionGateway.submitChoice(
            sessionId,
            SwitchChoice(
                sideId = command.sideId,
                outgoingUnitId = command.outgoingUnitId,
                incomingUnitId = command.incomingUnitId,
                priority = command.priority ?: 0,
                speed = command.speed ?: 0,
            ),
        )

    override fun submitRunChoice(
        sessionId: String,
        command: SubmitRunChoiceCommand,
    ): BattleSessionQuery =
        battleSessionGateway.submitChoice(
            sessionId,
            RunChoice(
                sideId = command.sideId,
                priority = command.priority ?: 0,
                speed = command.speed ?: resolveRunSpeed(battleSessionGateway.querySession(sessionId), command.sideId),
            ),
        )

    override fun submitReplacementChoice(
        sessionId: String,
        command: SubmitReplacementChoiceCommand,
    ): BattleSessionQuery = battleSessionGateway.submitReplacementChoice(sessionId, command.sideId, command.incomingUnitId)

    override fun resolveTurn(sessionId: String): BattleSessionTurnResult {
        val session = battleSessionGateway.querySession(sessionId)
        resolveAiChoiceProvider(session.snapshot.battle.battleKind)
            .provide(session)
            .forEach { choice ->
                battleSessionGateway.submitChoice(sessionId = sessionId, choice = choice)
            }
        return battleSessionGateway.resolveTurn(sessionId)
    }

    override fun buildSettlementPlan(sessionId: String): BattleSettlementPlan {
        val session = battleSessionGateway.querySession(sessionId)
        val lifecycle = session.snapshot.battle.lifecycle
        require(lifecycle == BattleLifecycle.ENDED_UNSETTLED || lifecycle == BattleLifecycle.SETTLED) {
            "Battle '$sessionId' must be in lifecycle '${BattleLifecycle.ENDED_UNSETTLED.name}' " +
                "or '${BattleLifecycle.SETTLED.name}', but was '${lifecycle.name}'."
        }
        return resolveSettlementPlanner(session.snapshot.battle.battleKind).buildPlan(sessionId, session)
    }

    override fun markSessionSettled(sessionId: String): BattleSessionQuery = battleSessionGateway.markSessionSettled(sessionId)

    private fun normalizeActiveUnitIds(side: InitializeBattleSideCommand): Set<String> {
        require(side.units.isNotEmpty()) { "Side '${side.sideId}' must contain at least one unit." }
        val allUnitIds = side.units.map { unit -> unit.id }.toSet()
        val normalized = side.activeUnitIds.ifEmpty { setOf(side.units.first().id) }
        require(normalized.all { unitId -> unitId in allUnitIds }) {
            "Side '${side.sideId}' has activeUnitIds that are not present in its unit list."
        }
        return normalized
    }

    private fun resolveCaptureSourceUnitId(
        session: BattleSessionQuery,
        requestedSourceUnitId: String?,
        targetId: String,
    ): String {
        if (!requestedSourceUnitId.isNullOrBlank()) {
            return requestedSourceUnitId
        }
        val capturableSideId = session.snapshot.battle.capturableSideId
        return session.snapshot.sides.values
            .firstOrNull { side -> side.id != capturableSideId && side.activeUnitIds.isNotEmpty() }
            ?.activeUnitIds
            ?.firstOrNull()
            ?: error("No active source unit is available for capture target '$targetId'.")
    }

    private fun resolveRunSpeed(
        session: BattleSessionQuery,
        sideId: String,
    ): Int =
        session.snapshot.sides[sideId]
            ?.activeUnitIds
            ?.mapNotNull(session.snapshot.units::get)
            ?.maxOfOrNull { unit -> unit.stats["speed"] ?: unit.stats["spe"] ?: 0 }
            ?: 0

    private fun resolveAiChoiceProvider(battleKind: BattleType): BattleAiChoiceProvider =
        battleAiChoiceProviders.firstOrNull { provider -> provider.supports(battleKind) }
            ?: error("No BattleAiChoiceProvider registered for battleKind '$battleKind'.")

    private fun resolveSettlementPlanner(battleKind: BattleType): BattleSettlementPlanner =
        battleSettlementPlanners.firstOrNull { planner -> planner.supports(battleKind) }
            ?: error("No BattleSettlementPlanner registered for battleKind '$battleKind'.")
}