Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 81838065ae Add delete button to games list in admin console
- Add delete button next to each game in the main games list
- Include confirmation modal with option to delete save files
- Reuses existing /games/{id}/delete endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 19:39:08 -08:00
34 changed files with 447 additions and 1383 deletions
+21 -12
View File
@@ -22,11 +22,6 @@ on:
default: 'false'
type: boolean
# Only allow one deployment at a time to prevent race conditions
concurrency:
group: docker-build-deploy
cancel-in-progress: false # Don't cancel running deployments, queue new ones
permissions:
contents: read
@@ -319,18 +314,32 @@ jobs:
echo "All images pulled successfully"
# Recreate shardok (stateless, safe to restart anytime)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok
# Recreate non-Eagle services
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin
# Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
chmod +x /opt/eagle0/scripts/*.sh
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"
# Deploy Eagle with blue-green
if [ -x "/opt/eagle0/scripts/deploy-blue-green.sh" ]; then
echo "Using blue-green deployment for Eagle..."
chmod +x /opt/eagle0/scripts/*.sh
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}" || {
echo "Blue-green failed, falling back to direct restart"
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
}
else
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
fi
# Note: jfr-sidecar is started by deploy-blue-green.sh (either jfr-sidecar or jfr-sidecar-green
# depending on which eagle instance is active)
# Ensure auth is running
docker compose -f docker-compose.prod.yml up -d auth
# Restart nginx
docker compose -f docker-compose.prod.yml up -d --force-recreate nginx
# Verify
sleep 10
docker compose -f docker-compose.prod.yml ps
-3
View File
@@ -3,9 +3,6 @@ events {
}
http {
# Allow large request bodies for game uploads (default is 1MB)
client_max_body_size 50M;
# Logging
log_format grpc_json escape=json '{'
'"time":"$time_iso8601",'
+23 -22
View File
@@ -224,12 +224,15 @@ main() {
log_warn "s3cmd not found, skipping S3 backup"
fi
# Stop the active instance (flushes state to storage)
# We must do this BEFORE switching so staging loads fresh data
# Stop the active instance (this flushes state to storage)
log_info "Stopping eagle-${active} (flushing state to storage)..."
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
# Prepare nginx config and .env BEFORE restarting anything
# Note: No need to explicitly reload games - Eagle uses lazy loading.
# Games are loaded on-demand when users reconnect after nginx switches traffic.
# This ensures games are always loaded from the freshest storage state.
# Switch nginx upstream
log_info "Switching nginx upstream to eagle-${staging}..."
if [ "$staging" = "green" ]; then
sed -i.bak 's/eagle-blue:40032/eagle-green:40032/g' "${NGINX_CONF}"
@@ -237,6 +240,21 @@ main() {
sed -i.bak 's/eagle-green:40032/eagle-blue:40032/g' "${NGINX_CONF}"
fi
# Restart nginx to ensure fresh config is loaded
# Note: We use restart instead of reload to ensure all workers pick up the new config
log_info "Restarting nginx..."
docker compose -f "${COMPOSE_FILE}" restart nginx
# Clean up old instance and its jfr-sidecar
log_info "Removing old eagle-${active} container..."
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}"
if [ "$active" = "green" ]; then
docker compose -f "${COMPOSE_FILE}" rm -f "jfr-sidecar-green" 2>/dev/null || true
else
docker compose -f "${COMPOSE_FILE}" rm -f "jfr-sidecar" 2>/dev/null || true
fi
# Update .env with new EAGLE_ADDR and JFR_SIDECAR_ADDR for admin service
local env_file="${APP_DIR}/.env"
if [ "$staging" = "green" ]; then
log_info "Updating .env for green instance..."
@@ -250,26 +268,9 @@ main() {
echo "JFR_SIDECAR_ADDR=jfr-sidecar:8081" >> "${env_file}"
fi
# Restart nginx (use reload for faster switch, restart if reload fails)
log_info "Reloading nginx..."
if ! docker exec nginx nginx -s reload 2>/dev/null; then
log_warn "Reload failed, restarting nginx..."
docker compose -f "${COMPOSE_FILE}" restart nginx
fi
# Restart admin to pick up new .env
# Restart admin to pick up new EAGLE_ADDR and JFR_SIDECAR_ADDR
log_info "Restarting admin service..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate admin
# Clean up old instance and its jfr-sidecar (in background, not critical path)
log_info "Cleaning up old eagle-${active} container..."
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}" &
if [ "$active" = "green" ]; then
docker compose -f "${COMPOSE_FILE}" rm -f "jfr-sidecar-green" 2>/dev/null &
else
docker compose -f "${COMPOSE_FILE}" rm -f "jfr-sidecar" 2>/dev/null &
fi
wait
docker compose -f "${COMPOSE_FILE}" up -d admin
log_info "Deployment complete!"
log_info "Active instance: eagle-${staging}"
@@ -109,10 +109,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
[Header("Status Display")]
public TextMeshProUGUI connectionStatusText;
[Header("Connection Panel Environment")]
[Tooltip("Environment dropdown in connection panel (fallback if lobby unreachable)")]
public TMP_Dropdown connectionEnvironmentDropdown;
public GameObject connectionPanel;
public GameObject gameSelectionPanel;
public GameObject customBattlePanel;
@@ -221,9 +217,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
// Initialize OAuth UI
SetupOAuthUI();
// Initialize connection panel environment dropdown
SetupConnectionEnvironmentDropdown();
// Initialize Lobby UI (logout button, etc.)
SetupLobbyUI();
@@ -234,22 +227,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
TryRestoreSession();
}
private void SetupConnectionEnvironmentDropdown() {
if (connectionEnvironmentDropdown == null) return;
connectionEnvironmentDropdown.ClearOptions();
connectionEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
connectionEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
connectionEnvironmentDropdown.onValueChanged.AddListener(OnConnectionEnvironmentChanged);
}
private void OnConnectionEnvironmentChanged(int newEnvironmentIndex) {
// Just save the preference - it will be used on next connection attempt
PlayerPrefs.SetInt(EnvironmentKey, newEnvironmentIndex);
Debug.Log(
$"[ConnectionHandler] Environment changed to {EnvironmentDisplayNames[newEnvironmentIndex]}");
}
private void SetupOAuthUI() {
// Set up OAuth button click handlers for new sign-ins
if (discordLoginButton != null) {
@@ -287,15 +264,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
// Show OAuth panel
if (oauthPanel != null) oauthPanel.SetActive(true);
// Sync connection environment dropdown with saved preference
if (connectionEnvironmentDropdown != null) {
connectionEnvironmentDropdown.onValueChanged.RemoveListener(
OnConnectionEnvironmentChanged);
connectionEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
connectionEnvironmentDropdown.onValueChanged.AddListener(
OnConnectionEnvironmentChanged);
}
// Refresh stored account buttons
RefreshStoredAccountButtons();
}
File diff suppressed because it is too large Load Diff
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d58277ecfc2d84d30a7ca71b8ccb994e
@@ -129,9 +129,70 @@ function cancelUpload() {
</div>
<div class="actions">
<a href="/games/{{.GameID}}" role="button" class="outline">View History</a>
<button class="btn-small btn-danger" onclick="showDeleteModal('{{.GameID}}')">Delete</button>
</div>
</article>
{{end}}
<!-- Delete Game Modal -->
<dialog id="delete-game-modal">
<form method="dialog">
<h3>Delete Game</h3>
<p>Are you sure you want to delete game <strong id="delete-game-id-display"></strong>?</p>
<div class="form-group">
<label>
<input type="checkbox" id="delete-save-files" name="delete_save_files">
Also delete save files from disk
</label>
</div>
<p class="warning" style="color: #e74c3c; font-size: 0.9em;">
<strong>Warning:</strong> This action cannot be undone.
</p>
<input type="hidden" id="delete-game-id" name="game_id">
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="document.getElementById('delete-game-modal').close()">Cancel</button>
<button type="button" class="btn-danger" onclick="submitDeleteGame()">Delete</button>
</div>
</form>
</dialog>
<div id="delete-result"></div>
<script>
function showDeleteModal(gameId) {
document.getElementById('delete-game-id').value = gameId;
document.getElementById('delete-game-id-display').textContent = gameId;
document.getElementById('delete-save-files').checked = false;
document.getElementById('delete-game-modal').showModal();
}
function submitDeleteGame() {
const gameId = document.getElementById('delete-game-id').value;
const deleteSaveFiles = document.getElementById('delete-save-files').checked;
fetch('/games/' + gameId + '/delete', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'delete_save_files=' + (deleteSaveFiles ? 'true' : 'false')
}).then(function(response) {
if (response.ok) {
// Reload page to show updated list
window.location.reload();
} else {
return response.text().then(function(text) {
document.getElementById('delete-result').innerHTML =
'<div class="alert alert-error">' + text + '</div>';
});
}
}).catch(function(error) {
document.getElementById('delete-result').innerHTML =
'<div class="alert alert-error">Delete failed: ' + error + '</div>';
});
document.getElementById('delete-game-modal').close();
}
</script>
{{else}}
<div class="empty-state">
<p>No games currently running.</p>
@@ -58,10 +58,8 @@ message PostCommandResponse {
UNKNOWN = 0;
SUCCESS = 1;
BAD_TOKEN = 2;
ERROR = 3;
}
Status status = 1;
string error_message = 2; // Only set if status is ERROR
}
message EnterLobbyRequest {
@@ -6,10 +6,9 @@ import net.eagle0.eagle.common.action_result_type.ActionResultType.*
import net.eagle0.eagle.common.round_phase.RoundPhase
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.view_filters.GameStateViewFilter
import net.eagle0.eagle.library.util.GameStateViewDiffer
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.views.action_result_view.ActionResultView
import net.eagle0.eagle.views.game_state_view.GameStateViewDiff
import net.eagle0.eagle.FactionId
@@ -107,7 +106,6 @@ object ActionResultFilter {
factionId: Option[FactionId]
): Option[ActionResultView] = {
val actionResult = result.actionResult
val factions = startingState.factions.values.map(FactionConverter.fromProto).toVector
val gsDiff = filteredGameStateDiff(
before = startingState,
@@ -123,10 +121,10 @@ object ActionResultFilter {
factionId.exists { askingFid =>
!result.actionResult.player.exists(targetFid =>
targetFid == askingFid ||
FactionUtils.hasAlliance(
LegacyFactionUtils.hasAlliance(
askingFid,
targetFid,
factions
startingState
)
)
} &&
@@ -104,9 +104,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/util:game_state_view_differ",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:game_state_view_filter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
],
)
@@ -7,9 +7,8 @@ import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus.{
DIPLOMACY_OFFER_STATUS_UNRESOLVED
}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.model.proto_converters.diplomacy_offer.status.StatusConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.DiplomacyOfferInfo
import net.eagle0.eagle.model.state.diplomacy_offer.{DiplomacyOffer, Invitation}
@@ -106,8 +105,8 @@ object AvailableResolveInvitationCommandFactory {
InvitationDetails(_ /* unknownFieldSet */ ),
_ /* unknownFieldSet */
) =>
val provinces = gameState.provinces.values.map(ProvinceConverter.fromProto)
FactionUtils.provinceCount(originatingFactionId, provinces) > 0
LegacyFactionUtils
.provinceCount(originatingFactionId, gameState) > 0
case _ => false
}
@@ -628,8 +628,8 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer/status",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
@@ -937,7 +937,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
@@ -1,8 +1,7 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.model.state.diplomacy_offer.status.{Accepted, Imprisoned, Rejected, Status}
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.FactionId
@@ -22,10 +21,7 @@ object EligibleDiplomacyStatuses {
def maybeImprisonStatus(
actingFactionId: FactionId,
gameState: GameState
): Option[Status] = {
val provinces = gameState.provinces.values.map(ProvinceConverter.fromProto)
Option.when(
FactionUtils.hasProvinces(actingFactionId, provinces)
)(Imprisoned)
}
): Option[Status] = Option.when(
LegacyFactionUtils.hasProvinces(actingFactionId, gameState)
)(Imprisoned)
}
@@ -1262,6 +1262,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:traveler_to_resident_chance",
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator/hero_generation_response",
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator/name_info",
@@ -81,6 +81,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
@@ -214,8 +214,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_power",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
@@ -5,9 +5,8 @@ import net.eagle0.eagle.api.command.util.army_stats.ArmyStats
import net.eagle0.eagle.internal.army.{Army, MovingArmy}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.library.util.BattalionPower
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.state.{Army as ArmyC, MovingArmy as MovingArmyC}
import net.eagle0.eagle.model.state.game_state.GameState as GameStateC
@@ -33,9 +32,8 @@ object IncomingArmyUtils {
def incomingArmiesAreMutuallyAllied(
province: Province,
gameState: GameState
): Boolean = {
val factions = gameState.factions.values.map(FactionConverter.fromProto).toVector
FactionUtils.factionsAreMutuallyAllied(
): Boolean =
LegacyFactionUtils.factionsAreMutuallyAllied(
fids = province.incomingArmies
.filter(_.arrivalRound == gameState.currentRoundId)
.map(_.getArmy.factionId)
@@ -43,18 +41,15 @@ object IncomingArmyUtils {
.flatMap(_.armies)
.map(_.getArmy.factionId)
.toVector,
factions = factions
gs = gameState
)
}
def isHostileProvince(
province: Province,
factionId: FactionId,
gameState: GameState
): Boolean = {
val factions = gameState.factions.values.map(FactionConverter.fromProto).toVector
province.rulingFactionId.exists(targetFid => FactionUtils.factionsAreHostile(targetFid, factionId, factions))
}
): Boolean =
province.rulingFactionId.exists(targetFid => LegacyFactionUtils.factionsAreHostile(targetFid, factionId, gameState))
def isUnderAttack(
provinceId: ProvinceId,
@@ -96,13 +91,12 @@ object IncomingArmyUtils {
): ArmyStats = {
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
val troopCount = battalions.map(_.size).sum
val factions = gs.factions.values.map(FactionConverter.fromProto).toVector
ArmyStats(
factionId = army.factionId,
heroCount = army.units.size,
troopCount = troopCount,
hostility = FactionUtils.hostilityStatus(toFid, army.factionId, factions),
hostility = LegacyFactionUtils.hostilityStatus(toFid, army.factionId, gs),
originProvinceId = originProvinceId
)
}
@@ -20,3 +20,20 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
scala_library(
name = "legacy_faction_utils",
srcs = ["LegacyFactionUtils.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/common:hostility_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
"//src/main/scala/net/eagle0/eagle/library/settings:prestige_per_supported_province",
],
)
@@ -0,0 +1,137 @@
package net.eagle0.eagle.library.util.faction_utils
import net.eagle0.common.hostility.Hostility
import net.eagle0.eagle.{FactionId, HeroId}
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, PrestigePerSupportedProvince}
/**
* Proto-based FactionUtils for code that operates on proto GameState.
*
* This mirrors the protoless FactionUtils but operates on proto types directly. Use FactionUtils for protoless code;
* use this for proto-based boundary code.
*/
object LegacyFactionUtils {
private val sortIgnoredChars: Set[Char] = Set('"', '\u201c', '\u201d', '\'', '\u2018', '\u2019')
def sortKey(faction: Faction): String =
faction.name.filterNot(sortIgnoredChars)
/** Returns proto FactionRelationship - kept as-is until callers are migrated */
def factionRelationship(
by: FactionId,
of: FactionId,
gameState: GameState
): FactionRelationship =
gameState.factions
.get(by)
.flatMap(_.factionRelationships.find(_.targetFactionId == of))
.getOrElse(
FactionRelationship(
targetFactionId = of,
relationshipLevel = FactionRelationship.RelationshipLevel.HOSTILE,
resetDate = None,
trustValue = 0
)
)
def prestige(factionId: FactionId, gameState: GameState): Double = {
val fromSupportedProvinces = supportedProvinceCount(
factionId,
gameState
).toDouble * PrestigePerSupportedProvince.doubleValue
fromSupportedProvinces + gameState
.factions(factionId)
.prestigeModifiers
.map(_.value)
.sum
}
private def supportedProvinceCount(
factionId: FactionId,
gameState: GameState
): Int =
gameState.provinces.values
.filter(_.rulingFactionId.contains(factionId))
.count(_.support >= MinSupportForTaxes.doubleValue)
def factionHeadId(
factionId: FactionId,
gameState: GameState
): Option[HeroId] =
gameState.factions.get(factionId).map(_.factionHeadId)
// These methods avoid full faction conversion since they only need specific fields
def isFactionHead(heroId: HeroId, gameState: GameState): Boolean =
gameState.factions.values.map(_.factionHeadId).exists(_ == heroId)
def leadersBesidesHead(
factionId: FactionId,
gameState: GameState
): Vector[HeroId] = {
val faction = gameState.factions(factionId)
faction.leaders.filterNot(_ == faction.factionHeadId).toVector
}
def isFactionLeader(heroId: HeroId, gameState: GameState): Boolean =
gameState.factions.values.flatMap(_.leaders).exists(_ == heroId)
/** Returns proto Province - kept as-is until callers are migrated */
def provinces(factionId: FactionId, gameState: GameState): Vector[Province] =
gameState.provinces.values
.filter(_.rulingFactionId.contains(factionId))
.toVector
// These methods avoid full province conversion since they only need rulingFactionId
def hasProvinces(factionId: FactionId, gameState: GameState): Boolean =
gameState.provinces.values.exists(_.rulingFactionId.contains(factionId))
def provinceCount(factionId: FactionId, gameState: GameState): Int =
gameState.provinces.values.count(_.rulingFactionId.contains(factionId))
// These methods use factionRelationship directly to avoid full faction conversion
// which would fail on incomplete test data (missing DiplomacyOffer fields)
def factionsAreMutuallyAllied(
fids: Vector[FactionId],
gs: GameState
): Boolean =
fids.forall(fid1 => fids.forall(fid2 => !factionsAreHostile(fid1, fid2, gs)))
def hasTruce(
fid1: FactionId,
fid2: FactionId,
gs: GameState
): Boolean =
factionRelationship(fid1, fid2, gs).relationshipLevel ==
FactionRelationship.RelationshipLevel.TRUCE
def hasTruceOrAlliance(
fid1: FactionId,
fid2: FactionId,
gs: GameState
): Boolean = hasTruce(fid1, fid2, gs) || hasAlliance(fid1, fid2, gs)
def hasAlliance(fid1: FactionId, fid2: FactionId, gs: GameState): Boolean =
factionRelationship(fid1, fid2, gs).relationshipLevel ==
FactionRelationship.RelationshipLevel.ALLY
def hostilityStatus(
fid1: FactionId,
fid2: FactionId,
gs: GameState
): Hostility =
if fid1 == fid2 then Hostility.SELF_HOSTILITY
else if factionsAreHostile(fid1, fid2, gs) then Hostility.ENEMY_HOSTILITY
else Hostility.ALLIED_HOSTILITY
def factionsAreHostile(
fid1: FactionId,
fid2: FactionId,
gs: GameState
): Boolean =
fid1 != fid2 && !hasTruceOrAlliance(fid1, fid2, gs)
}
@@ -38,6 +38,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
@@ -78,7 +79,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/views:shardok_battle_view_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
],
@@ -104,10 +105,9 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/views:faction_view_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
@@ -215,13 +215,13 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
"//src/main/scala/net/eagle0/eagle/library/util:stat_with_condition_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:profession",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:event",
@@ -249,7 +249,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
],
)
@@ -4,7 +4,7 @@ import net.eagle0.eagle.{BattalionId, FactionId}
import net.eagle0.eagle.internal.army.Army
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.model.state.{Army as ScalaArmy, MovingArmy}
import net.eagle0.eagle.model.state.game_state.GameState as ScalaGameState
import net.eagle0.eagle.model.state.province.ProvinceT
@@ -86,12 +86,12 @@ object BattalionNameFilter {
factionId.map { fid =>
val battalionIdsInVisibleProvinces = gs.factions.keys
.filter(fid => Visibility.hasFullVisibility(factionId, fid, gs))
.flatMap(fid => gs.provinces.values.filter(_.rulingFactionId.contains(fid)))
.flatMap(LegacyFactionUtils.provinces(_, gs))
.flatMap(_.battalionIds)
.toVector
val battalionIdsIncomingToMyProvinces = gs.provinces.values
.filter(_.rulingFactionId.contains(fid))
val battalionIdsIncomingToMyProvinces = LegacyFactionUtils
.provinces(fid, gs)
.flatMap(
_.incomingArmies
.filter(_.arrivalRound == gs.currentRoundId)
@@ -3,8 +3,7 @@ package net.eagle0.eagle.library.util.view_filters
import net.eagle0.common.hostility.Hostility
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle as ScalaShardokBattle
import net.eagle0.eagle.views.shardok_battle_view.{ShardokBattlePlayerInfo, ShardokBattleView}
@@ -91,17 +90,15 @@ object BattleFilter {
battle: ShardokBattle,
fid: Option[FactionId],
gs: GameState
): Vector[ShardokBattlePlayerInfo] = {
val factions = gs.factions.values.map(FactionConverter.fromProto).toVector
): Vector[ShardokBattlePlayerInfo] =
battle.players.zipWithIndex.map {
case (sp, i) =>
ShardokBattlePlayerInfo(
playerId = i,
eagleFactionId = sp.eagleFid,
hostility = fid
.map(f => FactionUtils.hostilityStatus(f, sp.eagleFid, factions))
.map(f => LegacyFactionUtils.hostilityStatus(f, sp.eagleFid, gs))
.getOrElse(Hostility.UNKNOWN_HOSTILITY)
)
}.toVector
}
}
@@ -4,11 +4,9 @@ import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship.RelationshipLevel as RelationshipLevelProto
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship as FactionRelationshipProto
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.diplomacy_offer.DiplomacyOfferConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.state.faction.{FactionRelationship, FactionT}
import net.eagle0.eagle.model.state.game_state.GameState as ScalaGameState
import net.eagle0.eagle.views.faction_relationship_view.FactionRelationshipView
@@ -106,16 +104,14 @@ object FactionViewFilter {
faction: Faction,
gs: GameState,
fid: Option[FactionId]
): FactionView = {
val scalaFaction = FactionConverter.fromProto(faction)
val scalaProvinces = gs.provinces.values.map(ProvinceConverter.fromProto).toVector
): FactionView =
if fid.isEmpty || Visibility.hasFullVisibility(fid, faction.id, gs) then
FactionView(
id = faction.id,
factionHeadId = faction.factionHeadId,
leaders = faction.leaders,
name = faction.name,
prestige = FactionUtils.prestige(scalaFaction, scalaProvinces).toInt,
prestige = LegacyFactionUtils.prestige(faction.id, gs).toInt,
factionRelationships = faction.factionRelationships.map(filteredFactionRelationshipView),
// You can see *your incoming diplomacy offers on them*, but they cannot
incomingDiplomacyOffers = faction.incomingDiplomacyOffers.filter(dof => fid.contains(dof.originatingFactionId)),
@@ -130,6 +126,4 @@ object FactionViewFilter {
factionRelationships = faction.factionRelationships.map(filteredFactionRelationshipView),
incomingDiplomacyOffers = faction.incomingDiplomacyOffers.filter(dof => fid.contains(dof.originatingFactionId))
)
end if
}
}
@@ -8,13 +8,12 @@ import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceEventUtils}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.StatWithConditionUtils.supportScala
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.hero.ProfessionConverter
import net.eagle0.eagle.model.proto_converters.province.{ProvinceEventConverter, ProvinceOrderTypeConverter}
@@ -396,10 +395,9 @@ object ProvinceViewFilter {
factionId: FactionId
): ProvinceView = {
import net.eagle0.eagle.library.util.DateProtoUtils.ordering
val factions = gs.factions.values.map(FactionConverter.fromProto).toVector
val relevantFactions = gs.factions.values.filter(possibleAlly =>
FactionUtils.hasAlliance(possibleAlly.id, factionId, factions)
LegacyFactionUtils.hasAlliance(possibleAlly.id, factionId, gs)
) ++ gs.factions.get(factionId)
// Take the most recent reconned view from this faction or any allies
@@ -624,13 +622,12 @@ object ProvinceViewFilter {
gs: GameState
): UnaffiliatedHeroBasics = gs.heroes(uh.heroId) match {
case hero: Hero =>
val factions = gs.factions.values.map(FactionConverter.fromProto).toVector
UnaffiliatedHeroBasics(
heroId = hero.id,
nameTextId = hero.nameTextId,
profession = ProfessionConverter.fromProto(hero.profession),
status = UnaffiliatedHeroConverter.unaffiliatedHeroTypeFromProto(uh.`type`),
isFactionLeader = FactionUtils.isFactionLeader(hero.id, factions),
isFactionLeader = LegacyFactionUtils.isFactionLeader(hero.id, gs),
recruitmentInfo =
uh.recruitmentInfo.fold(RecruitmentInfo.Unknown)(UnaffiliatedHeroConverter.recruitmentInfoFromProto)
)
@@ -1,8 +1,7 @@
package net.eagle0.eagle.library.util.view_filters
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.FactionId
@@ -36,10 +35,13 @@ object Visibility {
fid1: FactionId,
fid2: FactionId,
gs: GameState
): Boolean = {
val factions = gs.factions.values.map(FactionConverter.fromProto).toVector
fid1 == fid2 || FactionUtils.hasAlliance(fid1, fid2, factions)
}
): Boolean =
fid1 == fid2 || LegacyFactionUtils
.hasAlliance(
fid1,
fid2,
gs
)
def hasFullVisibility(
ofid1: Option[FactionId],
@@ -38,7 +38,7 @@ import net.eagle0.eagle.common.province_order_type.ProvinceOrderType as Province
import net.eagle0.eagle.model.state.command.common.*
import net.eagle0.eagle.model.state.command.selected.*
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.*
import net.eagle0.eagle.model.state.diplomacy_offer.status.{Accepted, Imprisoned, Rejected, Status}
import net.eagle0.eagle.model.state.diplomacy_offer.status.{Accepted, Rejected, Status}
/**
* Converts proto SelectedCommand to Scala SelectedCommand.
@@ -455,11 +455,11 @@ object SelectedCommandConverter {
private def statusFromProto(status: DiplomacyOfferStatusProto): Status =
status match {
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_ACCEPTED => Accepted
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_REJECTED => Rejected
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_IMPRISONED => Imprisoned
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_ACCEPTED => Accepted
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_REJECTED => Rejected
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_UNKNOWN |
DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_UNRESOLVED |
DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_IMPRISONED |
DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_INVALIDATED | DiplomacyOfferStatusProto.Unrecognized(_) =>
throw new IllegalArgumentException(s"Invalid resolution status: $status")
}
@@ -7,7 +7,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
exports = [
@@ -6,7 +6,6 @@ scala_library(
visibility = [
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
@@ -84,7 +84,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update_receiver",
"@maven//:com_thesamet_scalapb_scalapb_json4s_3",
"@maven//:io_sentry_sentry",
"@maven//:org_json4s_json4s_ast_3",
"@maven//:org_json4s_json4s_core_3",
"@maven//:org_json4s_json4s_native_3",
@@ -12,7 +12,6 @@ import scala.util.hashing.MurmurHash3
import com.google.protobuf.ByteString
import io.grpc.stub.StreamObserver
import io.grpc.StatusRuntimeException
import io.sentry.Sentry
import net.eagle0.common.{FunctionalRandom, RandomState, SimpleTimedLogger}
import net.eagle0.eagle.api.eagle.*
import net.eagle0.eagle.api.eagle.EagleGrpc.Eagle
@@ -319,23 +318,6 @@ class EagleServiceImpl(
)
)
)
}.recover {
case e: Exception =>
SimpleTimedLogger.printLogger.logLine(
s"Error processing command for ${responseObserver.userName}: ${e.getMessage}"
)
e.printStackTrace()
Sentry.captureException(e)
responseObserver.onNext(
UpdateStreamResponse(
responseDetails = UpdateStreamResponse.ResponseDetails.PostCommandResponse(
PostCommandResponse(
status = PostCommandResponse.Status.ERROR,
errorMessage = e.getMessage
)
)
)
)
}
()
@@ -153,10 +153,9 @@ scala_library(
"//src/main/scala/net/eagle0/common:grpc_retrier",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_to_constitution_xp_multiplier",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
@@ -23,9 +23,8 @@ import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero as EagleHero
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
import net.eagle0.eagle.library.settings.VigorToConstitutionXpMultiplier
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.state.unit_status.UnitStatus
@@ -370,8 +369,7 @@ class ShardokInterfaceGrpcClient(
private def toCommon(
armies: Vector[MovingArmy],
gameState: GameState
): Vector[CommonUnit] = {
val factions = gameState.factions.values.map(FactionConverter.fromProto).toVector
): Vector[CommonUnit] =
armies.flatMap { ma =>
ma.getArmy.units
.map(EagleUnit.ExpandUnit(_, gameState))
@@ -381,12 +379,11 @@ class ShardokInterfaceGrpcClient(
canFlee = ma.getArmy.fleeProvinceId.isDefined,
canStartFire = HeroUtils.startFireCapable(HeroConverter.fromProto(eu.hero)),
canArchery = isArcheryCapable(eu, gameState),
isVip = FactionUtils.isFactionLeader(eu.hero.id, factions),
isVip = LegacyFactionUtils.isFactionLeader(eu.hero.id, gameState),
startingPositionIndex = ma.startingPositionIndex
)
)
}
}
private def fromCommon(commonUnit: CommonUnit): EagleUnit =
EagleUnit(
@@ -11,3 +11,18 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
],
)
scala_test(
name = "legacy_faction_utils_test",
srcs = ["LegacyFactionUtilsTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
"//src/main/scala/net/eagle0/eagle/library/settings:prestige_per_supported_province",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
],
)
@@ -0,0 +1,113 @@
package net.eagle0.eagle.library.util.faction_utils
import net.eagle0.eagle.internal.faction.Faction
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, PrestigePerSupportedProvince}
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyProvinces}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
class LegacyFactionUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach {
behavior of "FactionUtilsTest"
override def beforeEach(): Unit = {
PrestigePerSupportedProvince.setDoubleValue(8)
MinSupportForTaxes.setDoubleValue(40)
}
private val gameState = GameState(
factions = mapifyFactions(
Faction(id = 5, factionHeadId = 10, leaders = Vector(10, 20, 15, 25)),
Faction(id = 7, factionHeadId = 14),
Faction(id = 12, factionHeadId = 24)
),
provinces = mapifyProvinces(
Province(
id = 1,
rulingFactionId = Some(5),
support = 60,
rulingFactionHeroIds = Vector(15, 16)
),
Province(
id = 2,
rulingFactionId = Some(5),
support = 39,
rulingFactionHeroIds = Vector(10, 11)
),
Province(id = 3, rulingFactionId = Some(7), support = 5),
Province(id = 4),
Province(id = 5, rulingFactionId = Some(12), support = 38),
Province(id = 6, rulingFactionId = Some(7), support = 50),
Province(
id = 7,
rulingFactionId = Some(5),
support = 41,
rulingFactionHeroIds = Vector(20)
),
Province(
id = 8,
rulingFactionId = Some(5),
support = 100,
rulingFactionHeroIds = Vector(25)
)
)
)
it should "calculate prestige based on number of supporting provinces" in {
LegacyFactionUtils.prestige(5, gameState) shouldBe 24
LegacyFactionUtils.prestige(7, gameState) shouldBe 8
LegacyFactionUtils.prestige(12, gameState) shouldBe 0
}
"isFactionLeader" should "return true if the hero is a faction leader" in {
val gs = GameState(
factions = mapifyFactions(Faction(id = 3, leaders = Vector(9, 10)))
)
LegacyFactionUtils.isFactionLeader(10, gs) shouldBe true
}
it should "return false if the hero is not a faction leader" in {
val gs = GameState(
factions = Map(3 -> Faction(id = 3, leaders = Vector(9, 10)))
)
LegacyFactionUtils.isFactionLeader(11, gs) shouldBe false
}
"isFactionHead" should "return true if the hero is a faction head" in {
val gs = GameState(
factions = mapifyFactions(
Faction(id = 3, factionHeadId = 10, leaders = Vector(9, 10))
)
)
LegacyFactionUtils.isFactionHead(10, gs) shouldBe true
}
it should "return false if the hero is not a faction head" in {
val gs = GameState(
factions = mapifyFactions(
Faction(id = 3, factionHeadId = 9, leaders = Vector(9, 10))
)
)
LegacyFactionUtils.isFactionHead(10, gs) shouldBe false
}
"sortKey" should "return the name for a normal string" in {
val faction = Faction(name = "Joe")
LegacyFactionUtils.sortKey(faction) shouldBe "Joe"
}
it should "exclude quotes" in {
val faction = Faction(name = "\"Mad\" Mal")
LegacyFactionUtils.sortKey(faction) shouldBe "Mad Mal"
}
}