Skip to content

Commit

Permalink
feat: teach a ship how to only survey
Browse files Browse the repository at this point in the history
We now have a ship which knows how to only survey in a loop
(with two mk2 surveyors).

Also added -o or --only to the cli.dart args
allowing only running a set of ships.

Refactored behavior dispatch as part of adding Surveyor.
Wired up recording Transactions from Shipyards
(both purchasing and mounting).
  • Loading branch information
eseidel committed Jul 27, 2023
1 parent 2f60be4 commit d32567e
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 70 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ Earning:
* buy-in-a-loop for small tradeVolumes gets worse as we have more ships.
This is likely the #1 contributor to "wait time".
Every place we return null to loop has the same problem.
* Record # of extractions per survey? (Does laser power matter?)
* Record which ship generated a survey?

Exploring:
* Explorers should explore an entire system and then go to the jump gate
Expand Down
26 changes: 24 additions & 2 deletions packages/cli/bin/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,26 @@ void printRequestStats(RateLimitedApiClient client) {
logger.info('Total: ${client.requestCounts.totalRequests()} requests.');
}

bool Function(Ship ship)? _shipFilterFromArgs(Agent agent, List<String> only) {
if (only.isEmpty) {
return null;
}
final onlyShips =
only.map((s) => ShipSymbol(agent.symbol, int.parse(s, radix: 16)));
if (onlyShips.isNotEmpty) {
logger.info('Only running ships: $onlyShips');
}
return (Ship ship) => onlyShips.contains(ship.shipSymbol);
}

Future<void> cliMain(List<String> args) async {
final parser = ArgParser()
..addFlag('verbose', abbr: 'v', negatable: false, help: 'Verbose logging.');
..addFlag('verbose', abbr: 'v', negatable: false, help: 'Verbose logging.')
..addMultiOption(
'only',
abbr: 'o',
help: 'Only run the given ship numbers (hex).',
);
final results = parser.parse(args);

logger.level = results['verbose'] as bool ? Level.verbose : Level.info;
Expand Down Expand Up @@ -76,7 +93,12 @@ Future<void> cliMain(List<String> args) async {
)
..info(describeFleet(caches.ships));

await logic(api, centralCommand, caches);
// We use defaultTo: [], so we don't have to check fo null here.
// This means that we won't notice `--only` being passed with no ships.
// But that's also OK since that's nonsentical.
final shipFilter =
_shipFilterFromArgs(agent, results['only'] as List<String>);
await logic(api, centralCommand, caches, shipFilter: shipFilter);
}

