Compare commits

...
Author SHA1 Message Date
admin fbd9e95875 Implement warden prisoner evacuation 2026-06-16 22:46:58 -07:00
35 changed files with 629 additions and 35 deletions
@@ -49,6 +49,7 @@ static auto IsDeterministic(const CommandType type) -> bool {
case net::eagle0::shardok::common::FORTIFY_COMMAND:
case net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND:
case net::eagle0::shardok::common::HOLY_WAVE_COMMAND:
case net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND:
case net::eagle0::shardok::common::REPAIR_COMMAND: return true;
default: return false;
}
@@ -226,6 +226,7 @@ double AIHeuristicWeighting::GetCommandWeight(
// === ZERO WEIGHT - NEVER SELECT (0.0) ===
// Explicitly bad actions
case CommandType::FLEE_COMMAND: return 0.0; // Never flee in simulation
case CommandType::EVACUATE_PRISONERS_COMMAND: return 0.0;
case CommandType::RETREAT_COMMAND: return 0.0;
case CommandType::BECOME_OUTLAW_COMMAND: return 0.0; // Never become outlaw
case CommandType::DISMISS_UNIT_COMMAND:
@@ -91,6 +91,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
@@ -725,6 +725,10 @@ void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Uni
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
ru.set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT);
break;
case net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT:
ru.set_status(
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT);
break;
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
@@ -28,6 +28,7 @@ auto IsResolved(const net::eagle0::shardok::storage::fb::UnitStatus status) -> b
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT: return true;
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
@@ -17,6 +17,7 @@ cc_library(
":challenge_duel_command_factory",
":charge_command_factory",
":control_command_factory",
":evacuate_prisoners_command_factory",
":extinguish_fire_command_factory",
":fear_command_factory",
":flee_command_factory",
@@ -178,6 +179,24 @@ cc_library(
],
)
cc_library(
name = "evacuate_prisoners_command_factory",
srcs = ["EvacuatePrisonersCommandFactory.cpp"],
hdrs = ["EvacuatePrisonersCommandFactory.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
deps = [
":command_factory",
"//src/main/cpp/net/eagle0/shardok/library:shardok_command",
"//src/main/cpp/net/eagle0/shardok/library/commands:evacuate_prisoners_command",
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
],
)
cc_library(
name = "extinguish_fire_command_factory",
srcs = ["ExtinguishFireCommandFactory.cpp"],
@@ -10,6 +10,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/ChallengeDuelCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/ChargeCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/ControlCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/EvacuatePrisonersCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/ExtinguishFireCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/FearCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/FleeCommandFactory.hpp"
@@ -47,6 +48,7 @@ auto MakeFactories(const SettingsGetter& settings) -> vector<shared_ptr<const Co
make_shared<BuildBridgeCommandFactory>(settings),
make_shared<ChallengeDuelCommandFactory>(settings),
make_shared<ChargeCommandFactory>(settings),
make_shared<EvacuatePrisonersCommandFactory>(settings),
make_shared<ExtinguishFireCommandFactory>(settings),
make_shared<FearCommandFactory>(settings),
make_shared<FleeCommandFactory>(settings),
@@ -0,0 +1,69 @@
//
// EvacuatePrisonersCommandFactory.cpp
// eagle0
//
#include "EvacuatePrisonersCommandFactory.hpp"
#include <algorithm>
#include <ranges>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/commands/EvacuatePrisonersCommand.hpp"
namespace shardok {
namespace {
[[nodiscard]] auto IsWarden(const Unit* unit) -> bool {
return unit->has_attached_hero() &&
unit->attached_hero().profession_info().profession() ==
net::eagle0::shardok::storage::fb::Profession_WARDEN;
}
[[nodiscard]] auto IsHostileToActor(
const Unit* unit,
const PlayerId actorPlayerId,
const vector<PlayerId>& allyPids) -> bool {
return unit->player_id() != actorPlayerId &&
!std::ranges::contains(allyPids, unit->player_id());
}
[[nodiscard]] auto CapturedHostileHeroUnitIds(
const Units* units,
const PlayerId actorPlayerId,
const vector<PlayerId>& allyPids) -> std::vector<UnitId> {
std::vector<UnitId> captiveUnitIds;
for (const auto* unit : *units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
unit->has_attached_hero() && IsHostileToActor(unit, actorPlayerId, allyPids)) {
captiveUnitIds.push_back(unit->unit_id());
}
}
return captiveUnitIds;
}
} // namespace
EvacuatePrisonersCommandFactory::EvacuatePrisonersCommandFactory(const SettingsGetter& getter)
: settings(getter) {}
void EvacuatePrisonersCommandFactory::AddAvailableCommands(
CommandList& commands,
const CommandFactory::CommandParams& params) const {
if (!params.unit->can_flee()) return;
if (!IsWarden(params.unit)) return;
if (!settings.ActionCostFor(settings.Backing().flee_action_point_cost())
.IsPossible(params.remainingActionPoints))
return;
const auto captiveUnitIds =
CapturedHostileHeroUnitIds(params.units, params.unit->player_id(), params.allyPids);
if (captiveUnitIds.empty()) return;
commands.push_back(std::make_shared<EvacuatePrisonersCommand>(
settings.ActionCostFor(settings.Backing().flee_action_point_cost()),
params.unit->player_id(),
params.unit->unit_id(),
captiveUnitIds));
}
} // namespace shardok
@@ -0,0 +1,25 @@
//
// EvacuatePrisonersCommandFactory.hpp
// eagle0
//
#ifndef EvacuatePrisonersCommandFactory_hpp
#define EvacuatePrisonersCommandFactory_hpp
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/CommandFactory.hpp"
namespace shardok {
class EvacuatePrisonersCommandFactory : public CommandFactory {
const SettingsGetter& settings;
public:
explicit EvacuatePrisonersCommandFactory(const SettingsGetter& getter);
void AddAvailableCommands(CommandList& commands, const CommandFactory::CommandParams& params)
const override;
};
} // namespace shardok
#endif /* EvacuatePrisonersCommandFactory_hpp */
@@ -189,6 +189,23 @@ cc_library(
],
)
cc_library(
name = "evacuate_prisoners_command",
srcs = ["EvacuatePrisonersCommand.cpp"],
hdrs = ["EvacuatePrisonersCommand.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
deps = [
"//src/main/cpp/net/eagle0/shardok/library:action_cost",
"//src/main/cpp/net/eagle0/shardok/library:shardok_command",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
],
)
cc_library(
name = "fear_command",
srcs = ["FearCommand.cpp"],
@@ -0,0 +1,58 @@
//
// EvacuatePrisonersCommand.cpp
// eagle0
//
#include "EvacuatePrisonersCommand.hpp"
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
namespace shardok {
auto EvacuatePrisonersCommand::InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
ActionResult result{};
result.mutable_player()->set_value(GetPlayerId());
result.mutable_actor()->set_value(actorId);
result.set_type(net::eagle0::shardok::common::ActionType::EVACUATED_PRISONERS);
const auto* actorUnit = currentState->units()->Get(actorId);
auto* resolvedActor = result.add_resolved_units();
AddUnitToResolved(*resolvedActor, *actorUnit);
resolvedActor->set_status(
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT);
for (const auto captiveUnitId : captiveUnitIds) {
const auto* captiveUnit = currentState->units()->Get(captiveUnitId);
auto* resolvedCaptive = result.add_resolved_units();
AddUnitToResolved(*resolvedCaptive, *captiveUnit);
resolvedCaptive->set_status(
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT);
}
return {result};
}
EvacuatePrisonersCommand::EvacuatePrisonersCommand(
const ActionCost& cost,
const PlayerId playerId,
const UnitId actorId,
std::vector<UnitId> captiveUnitIds)
: ShardokCommand(playerId),
cost(cost),
actorId(actorId),
captiveUnitIds(std::move(captiveUnitIds)) {}
auto EvacuatePrisonersCommand::GetCommandProto() const -> CommandProto {
CommandProto proto;
proto.set_player(GetPlayerId());
proto.set_type(net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND);
proto.mutable_actor()->set_value(actorId);
return proto;
}
} // namespace shardok
@@ -0,0 +1,48 @@
//
// EvacuatePrisonersCommand.hpp
// eagle0
//
#ifndef EvacuatePrisonersCommand_hpp
#define EvacuatePrisonersCommand_hpp
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ActionCost.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
namespace shardok {
class EvacuatePrisonersCommand : public ShardokCommand {
protected:
auto InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const ActionCost cost;
const UnitId actorId;
const std::vector<UnitId> captiveUnitIds;
public:
EvacuatePrisonersCommand(
const ActionCost& cost,
PlayerId playerId,
UnitId actorId,
std::vector<UnitId> captiveUnitIds);
~EvacuatePrisonersCommand() override = default;
[[nodiscard]] auto GetCommandType() const -> CommandType override {
return net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND;
}
[[nodiscard]] auto GetCommandProto() const -> CommandProto override;
[[nodiscard]] auto GetOddsPercentile() const -> int32_t override { return 100; }
[[nodiscard]] auto CanBeFollowUp() const -> bool override { return false; }
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
[[nodiscard]] int GetActorUnitId() const override { return actorId; }
};
} // namespace shardok
#endif /* EvacuatePrisonersCommand_hpp */
@@ -73,6 +73,9 @@ void DumpGameStateToFile(const GameStateW& gameState, const std::string& gameId)
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
out << "RETREATED_UNIT\n";
break;
case net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT:
out << "EVACUATED_UNIT\n";
break;
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
out << "PENDING_REINFORCEMENT\n";
break;
@@ -40,13 +40,16 @@ void Validate(const GameStateW &inState, const GameStateW &outState) {
inStatus != net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT &&
inStatus != net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
inStatus != net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT &&
inStatus != net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT)
inStatus != net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT &&
inStatus != net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT)
continue;
const auto outStatus =
outState->units()->Get(static_cast<unsigned int>(inUnit->unit_id()))->status();
internalAssert(
outStatus == inStatus,
outStatus == inStatus ||
(inStatus == net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
outStatus == net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT),
"Can't come back from fled/destroyed/captured status");
}
}
@@ -59,6 +59,7 @@ auto GameStateFilteredForPlayerWithAllies(
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
@@ -110,6 +111,7 @@ auto GameStateFilteredForPlayerWithAllies(
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_EVACUATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: break;
@@ -122,4 +124,4 @@ auto GameStateFilteredForPlayerWithAllies(
return gsv;
}
} // namespace shardok
} // namespace shardok
@@ -81,7 +81,8 @@ auto IncludeUnitProtoInReturn(
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT: return true;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT: return true;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
return false;
@@ -161,6 +162,9 @@ auto FromInternalStatus(
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
return net::eagle0::common::UnitStatus::OUTLAWED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT:
return net::eagle0::common::UnitStatus::EVACUATED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
case net::eagle0::shardok::storage::
@@ -20,6 +20,7 @@ enum UnitStatus : int8 {
RESERVE_UNIT = 8,
RESERVED_SLOT = 9,
PENDING_REINFORCEMENT = 10,
EVACUATED_UNIT = 11,
}
struct Unit {
@@ -53,4 +54,3 @@ struct Unit {
location:net.eagle0.shardok.storage.fb.Coords (native_inline);
battalion:net.eagle0.shardok.storage.fb.Battalion (native_inline);
}
@@ -98,6 +98,7 @@ enum UnitStatus {
RETREATED_UNIT = 4;
NEVER_ENTERED_UNIT = 5;
OUTLAWED_UNIT = 6;
EVACUATED_UNIT = 7;
}
message ResolvedUnit {
@@ -80,6 +80,7 @@ message FoughtInBattleBattalionBackstoryEvent {
UNIT_STATUS_RETREATED = 4;
UNIT_STATUS_NEVER_ENTERED = 5;
UNIT_STATUS_OUTLAWED = 6;
UNIT_STATUS_EVACUATED = 7;
}
UnitStatus status = 6;
@@ -126,6 +126,7 @@ message FoughtInBattleBackstoryEvent {
UNIT_STATUS_RETREATED = 4;
UNIT_STATUS_NEVER_ENTERED = 5;
UNIT_STATUS_OUTLAWED = 6;
UNIT_STATUS_EVACUATED = 7;
}
UnitStatus status = 6;
}
@@ -94,4 +94,5 @@ enum ActionType {
TUTORIAL_REINFORCEMENTS_ARRIVED = 79;
BLOW_BRIDGE = 80;
BLOW_BRIDGE_FAILED = 81;
EVACUATED_PRISONERS = 82;
}
@@ -47,4 +47,5 @@ enum CommandType {
BECOME_OUTLAW_COMMAND = 36;
// MANAGE_PRISONER = 37; // Removed: Eagle-only command, not used in Shardok
BLOW_BRIDGE_COMMAND = 38;
EVACUATE_PRISONERS_COMMAND = 39;
}
@@ -30,6 +30,8 @@ message ResolvedUnit {
DESTROYED_SUMMONED_UNIT = 5;
NEVER_ENTERED_UNIT = 6;
OUTLAWED_UNIT = 7;
// Keep this aligned with storage/unit.fbs; values 8-10 are active/reserve flatbuffer-only statuses.
EVACUATED_UNIT = 11;
}
bytes unit_bytes = 1;
@@ -16,7 +16,7 @@ import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedPro
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, StatAbsolute}
import net.eagle0.eagle.model.action_result.types.ActionResultType.BattleEnded
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.{AftermathClaimant, AftermathClaimantUnit, Army}
import net.eagle0.eagle.model.state.{AftermathClaimant, AftermathClaimantUnit, Army, CapturedHero}
import net.eagle0.eagle.model.state.{
BattleRevelation,
BattleRevelationType,
@@ -27,7 +27,7 @@ import net.eagle0.eagle.model.state.{
}
import net.eagle0.eagle.model.state.battalion.{BattalionT, EventForBattalionBackstory, EventForBattalionBackstoryT}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.{EventForHeroBackstory, EventForHeroBackstoryT, HeroT}
import net.eagle0.eagle.model.state.hero.{EventForHeroBackstory, EventForHeroBackstoryT, HeroT, Profession}
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.quest.{DefeatFactionQuest, Quest, WinBattleOutnumberedQuest, WinBattlesQuest}
import net.eagle0.eagle.model.state.shardok_battle.{BattleType, ShardokBattle, ShardokPlayer}
@@ -97,7 +97,7 @@ case class ResolveBattleAction(
if defenderProvince.rulingFactionId.contains(rsp.eagleFid)
rse <- rsp.units
if battle.reinforcementHeroIds.contains(rse.hero.id)
if rse.status != UnitStatus.Fled && rse.status != UnitStatus.Outlawed
if rse.status != UnitStatus.Fled && rse.status != UnitStatus.Outlawed && rse.status != UnitStatus.Evacuated
} yield rse).toVector
ChangedProvinceC(
@@ -113,7 +113,9 @@ case class ResolveBattleAction(
if unitReturned(
hid = rse.hero.id,
in = in,
out = rsp
out = rsp,
battle = battle,
battleResolution = battleResolution
) || rse.status == UnitStatus.Outlawed
} yield rse.hero.id).toVector,
newBattalionIds = survivingReinforcementUnits.flatMap(_.optionalBattalionId),
@@ -125,7 +127,9 @@ case class ResolveBattleAction(
if unitReturned(
hid = rse.hero.id,
in = in,
out = rsp
out = rsp,
battle = battle,
battleResolution = battleResolution
) || rse.status == UnitStatus.Outlawed
} yield rse.optionalBattalionId).flatten.toVector,
goldDelta = Some(
@@ -362,6 +366,9 @@ case class ResolveBattleAction(
batt = battle,
br = battleResolution,
destroyedBattalionIds = destroyedBattalionIds(battleResolution)
) ++ changedProvincesFromEvacuatedCaptives(
battle = battle,
battleResolution = battleResolution
) :+ defenderCP(
battle = battle,
battleResolution = battleResolution
@@ -422,10 +429,13 @@ case class ResolveBattleAction(
in = battle.players
.find(_.eagleFid == losingPlayer.eagleFid)
.get,
out = losingPlayer
out = losingPlayer,
battle = battle,
battleResolution = battleResolution
)
)
.filterNot(_.status == UnitStatus.Outlawed)
.filterNot(u => evacuatedCaptiveDestination(u, losingPlayer, battle, battleResolution).isDefined)
}
ProvinceHeldAction(
@@ -459,10 +469,20 @@ case class ResolveBattleAction(
unitReturned(
hid = u.hero.id,
in = wsp,
out = winningResolvedPlayers.find(_.eagleFid == wsp.eagleFid).get
out = winningResolvedPlayers.find(_.eagleFid == wsp.eagleFid).get,
battle = battle,
battleResolution = battleResolution
)
)
.filterNot(_.status == UnitStatus.Outlawed)
.filterNot(u =>
evacuatedCaptiveDestination(
u,
winningResolvedPlayers.find(_.eagleFid == wsp.eagleFid).get,
battle,
battleResolution
).isDefined
)
.map(u =>
ResolvedEagleUnit(
hero = startingState.heroes(u.hero.id),
@@ -481,10 +501,13 @@ case class ResolveBattleAction(
unitReturned(
hid = u.hero.id,
in = sp,
out = rsp
out = rsp,
battle = battle,
battleResolution = battleResolution
)
)
.filterNot(_.status == UnitStatus.Outlawed)
.filterNot(u => evacuatedCaptiveDestination(u, rsp, battle, battleResolution).isDefined)
} yield ResolvedEagleUnit(
hero = startingState.heroes(u.hero.id),
battalion = u.battalion.map(b => startingState.battalions(b.id)),
@@ -511,10 +534,13 @@ case class ResolveBattleAction(
unitReturned(
hid = u.hero.id,
in = sp,
out = rsp
out = rsp,
battle = battle,
battleResolution = battleResolution
)
)
.filterNot(_.status == UnitStatus.Outlawed)
.filterNot(u => evacuatedCaptiveDestination(u, rsp, battle, battleResolution).isDefined)
} yield AftermathClaimantUnit(
heroId = u.hero.id,
battalionId = u.optionalBattalionId.filterNot(destroyed.contains)
@@ -523,8 +549,17 @@ case class ResolveBattleAction(
val claimants = attackingWinners.map { wsp =>
val rsp = winningResolvedPlayers.find(_.eagleFid == wsp.eagleFid).get
val notFledUnits = rsp.units
.filterNot(u => unitReturned(hid = u.hero.id, in = wsp, out = rsp))
.filterNot(u =>
unitReturned(
hid = u.hero.id,
in = wsp,
out = rsp,
battle = battle,
battleResolution = battleResolution
)
)
.filterNot(_.status == UnitStatus.Outlawed)
.filterNot(u => evacuatedCaptiveDestination(u, rsp, battle, battleResolution).isDefined)
.toVector
val armySize = notFledUnits
@@ -683,6 +718,8 @@ case class ResolveBattleAction(
ma = ma,
sp = sp,
rsp = rsp,
battle = batt,
battleResolution = br,
destroyedBattalionIds = destroyedBattalionIds
)
} yield ma.army.fleeProvinceId
@@ -705,11 +742,21 @@ case class ResolveBattleAction(
ma: MovingArmy,
sp: ShardokPlayer,
rsp: ResolvedShardokPlayer,
battle: ShardokBattle,
battleResolution: BattleResolution,
destroyedBattalionIds: Vector[BattalionId]
): Option[Army] =
armyFromResolvedUnits(
units = ma.army.units
.filter(u => unitReturned(hid = u.heroId, in = sp, out = rsp))
.filter(u =>
unitReturned(
hid = u.heroId,
in = sp,
out = rsp,
battle = battle,
battleResolution = battleResolution
)
)
.flatMap(cu => rsp.units.find(_.hero.id == cu.heroId))
.toVector,
removedBattalionIds = destroyedBattalionIds,
@@ -774,29 +821,105 @@ case class ResolveBattleAction(
incomingArmy = shatteredArmy
)
private def changedProvincesFromEvacuatedCaptives(
battle: ShardokBattle,
battleResolution: BattleResolution
): Vector[ChangedProvinceC] = {
val capturedHeroesByProvince = (for {
rsp <- battleResolution.resolvedPlayers
unit <- rsp.units
(_, destinationProvince) <- evacuatedCaptiveDestination(
unit = unit,
resolvedPlayer = rsp,
battle = battle,
battleResolution = battleResolution
)
} yield destinationProvince -> CapturedHero(
heroId = unit.hero.id,
recruitmentAttempted = false,
previousFactionId = startingGameState.heroes(unit.hero.id).factionId.get
)).groupBy(_._1).view.mapValues(_.map(_._2).toVector).toMap
capturedHeroesByProvince.toVector
.sortBy(_._1)
.map {
case (provinceId, capturedHeroes) =>
ChangedProvinceC(
provinceId = provinceId,
newCapturedHeroes = capturedHeroes.sortBy(_.heroId)
)
}
}
private def evacuatedCaptiveDestination(
unit: ResolvedEagleUnit,
resolvedPlayer: ResolvedShardokPlayer,
battle: ShardokBattle,
battleResolution: BattleResolution
): Option[(FactionId, ProvinceId)] =
if unit.status != UnitStatus.Evacuated then None
else
for {
captivePlayer <- battle.players.find(_.eagleFid == resolvedPlayer.eagleFid)
destination <- evacuatedWardenDestinations(battle, battleResolution).find {
case (wardenPlayer, _, _) =>
wardenPlayer.isDefender != captivePlayer.isDefender
}
} yield (destination._2, destination._3)
private def evacuatedWardenDestinations(
battle: ShardokBattle,
battleResolution: BattleResolution
): Vector[(ShardokPlayer, FactionId, ProvinceId)] =
(for {
rsp <- battleResolution.resolvedPlayers
sp <- battle.players.find(_.eagleFid == rsp.eagleFid).toVector
unit <- rsp.units
if unit.status == UnitStatus.Evacuated
hero = startingGameState.heroes(unit.hero.id)
if hero.profession == Profession.Warden
destination <- fleeProvince(hero, battle).toVector
} yield (sp, rsp.eagleFid, destination)).toVector.sortBy {
case (_, factionId, provinceId) =>
(factionId, provinceId)
}
private def unitReturned(
hid: HeroId,
in: ShardokPlayer,
out: ResolvedShardokPlayer
out: ResolvedShardokPlayer,
battle: ShardokBattle,
battleResolution: BattleResolution
): Boolean =
out.units.find(_.hero.id == hid).get.status match {
case UnitStatus.Fled => true
case UnitStatus.NeverEntered =>
!in.isDefender &&
in.armyGroup.toVector
.flatMap(_.armies)
.find(_.army.units.exists(_.heroId == hid))
.get
.army
.fleeProvinceId
.isDefined &&
!out.endGameCondition.isVictory
case UnitStatus.Captured =>
false
case UnitStatus.Normal =>
false
case UnitStatus.Retreated => false
case UnitStatus.Outlawed => false
out.units.find(_.hero.id == hid).get match {
case unit if unit.status == UnitStatus.Evacuated =>
evacuatedCaptiveDestination(
unit = unit,
resolvedPlayer = out,
battle = battle,
battleResolution = battleResolution
).isEmpty && fleeProvince(startingGameState.heroes(hid), battle).isDefined
case unit =>
unit.status match {
case UnitStatus.Fled => true
case UnitStatus.NeverEntered =>
!in.isDefender &&
in.armyGroup.toVector
.flatMap(_.armies)
.find(_.army.units.exists(_.heroId == hid))
.get
.army
.fleeProvinceId
.isDefined &&
!out.endGameCondition.isVictory
case UnitStatus.Captured =>
false
case UnitStatus.Normal =>
false
case UnitStatus.Retreated => false
case UnitStatus.Outlawed => false
case UnitStatus.Evacuated => false
}
}
private def outstandingBattle(
@@ -132,6 +132,7 @@ object BattalionDescriptions {
case UnitStatus.Retreated => " The unit retreated in good order."
case UnitStatus.NeverEntered => " The unit never entered the battle."
case UnitStatus.Outlawed => " The unit's commander was outlawed."
case UnitStatus.Evacuated => " The unit evacuated prisoners from the battlefield."
}
val casualtyText =
if casualties > 0 then s", suffering $casualties casualties (from $startingSize to $endingSize soldiers)"
@@ -132,6 +132,7 @@ object HeroDescriptions {
case UnitStatus.Retreated => " $heroName retreated."
case UnitStatus.NeverEntered => " $heroName never entered the battle."
case UnitStatus.Outlawed => " $heroName became an outlaw."
case UnitStatus.Evacuated => " $heroName evacuated prisoners from the battlefield."
}
TextGenerationSuccess(
@@ -14,6 +14,7 @@ import net.eagle0.eagle.internal.event_for_battalion_backstory.{
}
import net.eagle0.eagle.internal.event_for_battalion_backstory.FoughtInBattleBattalionBackstoryEvent.UnitStatus.{
UNIT_STATUS_CAPTURED,
UNIT_STATUS_EVACUATED,
UNIT_STATUS_FLED,
UNIT_STATUS_NEVER_ENTERED,
UNIT_STATUS_NORMAL,
@@ -332,6 +333,7 @@ object EventForBattalionBackstoryConverter {
case UnitStatus.Retreated => UNIT_STATUS_RETREATED
case UnitStatus.NeverEntered => UNIT_STATUS_NEVER_ENTERED
case UnitStatus.Outlawed => UNIT_STATUS_OUTLAWED
case UnitStatus.Evacuated => UNIT_STATUS_EVACUATED
}
private def unitStatusFromProto(status: UnitStatusProto): UnitStatus =
@@ -342,6 +344,7 @@ object EventForBattalionBackstoryConverter {
case UNIT_STATUS_RETREATED => UnitStatus.Retreated
case UNIT_STATUS_NEVER_ENTERED => UnitStatus.NeverEntered
case UNIT_STATUS_OUTLAWED => UnitStatus.Outlawed
case UNIT_STATUS_EVACUATED => UnitStatus.Evacuated
case UNIT_STATUS_UNKNOWN | UnitStatusProto.Unrecognized(_) =>
throw new ProtoConversionException(s"Unknown unit status: $status")
}
@@ -39,6 +39,7 @@ import net.eagle0.eagle.internal.event_for_hero_backstory.{
}
import net.eagle0.eagle.internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.UnitStatus.{
UNIT_STATUS_CAPTURED,
UNIT_STATUS_EVACUATED,
UNIT_STATUS_FLED,
UNIT_STATUS_NEVER_ENTERED,
UNIT_STATUS_NORMAL,
@@ -406,6 +407,7 @@ object EventForHeroBackstoryConverter {
case UnitStatus.Retreated => UNIT_STATUS_RETREATED
case UnitStatus.NeverEntered => UNIT_STATUS_NEVER_ENTERED
case UnitStatus.Outlawed => UNIT_STATUS_OUTLAWED
case UnitStatus.Evacuated => UNIT_STATUS_EVACUATED
}
def fromProto(
@@ -798,6 +800,7 @@ object EventForHeroBackstoryConverter {
case UNIT_STATUS_RETREATED => UnitStatus.Retreated
case UNIT_STATUS_NEVER_ENTERED => UnitStatus.NeverEntered
case UNIT_STATUS_OUTLAWED => UnitStatus.Outlawed
case UNIT_STATUS_EVACUATED => UnitStatus.Evacuated
case UNIT_STATUS_UNKNOWN =>
throw new ProtoConversionException(
s"Unknown UnitStatusProto"
@@ -11,6 +11,7 @@ object UnitStatusConverter {
case UnitStatusProto.RETREATED_UNIT => UnitStatus.Retreated
case UnitStatusProto.NEVER_ENTERED_UNIT => UnitStatus.NeverEntered
case UnitStatusProto.OUTLAWED_UNIT => UnitStatus.Outlawed
case UnitStatusProto.EVACUATED_UNIT => UnitStatus.Evacuated
case UnitStatusProto.UNKNOWN_UNIT =>
throw new IllegalArgumentException("UnitStatus cannot be UNKNOWN_UNIT")
case UnitStatusProto.Unrecognized(value) =>
@@ -25,5 +26,6 @@ object UnitStatusConverter {
case UnitStatus.Retreated => UnitStatusProto.RETREATED_UNIT
case UnitStatus.NeverEntered => UnitStatusProto.NEVER_ENTERED_UNIT
case UnitStatus.Outlawed => UnitStatusProto.OUTLAWED_UNIT
case UnitStatus.Evacuated => UnitStatusProto.EVACUATED_UNIT
}
}
@@ -7,4 +7,5 @@ enum UnitStatus {
case Retreated
case NeverEntered
case Outlawed
case Evacuated
}
@@ -585,6 +585,8 @@ class ShardokInterfaceGrpcClient(
UnitStatus.NeverEntered
case net.eagle0.common.common_unit.UnitStatus.OUTLAWED_UNIT =>
UnitStatus.Outlawed
case net.eagle0.common.common_unit.UnitStatus.EVACUATED_UNIT =>
UnitStatus.Evacuated
case other =>
throw new IllegalArgumentException(s"Unrecognized UnitStatus: $other")
}
@@ -101,6 +101,22 @@ cc_test(
],
)
cc_test(
name = "evacuate_prisoners_command_factory_test",
srcs = ["EvacuatePrisonersCommandFactory_test.cpp"],
copts = TEST_COPTS,
linkstatic = True,
deps = [
"//src/main/cpp/net/eagle0/shardok/library/command_factories:evacuate_prisoners_command_factory",
"//src/test/cpp/net/eagle0/shardok/library:game_settings_test_utils",
"//src/test/cpp/net/eagle0/shardok/library:game_start_test_data",
"//src/test/cpp/net/eagle0/shardok/library:gtest_extensions",
"//src/test/cpp/net/eagle0/shardok/library:hex_map_test_data",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)
cc_test(
name = "fear_command_factory_test",
srcs = ["FearCommandFactory_test.cpp"],
@@ -0,0 +1,104 @@
//
// Created by Codex on 6/17/26.
//
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/EvacuatePrisonersCommandFactory.hpp"
#include "gtest/gtest.h"
#include "src/test/cpp/net/eagle0/shardok/library/GameSettings_test_utils.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GameStart_test_data.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GtestExtensions.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/HexMap_test_data.hpp"
using net::eagle0::shardok::storage::fb::GameStateBuilder;
using namespace shardok;
class EvacuatePrisonersCommandFactoryTest : public ::testing::Test {
void SetUp() override {
InitializeGameSettings();
GetGameSettingsSetter().SetInt(kSettingsKeys.fleeActionPointCost, 5);
fbb.ForceDefaults(true);
auto actorOff = AddGenericUnit(0, 0, position);
auto enemyCaptiveOff = AddGenericUnit(1, 1, Coords(4, 4));
auto alliedCaptiveOff = AddGenericUnit(2, 2, Coords(5, 5));
auto unitsVec = std::vector<Unit>{actorOff, enemyCaptiveOff, alliedCaptiveOff};
auto unitsOff = fbb.CreateVectorOfSortedStructs(&unitsVec);
GameStateBuilder gsb(fbb);
gsb.add_units(unitsOff);
fbb.Finish(gsb.Finish());
gameState = GameStateW(fbb);
actor = const_cast<Unit*>(gameState->mutable_units()->GetMutableObject(0));
enemyCaptive = const_cast<Unit*>(gameState->mutable_units()->GetMutableObject(1));
alliedCaptive = const_cast<Unit*>(gameState->mutable_units()->GetMutableObject(2));
actor->mutate_can_flee(true);
actor->mutate_remaining_action_points(remainingActionPoints);
actor->mutable_attached_hero().mutable_profession_info().mutate_profession(
net::eagle0::shardok::storage::fb::Profession_WARDEN);
enemyCaptive->mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT);
alliedCaptive->mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT);
factory = std::make_shared<EvacuatePrisonersCommandFactory>(GetGameSettingsGetter());
}
protected:
GameStateW gameState;
flatbuffers::FlatBufferBuilder fbb;
Unit* actor = nullptr;
Unit* enemyCaptive = nullptr;
Unit* alliedCaptive = nullptr;
CommandList commands{};
PossibleChargees possibleChargees{};
vector<PlayerId> allyPids{2};
const Coords position = Coords(3, 3);
const int remainingActionPoints = 9;
std::shared_ptr<EvacuatePrisonersCommandFactory> factory = nullptr;
[[nodiscard]] auto Params() const -> CommandFactory::CommandParams {
return CommandFactory::CommandParams{
actor,
static_cast<ActionPoints>(remainingActionPoints),
position,
nullptr,
false,
possibleChargees,
BASIC_MAP_FB,
gameState->units(),
allyPids,
true,
false,
false};
}
};
TEST_F(EvacuatePrisonersCommandFactoryTest, wardenWithCapturedHostileHeroCanEvacuatePrisoners) {
factory->AddAvailableCommands(commands, Params());
ASSERT_EQ(1, commands.size());
EXPECT_EQ(
net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND,
commands[0]->GetCommandType());
const auto results = commands[0]->Execute(gameState, nullptr);
const auto result = ResultWithType(results, net::eagle0::shardok::common::EVACUATED_PRISONERS);
EXPECT_EQ(2, result.resolved_units_size());
EXPECT_EQ(actor->unit_id(), ((Unit*)result.resolved_units(0).unit_bytes().data())->unit_id());
EXPECT_EQ(
enemyCaptive->unit_id(),
((Unit*)result.resolved_units(1).unit_bytes().data())->unit_id());
}
TEST_F(EvacuatePrisonersCommandFactoryTest, nonWardenCannotEvacuatePrisoners) {
actor->mutable_attached_hero().mutable_profession_info().mutate_profession(
net::eagle0::shardok::storage::fb::Profession_PALADIN);
factory->AddAvailableCommands(commands, Params());
EXPECT_TRUE(commands.empty());
}
@@ -122,6 +122,21 @@ cc_test(
],
)
cc_test(
name = "evacuate_prisoners_command_test",
srcs = ["EvacuatePrisonersCommand_test.cpp"],
copts = TEST_COPTS,
linkstatic = True,
deps = [
"//src/main/cpp/net/eagle0/shardok/library/commands:evacuate_prisoners_command",
"//src/test/cpp/net/eagle0/shardok/library:game_settings_test_utils",
"//src/test/cpp/net/eagle0/shardok/library:game_start_test_data",
"//src/test/cpp/net/eagle0/shardok/library:gtest_extensions",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)
cc_test(
name = "fear_command_test",
srcs = ["FearCommand_test.cpp"],
@@ -0,0 +1,58 @@
//
// Created by Codex on 6/17/26.
//
#include "src/main/cpp/net/eagle0/shardok/library/commands/EvacuatePrisonersCommand.hpp"
#include "gtest/gtest.h"
#include "src/test/cpp/net/eagle0/shardok/library/GameSettings_test_utils.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GameStart_test_data.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GtestExtensions.hpp"
using net::eagle0::shardok::storage::fb::GameStateBuilder;
using namespace shardok;
class EvacuatePrisonersCommandTest : public ::testing::Test {
void SetUp() override {
InitializeGameSettings();
flatbuffers::FlatBufferBuilder fbb;
fbb.ForceDefaults(true);
auto actorOff = AddGenericUnit(0, 0, Coords(3, 3));
auto captiveOff = AddGenericUnit(1, 1, Coords(4, 4));
auto unitsVec = std::vector<Unit>{actorOff, captiveOff};
auto unitsOff = fbb.CreateVectorOfSortedStructs(&unitsVec);
GameStateBuilder gsb(fbb);
gsb.add_units(unitsOff);
fbb.Finish(gsb.Finish());
gameState = GameStateW(fbb);
}
public:
const ActionCost cost = ActionCost::UsesAllActionCost(5);
GameStateW gameState;
};
TEST_F(EvacuatePrisonersCommandTest, execute_marksActorAndCaptivesEvacuated) {
const auto results = EvacuatePrisonersCommand(cost, 0, 0, {1}).Execute(gameState, nullptr);
const ActionResult result =
ResultWithType(results, net::eagle0::shardok::common::EVACUATED_PRISONERS);
EXPECT_EQ(2, result.resolved_units_size());
EXPECT_EQ(0, result.actor().value());
EXPECT_EQ(0, ((Unit*)result.resolved_units(0).unit_bytes().data())->unit_id());
EXPECT_EQ(
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT,
result.resolved_units(0).status());
EXPECT_EQ(1, ((Unit*)result.resolved_units(1).unit_bytes().data())->unit_id());
EXPECT_EQ(
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_EVACUATED_UNIT,
result.resolved_units(1).status());
}