Skip to content

Commit

Permalink
feat: start to break out supply chain logic
Browse files Browse the repository at this point in the history
  • Loading branch information
eseidel committed Feb 25, 2024
1 parent ff52f2a commit 7bbf2d9
Showing 1 changed file with 310 additions and 0 deletions.
310 changes: 310 additions & 0 deletions packages/cli/bin/new_supply_chain.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import 'package:cli/cache/caches.dart';
import 'package:cli/cli.dart';

final minable = <TradeSymbol>{
TradeSymbol.ALUMINUM_ORE,
TradeSymbol.COPPER_ORE,
TradeSymbol.GOLD_ORE,
TradeSymbol.IRON_ORE,
TradeSymbol.MERITIUM_ORE,
TradeSymbol.SILVER_ORE,
TradeSymbol.AMMONIA_ICE,
TradeSymbol.ICE_WATER,
TradeSymbol.PRECIOUS_STONES,
TradeSymbol.QUARTZ_SAND,
TradeSymbol.SILICON_CRYSTALS,
};

final siphonable = <TradeSymbol>{
TradeSymbol.LIQUID_HYDROGEN,
TradeSymbol.LIQUID_NITROGEN,
};

final extractable = <TradeSymbol>{
...minable,
...siphonable,
};

abstract class Node {
Node(this.tradeSymbol);
final TradeSymbol tradeSymbol;

void describe({int indent = 0});
}

abstract class Produce extends Node {
Produce(super.tradeSymbol, this.waypointSymbol);
final WaypointSymbol waypointSymbol;
}

class Extract extends Produce {
Extract(super.tradeSymbol, super.waypointSymbol);

@override
void describe({int indent = 0}) {
logger.info('${' ' * indent}Extract $tradeSymbol from $waypointSymbol');
}
}

class Shuttle extends Node {
Shuttle(super.tradeSymbol, this.destination, this.source);
final WaypointSymbol destination;

// This could be a list if we wanted to support options.
final Produce source;

@override
void describe({int indent = 0}) {
logger.info(
'${' ' * indent}Shuttle $tradeSymbol from ${source.waypointSymbol} to $destination',
);

source.describe(indent: indent + 1);
}
}

class Manufacture extends Produce {
Manufacture(super.tradeSymbol, super.waypointSymbol, this.inputs);

// This could map String -> List if we wanted to support options.
final Map<TradeSymbol, Shuttle> inputs;

@override
void describe({int indent = 0}) {
logger.info(
'${' ' * indent}Manufacture $tradeSymbol at $waypointSymbol from $inputs',
);
for (final input in inputs.values) {
input.describe(indent: indent + 1);
}
}
}

Set<TradeSymbol> extractableFrom(SystemWaypoint waypoint) {
if (waypoint.isAsteroid) {
return minable;
}
if (waypoint.type == WaypointType.GAS_GIANT) {
return siphonable;
}
return {};
}

WaypointSymbol? nearestExtractionSiteFor(
SystemsCache systemsCache,
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol,
) {
final destination = systemsCache.waypoint(waypointSymbol);
final candidates = systemsCache
.waypointsInSystem(waypointSymbol.system)
.where(
(waypoint) =>
waypoint.isAsteroid || waypoint.type == WaypointType.GAS_GIANT,
)
.toList()
..sort(
(a, b) => a.distanceTo(destination).compareTo(b.distanceTo(destination)),
);
return candidates.firstOrNull?.symbol;
}

MarketListing? nearestListingWithExport(
SystemsCache systemsCache,
MarketListingSnapshot marketListings,
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol,
) {
final listings = marketListings.listings
// Listings in this same system which export the good.
.where(
(entry) =>
entry.waypointSymbol.system == waypointSymbol.system &&
entry.exports.contains(tradeSymbol),
)
.toList();
final destination = systemsCache.waypoint(waypointSymbol);
listings.sort(
(a, b) => systemsCache
.waypoint(a.waypointSymbol)
.distanceTo(destination)
.compareTo(
systemsCache.waypoint(b.waypointSymbol).distanceTo(destination),
),
);
return listings.firstOrNull;
}

// Returns the distance between two waypoints, or null if they are in different
// systems.
int? distanceBetween(
SystemsCache systemsCache,
WaypointSymbol a,
WaypointSymbol b,
) {
final aWaypoint = systemsCache.waypoint(a);
final bWaypoint = systemsCache.waypoint(b);
if (aWaypoint.system != bWaypoint.system) {
return null;
}
return aWaypoint.distanceTo(bWaypoint).toInt();
}