Future<void> main(List<String> args) async {
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/lib/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ extension ConnectedSystemUtils on ConnectedSystem {
SystemPosition get position => SystemPosition(x, y);

/// Returns the distance to the given system.
int distanceTo(System other) => position.distanceTo(other.position);
int distanceTo(ConnectedSystem other) => position.distanceTo(other.position);
}

/// Extensions onto SystemWaypoint to make it easier to work with.
Expand Down Expand Up @@ -485,11 +485,11 @@ extension ShipUtils on Ship {

/// Returns true if the ship has a surveyor module.
bool get hasSurveyor {
const surveyerMounts = [
const surveyerMounts = {
ShipMountSymbolEnum.SURVEYOR_I,
ShipMountSymbolEnum.SURVEYOR_II,
ShipMountSymbolEnum.SURVEYOR_III,
];
};
return mounts.any((m) => surveyerMounts.contains(m.symbol));
}

Expand Down Expand Up @@ -658,8 +658,9 @@ extension MarketTransactionUtils on MarketTransaction {

/// Extensions onto ShipyardTransaction to make it easier to work with.
extension ShipyardTransactionUtils on ShipyardTransaction {
/// Returns the ShipSymbol for the given transaction.
ShipSymbol get shipSymbolObject => ShipSymbol.fromString(shipSymbol);
// Note: shipSymbol on the transaction is actually shipType.
/// Returns the ShipType purchased in the transaction.
ShipType get shipType => ShipType.fromJson(shipSymbol)!;

/// Returns the WaypointSymbol for the given transaction.
WaypointSymbol get waypointSymbolObject =>
Expand Down
101 changes: 51 additions & 50 deletions packages/cli/lib/behavior/advance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,53 @@ import 'package:cli/behavior/change_mounts.dart';
import 'package:cli/behavior/deliver.dart';
import 'package:cli/behavior/explorer.dart';
import 'package:cli/behavior/miner.dart';
import 'package:cli/behavior/surveyor.dart';
import 'package:cli/behavior/trader.dart';
import 'package:cli/cache/caches.dart';
import 'package:cli/logger.dart';
import 'package:cli/nav/navigation.dart';

Future<DateTime?> _advanceIdle(
Api api,
CentralCommand centralCommand,
Caches caches,
Ship ship, {
DateTime Function() getNow = defaultGetNow,
}) async {
shipDetail(ship, 'Idling');
// Make sure ships don't stay idle forever.
caches.behaviors.completeBehavior(ship.shipSymbol);
// Return a time in the future so we don't spin hot.
return DateTime.now().add(const Duration(minutes: 10));
}

Future<DateTime?> Function(
Api api,
CentralCommand centralCommand,
Caches caches,
Ship ship, {
DateTime Function() getNow,
}) _behaviorFunction(Behavior behavior) {
switch (behavior) {
case Behavior.buyShip:
return advanceBuyShip;
case Behavior.trader:
return advanceTrader;
case Behavior.miner:
return advanceMiner;
case Behavior.surveyor:
return advanceSurveyor;
case Behavior.explorer:
return advanceExplorer;
case Behavior.deliver:
return advanceDeliver;
case Behavior.changeMounts:
return advanceChangeMounts;
case Behavior.idle:
return _advanceIdle;
}
}

/// Advance the behavior of the given ship.
/// Returns the time at which the behavior should be advanced again
/// or null if can be advanced immediately.
Expand All @@ -34,54 +76,13 @@ Future<DateTime?> advanceShipBehavior(
}

// shipDetail(ship, 'Advancing behavior: ${behavior.behavior.name}');
switch (behavior.behavior) {
case Behavior.buyShip:
return advanceBuyShip(
api,
centralCommand,
caches,
ship,
);
case Behavior.trader:
return advanceTrader(
api,
centralCommand,
caches,
ship,
);
case Behavior.miner:
return advanceMiner(
api,
centralCommand,
caches,
ship,
);
case Behavior.explorer:
return advanceExplorer(
api,
centralCommand,
caches,
ship,
);
case Behavior.deliver:
return advanceDeliver(
api,
centralCommand,
caches,
ship,
);
case Behavior.changeMounts:
return advanceChangeMounts(
api,
centralCommand,
caches,
ship,
);
case Behavior.idle:
shipDetail(ship, 'Idling');
// Make sure ships don't stay idle forever.
caches.behaviors.completeBehavior(ship.shipSymbol);
// Return a time in the future so we don't spin hot.
return DateTime.now().add(const Duration(minutes: 10));
}
final behaviorFunction = _behaviorFunction(behavior.behavior);
final waitUntil = await behaviorFunction(
api,
centralCommand,
caches,
ship,
getNow: getNow,
);
return waitUntil;
}
3 changes: 3 additions & 0 deletions packages/cli/lib/behavior/behavior.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ enum Behavior {
/// Mine asteroids and sell the ore.
miner,

/// Survey indefinitely.
surveyor,

/// Idle.
idle,

Expand Down
22 changes: 18 additions & 4 deletions packages/cli/lib/behavior/central_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ class CentralCommand {
return Behavior.idle;
}

// final forceIdle = ['ESEIDEL-1B'];
// if (forceIdle.contains(ship.symbol)) {
// return Behavior.changeMounts;
// }

final disableBehaviors = <Behavior>[
// Behavior.buyShip,
// Behavior.trader,
Expand Down Expand Up @@ -253,7 +258,8 @@ class CentralCommand {
ShipRole.EXCAVATOR: [
// We'll always upgrade the ship as our best option.
Behavior.changeMounts,
Behavior.miner
if (ship.canMine) Behavior.miner,
if (!ship.canMine && ship.hasSurveyor) Behavior.surveyor,
],
ShipRole.SATELLITE: [Behavior.explorer],
}[ship.registration.role];
Expand Down Expand Up @@ -829,9 +835,8 @@ class CentralCommand {
final shipyard = await getShipyard(api, waypoint);
await recordShipyardDataAndLog(shipyardPrices, shipyard, ship);

// Buy ship if we should.
// For now lets always by haulers if we can afford them and we have
// fewer than 3 haulers idle.
// Buy ship if we should. Important to do this after
// recordShipyardDataAndLog as it depends on having price data.
await buyShipIfPossible(
api,
shipyardPrices,
Expand Down Expand Up @@ -863,8 +868,17 @@ class CentralCommand {
isBehaviorDisabledForShip(ship, Behavior.buyShip)) {
return false;
}

// This assumes the ship in question is at a shipyard and already docked.
final shipyardSymbol = ship.waypointSymbol;
// This only works if we've recorded prices from this shipyard before.
final knownPrices = shipyardPrices.pricesAtShipyard(ship.waypointSymbol);
final availableTypes = knownPrices.map((p) => p.shipType);
shipInfo(
ship,
'Visiting shipyard $shipyardSymbol, available: $availableTypes',
);

final shipType = shipTypeToBuy(
ship,
shipyardPrices,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/lib/behavior/change_mounts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ Future<DateTime?> advanceChangeMounts(
}) async {
final toMount = centralCommand.getMountToAdd(ship.shipSymbol);

shipInfo(ship, 'Changing mounts. Mounting $toMount.');

// Re-validate every loop in case resuming from error.
final template = centralCommand.templateForShip(ship);
if (template == null) {
Expand All @@ -59,6 +57,8 @@ Future<DateTime?> advanceChangeMounts(
return null;
}

shipInfo(ship, 'Changing mounts. Mounting $toMount.');

// We've already started a change-mount job, continue.
if (toMount != null) {
final currentWaypoint =
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/lib/behavior/miner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,12 @@ Future<DateTime?> advanceMiner(
// We could sell here before putting ourselves to sleep.
return response.cooldown.expiration;
} on ApiException catch (e) {
/// ApiException 400: {"error":{"message":
/// Ship ESEIDEL-1B does not have a required mining laser mount.",
/// "code":4243,"data":{"shipSymbol":"ESEIDEL-1B",
/// "miningLasers":["MOUNT_MINING_LASER_I","MOUNT_MINING_LASER_II",
/// "MOUNT_MINING_LASER_III"]}}}
if (isSurveyExhaustedException(e)) {
// If the survey is exhausted, record it as such and try again.
shipDetail(ship, 'Survey ${maybeSurvey!.signature} exhausted.');
Expand Down
54 changes: 54 additions & 0 deletions packages/cli/lib/behavior/surveyor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:cli/behavior/central_command.dart';
import 'package:cli/cache/caches.dart';
import 'package:cli/logger.dart';
import 'package:cli/nav/navigation.dart';
import 'package:cli/net/actions.dart';

/// For dedicated survey ships.
Future<DateTime?> advanceSurveyor(
Api api,
CentralCommand centralCommand,
Caches caches,
Ship ship, {
DateTime Function() getNow = defaultGetNow,
}) async {
shipInfo(ship, '🔭 Surveyor');
final currentWaypoint = await caches.waypoints.waypoint(ship.waypointSymbol);

final mineSymbol =
centralCommand.mineSymbolForShip(caches.systems, caches.agent, ship);
if (mineSymbol == null) {
centralCommand.disableBehaviorForShip(
ship,
'No desired mine for ship.',
const Duration(hours: 1),
);
return null;
}
if (ship.waypointSymbol != mineSymbol) {
return beingNewRouteAndLog(
api,
ship,
caches.ships,
caches.systems,
caches.routePlanner,
centralCommand,
mineSymbol,
);
}
// TODO(eseidel): Throw exceptions which disable the behavior for a time.
assert(currentWaypoint.canBeMined, 'Must be at a mineable waypoint.');
assert(ship.hasSurveyor, 'Must have a surveyor.');

// Surveying requires being undocked.
await undockIfNeeded(api, caches.ships, ship);

final outer = await api.fleet.createSurvey(ship.symbol);
final response = outer!.data;
// shipDetail(ship, '🔭 ${ship.waypointSymbol}');
shipInfo(ship, '🔭 Got ${response.surveys.length} surveys!');
await caches.surveys.recordSurveys(response.surveys, getNow: getNow);
// Each survey is the whole behavior.
centralCommand.completeBehavior(ship.shipSymbol);
return response.cooldown.expiration;
}
5 changes: 5 additions & 0 deletions packages/cli/lib/cache/shipyard_prices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ class ShipyardPrices extends JsonListStore<ShipyardPrice> {
}
return pricesForSymbolSorted.last.purchasePrice;
}

/// Returns all known prices for a given shipyard.
List<ShipyardPrice> pricesAtShipyard(WaypointSymbol marketSymbol) {
return _prices.where((e) => e.waypointSymbol == marketSymbol).toList();
}
}

/// Record shipyard data and log the result.
Expand Down
12 changes: 8 additions & 4 deletions packages/cli/lib/cache/transactions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,18 @@ class Transaction {
/// Create a new transaction from a shipyard transaction.
factory Transaction.fromShipyardTransaction(
ShipyardTransaction transaction,
ShipType shipType,
int agentCredits,
ShipSymbol purchaser,
) {
return Transaction(
// shipSymbol is the new ship, not the ship that made the transaction.
shipSymbol: transaction.shipSymbolObject,
// .shipSymbol is the new ship type, not a ShipSymbol involved
// in the transaction.
// https://github.com/SpaceTradersAPI/api-docs/issues/68
shipSymbol: purchaser,
waypointSymbol: transaction.waypointSymbolObject,
tradeSymbol: shipType.value,
// shipSymbol is the trade symbol for the shipyard transaction, not
// the new ship's id.
tradeSymbol: transaction.shipSymbol,
quantity: 1,
tradeType: MarketTransactionTypeEnum.PURCHASE,
perUnitPrice: transaction.price,
Expand Down
Loading

0 comments on commit d32567e

Please sign in to comment.