Use active Shardok battles in text client (#8452)

This commit is contained in:
2026-07-11 12:14:07 -07:00
committed by GitHub
parent 21a311e9eb
commit a714271ef2
3 changed files with 127 additions and 7 deletions
+6
View File
@@ -119,6 +119,12 @@ post-shardok <shardok-game-id> <index>
post-shardok <shardok-game-id> <index> <integer-roll>
```
The battle ID may be omitted only when exactly one outstanding battle currently
has postable commands. If multiple battles have commands, the client refuses the
ambiguous shorthand; use the battle ID printed by `shardok-commands`. Battles
removed from Eagle's outstanding-battle list are no longer postable, and a
Shardok update with no available commands clears the previous command list.
Use `shardok-commands-json` or `shardok-units-json` when the concise output omits a needed field.
## Reconnects And Shutdown
@@ -531,14 +531,21 @@ final class ClientState {
println(s"post-command: ${value.status} game=${value.gameId} ${value.errorMessage}")
case UpdateStreamResponse.ResponseDetails.GameUpdate(value) =>
latestGameId = Some(value.gameId)
value.startingState.foreach(state => latestState = Some(state))
value.startingState.foreach { state =>
latestState = Some(state)
latestShardokBattles = Map.empty
}
if value.serverGameStatus.isDefined then latestStatus = value.serverGameStatus
value.gameUpdateDetails match {
case GameUpdate.GameUpdateDetails.ActionResultResponse(ar) =>
latestUnfilteredResultCount = ar.unfilteredResultCountAfter
latestState = latestState.map(state =>
ar.actionResultViews.foldLeft(state) { (current, actionResult) =>
actionResult.gameStateDiff.map(applyGameStateDiff(current, _)).getOrElse(current)
actionResult.gameStateDiff.map { diff =>
latestShardokBattles --= diff.removedBattleIds
applyGameStateDiff(current, diff)
}
.getOrElse(current)
}
)
if ar.availableCommands.isDefined then latestCommands = ar.availableCommands
@@ -607,7 +614,7 @@ final class ClientState {
val updated = ShardokBattleState(
shardokGameId = response.shardokGameId,
token = response.newResultViewCount,
availableCommands = response.availableCommands.orElse(existing.flatMap(_.availableCommands)),
availableCommands = response.availableCommands,
status = response.shardokServerStatus.orElse(existing.flatMap(_.status)),
units = snapshot.units,
reserveUnits = snapshot.reserveUnits,
@@ -914,9 +921,17 @@ final class ClientState {
.flatMap(_.commands.collectFirst { case command: MarchAvailableCommand => command })
def shardokCommandPost(shardokGameId: Option[String], index: Int): Option[ShardokCommandPost] = synchronized {
val candidates = shardokGameId match {
case Some(id) => latestShardokBattles.get(id).toVector
case None => latestShardokBattles.values.toVector
val outstandingBattleIds = latestState.toVector
.flatMap(_.outstandingBattles)
.flatMap(_.shardokGameId)
.toSet
val activeCandidates = latestShardokBattles.values.toVector
.filter(battle => outstandingBattleIds.contains(battle.shardokGameId))
val candidates = shardokGameId match {
case Some(id) => activeCandidates.filter(_.shardokGameId == id)
case None =>
val postable = activeCandidates.filter(_.availableCommands.exists(_.currentCommand.nonEmpty))
Option.when(postable.size == 1)(postable.head).toVector
}
candidates
@@ -1,13 +1,57 @@
package net.eagle0.eagle.text_client
import net.eagle0.eagle.api.eagle.{ActionResultResponse, GameUpdate, UpdateStreamResponse}
import net.eagle0.eagle.api.eagle.{ActionResultResponse, GameUpdate, ShardokActionResultResponse, UpdateStreamResponse}
import net.eagle0.eagle.views.action_result_view.ActionResultView
import net.eagle0.eagle.views.game_state_view.{GameStateView, GameStateViewDiff}
import net.eagle0.eagle.views.province_view.{ProvinceView, ProvinceViewDiff}
import net.eagle0.eagle.views.shardok_battle_view.ShardokBattleView
import net.eagle0.shardok.api.command_descriptor.{AvailableCommands, CommandDescriptor}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class ClientStateTest extends AnyFlatSpec with Matchers {
private def startingState(battleIds: String*): UpdateStreamResponse =
UpdateStreamResponse(
UpdateStreamResponse.ResponseDetails.GameUpdate(
GameUpdate(
gameId = 1L,
startingState = Some(
GameStateView(
gameId = 1L,
outstandingBattles = battleIds.map(id => ShardokBattleView(shardokGameId = Some(id))).toVector
)
)
)
)
)
private def shardokUpdate(
battleId: String,
commands: Option[AvailableCommands]
): UpdateStreamResponse =
UpdateStreamResponse(
UpdateStreamResponse.ResponseDetails.GameUpdate(
GameUpdate(
gameId = 1L,
gameUpdateDetails = GameUpdate.GameUpdateDetails.ShardokActionResultResponse(
ShardokActionResultResponse(
shardokGameResponses = Vector(
ShardokActionResultResponse.SingleShardokGameResultResponse(
shardokGameId = battleId,
newResultViewCount = 1,
availableCommands = commands
)
)
)
)
)
)
)
private val oneShardokCommand = AvailableCommands(
currentCommand = Vector(CommandDescriptor(player = 3))
)
"ClientTextStore" should "resume UTF-8 streaming text from byte offsets" in {
val store = new ClientTextStore
@@ -75,4 +119,59 @@ class ClientStateTest extends AnyFlatSpec with Matchers {
state.describeState should include("province 39: Kojaria ruler=3")
}
it should "clear Shardok commands when a later update omits them" in {
val state = new ClientState
state.handle(startingState("battle-1"))
state.handle(shardokUpdate("battle-1", Some(oneShardokCommand)))
state.shardokCommandPost(Some("battle-1"), index = 0).map(_.shardokGameId) shouldBe Some("battle-1")
state.handle(shardokUpdate("battle-1", commands = None))
state.shardokCommandPost(Some("battle-1"), index = 0) shouldBe None
}
it should "post implicitly only when exactly one outstanding battle has commands" in {
val state = new ClientState
state.handle(startingState("battle-1", "battle-2"))
state.handle(shardokUpdate("battle-1", Some(oneShardokCommand)))
state.handle(shardokUpdate("battle-2", commands = None))
state.shardokCommandPost(shardokGameId = None, index = 0).map(_.shardokGameId) shouldBe Some("battle-1")
state.handle(shardokUpdate("battle-2", Some(oneShardokCommand)))
state.shardokCommandPost(shardokGameId = None, index = 0) shouldBe None
state.shardokCommandPost(Some("battle-2"), index = 0).map(_.shardokGameId) shouldBe Some("battle-2")
}
it should "remove cached Shardok state when Eagle removes the battle" in {
val state = new ClientState
state.handle(startingState("battle-1"))
state.handle(shardokUpdate("battle-1", Some(oneShardokCommand)))
state.handle(
UpdateStreamResponse(
UpdateStreamResponse.ResponseDetails.GameUpdate(
GameUpdate(
gameId = 1L,
gameUpdateDetails = GameUpdate.GameUpdateDetails.ActionResultResponse(
ActionResultResponse(
unfilteredResultCountAfter = 1,
actionResultViews = Vector(
ActionResultView(
gameStateDiff = Some(GameStateViewDiff(removedBattleIds = Vector("battle-1")))
)
)
)
)
)
)
)
)
state.describeShardokCommands(rawJson = false) shouldBe "No Shardok battles yet."
state.shardokCommandPost(Some("battle-1"), index = 0) shouldBe None
}
}