String describeMarket(WaypointSymbol waypointSymbol, MarketPrice? price) {
if (price == null) {
return '$waypointSymbol (no market)';
}
return '$waypointSymbol (${price.supply}, ${price.activity})';
}

class Sourcer {
const Sourcer({
required this.marketListings,
required this.systemsCache,
required this.exportCache,
required this.marketPrices,
});

final MarketListingSnapshot marketListings;
final SystemsCache systemsCache;
final TradeExportCache exportCache;
final MarketPriceSnapshot marketPrices;

Produce? _shuttleSource(
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol,
) {
// No need to manufacture if we can extract.
if (extractable.contains(tradeSymbol)) {
// Find the nearest extraction location?
final location = nearestExtractionSiteFor(
systemsCache,
tradeSymbol,
waypointSymbol,
);
if (location == null) {
logger.warn('No extraction site for $tradeSymbol for $waypointSymbol');
return null;
}
return Extract(tradeSymbol, location);
}

// Look for the nearest export of the good.
final closest = nearestListingWithExport(
systemsCache,
marketListings,
tradeSymbol,
waypointSymbol,
);
if (closest == null) {
logger.warn('No export for $tradeSymbol for $waypointSymbol');
return null;
}
return sourceViaManufacture(tradeSymbol, closest.waypointSymbol);
}

Shuttle? sourceViaShuttle(
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol,
) {
final source = _shuttleSource(tradeSymbol, waypointSymbol);
if (source == null) {
return null;
}
return Shuttle(tradeSymbol, waypointSymbol, source);
}

Manufacture? sourceViaManufacture(
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol,
) {
final listing = marketListings[waypointSymbol];
if (listing == null) {
throw ArgumentError('No market listing for $waypointSymbol');
}
final imports = exportCache[tradeSymbol]!.imports;
final inputs = <TradeSymbol, Shuttle>{};
for (final import in imports) {
final source = sourceViaShuttle(import, waypointSymbol);
if (source == null) {
return null;
}
inputs[import] = source;
}
return Manufacture(tradeSymbol, waypointSymbol, inputs);
}

Node? sourceGoodsFor(
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol, {
int indent = 0,
}) {
final listing = marketListings[waypointSymbol];
// If the end isn't a market this must be a shuttle step.
if (listing == null) {
return sourceViaShuttle(tradeSymbol, waypointSymbol);
} else {
// If we're sourcing for an export, this must be a manufacture step.
if (listing.exports.contains(tradeSymbol)) {
return sourceViaManufacture(tradeSymbol, waypointSymbol);
} else {
// If we're sourcing for an import, this must be a shuttle step.
return sourceViaShuttle(tradeSymbol, waypointSymbol);
}
}
}
}

void source(
MarketListingSnapshot marketListings,
SystemsCache systemsCache,
TradeExportCache exportCache,
MarketPriceSnapshot marketPrices,
TradeSymbol tradeSymbol,
WaypointSymbol waypointSymbol,
) {
logger.info('Sourcing $tradeSymbol for $waypointSymbol');
final action = Sourcer(
marketListings: marketListings,
systemsCache: systemsCache,
exportCache: exportCache,
marketPrices: marketPrices,
).sourceGoodsFor(tradeSymbol, waypointSymbol);
if (action == null) {
logger.warn('No source for $tradeSymbol for $waypointSymbol');
return;
}
action.describe();
}

Future<void> command(FileSystem fs, Database db, ArgResults argResults) async {
final exportCache = TradeExportCache.load(fs);
final systemsCache = SystemsCache.load(fs)!;
final marketListings = await MarketListingSnapshot.load(db);
final marketPrices = await MarketPriceSnapshot.load(db);
final agent = await myAgent(db);
final constructionCache = ConstructionCache(db);

final jumpgate =
systemsCache.jumpGateWaypointForSystem(agent.headquarters.system)!;
final waypointSymbol = jumpgate.symbol;
final construction = await constructionCache.getConstruction(waypointSymbol);

final neededExports = construction!.materials
.where((m) => m.required_ > m.fulfilled)
.map((m) => m.tradeSymbol);
for (final tradeSymbol in neededExports) {
source(
marketListings,
systemsCache,
exportCache,
marketPrices,
tradeSymbol,
waypointSymbol,
);
}
}

Future<void> main(List<String> args) async {
await runOffline(args, command);
}

0 comments on commit 7bbf2d9

Please sign in to comment.