mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
Expose Eagle province graph in text client (#8625)
* Expose Eagle province graph in text client * Limit province views to strategic adjacency * Isolate queued battle result Unity test
This commit is contained in:
@@ -131,6 +131,17 @@ 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.
|
||||
|
||||
The initial Eagle state includes the complete strategic province graph. Inspect it with:
|
||||
|
||||
```text
|
||||
eagle-map
|
||||
eagle-map-json
|
||||
```
|
||||
|
||||
Both forms list every province and all of its neighbors by ID and name. Use the JSON form when exact IDs or
|
||||
machine-readable relationships are needed; unlike March destinations, this is the complete map graph and is not
|
||||
filtered by the commands currently available.
|
||||
|
||||
During battle setup, `shardok-commands` prints any server-recommended unit placements in order and identifies the
|
||||
ordinary current command index for the next recommendation:
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ namespace eagle0.Tests {
|
||||
};
|
||||
updater.ReceiveGameUpdate(new GameUpdate {
|
||||
GameId = gameId,
|
||||
StartingState = new GameStateView { GameId = gameId },
|
||||
ActionResultResponse = new ActionResultResponse()
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ message ProvinceView {
|
||||
repeated IncomingArmyView known_incoming_armies = 5;
|
||||
|
||||
.net.eagle0.eagle.common.Date as_of = 7;
|
||||
|
||||
repeated int32 neighbor_province_ids = 8;
|
||||
}
|
||||
|
||||
message UnaffiliatedHeroBasics {
|
||||
|
||||
@@ -69,7 +69,8 @@ object ProvinceViewFilter {
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(info),
|
||||
knownEvents = Vector.empty,
|
||||
asOf = None
|
||||
asOf = None,
|
||||
neighborProvinceIds = province.neighbors.map(_.provinceId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,7 +132,8 @@ object ProvinceViewFilter {
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(info),
|
||||
knownEvents = Vector.empty,
|
||||
asOf = Some(gs.currentDate)
|
||||
asOf = Some(gs.currentDate),
|
||||
neighborProvinceIds = province.neighbors.map(_.provinceId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,7 +205,8 @@ object ProvinceViewFilter {
|
||||
fullInfo = Some(fullInfo),
|
||||
knownEvents = knownEvents.toVector,
|
||||
asOf = reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers
|
||||
knownIncomingArmies = incomingAttackers,
|
||||
neighborProvinceIds = province.neighbors.map(_.provinceId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -273,7 +276,8 @@ object ProvinceViewFilter {
|
||||
fullInfo = fullInfo,
|
||||
knownEvents = knownEvents,
|
||||
asOf = if showAll then None else reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers ++ incomingSupplies
|
||||
knownIncomingArmies = incomingAttackers ++ incomingSupplies,
|
||||
neighborProvinceIds = province.neighbors.map(_.provinceId)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/incoming_army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
|
||||
+4
-2
@@ -15,7 +15,8 @@ object ProvinceViewConverter {
|
||||
fullInfo = view.fullInfo.map(FullProvinceInfoConverter.toProto),
|
||||
knownEvents = view.knownEvents.map(ProvinceEventConverter.toProto),
|
||||
knownIncomingArmies = view.knownIncomingArmies.map(IncomingArmyViewConverter.toProto),
|
||||
asOf = view.asOf.map(DateConverter.toProto)
|
||||
asOf = view.asOf.map(DateConverter.toProto),
|
||||
neighborProvinceIds = view.neighborProvinceIds
|
||||
)
|
||||
|
||||
def fromProto(proto: ProvinceViewProto): ProvinceView =
|
||||
@@ -26,6 +27,7 @@ object ProvinceViewConverter {
|
||||
fullInfo = proto.fullInfo.map(FullProvinceInfoConverter.fromProto),
|
||||
knownEvents = proto.knownEvents.map(ProvinceEventConverter.fromProto).toVector,
|
||||
knownIncomingArmies = proto.knownIncomingArmies.map(IncomingArmyViewConverter.fromProto).toVector,
|
||||
asOf = proto.asOf.map(d => DateConverter.fromProto(Some(d)))
|
||||
asOf = proto.asOf.map(d => DateConverter.fromProto(Some(d))),
|
||||
neighborProvinceIds = proto.neighborProvinceIds.toVector
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ scala_library(
|
||||
":full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
],
|
||||
|
||||
@@ -12,5 +12,6 @@ case class ProvinceView(
|
||||
fullInfo: Option[FullProvinceInfo],
|
||||
knownEvents: Vector[ProvinceEvent],
|
||||
knownIncomingArmies: Vector[IncomingArmyView] = Vector.empty,
|
||||
asOf: Option[Date]
|
||||
asOf: Option[Date],
|
||||
neighborProvinceIds: Vector[ProvinceId] = Vector.empty
|
||||
)
|
||||
|
||||
@@ -278,7 +278,9 @@ final class EagleTextClient(config: Config) {
|
||||
case Vector("status") => println(state.describeStatus)
|
||||
case Vector("state") => println(state.describeState)
|
||||
case Vector("state-json") => println(state.stateJson)
|
||||
case Vector("text", textId) => println(state.describeText(textId))
|
||||
case Vector("eagle-map") => println(state.describeEagleMap(rawJson = false))
|
||||
case Vector("eagle-map-json") => println(state.describeEagleMap(rawJson = true))
|
||||
case Vector("text", textId) => println(state.describeText(textId))
|
||||
case Vector("commands") => println(state.describeCommands(rawJson = false))
|
||||
case Vector("commands-json") => println(state.describeCommands(rawJson = true))
|
||||
case Vector("shardok-commands") => println(state.describeShardokCommands(rawJson = false))
|
||||
@@ -1202,6 +1204,35 @@ final class ClientState {
|
||||
latestState.map(toJson).getOrElse("{}")
|
||||
}
|
||||
|
||||
def describeEagleMap(rawJson: Boolean): String = synchronized {
|
||||
latestState match {
|
||||
case None => "No state yet. Use stream <game-id>."
|
||||
case Some(gs) =>
|
||||
val provinces = gs.provinces.values.toVector.sortBy(_.id)
|
||||
if rawJson then {
|
||||
val provinceJson = provinces.map { province =>
|
||||
val neighbors = province.neighborProvinceIds.sorted.map { neighborProvinceId =>
|
||||
val neighborName = gs.provinces.get(neighborProvinceId).map(_.name).getOrElse("")
|
||||
s"{\"provinceId\":$neighborProvinceId,\"name\":\"${jsonEscape(neighborName)}\"}"
|
||||
}
|
||||
.mkString("[", ",", "]")
|
||||
s"{\"id\":${province.id},\"name\":\"${jsonEscape(province.name)}\",\"neighbors\":$neighbors}"
|
||||
}.mkString("[", ",", "]")
|
||||
s"{\"provinces\":$provinceJson}"
|
||||
} else {
|
||||
val lines = provinces.map { province =>
|
||||
val neighbors = province.neighborProvinceIds.sorted.map { neighborProvinceId =>
|
||||
val name = gs.provinces.get(neighborProvinceId).map(_.name).filter(_.nonEmpty)
|
||||
name.map(value => s"$neighborProvinceId $value").getOrElse(neighborProvinceId.toString)
|
||||
}
|
||||
.mkString(", ")
|
||||
s" ${province.id} ${province.name}: $neighbors"
|
||||
}
|
||||
(s"Eagle map: provinces=${provinces.size}" +: lines).mkString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def describeText(textId: String): String = synchronized {
|
||||
textStore.entry(textId) match {
|
||||
case Some(entry) => s"text $textId complete=${entry.completed}: ${entry.text}"
|
||||
@@ -1931,6 +1962,8 @@ object Help {
|
||||
| status
|
||||
| state
|
||||
| state-json
|
||||
| eagle-map
|
||||
| eagle-map-json
|
||||
| text <text-id>
|
||||
| commands
|
||||
| commands-json
|
||||
|
||||
+9
-2
@@ -9,9 +9,8 @@ import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.hero.Profession
|
||||
import net.eagle0.eagle.model.state.province.{Neighbor, ProvinceOrderType, ProvinceT}
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.province.ProvinceOrderType
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
|
||||
@@ -136,6 +135,14 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
private def filteredProvinceView(province: ProvinceT, gs: GameState, factionId: Int): ProvinceView =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gs, factionId, FilterContext.build(gs))
|
||||
|
||||
it should "expose the static province neighbor graph" in {
|
||||
val neighbors = Vector(Neighbor(provinceId = 8, startingPositionIndex = 3))
|
||||
val province = ProvinceC(id = 5, neighbors = neighbors)
|
||||
val gs = minimalGameState(provinces = Map(5 -> province))
|
||||
|
||||
ProvinceViewFilter.filteredProvinceView(province, gs).neighborProvinceIds shouldBe Vector(8)
|
||||
}
|
||||
|
||||
it should "keep a devastation of 0 at 0" in {
|
||||
val province = ProvinceC(id = 5, rulingFactionId = Some(3), economyDevastation = 0.0)
|
||||
val gs = minimalGameState(provinces = Map(5 -> province))
|
||||
|
||||
@@ -51,6 +51,7 @@ scala_test(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
|
||||
+13
-5
@@ -83,7 +83,8 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
unitDetails = Vector(UnitDetails(heroId = 20: HeroId, battalion = None))
|
||||
)
|
||||
|
||||
private val sampleDate = Date(year = 1000, month = Date.Month.March)
|
||||
private val sampleDate = Date(year = 1000, month = Date.Month.March)
|
||||
private val sampleNeighborProvinceId = 43: ProvinceId
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = ProvinceView(
|
||||
@@ -93,7 +94,8 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
fullInfo = Some(sampleFullInfo),
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
asOf = Some(sampleDate),
|
||||
neighborProvinceIds = Vector(sampleNeighborProvinceId)
|
||||
)
|
||||
|
||||
val proto = ProvinceViewConverter.toProto(scala)
|
||||
@@ -108,6 +110,7 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
knownEvents,
|
||||
knownIncomingArmies,
|
||||
asOf,
|
||||
neighborProvinceIds,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
id shouldBe 42
|
||||
@@ -117,6 +120,7 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
knownEvents.size shouldBe 0
|
||||
knownIncomingArmies.size shouldBe 1
|
||||
asOf shouldBe defined
|
||||
neighborProvinceIds should contain only 43
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +132,8 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
fullInfo = Some(sampleFullInfo),
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
asOf = Some(sampleDate),
|
||||
neighborProvinceIds = Vector(sampleNeighborProvinceId)
|
||||
)
|
||||
|
||||
val proto = ProvinceViewConverter.toProto(original)
|
||||
@@ -143,7 +148,8 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
fullInfo,
|
||||
knownEvents,
|
||||
knownIncomingArmies,
|
||||
asOf
|
||||
asOf,
|
||||
neighborProvinceIds
|
||||
) =>
|
||||
id shouldBe (55: ProvinceId)
|
||||
name shouldBe "Another Province"
|
||||
@@ -152,6 +158,7 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
knownEvents.size shouldBe 0
|
||||
knownIncomingArmies.size shouldBe 1
|
||||
asOf shouldBe defined
|
||||
neighborProvinceIds should contain only sampleNeighborProvinceId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +188,8 @@ class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
fullInfo = Some(sampleFullInfo),
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
asOf = Some(sampleDate),
|
||||
neighborProvinceIds = Vector(sampleNeighborProvinceId)
|
||||
)
|
||||
|
||||
val roundTripped = ProvinceViewConverter.fromProto(
|
||||
|
||||
@@ -232,6 +232,45 @@ class ClientStateTest extends AnyFlatSpec with Matchers {
|
||||
output should include("instruction: Click Fight!")
|
||||
}
|
||||
|
||||
it should "describe the complete Eagle province neighbor graph" in {
|
||||
val state = new ClientState
|
||||
state.handle(
|
||||
UpdateStreamResponse(
|
||||
UpdateStreamResponse.ResponseDetails.GameUpdate(
|
||||
GameUpdate(
|
||||
gameId = 1L,
|
||||
startingState = Some(
|
||||
GameStateView(
|
||||
gameId = 1L,
|
||||
provinces = Map(
|
||||
1 -> ProvinceView(
|
||||
id = 1,
|
||||
name = "North & West",
|
||||
neighborProvinceIds = Vector(2)
|
||||
),
|
||||
2 -> ProvinceView(
|
||||
id = 2,
|
||||
name = "East",
|
||||
neighborProvinceIds = Vector(1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
state.describeEagleMap(rawJson = false) shouldBe
|
||||
"Eagle map: provinces=2\n" +
|
||||
" 1 North & West: 2 East\n" +
|
||||
" 2 East: 1 North & West"
|
||||
|
||||
val json = state.describeEagleMap(rawJson = true)
|
||||
json should include("\"id\":1,\"name\":\"North & West\"")
|
||||
json should include("\"provinceId\":2,\"name\":\"East\"")
|
||||
}
|
||||
|
||||
it should "retain unresolved hero-departure dialogue across a refreshed starting state" in {
|
||||
val state = new ClientState
|
||||
val gameState = GameStateView(
|
||||
|
||||
Reference in New Issue
Block a user