From ff8144d773624afa527c5b522b14c3ae92aefec9 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 15 Jun 2025 21:36:15 -0500 Subject: [PATCH 1/2] Allow AI players to try to make deals with the human player This adds a nice bit of interactivity to the exploration phase - now the AI can act by itself. --- C7/Game.cs | 17 ++++++++++- C7/UIElements/Diplomacy/DealScreen.cs | 4 ++- C7/UIElements/Diplomacy/Diplomacy.cs | 6 +++- C7Engine/AI/PlayerAI.cs | 41 +++++++++++++++++++-------- C7Engine/EntryPoints/MessageToUI.cs | 14 +++++++++ 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/C7/Game.cs b/C7/Game.cs index 87384d17..f46c77d6 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -32,6 +32,10 @@ enum GameState { GameState CurrentState = GameState.PreGame; + // True if the deal screen is open because an AI player is offering something + // to the human player. + private bool waitingForDealScreen = false; + // CurrentlySelectedUnit is a reference directly into the game state so be careful of race conditions. TODO: Consider storing a GUID instead. public MapUnit CurrentlySelectedUnit = MapUnit.NONE; //The selected unit. May be changed by clicking on a unit or the next unit being auto-selected after orders are given for the current one. private bool HasCurrentlySelectedUnit() => CurrentlySelectedUnit != MapUnit.NONE; @@ -283,6 +287,13 @@ public void processEngineMessagesLocked(GameData gameData) { } EmitSignal(SignalName.ShowSpecificAdvisor, "F1"); break; + case MsgShowTradeOffer mSTO: + diplomacy.ShowDealScreenForPlayer( + mSTO.humanPlayer.id, mSTO.aiPlayer.id, + humanGives: mSTO.aiWant, + humanWants: mSTO.aiGive); + waitingForDealScreen = true; + break; case MsgDisplayHurryProductionPopup mDHPP: if (mDHPP.details.errorMessage != null) { popupOverlay.ShowPopup( @@ -328,6 +339,11 @@ public void updateAnimations(GameData gameData) { } public override void _Process(double delta) { + if (CurrentState == GameState.ComputerTurn && waitingForDealScreen && !diplomacy.Visible) { + waitingForDealScreen = false; + EngineStorage.FinishUiEvent(); + } + ProcessActions(); processEngineMessages(); @@ -881,7 +897,6 @@ private void ProcessAction(string currentAction) { return; } - if (currentAction == C7Action.UnitHold) { new ActionToEngineMsg(() => CurrentlySelectedUnit?.skipTurn()).send(); } diff --git a/C7/UIElements/Diplomacy/DealScreen.cs b/C7/UIElements/Diplomacy/DealScreen.cs index 15faf489..4e7d36be 100644 --- a/C7/UIElements/Diplomacy/DealScreen.cs +++ b/C7/UIElements/Diplomacy/DealScreen.cs @@ -44,9 +44,11 @@ public partial class DealScreen : TextureRect { TradeOffer opponentOffer = new(); TradeOffer humanOffer = new(); - public DealScreen(ID humanPlayer, ID opponentPlayer) { + public DealScreen(ID humanPlayer, ID opponentPlayer, TradeOffer humanGives, TradeOffer humanWants) { this.humanPlayerId = humanPlayer; this.opponentPlayerId = opponentPlayer; + this.humanOffer = humanGives; + this.opponentOffer = humanWants; } public override void _Ready() { diff --git a/C7/UIElements/Diplomacy/Diplomacy.cs b/C7/UIElements/Diplomacy/Diplomacy.cs index 90c86b64..e57a4f92 100644 --- a/C7/UIElements/Diplomacy/Diplomacy.cs +++ b/C7/UIElements/Diplomacy/Diplomacy.cs @@ -33,9 +33,13 @@ private void RemoveOtherScreens() { } public void ShowDealScreenForPlayer(ID humanPlayer, ID opponentPlayer) { + ShowDealScreenForPlayer(humanPlayer, opponentPlayer, new TradeOffer(), new TradeOffer()); + } + + public void ShowDealScreenForPlayer(ID humanPlayer, ID opponentPlayer, TradeOffer humanGives, TradeOffer humanWants) { RemoveOtherScreens(); - dealScreen = new DealScreen(humanPlayer, opponentPlayer); + dealScreen = new DealScreen(humanPlayer, opponentPlayer, humanGives, humanWants); AddChild(dealScreen); this.Show(); } diff --git a/C7Engine/AI/PlayerAI.cs b/C7Engine/AI/PlayerAI.cs index 143cb5fe..9289aa7f 100644 --- a/C7Engine/AI/PlayerAI.cs +++ b/C7Engine/AI/PlayerAI.cs @@ -316,11 +316,6 @@ private static void AttemptTrading(Player us) { continue; } - // TODO: Implement AI -> Human tech trading - if (them.isHuman) { - continue; - } - // Figure out what techs are available for trading. List techsTheyCanTrade = gD.techs.FindAll(x => { return them.knownTechs.Contains(x.id) && !us.knownTechs.Contains(x.id); @@ -338,18 +333,25 @@ private static void AttemptTrading(Player us) { continue; } - // Figure out the value of what we have available to trade. TradeOffer weGive = new(); + Func CalculateWeGiveValue = () => { + return Math.Max(weGive.GoldEquivalentFor(gD, them), weGive.GoldEquivalentFor(gD, us)); + }; + TradeOffer weWant = new(); + Func CalculateWeWantValue = () => { + return Math.Min(weWant.GoldEquivalentFor(gD, them), weWant.GoldEquivalentFor(gD, us)); + }; + + // Figure out the value of what we have available to trade. weGive.gold = us.gold; foreach (Tech t in techsWeCanTrade) { weGive.techs.Add(t); } - int ourMaxPossibleOffer = weGive.GoldEquivalentFor(gD, them); + int ourMaxPossibleOffer = CalculateWeGiveValue(); // Going from the most to the least valuable valuable techs, see // if we can afford them. This greedy algorithm should be good // enough - we don't need perfect binpacking. - TradeOffer weWant = new(); foreach (Tech t in techsTheyCanTrade) { int cost = gD.TechCostFor(t, us); if (cost < ourMaxPossibleOffer) { @@ -365,9 +367,9 @@ private static void AttemptTrading(Player us) { // from the opponent. However, we might be overpaying, possibly // by a significant amount. Keep removing techs from our offer // as long as it doesn't make our offer worse than theirs. - int theirOfferValue = weWant.GoldEquivalentFor(gD, us); + int theirOfferValue = CalculateWeWantValue(); for (int i = 0; i < weGive.techs.Count;) { - if (weGive.GoldEquivalentFor(gD, them) - gD.TechCostFor(weGive.techs[i], them) >= theirOfferValue) { + if (CalculateWeGiveValue() - gD.TechCostFor(weGive.techs[i], them) >= theirOfferValue) { weGive.techs.RemoveAt(0); } else { ++i; @@ -375,7 +377,7 @@ private static void AttemptTrading(Player us) { } // Now use any gold to even things out, if possible. - int remainingDelta = Math.Max(0, weGive.GoldEquivalentFor(gD, them) - theirOfferValue); + int remainingDelta = Math.Max(0, CalculateWeGiveValue() - theirOfferValue); weGive.gold -= Math.Min(remainingDelta, weGive.gold.Value); // And ensure we minimize the total gold traded, to keep the @@ -383,15 +385,30 @@ private static void AttemptTrading(Player us) { int redundantGold = Math.Min(weGive.gold.Value, weWant.gold.Value); weGive.gold -= redundantGold; weWant.gold -= redundantGold; + if (weGive.gold == 0) { + weGive.gold = null; + } + if (weWant.gold == 0) { + weWant.gold = null; + } // Finally if the deal is too mismatched or only contains a swap // of gold, abandon it. Otherwise we can execute the deal. - if (weGive.GoldEquivalentFor(gD, them) > 1.1 * weWant.GoldEquivalentFor(gD, us)) { + // TODO: Figure out how the real trade factor in the difficulty + // works. + float tradeFactor = them.isHuman ? 1.0f : 1.1f; + if (CalculateWeGiveValue() > tradeFactor * CalculateWeWantValue()) { continue; } if (weGive.techs.Count == 0 && weWant.techs.Count == 0) { continue; } + + if (them.isHuman) { + new MsgShowTradeOffer(us, them, weWant, weGive).send(); + EngineStorage.WaitForUiEvent(); + } + us.ExecuteDeal(gD, them, weWant, weGive); } } diff --git a/C7Engine/EntryPoints/MessageToUI.cs b/C7Engine/EntryPoints/MessageToUI.cs index c7077e02..178aac88 100644 --- a/C7Engine/EntryPoints/MessageToUI.cs +++ b/C7Engine/EntryPoints/MessageToUI.cs @@ -115,4 +115,18 @@ public MsgShowTemporaryPopup(string message, Tile location) { this.location = location; } } + + public class MsgShowTradeOffer : MessageToUI { + public Player aiPlayer; + public Player humanPlayer; + public TradeOffer aiWant; + public TradeOffer aiGive; + + public MsgShowTradeOffer(Player aiPlayer, Player humanPlayer, TradeOffer aiWant, TradeOffer aiGive) { + this.aiPlayer = aiPlayer; + this.humanPlayer = humanPlayer; + this.aiWant = aiWant; + this.aiGive = aiGive; + } + } } From c28c9790446508928aec0eec76ff36e261009afa Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 17 Jun 2025 19:52:49 -0500 Subject: [PATCH 2/2] Removing more direct path accesses --- C7/Credits.cs | 2 +- C7/TextureLoader.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/C7/Credits.cs b/C7/Credits.cs index 444fcc26..34d25597 100644 --- a/C7/Credits.cs +++ b/C7/Credits.cs @@ -10,7 +10,7 @@ public partial class Credits : Node2D { public override void _Ready() { log.Information("Now rolling the credits!"); try { - creditsText = System.IO.File.ReadAllText("./Text/credits.txt"); + creditsText = System.IO.File.ReadAllText(ProjectSettings.GlobalizePath(@"res://Text/credits.txt")); } catch (System.Exception ex) { log.Error(ex, "Failed to read from credits.txt!"); } diff --git a/C7/TextureLoader.cs b/C7/TextureLoader.cs index 043e21aa..75e044e8 100644 --- a/C7/TextureLoader.cs +++ b/C7/TextureLoader.cs @@ -58,10 +58,11 @@ public ConfigEntry() { static TextureLoader() { lua = new Lua(); - lua.DoString($"package.path = './Text/TextureConfigs/?.lua;./Text/TextureConfigs/*/?.lua'"); + string textureConfigFolder = ProjectSettings.GlobalizePath(@"res://Text/TextureConfigs"); + lua.DoString($"package.path = '{textureConfigFolder}/?.lua;{textureConfigFolder}/*/?.lua'"); - civ3TextureConfig = (LuaTable)lua.DoFile("./Text/TextureConfigs/civ3.lua")[0]; - c7TextureConfig = (LuaTable)lua.DoFile("./Text/TextureConfigs/c7.lua")[0]; + civ3TextureConfig = (LuaTable)lua.DoFile($"{textureConfigFolder}/civ3.lua")[0]; + c7TextureConfig = (LuaTable)lua.DoFile($"{textureConfigFolder}/c7.lua")[0]; textureConfig = civ3TextureConfig; modernGraphics = false;