Add Fair trades to MatchActively (#1375)

* Add Fair trades to MatchActively

* Address code review

- Copy inventory before comparing because the functions modifies it
- Preserve state between matches
- Take comparing users non tradable items into account
- Prioritize any bots over fair bots

* Don't split the search

* Improve sort order and

hopefully give a copy to IsTradeNeutralOrBetter()

* finally create a copy

* Also filter for type and rarity

* Update the value

* Optimizations

* Preserve state over trades

and give copies to functions.

* More copies

* Add itemsToGive to theirInventory

* Don't create two hashsets
This commit is contained in:
Lucki
2019-08-26 00:21:54 +02:00
committed by Łukasz Domeradzki
parent d74f13c567
commit b37e8cc757
3 changed files with 91 additions and 35 deletions

View File

@@ -70,6 +70,8 @@ namespace ArchiSteamFarm.Json {
[PublicAPI]
public EType Type { get; internal set; }
internal Asset CreateShallowCopy() => (Asset) MemberwiseClone();
#pragma warning disable IDE0051
[JsonProperty(PropertyName = "amount", Required = Required.Always)]
[NotNull]

View File

@@ -228,7 +228,7 @@ namespace ArchiSteamFarm {
[ItemCanBeNull]
private async Task<ImmutableHashSet<ListedUser>> GetListedUsers() {
const string request = URL + "/Api/Bots?matchEverything=1";
const string request = URL + "/Api/Bots";
WebBrowser.ObjectResponse<ImmutableHashSet<ListedUser>> objectResponse = await Bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<ImmutableHashSet<ListedUser>>(request).ConfigureAwait(false);
@@ -357,11 +357,11 @@ namespace ArchiSteamFarm {
return false;
}
(Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> fullState, Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> tradableState) = Trading.GetDividedInventoryState(ourInventory);
(Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory);
if (Trading.IsEmptyForMatching(fullState, tradableState)) {
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
Bot.ArchiLogger.LogGenericTrace(string.Format(Strings.ErrorIsEmpty, nameof(fullState) + " || " + nameof(tradableState)));
Bot.ArchiLogger.LogGenericTrace(string.Format(Strings.ErrorIsEmpty, nameof(ourFullState) + " || " + nameof(ourTradableState)));
return false;
}
@@ -377,8 +377,8 @@ namespace ArchiSteamFarm {
byte emptyMatches = 0;
HashSet<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity)> skippedSetsThisRound = new HashSet<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity)>();
foreach (ListedUser listedUser in listedUsers.Where(listedUser => listedUser.MatchEverything && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong> GivenAssetIDs, ISet<ulong> ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong> GivenAssetIDs, ISet<ulong> ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(listedUser => listedUser.Score).Take(MaxMatchedBotsHard)) {
HashSet<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity)> wantedSets = tradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
foreach (ListedUser listedUser in listedUsers.Where(listedUser => acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong> GivenAssetIDs, ISet<ulong> ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong> GivenAssetIDs, ISet<ulong> ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(listedUser => listedUser.MatchEverything).ThenByDescending(listedUser => listedUser.Score).Take(MaxMatchedBotsHard)) {
HashSet<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
if (wantedSets.Count == 0) {
continue;
@@ -386,7 +386,7 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericTrace(listedUser.SteamID + "...");
HashSet<Steam.Asset> theirInventory = await Bot.ArchiWebHandler.GetInventory(listedUser.SteamID, tradable: true, wantedSets: wantedSets).ConfigureAwait(false);
HashSet<Steam.Asset> theirInventory = await Bot.ArchiWebHandler.GetInventory(listedUser.SteamID, tradable: listedUser.MatchEverything ? true : (bool?) null, wantedSets: wantedSets).ConfigureAwait(false);
if ((theirInventory == null) || (theirInventory.Count == 0)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(Strings.ErrorIsEmpty, nameof(theirInventory)));
@@ -396,7 +396,7 @@ namespace ArchiSteamFarm {
HashSet<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity)> skippedSetsThisUser = new HashSet<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity)>();
Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetInventoryState(theirInventory);
Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> inventoryStateChanges = new Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>>();
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
@@ -405,13 +405,15 @@ namespace ArchiSteamFarm {
Dictionary<ulong, uint> classIDsToGive = new Dictionary<ulong, uint>();
Dictionary<ulong, uint> classIDsToReceive = new Dictionary<ulong, uint>();
Dictionary<ulong, uint> fairClassIDsToGive = new Dictionary<ulong, uint>();
Dictionary<ulong, uint> fairClassIDsToReceive = new Dictionary<ulong, uint>();
foreach (((uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in fullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(count => count > 1))) {
if (!tradableState.TryGetValue(set, out Dictionary<ulong, uint> ourTradableItems) || (ourTradableItems.Count == 0)) {
foreach (((uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(count => count > 1))) {
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint> ourTradableItems) || (ourTradableItems.Count == 0)) {
continue;
}
if (!theirTradableState.TryGetValue(set, out Dictionary<ulong, uint> theirItems) || (theirItems.Count == 0)) {
if (!theirTradableState.TryGetValue(set, out Dictionary<ulong, uint> theirTradableItems) || (theirTradableItems.Count == 0)) {
continue;
}
@@ -419,7 +421,7 @@ namespace ArchiSteamFarm {
Dictionary<ulong, uint> ourFullSet = new Dictionary<ulong, uint>(ourFullItems);
Dictionary<ulong, uint> ourTradableSet = new Dictionary<ulong, uint>(ourTradableItems);
// We also have to take into account changes that happened in previoius trades with this user, so this block will adapt to that
// We also have to take into account changes that happened in previous trades with this user, so this block will adapt to that
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint> pastChanges) && (pastChanges.Count > 0)) {
foreach ((ulong classID, uint amount) in pastChanges) {
if (!ourFullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0) || (fullAmount < amount)) {
@@ -457,22 +459,54 @@ namespace ArchiSteamFarm {
do {
match = false;
foreach ((ulong ourItem, uint ourAmount) in ourFullSet.Where(item => item.Value > 1).OrderByDescending(item => item.Value)) {
if (!ourTradableSet.TryGetValue(ourItem, out uint tradableAmount) || (tradableAmount == 0)) {
foreach ((ulong ourItem, uint ourFullAmount) in ourFullSet.Where(item => item.Value > 1).OrderByDescending(item => item.Value)) {
if (!ourTradableSet.TryGetValue(ourItem, out uint ourTradableAmount) || (ourTradableAmount == 0)) {
continue;
}
foreach ((ulong theirItem, _) in theirItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourAmount <= ourAmountOfTheirItem + 1)) {
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
continue;
}
if (!listedUser.MatchEverything) {
// We have a potential match, let's check fairness for them
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
// Filter their inventory for the sets we're trading or have traded with this user
HashSet<Steam.Asset> fairFiltered = theirInventory.Where(item => (item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity) || skippedSetsThisTrade.Any(skippedSets => skippedSets.RealAppID == item.RealAppID && skippedSets.Type == item.Type && skippedSets.Rarity == item.Rarity)).Select(item => item.CreateShallowCopy()).ToHashSet();
// Copy list to HashSet<Steam.Asset>
HashSet<Steam.Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Where(item => (item.RealAppID == set.RealAppID && item.Type == set.Type && item.Rarity == set.Rarity) || skippedSetsThisTrade.Any(skippedSets => skippedSets.RealAppID == item.RealAppID && skippedSets.Type == item.Type && skippedSets.Rarity == item.Rarity)).Select(item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(classID => classID.Key, classID => classID.Value));
HashSet<Steam.Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(classID => classID.Key, classID => classID.Value));
// Actual check:
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
if (fairGivenAmount > 1) {
fairClassIDsToGive[ourItem] = fairGivenAmount - 1;
} else {
fairClassIDsToGive.Remove(ourItem);
}
if (fairReceivedAmount > 1) {
fairClassIDsToReceive[theirItem] = fairReceivedAmount - 1;
} else {
fairClassIDsToReceive.Remove(theirItem);
}
continue;
}
}
// Skip this set from the remaining of this round
skippedSetsThisTrade.Add(set);
// Update our state based on given items
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint givenAmount) ? givenAmount + 1 : 1;
ourFullSet[ourItem] = ourAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint> currentChanges)) {
currentChanges[ourItem] = currentChanges.TryGetValue(ourItem, out uint amount) ? amount + 1 : 1;
@@ -483,26 +517,20 @@ namespace ArchiSteamFarm {
}
// Update our state based on received items
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint receivedAmount) ? receivedAmount + 1 : 1;
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
if (tradableAmount > 1) {
ourTradableSet[ourItem] = tradableAmount - 1;
if (ourTradableAmount > 1) {
ourTradableSet[ourItem] = ourTradableAmount - 1;
} else {
ourTradableSet.Remove(ourItem);
}
// Update their state based on taken items
if (!theirItems.TryGetValue(theirItem, out uint theirAmount) || (theirAmount == 0)) {
Bot.ArchiLogger.LogNullError(nameof(theirAmount));
return false;
}
if (theirAmount > 1) {
theirItems[theirItem] = theirAmount - 1;
if (theirTradableAmount > 1) {
theirTradableItems[theirItem] = theirTradableAmount - 1;
} else {
theirItems.Remove(theirItem);
theirTradableItems.Remove(theirItem);
}
itemsInTrade += 2;
@@ -529,6 +557,7 @@ namespace ArchiSteamFarm {
break;
}
// Remove the items from inventories
HashSet<Steam.Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory, classIDsToGive);
HashSet<Steam.Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive);
@@ -578,6 +607,9 @@ namespace ArchiSteamFarm {
break;
}
// Add itemsToGive to theirInventory to reflect their current state if we're over MaxItemsPerTrade
theirInventory.UnionWith(itemsToGive);
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
Bot.ArchiLogger.LogGenericTrace(Strings.Success);
}
@@ -598,17 +630,17 @@ namespace ArchiSteamFarm {
skippedSetsThisRound.UnionWith(skippedSetsThisUser);
foreach ((uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity) skippedSet in skippedSetsThisUser) {
fullState.Remove(skippedSet);
tradableState.Remove(skippedSet);
ourFullState.Remove(skippedSet);
ourTradableState.Remove(skippedSet);
}
if (Trading.IsEmptyForMatching(fullState, tradableState)) {
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
break;
}
fullState.TrimExcess();
tradableState.TrimExcess();
ourFullState.TrimExcess();
ourTradableState.TrimExcess();
}
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.ActivelyMatchingItemsRound, skippedSetsThisRound.Count));

View File

@@ -217,6 +217,28 @@ namespace ArchiSteamFarm {
return (fullState, tradableState);
}
internal static Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> GetTradableInventoryState(IReadOnlyCollection<Steam.Asset> inventory) {
if ((inventory == null) || (inventory.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory));
return null;
}
Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> tradableState = new Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>>();
foreach (Steam.Asset item in inventory.Where(item => item.Tradable)) {
(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint> tradableSet)) {
tradableSet[item.ClassID] = tradableSet.TryGetValue(item.ClassID, out uint amount) ? amount + item.Amount : item.Amount;
} else {
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
}
}
return tradableState;
}
internal static Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary<ulong, uint>> GetInventoryState(IReadOnlyCollection<Steam.Asset> inventory) {
if ((inventory == null) || (inventory.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory));