Skip to content

Commit

Permalink
feat: teach miners how to travel to sell
Browse files Browse the repository at this point in the history
I had to teach findBestMarketToSell about minimum duration
as well as sell volume and units to sell.

The previous usage was wrong, since it didn't include unitsToSell
and would thus undervalue the extra trip time since it was
comparing against an absolute earnings per second, not one
per unit.

minimumDuration was necessary in order to get the miner to want to
travel to sell, since it accounts for the fact that the miner will
otherwise sit idle waiting for its reactor cooldown.

includeRoundTripCost was needed to model the return trip for miners.

This still does not account for fuel costs (which is wrong) and
could cause miners to end up traveling to sell at a loss when fuel
prices are high.
  • Loading branch information
eseidel committed Sep 30, 2023
1 parent 753c0f4 commit 68478ad
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 7 deletions.
8 changes: 5 additions & 3 deletions packages/cli/lib/behavior/miner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -534,15 +534,17 @@ Future<JobResult> 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,
caches.routePlanner,
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),
Expand Down Expand Up @@ -615,5 +617,5 @@ final advanceMiner = const MultiJob('Miner', [
// space?
emptyCargoIfNeeded,
doMineJob,
emptyCargoIfNeeded,
sellCargoIfNeeded,
]).run;
1 change: 1 addition & 0 deletions packages/cli/lib/behavior/trader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ Future<JobResult> handleUnwantedCargoIfNeeded(
ship,
unwantedCargo.tradeSymbol,
expectedCreditsPerSecond: centralCommand.expectedCreditsPerSecond(ship),
unitsToSell: unwantedCargo.units,
),
'No market for ${unwantedCargo.symbol}.',
const Duration(hours: 1),
Expand Down
45 changes: 41 additions & 4 deletions packages/cli/lib/trading.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/test/trading_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ void main() {
ship,
TradeSymbol.ALUMINUM,
expectedCreditsPerSecond: 1,
unitsToSell: 1,
),
);

Expand Down
16 changes: 16 additions & 0 deletions packages/types/lib/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 68478ad

Please sign in to comment.