/// <summary> /// Given a set of cash and a desired price, get the amount of the cash /// to meet the price (rounded up for safety) /// </summary> /// <param name="AvailableCash">The amount of cash avaliable.</param> /// <param name="price">The price to meet (roughly)</param> /// <returns>The appropriate cash for the price.</returns> /// <exception cref="ArgumentNullException">If <paramref name="AvailableCash"/> is null.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="price"/> is less than or equal to 0.</exception> public IProductAmountCollection ChangeForPrice(IProductAmountCollection AvailableCash, double price) { // ensure cash is not null. if (AvailableCash is null) { throw new ArgumentNullException(nameof(AvailableCash)); } // ensure that the price is greater than 0 if (price <= 0) // TODO, allow this to savely give change for 0. It's not that hard. { throw new ArgumentOutOfRangeException("Price must be greater than 0."); } // first, check that all available cash can meet the price. var totalCash = AvailableCash.Sum(x => x.Item2 * ProductPrices.GetProductValue(x.Item1)); // If the total cash available is less than the price sought, return a copy of the available cash. if (totalCash < price) { return(AvailableCash.Copy()); } // since we have more than we need, var result = new ProductAmountCollection(); // start from the best and start making change, highest to lowest value. foreach (var coin in AvailableCash.OrderByDescending(x => ProductPrices.GetProductValue(x.Item1))) { // if none of that coin exist if (coin.Item2 == 0) { // add it as zero result.AddProducts(coin.Item1, 0); // and skip to the next loop continue; } // coin type var curr = coin.Item1; // coin value var val = ProductPrices.GetProductValue(curr); // if value is higher then remaining price. if (val > price) { // add it as zero result.AddProducts(curr, 0); // and skip continue; } // coin amount var amt = coin.Item2; // how many whole coins can fit into the price. var count = Math.Floor(price / val); // select cap coins at the available level. count = Math.Min(count, amt); // add to our change result.AddProducts(curr, count); // subtract the value we took out price -= val * count; // then go to the next coin. } // if there is a remainder if (price > 0) { // find the smallest coin available foreach (var coin in AvailableCash.OrderBy(x => ProductPrices.GetProductValue(x.Item1))) { // if we have any available if (result.GetProductValue(coin.Item1) < AvailableCash.GetProductValue(coin.Item1)) { // add one result.AddProducts(coin.Item1, 1); // and move on. break; // by logic, only one should be needed as we guaranteed have // more value in coins then the requested price and the // remainder should be smaller than the smallest coin. } } } // return the change. return(result); }
public IProductAmountCollection BuyGood(IProductAmountCollection cash, IProduct good, double amount, IMarket market) { // Sanity check for nulls. if (cash is null) { throw new ArgumentNullException(nameof(cash)); } if (good is null) { throw new ArgumentNullException(nameof(good)); } if (market is null) { throw new ArgumentNullException(nameof(market)); } if (amount <= 0) { throw new ArgumentOutOfRangeException(nameof(amount)); } // The collection we're returning. var result = new ProductAmountCollection(); // get how much to buy, what's available, or what's desired. var available = Math.Min(amount, ForSale.GetProductValue(good)); // get the price of that good var totalPrice = GetPrice(good, market.GetPrice(good)) * available; // get the cash needed for the goods var money = market.ChangeForPrice(cash, totalPrice); // get our available money total var totalMoney = money.Sum(x => market.GetPrice(x.Item1, x.Item2)); // if the money is enough, just buy outright if (totalMoney >= totalPrice) { // Add what they're buying. result.AddProducts(good, available); // remove what they spent result.AddProducts(money.Multiply(-1)); } else if (!good.Fractional && totalMoney < market.GetPrice(good)) {// If the total money is not enough for a single unit, and the good can't be divided // we can't actually buy anything, so just return an empty transaction return(new ProductAmountCollection()); } else // if it's not enough { // get the buyable units of the good double buyableUnits = 0; // if we can buy fractionally if (good.Fractional) { // just do the math straight. buyableUnits = totalMoney / market.GetPrice(good); } else // if we can't { // take what we can and round down, seller should always make more than the buyer here. buyableUnits = Math.Floor(totalMoney / market.GetPrice(good)); } // if we can buy any units if (buyableUnits > 0) { // buy them add the units to the results result.AddProducts(good, buyableUnits); // subtract the cash. result.AddProducts(money.Multiply(-1)); } } // Get change, if any. var change = totalMoney - market.GetPrice(good) * result.GetProductValue(good); // get the change, if any IProductAmountCollection buyersChange = new ProductAmountCollection(); // if change is greater than 0. if (change > 0) // Make said change. { buyersChange = market.ChangeForPrice(GetCash(market.AcceptedCurrencies), change); } // add back the buyer's change result.AddProducts(buyersChange); // TODO, maybe add check to see if the seller shortchanged the buyer. // complete the transaction for the seller, and subtract the result. CompleteTransaction(result.Multiply(-1)); // we're done, return the change in the buyer's goods. return(result); }
/// <summary> /// Buys good from the market. /// </summary> /// <param name="buyer">The one buying the good.</param> /// <param name="good">The good they are trying to buy.</param> /// <param name="amount">How much they are trying to buy.</param> /// <returns>The Receipt of purchases</returns> public IProductAmountCollection BuyGoodsFromMarket(IPopulationGroup buyer, IProduct good, double amount) { if (buyer is null) { throw new ArgumentNullException(nameof(buyer)); } if (good is null) { throw new ArgumentNullException(nameof(good)); } if (amount <= 0) { throw new ArgumentOutOfRangeException(nameof(amount)); } // The result of the purchases. IProductAmountCollection result = new ProductAmountCollection(); // First buy from local merchants, they only accept cash. result = Populous.Merchants .BuyGood(buyer.GetCash(AcceptedCurrencies), good, amount, this); // see how much was bought. double remainder = 0; // if any was bought, update what we are seeking. if (result.Contains(good)) { remainder = amount - result.GetProductValue(good); // and complete the transaction buyer.CompleteTransaction(result); } // if no remainder, return if (remainder <= 0) { return(result); } // Then buy from everyone else via both cash and barter. foreach (var seller in Populous.GetPopsSellingProduct(good)) { // If someone is selling the good, buy or barter with them. var reciept = BuyGoods(buyer, good, remainder, seller); // if something was bought if (reciept.Count() > 0) { // remove it from the remainder remainder -= reciept.GetProductValue(good); // add it to our result result.AddProducts(reciept); } // if nothing remains, we're done. if (remainder <= 0) { return(result); } } // Finish buy going to the travelling merchants, if all else fails. foreach (var travSeller in TravellingMerchantsSelling(good)) { var reciept = travSeller.BuyGood(buyer.GetCash(AcceptedCurrencies), good, remainder, this); // if something was bought if (reciept.Count() > 0) { // remove it from remainder remainder -= reciept.GetProductValue(good); // Complete the transaction for the buyer buyer.CompleteTransaction(reciept); // add it to the result result.AddProducts(reciept); } // if nothing remains, return if (remainder <= 0) { return(result); } } // return the ultimate receipt. return(result); }