mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Batch AI client updates and saves (#8711)
This commit is contained in:
@@ -194,7 +194,8 @@ object GameController {
|
||||
* ended.
|
||||
*/
|
||||
private def endGameAllHumansDefeated(
|
||||
gcwpr: GameControllerWithPostResults
|
||||
gcwpr: GameControllerWithPostResults,
|
||||
updateClientsAndSave: Boolean = true
|
||||
): GameControllerWithPostResults = {
|
||||
val gc = gcwpr.gameController
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
@@ -202,7 +203,10 @@ object GameController {
|
||||
)
|
||||
|
||||
// End the game with no victor (humans lost)
|
||||
gc.withHandledEngineAndResults(gc.engine.endGame(victorFactionId = None))
|
||||
gc.withHandledEngineAndResults(
|
||||
gc.engine.endGame(victorFactionId = None),
|
||||
updateClientsAndSave = updateClientsAndSave
|
||||
)
|
||||
}
|
||||
|
||||
def performAiCommands(
|
||||
@@ -215,6 +219,8 @@ object GameController {
|
||||
else if allHumansDefeated(gameControllerWithPostResults) then
|
||||
endGameAllHumansDefeated(gameControllerWithPostResults)
|
||||
else {
|
||||
val initialResultCount = gameControllerWithPostResults.postResults.results.size
|
||||
|
||||
@tailrec
|
||||
def go(
|
||||
acc: GameControllerWithPostResults
|
||||
@@ -262,13 +268,29 @@ object GameController {
|
||||
|
||||
// Check if game ended or all humans defeated after AI commands
|
||||
if next.gameController.engine.currentState.runStatus == RunStatus.Over then next
|
||||
else if allHumansDefeated(next) then endGameAllHumansDefeated(next)
|
||||
else go(next)
|
||||
else if allHumansDefeated(next) then {
|
||||
val ended = endGameAllHumansDefeated(next, updateClientsAndSave = false)
|
||||
GameControllerWithPostResults(
|
||||
gameController = ended.gameController,
|
||||
postResults = next.postResults.append(ended.postResults)
|
||||
)
|
||||
} else go(next)
|
||||
}
|
||||
end if
|
||||
}
|
||||
|
||||
go(gameControllerWithPostResults)
|
||||
val result = go(gameControllerWithPostResults)
|
||||
val deferredAiResults = result.postResults.results.drop(initialResultCount)
|
||||
if deferredAiResults.isEmpty then result
|
||||
else
|
||||
result.copy(
|
||||
gameController = result.gameController
|
||||
.withHumanClientsUpdated(
|
||||
results = deferredAiResults,
|
||||
clientTextStoreForUpdate = result.gameController.clientTextStore
|
||||
)
|
||||
.copyWithAtomicSave
|
||||
)
|
||||
}
|
||||
|
||||
def performAutomaticContinuation(
|
||||
@@ -664,24 +686,28 @@ final case class GameController(
|
||||
factionId: FactionId,
|
||||
selectedProvinceId: ProvinceId,
|
||||
command: ScalaSelectedCommand,
|
||||
@unused save: Boolean,
|
||||
save: Boolean,
|
||||
runUpdateChecks: Boolean = true
|
||||
): GameControllerWithPostResults =
|
||||
): GameControllerWithPostResults = {
|
||||
val engineAndResults =
|
||||
if runUpdateChecks then
|
||||
this.engine.postCommand(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
else
|
||||
this.engine.postCommandWithoutUpdateChecks(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
|
||||
withHandledEngineAndResults(
|
||||
engineAndResults =
|
||||
if runUpdateChecks then
|
||||
this.engine.postCommand(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
else
|
||||
this.engine.postCommandWithoutUpdateChecks(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
engineAndResults = engineAndResults,
|
||||
updateClientsAndSave = save
|
||||
)
|
||||
}
|
||||
|
||||
def withNewAiClientWithFid(fid: FactionId): GameController =
|
||||
this.withAiClient(AIClient(gameId = gameId, factionId = fid))
|
||||
@@ -1094,7 +1120,8 @@ final case class GameController(
|
||||
}
|
||||
|
||||
private def withHandledEngineAndResults(
|
||||
engineAndResults: EngineAndResults
|
||||
engineAndResults: EngineAndResults,
|
||||
updateClientsAndSave: Boolean = true
|
||||
): GameControllerWithPostResults = {
|
||||
// Check for tutorial strategic events that should fire based on the new game state
|
||||
val tutorialEvents = TutorialStrategicEvents.checkForEvents(engineAndResults.engine.currentState)
|
||||
@@ -1112,23 +1139,14 @@ final case class GameController(
|
||||
|
||||
val newFullHistory = extractFullHistory(finalEngineAndResults.engine)
|
||||
|
||||
val newGc = copy(
|
||||
val handledGc = copy(
|
||||
engine = finalEngineAndResults.engine,
|
||||
fullHistory = newFullHistory,
|
||||
userIdToFactionId = userIdToFactionId.filter {
|
||||
case (_, fid) =>
|
||||
finalEngineAndResults.engine.factionIds.contains(fid)
|
||||
},
|
||||
humanClients = GameController.humanClientsAfterPostingResults(
|
||||
humanClients = humanClients,
|
||||
clientTextStore = this.clientTextStore,
|
||||
visibilityExtensions = clientVisibilityExtensions,
|
||||
engine = finalEngineAndResults.engine,
|
||||
fullHistory = newFullHistory,
|
||||
aiFactionIds = aiClientFactionIds.toSet,
|
||||
battleProgress = battleProgress,
|
||||
lastRevealedRound = lastRevealedRound
|
||||
),
|
||||
humanClients = humanClients,
|
||||
aiClients = {
|
||||
val afterRemovals = aiClients
|
||||
.filterNot(aic =>
|
||||
@@ -1150,13 +1168,17 @@ final case class GameController(
|
||||
foldInLlmRequests(this.clientTextStore, llmRequestsWithHistoryCounts),
|
||||
clientVisibilityExtensions
|
||||
)
|
||||
) match {
|
||||
// Only save when there are new Eagle results. Shardok results are saved
|
||||
// in withNewShardokResults, so we don't need to re-serialize Eagle history
|
||||
// for Shardok-only updates.
|
||||
case gc if finalEngineAndResults.results.nonEmpty => gc.copyWithAtomicSave
|
||||
case gc => gc
|
||||
}
|
||||
)
|
||||
|
||||
val newGc =
|
||||
if updateClientsAndSave then {
|
||||
val withUpdatedClients = handledGc.withHumanClientsUpdated(
|
||||
results = finalEngineAndResults.results,
|
||||
clientTextStoreForUpdate = this.clientTextStore
|
||||
)
|
||||
if finalEngineAndResults.results.nonEmpty then withUpdatedClients.copyWithAtomicSave
|
||||
else withUpdatedClients
|
||||
} else handledGc
|
||||
|
||||
GameControllerWithPostResults(
|
||||
gameController = newGc,
|
||||
@@ -1168,6 +1190,23 @@ final case class GameController(
|
||||
)
|
||||
}
|
||||
|
||||
private def withHumanClientsUpdated(
|
||||
results: Seq[ActionResultT],
|
||||
clientTextStoreForUpdate: ClientTextStore
|
||||
): GameController =
|
||||
copy(
|
||||
humanClients = GameController.humanClientsAfterPostingResults(
|
||||
humanClients = humanClients,
|
||||
clientTextStore = clientTextStoreForUpdate,
|
||||
visibilityExtensions = results.flatMap(_.clientTextVisibilityExtensions).toVector,
|
||||
engine = engine,
|
||||
fullHistory = fullHistory,
|
||||
aiFactionIds = aiClientFactionIds.toSet,
|
||||
battleProgress = battleProgress,
|
||||
lastRevealedRound = lastRevealedRound
|
||||
)
|
||||
)
|
||||
|
||||
def streamUpdates(
|
||||
userId: UserId,
|
||||
clientId: String,
|
||||
|
||||
@@ -4,6 +4,8 @@ import java.io.PrintWriter
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
|
||||
import net.eagle0.common.{RandomState, SeededRandom}
|
||||
import net.eagle0.eagle.ai.AIClient
|
||||
import net.eagle0.eagle.api.eagle.UpdateStreamRequest.StreamGameRequest.StreamingTextStatus
|
||||
import net.eagle0.eagle.api.selected_command.{AttackDecisionSelectedCommand, ReturnSelectedCommand}
|
||||
import net.eagle0.eagle.client_text.{ClientTextStore, ClientTextStoreWithUpdate, CompleteClientText}
|
||||
@@ -11,6 +13,8 @@ import net.eagle0.eagle.library.*
|
||||
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType
|
||||
import net.eagle0.eagle.model.state.{GameType, RoundPhase, TutorialPhase}
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.ReturnAvailable
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand as ScalaSelectedCommand
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
@@ -18,7 +22,7 @@ import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
import net.eagle0.eagle.service.{FullGameHistory, InMemoryHistory, SyncResponseObserver}
|
||||
import net.eagle0.eagle.service.{FullGameHistory, InMemoryHistory, PostResults, SyncResponseObserver}
|
||||
import net.eagle0.eagle.UserId
|
||||
import org.scalamock.scalatest.MockFactory
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
@@ -769,4 +773,127 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
}
|
||||
newController.clientTextStore shouldBe mockPrunedClientTextStore
|
||||
}
|
||||
|
||||
"performAiCommands" should "update human clients and save once after an AI command batch" in {
|
||||
val humanFactionId = 1
|
||||
val aiFactionId = 2
|
||||
val humanHeroId = 101
|
||||
val aiHeroId = 202
|
||||
val aiProvinceId = 20
|
||||
val state = testGameState.copy(
|
||||
provinces = Map(
|
||||
10 -> ProvinceC(
|
||||
id = 10,
|
||||
name = "Human Province",
|
||||
rulingFactionId = Some(humanFactionId),
|
||||
rulingFactionHeroIds = Vector(humanHeroId)
|
||||
),
|
||||
aiProvinceId -> ProvinceC(
|
||||
id = aiProvinceId,
|
||||
name = "AI Province",
|
||||
rulingFactionId = Some(aiFactionId),
|
||||
rulingFactionHeroIds = Vector(aiHeroId)
|
||||
)
|
||||
),
|
||||
heroes = Map(
|
||||
humanHeroId -> HeroC(id = humanHeroId, factionId = Some(humanFactionId)),
|
||||
aiHeroId -> HeroC(id = aiHeroId, factionId = Some(aiFactionId))
|
||||
),
|
||||
factions = Map(
|
||||
humanFactionId -> FactionC(
|
||||
id = humanFactionId,
|
||||
factionHeadId = humanHeroId,
|
||||
name = "Human",
|
||||
leaderIds = Vector(humanHeroId)
|
||||
),
|
||||
aiFactionId -> FactionC(
|
||||
id = aiFactionId,
|
||||
factionHeadId = aiHeroId,
|
||||
name = "AI",
|
||||
leaderIds = Vector(aiHeroId)
|
||||
)
|
||||
)
|
||||
)
|
||||
val command = OneProvinceAvailableCommands(
|
||||
provinceId = aiProvinceId,
|
||||
commands = Vector(ReturnAvailable(actingProvinceId = aiProvinceId))
|
||||
)
|
||||
val aiCommands = SortedMap(aiProvinceId -> command)
|
||||
val result1 = ActionResultC(
|
||||
actionResultType = ActionResultType.UnknownActionUnspecified,
|
||||
newDate = Some(state.currentDate),
|
||||
newRandomSeed = Some(2L)
|
||||
)
|
||||
val result2 = result1.copy(newRandomSeed = Some(3L))
|
||||
|
||||
val engine0 = mock[Engine]
|
||||
val engine1 = mock[Engine]
|
||||
val engine2 = mock[Engine]
|
||||
val history0 = mock[FullGameHistory]
|
||||
val history1 = mock[FullGameHistory]
|
||||
val history2 = mock[FullGameHistory]
|
||||
val ear1 = mock[EngineAndResults]
|
||||
val ear2 = mock[EngineAndResults]
|
||||
|
||||
Vector((engine0, history0, 0), (engine1, history1, 1), (engine2, history2, 2)).foreach {
|
||||
case (engine, history, count) =>
|
||||
(() => engine.currentState).expects().returning(state.copy(actionResultCount = count)).anyNumberOfTimes(): Unit
|
||||
(() => engine.factionIds).expects().returning(Vector(humanFactionId, aiFactionId)).anyNumberOfTimes(): Unit
|
||||
(() => engine.history).expects().returning(history).anyNumberOfTimes(): Unit
|
||||
(() => engine.gameId).expects().returning(eagleGameId).anyNumberOfTimes(): Unit
|
||||
(() => history.count).expects().returning(count).anyNumberOfTimes(): Unit
|
||||
}
|
||||
engine0.getAvailablePlayerCommands.expects(aiFactionId).returning(aiCommands): Unit
|
||||
engine0.postCommand
|
||||
.expects(aiFactionId, aiProvinceId, ScalaSelectedCommand.ReturnSelected)
|
||||
.returning(ear1): Unit
|
||||
(() => ear1.engine).expects().returning(engine1).anyNumberOfTimes(): Unit
|
||||
(() => ear1.results).expects().returning(Vector(result1)).anyNumberOfTimes(): Unit
|
||||
|
||||
engine1.getAvailablePlayerCommands.expects(aiFactionId).returning(aiCommands): Unit
|
||||
engine1.postCommand
|
||||
.expects(aiFactionId, aiProvinceId, ScalaSelectedCommand.ReturnSelected)
|
||||
.returning(ear2): Unit
|
||||
(() => ear2.engine).expects().returning(engine2).anyNumberOfTimes(): Unit
|
||||
(() => ear2.results).expects().returning(Vector(result2)).anyNumberOfTimes(): Unit
|
||||
|
||||
engine2.getAvailablePlayerCommands.expects(*).returning(SortedMap.empty).anyNumberOfTimes(): Unit
|
||||
engine2.filteredHistoryWithUnfilteredCount
|
||||
.expects(humanFactionId, 0)
|
||||
.returning(FilteredHistoryWithUnfilteredCount(Vector.empty, 2)): Unit
|
||||
(() => engine2.saveNow).expects().returning(engine2).once(): Unit
|
||||
|
||||
val observer = mock[SyncResponseObserver]
|
||||
observer.onNext.expects(*).once(): Unit
|
||||
val client = new HumanPlayerClientConnectionState(
|
||||
eagleGameId = eagleGameId,
|
||||
factionId = humanFactionId,
|
||||
clientId = "human-client",
|
||||
grpcObserver = observer,
|
||||
unfilteredKnownHistoryCount = 0,
|
||||
knownShardokResultCounts = Vector.empty,
|
||||
streamingTextStatusMap = Map.empty,
|
||||
logWriter = mockLogWriter
|
||||
)
|
||||
val textStore = mock[ClientTextStore]
|
||||
textStore.hasIncompleteTextsAccessibleTo.expects(humanFactionId).returning(false): Unit
|
||||
(() => textStore.saved).expects().returning(textStore).once(): Unit
|
||||
|
||||
val controller = GameController(
|
||||
engine = engine0,
|
||||
fullHistory = history0,
|
||||
userIdToFactionId = Map(UserId("human") -> humanFactionId),
|
||||
humanClients = Vector(client),
|
||||
aiClients = Vector(RandomState(AIClient(eagleGameId, aiFactionId), SeededRandom(1L))),
|
||||
clientTextStore = textStore
|
||||
)
|
||||
|
||||
val updated = GameController.performAiCommands(
|
||||
GameControllerWithPostResults(controller, PostResults(Some(state), Vector.empty, Vector.empty))
|
||||
)
|
||||
|
||||
updated.postResults.results shouldBe Vector(result1, result2)
|
||||
updated.gameController.engine shouldBe engine2
|
||||
updated.gameController.humanClients.head.unfilteredKnownHistoryCount shouldBe 2
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user