diff --git a/packages/cli/lib/behavior/miner.dart b/packages/cli/lib/behavior/miner.dart index 0c450500..c40214c4 100644 --- a/packages/cli/lib/behavior/miner.dart +++ b/packages/cli/lib/behavior/miner.dart @@ -534,8 +534,6 @@ Future sellCargoIfNeeded( // This currently optimizes for price and does not consider requests. // Some cargo should jettison and some should transfer to haulers. - // FIXME(eseidel): This does not consider the round-trip cost, or that it will - // take ship.cooldown until we can mine again. final costedTrip = assertNotNull( findBestMarketToSell( caches.marketPrices, @@ -543,6 +541,10 @@ Future sellCargoIfNeeded( ship, largestCargo.tradeSymbol, expectedCreditsPerSecond: centralCommand.expectedCreditsPerSecond(ship), + unitsToSell: largestCargo.units, + // Don't use ship.cooldown.remainingSeconds because it may be stale. + minimumDuration: ship.remainingCooldown(getNow()), + includeRoundTripCost: true, ), 'No market for ${largestCargo.symbol}.', const Duration(minutes: 10), @@ -615,5 +617,5 @@ final advanceMiner = const MultiJob('Miner', [ // space? emptyCargoIfNeeded, doMineJob, - emptyCargoIfNeeded, + sellCargoIfNeeded, ]).run; diff --git a/packages/cli/lib/behavior/trader.dart b/packages/cli/lib/behavior/trader.dart index b70e756a..8ff7451d 100644 --- a/packages/cli/lib/behavior/trader.dart +++ b/packages/cli/lib/behavior/trader.dart @@ -633,6 +633,7 @@ Future handleUnwantedCargoIfNeeded( ship, unwantedCargo.tradeSymbol, expectedCreditsPerSecond: centralCommand.expectedCreditsPerSecond(ship), + unitsToSell: unwantedCargo.units, ), 'No market for ${unwantedCargo.symbol}.', const Duration(hours: 1), diff --git a/packages/cli/lib/trading.dart b/packages/cli/lib/trading.dart index 53c31a64..44752090 100644 --- a/packages/cli/lib/trading.dart +++ b/packages/cli/lib/trading.dart @@ -566,13 +566,24 @@ MarketTrip? findBestMarketToBuy( /// Find the best market to sell a given item to. /// expectedCreditsPerSecond is the time value of money (e.g. 7c/s) -/// used for evaluating the trade-off between "closest" vs. "cheapest".\ +/// used for evaluating the trade-off between "closest" vs. "cheapest". +/// This does not account for fuel costs. MarketTrip? findBestMarketToSell( MarketPrices marketPrices, RoutePlanner routePlanner, Ship ship, TradeSymbol tradeSymbol, { required int expectedCreditsPerSecond, + required int unitsToSell, + + /// Used to express the minimum time until the next action. This is useful + /// for modeling when the next thing we plan to do involves the cooldown and + /// lets us consider longer routes as the same cost as shorter routes. + Duration? minimumDuration, + + /// If true, we'll include the cost of returning to the current location + /// in the calculation, which makes longer distances less attractive. + bool includeRoundTripCost = false, }) { // Some callers might want to use a round trip cost? // e.g. if just trying to empty inventory and return to current location. @@ -585,18 +596,44 @@ MarketTrip? findBestMarketToSell( if (sorted.isEmpty) { return null; } + Duration applyMin(Duration duration) { + return minimumDuration == null || duration > minimumDuration + ? duration + : minimumDuration; + } + + var printCount = 5; + void log(String message) { + if (printCount > 0) { + shipDetail(ship, message); + printCount--; + } + } + + final roundTripMultiplier = includeRoundTripCost ? 2 : 1; final nearest = sorted.first; var best = nearest; // Pick any one further that earns more than expectedCreditsPerSecond for (final trip in sorted.sublist(1)) { final priceDiff = trip.price.sellPrice - nearest.price.sellPrice; - final earnings = priceDiff; - final extraTime = trip.route.duration - nearest.route.duration; - final earningsPerSecond = earnings / extraTime.inSeconds; + final extraEarnings = priceDiff * unitsToSell; + final extraTime = + applyMin(trip.route.duration) - applyMin(nearest.route.duration); + final earningsPerSecond = + extraEarnings / (extraTime.inSeconds * roundTripMultiplier); if (earningsPerSecond > expectedCreditsPerSecond) { + log('Selecting ${trip.price.waypointSymbol} earns ' + '${creditsString(extraEarnings)} extra ' + 'over ${approximateDuration(extraTime)} ' + '($earningsPerSecond/s)'); best = trip; break; + } else { + log('Skipping ${trip.price.waypointSymbol} would add ' + '${creditsString(extraEarnings)} for ${approximateDuration(extraTime)} ' + '($earningsPerSecond/s)'); } + printCount--; } return best; diff --git a/packages/cli/test/trading_test.dart b/packages/cli/test/trading_test.dart index 057686fa..7ccc7072 100644 --- a/packages/cli/test/trading_test.dart +++ b/packages/cli/test/trading_test.dart @@ -643,6 +643,7 @@ void main() { ship, TradeSymbol.ALUMINUM, expectedCreditsPerSecond: 1, + unitsToSell: 1, ), ); diff --git a/packages/types/lib/api.dart b/packages/types/lib/api.dart index 6508e59b..bdeb52e2 100644 --- a/packages/types/lib/api.dart +++ b/packages/types/lib/api.dart @@ -459,6 +459,22 @@ extension ShipUtils on Ship { } return 'Unknown'; } + + /// Returns the Duration until the ship is ready to use its reactor again. + /// Will never return a negative duration, will instead return null. + /// Use this instead of cooldown.remainingSeconds since that can be stale + /// and does not reflect the current time. + Duration? remainingCooldown(DateTime now) { + final expiration = cooldown.expiration; + if (expiration == null) { + return null; + } + final duration = expiration.difference(now); + if (duration.isNegative) { + return null; + } + return duration; + } } /// Extensions onto ShipNav to make it easier to work with.