You can find additional information, guides, support, and API documentation about SpaceTraders on their website here: https://spacetraders.io/.
The SpaceTraders team will not be able to assist you with any issues regarding SnakesInSpace. Please open a ticket here if you experience any reproducible issues with the Library.
The minimum Python version supported is currently Python 3.9.
You can install SnakesInSpace from PyPi
% pip install SnakesInSpace
Note, the SnakesInSpace
package will have an importable name of snisp
.
If you want to build from source, SnakesInSpace will require httpx
and python-dateutil
to play and pytest
and respx
if you want to run the tests yourself.
My current Agent is playing under the Symbol "SNAKESINSPACE."
- Get or Create your Agent
- Get your COMMAND ship, which comes with extraction mounts
- Find the closest Asteroid
- Navigate your ship to the Asteroid
- Extract until your ship's cargo is full while also jettisoning any ICE_WATER
from snisp.agent import Agent
>>> agent = Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
>>> ship = next(iter(agent.fleet))
>>> asteroid = ship.closest(ship.waypoints.asteroids())
>>> ship.autopilot(asteroid) # Autopilot blocks until at destination
>>> while ship.cargo.units < ship.cargo.capacity:
... extraction = ship.extract() # ship.extract will handle Cooldowns automatically
... if extraction.symbol == 'ICE_WATER':
... ship.jettison(extraction.symbol, exctraction.units)
>>> ship.cargo.units == ship.cargo.capacity
True
Agent
The Agent
represents your Player in SpaceTraders.
from snisp.agent import Agent
>>> agent = Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
>>> agent.symbol
'your_symbol_here'
>>> agent.token
'AGENT_TOKEN_STRING'
Your symbol
is your callsign in SpaceTraders. The email
field is entirely optional.
If you already know your token
, you can also access your Agent
directly by
>>> agent = Agent(symbol='your_symbol_here', token='your_spacetraders_token')
>>> agent.symbol
'your_symbol_here'
>>> agent.token
'your_spacetraders_token'
SpaceTraders is still in the Alpha phase so expect a full system restart every 2-3 weeks. If a restart occurs, you will have to manually reset your local config and attempt to log in again.
from snisp import agent
>>> agent = agent.Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
>>> agent.data
snisp.exceptions.ClientError: Message: Failed to parse token. Token version does not match the server. Server resets happen on a weekly to bi-weekly frequency during alpha. After a reset, you should re-register your agent. Expected: v2.2.0, Actual: v2.1.5 | Code: 401 | Data: {'expected': 'v2.2.0', 'actual': 'v2.1.5'}
>>> snisp.agent.reset()
>>> agent = agent.Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
>>> agent.data
PlayerData({'accountId': 'AGENT_TOKEN_ID', 'symbol': 'YOUR_SYMBOL_HERE', 'headquarters': 'X1-CC27-A1', 'credits': 100000, 'startingFaction': 'COSMIC', 'shipCount': 2})
It is up to the user to handle resetting their own account and managing their tokens across devices.
The default Faction
is "COSMIC," which is guaranteed to work. The complete list of Factions is
AEGIS
, ANCIENTS
, ASTRO
, COBALT
, CORSAIRS
, COSMIC
, CULT
, DOMINION
, ECHO
, ETHEREAL
, GALACTIC
, LORDS
, OBSIDIAN
, OMEGA
, QUANTUM
, SHADOW
, SOLITARY
, UNITED
, and VOID
. Not all Factions
may be supported at this time by SpaceTraders. If in doubt, use "COSMIC."
Accessing Your Agent
You can access your Agent from any SnakesInSpace object.
From your ship,
>>> ship = agent.fleet('SHIP_SYMBOL')
>>> ship.agent
Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
from a waypoint,
>>> ship = agent.fleet('SHIP_SYMBOL')
>>> waypoint = next(iter(ship.waypoints))
>>> waypoint.agent
Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
and so on.
Each Agent
will have its own lock accessible at agent.lock
. The lock is used internally but can also be used by the user. The lock type is a reentrant lock (threading.RLock
).
Contracts
Every new Agent
starts with an open Contract
. Contract
s can be a great way to jump-start a new game.
Your Contracts
will be accessible via your Agent
. For instance, to get the current Contract
, simply call
>>> contract = agent.contracts.current
>>> contract
Contract({'id': 'CONTRACT_ID', 'factionSymbol': 'COSMIC', 'type': 'PROCUREMENT', 'terms': {'deadline': '2024-08-24T14:15:22Z', 'payment': {'onAccepted': 151237, 'onFulfilled': 388895}, 'deliver': [{'tradeSymbol': 'IRON_ORE', 'destinationSymbol': 'X1-BD70-A1', 'unitsRequired': 60, 'unitsFulfilled': 0}]}, 'accepted': False, 'fulfilled': False, 'expiration': '2024-08-24T14:15:22Z', 'deadlineToAccept': '2024-08-24T14:15:22Z'})
According to your current Contract
, you need to deliver 60 units of "IRON_ORE" to a Waypoint
at "X1-BD70-A1." Before you can deliver items to a Contract
, you will first need to accept it. According to,
>>> contract.accepted
False
the Contract
has not been accepted. To accept the Contract
, accept it via the Contract.accept
method like
>>> contract.accept()
>>> contract.accepted
True
The Contract.accept
method is idempotent, so you can call it as many times as you want.
If you have "IRON_ORE" in your ship
's cargo and you're already at the contract.terms.deliver[0].destination_symbol
, you can deliver the resources by using the contract.deliver
method, as shown below
>>> ship = agent.fleet('SHIP_SYMBOL') # The ship at destinationSymbol
>>> ship.cargo.inventory # This ship has 10 units of IRON_ORE in it's cargo
[Inventory({'symbol': 'IRON_ORE', 'name': 'Iron Ore', 'description': 'DESCRIPTION', 'units': 10})]
>>> contract.deliver(ship, 'IRON_ORE') # or contract.deliver(ship, 'IRON_ORE', max_units=10)
>>> contract
Contract({'id': 'CONTRACT_ID', 'factionSymbol': 'COSMIC', 'type': 'PROCUREMENT', 'terms': {'deadline': '2024-08-24T14:15:22Z', 'payment': {'onAccepted': 151237, 'onFulfilled': 388895}, 'deliver': [{'tradeSymbol': 'IRON_ORE', 'destinationSymbol': 'X1-BD70-A1', 'unitsRequired': 60, 'unitsFulfilled': 10}]}, 'accepted': True, 'fulfilled': False, 'expiration': '2024-08-24T14:15:22Z', 'deadlineToAccept': '2024-08-24T14:15:22Z'})
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
A contract
will automatically update to show you delivered the 10 units of "IRON_ORE" from your ship to the contract
. Your ship
's cargo will be updated automatically as well.
The max_units
kwarg is entirely optional. If not specified, the deliver
method will automatically deliver either the maximum units of trade_symbol
in the ship
's cargo or the maximum remaining required units of trade_symbol
for the contract
.
Warnings will be logged if you attempt to deliver
items to a contract that has either had all the remaining units fulfilled or if there are no units of trade_symbol
in the ship
's cargo.
Once you've delivered all of the necessary units to the contract
's Waypoint
, you can fulfill it by calling the contract.fulfill
method, like
>>> contract.fuflill()
>>> contract.fulfilled
True
fulfill
works like accept
in that it is idempotent.
A contract
instance is not thread safe and may become stale if ships in other threads deliver
items. To get a fresh instance of the contract
, you can always call
>>> contract = contract.refresh()
which will return a new contract
object.
New Contracts
Once you've fulfilled a contract
, you can move on to the next one.
Begin by navigating any ship to a Waypoint
with a Faction
and then calling ship.negotiate_contract()
. The new contract
will not be accepted by default, so you will have to call contract.accept()
before you can begin delivering items to it.
The quickest way to get a new contract
is reproduced below for convenience:
>>> ship = agent.fleet('SHIP_SYMBOL')
>>> wp_with_faction = (i for i in ship.waypoints if i.faction.symbol)
>>> wp = ship.closest(wp_with_faction)
>>> ship.autopilot(wp)
>>> contract = ship.negotiate_contract()
>>> contract.accept()
ship.autopilot
and ship.closest
will be covered in the Fleet section.
Exceptions
ContractNotAcceptedError
will be raised if you attempt to deliver
items to a contract
that has not been accepted.
ContractFulfilledError
will be raised if you attempt to deliver
items to a contract
that has already been fulfilled.
ContractDeadlineError
will be raised if you attempt to deliver
items to a contract
that has already expired.
For additional Errors, see the "Contract Error Codes" found in snisp.exceptions
.
Helper Properties
A contract
has a few helper properties.
>>> contract.expired # Boolean for if the contract has already expired
>>> contract.extractable # Boolean for if items not yet fulfilled in the contract have tradeSymbols that can be extracted from an Asteroid
>>> contract.siphonable # Boolean for if items not yet fulfilled in the contract have tradeSymbols that can be siphoned from a Gas Giant
Fleet
By default, a new Agent will receive a command ship
and a probe
. As you continue to play and purchase additional ships
, probes
, and drones
, your fleet will be easily managable from your agent.fleet
.
# Iterate over the whole fleet for all available ships
>>> whole_fleet = list(agent.fleet)
# Get a ship by symbol
>>> ship = agent.fleet('SHIP_SYMBOL')
# Get the first ship, which will be the default COMMAND ship
>>> ship = next(iter(agent.fleet))
# Get all available Drones
>>> drones = list(agent.fleet.drones())
# Get all available Mining Drones
>>> mining_drones = list(agent.fleet.mining_drones())
# Get all available Siphon Drones
>>> siphon_drones = list(agent.fleet.siphon_drones())
# Get all available Probes
>>> probes = list(agent.fleet.probes())
# Get all available Shuttles
>>> shuttles = list(agent.fleet.shuttles())
# Get all available Ships, excluding probes and drones
>>> ships = list(agent.fleet.ships())
It's important to remember that fleet
itself, drones
, mining_drones
, siphon_drones
, probes
, ships
, and shuttles
are iterables. This can cause issues if you assign a variable to an initialized iterable and attempt to iterate over it more than once.
>>> probes = agent.fleet.probes()
>>> _ = list(probes) # Default, the single starting probe
>>> list(probes)
[] # "probes" iterable has been exhausted
This is intentional as there may have been additional probes purchased between the two calls.
A nice side effect of this is that any additional ships purchased between when iterating over agent.fleet
starts and finishes will be returned. For example,
>>> for ship in agent.fleet:
... do_something(ship) # operation that takes a long time
... # Additional ship was purchased in a another thread
... # The new ship will be called to `do_something`
As always, you can avoid this side effect by building the list of ships ahead of time with ships = list(agent.fleet)
.
A ship
returned by agent.fleet
will be a snisp.fleet.Ship
instance.
>>> ship = next(iter(agent.fleet)) # Starting ship. This will be the default COMMAND ship
>>> ship.registration.role
'COMMAND'
>>> ship.symbol
'YOUR_SYMBOL_HERE-1'
Like all SnakesInSpace objects, it will be created from the JSON data returned by the SpaceTraders API. For instance, below is the example data for GET https://api.spacetraders.io/v2/my/ships/{shipSymbol}
as returned by the SpaceTraders API:
{
"data": {
"symbol": "YOUR_SYMBOL_HERE-1",
"registration": {
"name": "string",
"factionSymbol": "string",
"role": "COMMAND"
},
"nav": {
"systemSymbol": "string",
"waypointSymbol": "string",
"route": {
"destination": {
"symbol": "string",
"type": "PLANET",
"systemSymbol": "string",
"x": 0,
"y": 0
},
"origin": {
"symbol": "string",
"type": "PLANET",
"systemSymbol": "string",
"x": 0,
"y": 0
},
"departureTime": "2024-08-24T14:15:22Z",
"arrival": "2024-08-24T14:15:22Z"
},
"status": "IN_TRANSIT",
"flightMode": "CRUISE"
},
"crew": {
"current": 0,
"required": 0,
"capacity": 0,
"rotation": "STRICT",
"morale": 0,
"wages": 0
},
"frame": {
"symbol": "FRAME_PROBE",
"name": "string",
"description": "string",
"condition": 0,
"moduleSlots": 0,
"mountingPoints": 0,
"fuelCapacity": 0,
"requirements": {
"power": 0,
"crew": 0,
"slots": 0
}
},
"reactor": {
"symbol": "REACTOR_SOLAR_I",
"name": "string",
"description": "string",
"condition": 0,
"powerOutput": 1,
"requirements": {
"power": 0,
"crew": 0,
"slots": 0
}
},
"engine": {
"symbol": "ENGINE_IMPULSE_DRIVE_I",
"name": "string",
"description": "string",
"condition": 0,
"speed": 1,
"requirements": {
"power": 0,
"crew": 0,
"slots": 0
}
},
"cooldown": {
"shipSymbol": "string",
"totalSeconds": 0,
"remainingSeconds": 0,
"expiration": "2024-08-24T14:15:22Z"
},
"modules": [
{
"symbol": "MODULE_MINERAL_PROCESSOR_I",
"capacity": 0,
"range": 0,
"name": "string",
"description": "string",
"requirements": {
"power": 0,
"crew": 0,
"slots": 0
}
}
],
"mounts": [
{
"symbol": "MOUNT_GAS_SIPHON_I",
"name": "string",
"description": "string",
"strength": 0,
"deposits": [
"QUARTZ_SAND"
],
"requirements": {
"power": 0,
"crew": 0,
"slots": 0
}
}
],
"cargo": {
"capacity": 0,
"units": 0,
"inventory": [
{
"symbol": "PRECIOUS_STONES",
"name": "string",
"description": "string",
"units": 1
}
]
},
"fuel": {
"current": 300,
"capacity": 400,
"consumed": {
"amount": 100,
"timestamp": "2024-08-24T14:15:22Z"
}
}
}
}
This data, like all objects returned by agent
, fleet
, waypoints
, markets
, and systems
, will be directly accessible to their respective object via dot-lookups. For convenience, the "data" in the JSON data is skipped to make accessing more convenient.
Need to check the current fuel level?
>>> ship.fuel.current
300
Need to check the current status of a ship
's flight mode?
>>> ship.nav.flight_mode
'CRUISE'
For convenience, you can access attributes via their original camelCase, e.g., flightMode, or via snake_case, e.g., flight_mode. For instance, the previous lookup could also be done instead as
>>> ship.nav.flightMode
'CRUISE'
The .to_dict()
helper function can translate the data into a Python dictionary for convenience, too. This is especially handy for pretty-printing.
>>> ship.fuel.to_dict()
{'current': 300, 'capacity': 400, 'consumed': {'amount': 100, 'timestamp': '2024-08-24T14:15:22Z'}}
Each dict key will represent a dot-lookup attribute.
>>> ship.fuel.consumed.amount
100
which is equivalent to
>>> ship.to_dict()['fuel']['consumed']['amount']
100
Like all objects created by SnakesInSpace, they are not inherentely thread safe. If a seperate thread updates the ship
associated with the ship
's ship.symbol
, the reference may become stale. To return a new, up-to-date instance of a ship
, call
>>> ship = ship.refresh()
>>> ship.refresh().cargo # Can be done in-place, too
Navigation
Moving a ship between Waypoints
is the most important aspect of the game and much care has been taken to make it as simple as possible.
Navigate
The default ship.navigate
method accepts a Waypoint
as an arg and a boolean raise_error
kwarg. The Waypoint
can be any Waypoint
type that is found in the same System as the ship. The optional kwarg, raise_error
, tells the function if it should raise an error if any exceptions occur or suppress and return the error. By default, raise_error
is True
.
The raise_error
kwarg can be ignored by most users.
A typical navigate
scenario would be to vist the closest Waypoint
.
>>> ship = next(iter(agent.fleet))
>>> waypoint = ship.closest(ship.waypoints)
>>> waypoint.symbol
X1-BD70-J64
>>> waypoint.type
ASTEROID
>>> ship.navigate(waypoint)
If the navigate
request is succesful, control will be returned back to the caller immediately and you can begin processing requests for other ships
in the same thread.
Autopilot
A convenience method, ship.autopilot
, was created to take the guesswork out of navigating, refueling, and controlling flight modes that is inherit to ship.navigate
. Any situation in which a ship
can use ship.navigate
, it could more easily use ship.autopilot
>>> ship = next(iter(agent.fleet))
>>> waypoint = ship.closest(ship.waypoints)
>>> waypoint.symbol
X1-BD70-J64
>>> waypoint.type
ASTEROID
>>> ship.autopilot(waypoint) # Blocks until at destination
>>> ship.nav.waypoint_symbol # Current location
X1-BD70-J64
>>> ship.nav.route.destination.symbol # Last navigation's destination symbol
X1-BD70-J64
>>> ship.nav.route.destination.type # Last navigation's destination type
ASTEROID
Now, with ship.autopilot
, the library will attempt to navigate
to the Waypoint
, but, if it fails due to distance or lack of fuel, autopilot
will automatically control refuelling and updating flight modes accordingly to get you to the Waypoint
as quickly as possible.
There are a few caveats with this approach, namely, by design, autopilot
will block so control will not be returned to the thread until the ship
reaches the Waypoint
; and, there is always an off-chance the call to navigate
from within autopilot
will use exactly the correct amount of fuel to get you to the destination.
For the former, all calls to ship.autopilot
should be done via a thread if blocking is an issue.
For the latter, if a ship
does become "dead" in the water in that it cannot refuel itself at the destination, the ship
will be added to the agent
's agent.dead_ships
dictionary and skipped on all subsequent agent.fleet
iterations. It is possible to navigate
another ship
to the dead ship
's location to manually transfer
fuel, but that is up to the user.
ship.autopilot
does accept a done_callback
kwarg. The callback, so long as it is callable()
, will be executed before returning control back to the thread. This is convenient if you, say, want to navigate to a Waypoint
and make an extraction before waiting for the next thread loop.
>>> ship = next(iter(agent.fleet))
>>> asteroid = ship.closest(ship.waypoints.asteroids())
>>> ship.autopilot(asteroid, done_callback=ship.extract)
By default, ship.autopilot
will attempt to refuel
at every navigation stage, as well as before returning control back to the thread.
Navigating with Probes
In the current iteration, Probes
in SpaceTraders do not require fuel.
>>> probe = next(iter(agent.fleet.probes()))
>>> probe.fuel
Fuel({'current': 0, 'capacity': 0, 'consumed': {'amount': 0, 'timestamp': '2024-03-03T16:18:13.155Z'}})
Due to this, you can just yeet them without regard by calling .navigate
and fire-and-forgetting it.
>>> for probe in agent.fleet.probes():
... if probe.nav.status != 'IN_TRANSIT:
... if probe.nav.flight_mode != 'CRUISE':
... probe.update_flight_mode('CRUISE')
... probe.navigate(AnyWaypointInTheSystem)
You do not need to use .autopilot
in this situation.
Changing Flight Modes
You can manually change flight modes by calling ship.update_flight_mode
with 'DRIFT', 'STEALTH', 'CRUISE', or 'BURN'. Note that ship.autopilot
will change your flight mode to CRUISE
by default.
Refuel
As mentioned previously, if you stick with ship.autopilot
, you will not need to manually refuel. If you wish to refuel on your own, you can always call ship.refuel
when located at a Waypoint
that exports or exchanges FUEL.
For convenience, the ship
object has a closest_fuel
method that can find the closest available Waypoint
that sells fuel.
>>> ship = next(iter(agent.fleet))
>>> fuel_station = ship.closest_fuel()
>>> ship.navigate(fuel_station)
>>> ship.refuel()
You can also refuel from fuel found in the ship.cargo
by calling
>>> ship = next(iter(agent.fleet))
>>> ship.refuel(from_cargo=True)
This can be helpful for refueling dead ships.
Refuel also has an ignore_errors
kwarg that accepts a boolean. If True
, any exception raised while refueling will be suppressed and the exception will be returned instead. This is convenient for attempting to refuel at every Waypoint
because why not.
Jump
Each System has at least one JumpGate
that allows the ship
to navigate between Systems. To use a JumpGate
, the gate will need to be fully constructed and the ship
will need to be located at the Waypoint
.
>>> ship = next(iter(agent.fleet))
>>> jump_gate = next(iter(ship.waypoints.jump_gates()))
>>> jump_gate.is_under_construction
False
>>> ship.autpilot(jump_gate)
>>> next_system = jump_gate.data.connections[0] # .connections will contain a list with all of the connected systems
>>> ship.jump(next_system)
Warp
A ship
with the Warp Drive
Mount installed can also warp
to other Systems.
>>> ship = next(iter(agent.fleet))
>>> next_system = next(i for i in ship.agent.systems if i.symbol != ship.nav.system_symbol)
>>> ship.warp(next_system)
Market Actions
Purchase
"Traders" is in the name of the game so purchasing items at Markets
is an integral function.
Purchasing items is as simple as navigating to the Market
and purchasing as much as you can afford.
>>> ship = next(iter(agent.fleet))
>>> ship.cargo.inventory
[]
>>> market = ship.closest(ship.markets.exports('GOLD'))
>>> ship.autopilot(market)
>>> transaction = ship.purchase('GOLD', 40)
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD', 'name': 'Gold', 'description': 'DESCRIPTION', 'units': 40})]
Autopurchase
There are some caveats to purchase
, such as, having the necessary credits, the Market
selling the good in the required number of units, etc., that are handled for you with ship.autopurchase
.
ship.autopurchase
accepts the good symbol to purchase as the first arg and then optional kwargs of max_units
and buffer
, with defaults of 0 and 200_000, respectively.
If max_units
> 0, at most max_units
will be purchased. Otherwise, ship.autopurchase
will purchase as many units of the goods as you can either hold or afford.
The buffer
will be your credits buffer. The default 200_000 limit means you will be able to purchase up to max_units
so long as your current agent.data.credits
- buffer
>= purchase price. To remove the buffer
, just pass a 0.
Another benefit of ship.autopurchase
is it handles the maximum units per transaction that the Market
will allow.
>>> agent.data.credits
1_000_000
>>> ship = next(iter(agent.fleet))
>>> ship.cargo.inventory
[]
>>> market = ship.closest(ship.markets.exports('GOLD'))
>>> ship.autopilot(market)
>>> transactions = ship.autopurchase('GOLD')
>>> for transaction in transactions:
... print(transaction.units)
20
20
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD', 'name': 'Gold', 'description': 'DESCRIPTION', 'units': 40})]
>>> agent.data.credits
800_000
limited by a buffer
>>> agent.data.credits
300_000
>>> ship = next(iter(agent.fleet))
>>> ship.cargo.inventory
[]
>>> market = ship.closest(ship.markets.exports('GOLD'))
>>> ship.autopilot(market)
>>> transactions = ship.autopurchase('GOLD', buffer=200_000) # Default buffer
>>> for transaction in transactions:
... print(transaction.units)
20
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD', 'name': 'Gold', 'description': 'DESCRIPTION', 'units': 20})]
>>> agent.data.credits
200_000
As you can see, ship.autopurchase
absolves you of tracking units per transaction, credits
and cargo.capacity
.
Each attempt to make a purchase in autopurchase
will be done under the agent.lock
to gaurantee there are no race conditions with regards to your credit buffer when multiple threads are attempting purchases.
Sell, Sell All, Sell off Cargo
Once you've extracted or purchased items you wish to sell, you can do so with three different methods: ship.sell
, ship.sell_all
, and ship.sell_off_cargo
.
The basic ship.sell
works as
>>> agent.data.credits
100_000
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 35})]
>>> market = ship.closest(ship.markets.imports('GOLD_ORE'))
>>> ship.autopilot(market)
>>> transaction = ship.sell('GOLD_ORE', 20)
>>> transaction.units
20
>>> transaction.trade_symbol
GOLD_ORE
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 15})]
>> agent.data.credits
100_500
Attempting to sell items not found in your ship.cargo
or more than the number of units in the ship.cargo
will lead to exceptions.
Your ship
will need to be located at a Market
that imports the good.
To sell all items of a good in your ship.cargo
, you can use the ship.sell_all
method
>>> agent.data.credits
100_000
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 35})]
>>> market = ship.closest(ship.markets.imports('GOLD_ORE'))
>>> ship.autopilot(market)
>>> transactions = ship.sell_all('GOLD_ORE')
>>> for transaction in transactions:
... print(transaction.units)
20
15
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
>> agent.data.credits
100_750
The ship.sell_all
method will perform the necessary number of ship.sell
's until all units of the good in your ship.cargo
have been sold. You can see from the returned transactions
the Market
the ship
was located at accepted at most 20 units per transaction. The number of units per transaction will differ from Market
to Market
and from trade good to trade good within a single Market
.
ship.sell_all
handles trade volume automatically and is the preferred means of selling goods.
As always, your ship
will still need to be located at a Market
that imports the good.
Sometimes you just want to clear out your ship.cargo
without jettisoning everything and losing the potential credits. To do this, call ship.sell_off_cargo
>>> agent.data.credits
100_000
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 35}), Inventory({'symbol': 'IRON_ORE', 'name': 'Iron Ore', 'description': 'DESCRIPTION', 'units': 5})
>>> transactions = ship.sell_off_cargo()
>>> for transaction in transactions:
... print(transaction.symbol, transaction.units)
('GOLD_ORE', 20)
('GOLD_ORE', 15)
('IRON_ORE', 5)
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
>> agent.data.credits
100_850
If you only want to sell off all of your GOLD_ORE
, you could call ship.sell_off_cargo("GOLD_ORE")
to leave your IRON_ORE
safe in your ship.cargo
.
The ship.sell_off_cargo
method is convenient because it will automatically navigate to the closest Market
which imports each good. Note, this entails the method will block until all navigation has completed.
Ship Actions
A ship
has a number of actions it can perform, depending on the installed Mounts and Modules.
Survey
If the ship is located at a Waypoint
that supports survey
and the ship.can_survey
, a survey can be performed via
>>> survey = ship.waypoints.survey()
A single call to survey
will return multiple "surveys." For example, the "surveys" in the previous survey
can be found at
>>> survey.surveys
[{'signature': 'string', 'symbol': 'string', 'deposits': [{'symbol': 'string'}], 'expiration': '2024-08-24T14:15:22Z', 'size': 'SMALL'}]
When using a survey
with a extract_with_survey
, you will only pass one of the survey.surveys
to the method.
A ship
will enter a cooldown period after performing a survey. The cooldown will prevent the ship from performing various tasks until the cooldown period has completed. SnakesInSpace handles this automaticaly for you, so you can attempt another survey and SnakesInSpace will block until the cooldown period has passed before making the request.
All actions that require a cooldown period to pass will be handled automatically.
Extraction
If the ship is located at a Waypoint
that supports extraction
and the ship.can_mine
, exctraction can be done via
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
>>> extraction = ship.extract()
>>> extraction.symbol
GOLD_ORE
>>> extraction.units
4
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 4})]
Extraction with Survey
To get the best extraction results, you can make an extraction with a survey by calling exctract_with_survey
while passing a survey
from a survey.surveys
to the method. The survey
in question can be created by any ship.
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
>>> survey = ship.waypoints.survey()
>>> survey.surveys[0]
Survey({'signature': 'X1-CC27-CB5A-F0AB4D', 'symbol': 'X1-CC27-CB5A', 'deposits': [{'symbol': 'ALUMINUM_ORE'}, {'symbol': 'QUARTZ_SAND'}, {'symbol': 'COPPER_ORE'}, {'symbol': 'IRON_ORE'}, {'symbol': 'COPPER_ORE'}], 'expiration': '2024-03-10T23:47:07.218Z', 'size': 'SMALL'})
>>> extraction = ship.extract_with_survey(survey[0]) # Will block until cooldown from survey has finished
>>> extraction.symbol
COPPER_ORE
>>> extraction.units
8
>>> ship.cargo.inventory
[Inventory({'symbol': 'COPPER_ORE', 'name': 'Copper Ore', 'description': 'DESCRIPTION', 'units': 8})]
The Survey
class has a helper method, .best
, that accepts a survey
object which was returned by ship.survey()
. .best
will return the "best" survey
in survey.surveys
. If you're looking for specific deposits, you can pass the the deposit symbols to .best
so only surveys
which contain all of the deposit symbols will be returned or None
if none of the deposit symbols are found in a survey.surveys
.
>>> survey = ship.waypoints.survey()
>>> survey.best()
Survey({'signature': 'X1-CC27-CB5A-F0AB4D', 'symbol': 'X1-CC27-CB5A', 'deposits': [{'symbol': 'ALUMINUM_ORE'}, {'symbol': 'QUARTZ_SAND'}, {'symbol': 'COPPER_ORE'}, {'symbol': 'IRON_ORE'}, {'symbol': 'COPPER_ORE'}], 'expiration': '2024-03-10T23:47:07.218Z', 'size': 'LARGE'})
>>> survey.best('QUARTZ_SAND', 'GOLD_ORE') # No GOLD_ORE found in any of the survey.surveys
None
>>> survey.best('QUARTZ_SAND', 'IRON_ORE')
Survey({'signature': 'X1-CC27-CB5A-F0AB4D', 'symbol': 'X1-CC27-CB5A', 'deposits': [{'symbol': 'ALUMINUM_ORE'}, {'symbol': 'QUARTZ_SAND'}, {'symbol': 'COPPER_ORE'}, {'symbol': 'IRON_ORE'}, {'symbol': 'COPPER_ORE'}], 'expiration': '2024-03-10T23:47:07.218Z', 'size': 'LARGE'})
Attempting to extract_with_survey
with a survey
that has already expired will raise a snisp.exceptions.ShipSurveyExpirationError
error.
Attempting to extract_with_survey
with an invalid survey
will raise a snisp.exceptions.ShipSurveyVerificationError
error.
To see all Survey
related errors, see snisp.exceptions
.
Siphon
If the ship is located at a Waypoint that supports siphon
and the ship.can_siphon
, siphoning can be done via
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
>>> siphon = ship.siphon()
>>> siphon.symbol
LIQUID_HYDROGEN
>>> siphon.units
4
>>> ship.cargo.inventory
[Inventory({'symbol': 'LIQUID_HYDROGEN', 'name': 'Liquid Hydrogen', 'description': 'DESCRIPTION', 'units': 4})]
Jettison
You can jettison unwanted items in your ship.cargo
by calling ship.jettison
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 4})]
>>> ship.jettison('GOLD_ORE', units=2)
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 2})]
Refine
If there are at least 30 units of an Ore in your ship.cargo.inventory
and the ship.can_refine
, you can refine the 30 units of Ore into 1 refined unit through ship.refine
.
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 35})]
>>> ship.refine('GOLD')
>>> ship.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 5}), Inventory({'symbol': 'GOLD', 'name': 'Gold', 'description': 'DESCRIPTION', 'units': 1})]
Scan
You can scan for nearby ships in the current System with ship.scan
>>> nearby_ships = list(ship.scan())
SpaceTraders does not make it explicit who the owner of a scanned ship is, so SnakesInSpace currently assumes scanned ships are owned by the Agent. Attempting to perform actions on a ship you do not own will lead to undefined consequences.
Chart
You can create a Chart
of an uncharted Waypoint
that a ship
is located at by calling .chart
>>> ship.waypoints.chart()
If the Waypoint
was already charted, a Warning will be logged but no Exception will be raised.
There is a convenience function, is_uncharted
, in snisp.waypoints
for checking if a Waypoint
is uncharted.
from snisp.waypoints import is_uncharted
>>> waypoint = ship.waypoints.get() # Get the current Waypoint at the ship's location
>>> if is_uncharted(waypoint):
... ship.waypoints.chart()
Transfer
You can transfer cargo beteween two ship
s via ship.transfer
>>> ship_from = agent.fleet('FROM_SHIP')
>>> ship_to = agent.fleet('TO_SHIP')
>>> ship_from.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 35}), Inventory({'symbol': 'IRON_ORE', 'name': 'Iron Ore', 'description': 'DESCRIPTION', 'units': 5})
>>> ship_to.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
>>> ship_from.transfer(ship_to, symbol='GOLD_ORE', units=35)
>>> ship_from.cargo.inventory
[Inventory({'symbol': 'IRON_ORE', 'name': 'Iron Ore', 'description': 'DESCRIPTION', 'units': 5})
>>> ship_to.cargo.inventory
[Inventory({'symbol': 'GOLD_ORE', 'name': 'Gold Ore', 'description': 'DESCRIPTION', 'units': 35})]
Exceptions will be raised if you attempt to transfer goods not found in the "from" ship or if there are unit mismatches in either ship.
Orbit, Dock
Almost every action will require the ship
to either be IN_ORBIT
or DOCKED
before it can be executed. You can check the status of the ship
with ship.nav.status
. To ensure you have the most up-to-date version, you can always do ship.refresh().nav.status
.
The library will take care of all calls to ship.orbit()
and ship.dock()
, so this is not something a user has to worry about.
Closest, Farthest
As you've seen sprinkled throughout the guide, the ship
object does have helper methods of ship.closest
and ship.farthest
. Each method will accept any number of iterables and return either the closest or farthest Waypoint
to the ship
's current location.
>>> ship = next(iter(agent.fleet))
>>> closest_asteroid = ship.closest(
... ship.waypoints.asteroid_bases(),
... ship.waypoints.asteroids(),
... ship.waypoints.enginereed_asteroids(),
... ship.waypoints.asteroids_fields()
... )
>>> ship.autopilot(closest_asteroid)
You can also do ship.farthest
if you want?
Waypoints, Markets, Shipyards
The respective Waypoints
, Markets
, and Shipyards
in the ship
's current System are directly accesible via
>>> ship.waypoints
>>> ship.markets
>>> ship.shipyards
See their respective sections for for additional information.
Exceptions
There are too many agent.fleet
and ship
related exceptions to cover here. Please check snisp.exceptions
for an exhaustive list of exceptions.
Helper Properties
Below are some helper properties for things like arrival times, current location, and capabilities.
>>> ship.arrival # Seconds to arriving at destination or 0
>>> ship.location # The ships current Location object
>>> ship.at_market # Boolean for if the ship is currently DOCKED or IN_ORBIT at a Market
>>> ship.at_shipyard # Boolean for if the ship is currently DOCKED or IN_ORBIT at a Shipyard
>>> ship.can_mine # Boolean for if the ship has a Mining Mount
>>> ship.can_siphon # Boolean for if the ship has a Siphoning Mount
>>> ship.can_refine_gas # Boolean for if the ship can refine gas onboard
>>> ship.can_refine_ore # Boolean for if the ship can refine ore onboard
>>> ship.can_survey # Boolean for if the ship has a Surveying Mount
Waypoints
In SpaceTraders, Waypoints
are the fundamental location points within a System. All Markets
, Shipyards
, Asteroids
, etc., are necessarily and suffiecient to being a Waypoint
.
You can see all Waypoints
in a ship
's System by iterating over ship.waypoint
directly.
>>> ship = next(iter(agent.fleet))
>>> waypoints = list(ship.waypoints)
Each Waypoint
type
has a convenience method for quick and convenient iteration.
For instance, to find all Asteroids
,
>>> ship = next(iter(agent.fleet))
>>> asteroids = list(ship.waypoints.asteroids())
>>> all(asteroid.type == 'ASTEROID' for asteroid in asteroids)
True
>>> asteroids == [waypoint for waypoint in ship.waypoints if waypoint.type == 'ASTEROID']
True
all JumpGates
,
>>> ship = next(iter(agent.fleet))
>>> jump_gates = list(ship.waypoints.jump_gates())
>>> all(jump_gate.type == 'JUMP_GATE' for jump_gate in jump_gates)
True
>>> jump_gates == [waypoint for waypoint in ship.waypoints if waypoint.type == 'JUMP_GATE']
True
and so on.
Some Waypoints
have unique traits that provide additional information about itself. You can filter for these traits by using the traits
kwarg in the respective ship.waypoints
methods.
>>> ship = next(iter(agent.fleet))
>>> asteroids = list(ship.waypoints.asteroids())
>>> sorted({i.symbol for w in asteroids for i in w.traits})
['COMMON_METAL_DEPOSITS', 'DEEP_CRATERS', 'EXPLOSIVE_GASES', 'HOLLOWED_INTERIOR', 'MICRO_GRAVITY_ANOMALIES', 'MINERAL_DEPOSITS', 'PRECIOUS_METAL_DEPOSITS', 'RADIOACTIVE', 'RARE_METAL_DEPOSITS', 'SHALLOW_CRATERS', 'UNSTABLE_COMPOSITION']
>>> radioactive_asteroids = list(ship.waypoints.asteroids(traits='RADIOACTIVE'))
>>> radioactive_asteroids == [w for w in ship.waypoints.asteroids() for i in w.traits if i.symbol == 'RADIOACTIVE']
True
You can use traits
and types
while calling ship.waypoints
(__call__
) directly as well. Internally, this is how SnakesInSpace finds Markets
.
>>> ship = next(iter(agent.fleet))
>>> market_waypoints = list(ship.waypoints(traits='MARKETPLACE'))
>>> markets = list(ship.markets)
>>> len(market_waypoints) == len(markets)
True
>>> {i.symbol for i in market_waypoints} == {i.symbol for i in markets}
True
>>> sorted({market.type for market in markets})
['ASTEROID_BASE', 'ENGINEERED_ASTEROID', 'FUEL_STATION', 'JUMP_GATE', 'MOON', 'ORBITAL_STATION', 'PLANET']
For convenience, the Waypoint
types methods are:
artificial_gravity_wells
, asteroid_bases
, asteroid_fields
, asteroids
, debris_fields
, engineered_asteroids
, gas_giants
, gravity_wells
, jump_gates
, moons
, nebulas
, orbital_stations
, planets
, and shipyards
.
Also, for convenience, there is a construction_sites
method for iterating over all Waypoints
that are currently under construction. A Waypoint
under construction will not have a specific type but will report if it's under construction by waypiont.is_under_construction
.
Markets
A Market
is a Waypoint
type that imports, exports, or exchanges goods. Not all Waypoints
are Markets
but all Markets
are Waypoints
.
You can see all of the Markets
in the System in which a ship
is located by iterating over the ship
's Markets
>>> ship = next(iter(agent.fleet))
>>> markets = list(ship.markets)
A Market
object can be created from a Waypoint
or waypoint.symbol
, if the Waypoint
is also a Market
. This can be convenient when jumping around between object types.
>>> ship = next(iter(agent.fleet))
>>> market = ship.markets(waypoint_symbol='MARKET_WAYPOINT_SYMBOL')
>>> market_as_a_waypoint = ship.waypoints(waypoint_symbol='MARKET_WAYPOINT_SYMBOL')
>>> market = ship.markets(waypoint=market_as_a_waypoint)
which can be converted back to a Waypoint
like so
>>> waypoint = ship.waypoints(waypoint=market)
This is helpful if you're iterating over Asteroid
s and an Asteroid
is also a Market
, for instance.
You can find which Markets
import, exchange, or export specific goods via their respective iterable methods.
>>> ship = next(iter(agent.fleet))
>>> iron_importers = list(ship.markets.imports('IRON'))
>>> iron_exports = list(ship.markets.exports('IRON'))
>>> iron_exchanges = list(ship.markets.exchanges('IRON'))
Imports, Exports, Exchange
imports
, exports
, and exchange
is a bit a misnomer with regards to what you can do at a Market
. A ship
can always purchase a good at a Market
that has the good listed as an imports
or exchange
. import
means the good is consumed at the market; export
means the good is produced at the market
; and, exchange
means the Market
does not create or use the good but simply buys/sells it from/to users.
A convenience function, ship.markets.sells
returns an iterable of all Markets
that sell the listed good. As mentioned above, if the market
sells the good, it also purchases the good.
>>> ship = next(iter(agent.fleet))
>>> iron_markets = list(ship.markets.sells('IRON'))
A Market
that imports
the good will almost always pay more than a Market
that exports
it, while a Market
that exports
a good will almost always be cheaper to purchase at than at a Market
that imports
it.
Market Data
The most important feature of a Market
is to report the current purchase price and sell price of goods. To access the data, you must have a ship
or probe
located at the waypoint. In a typical game, the first thing you do is purchase enough cheap probes
to park at every Market
.
With a ship
or probe
located at a Market
, you can begin tracking the live prices and transactions two ways.
Either by calling ship.markets()
directly,
>>> ship = next(iter(agent.fleet))
>>> markets_data = [ship.markets() for ship in agent.fleet if ship.at_market]
or by calling the .data
property of a Market
obect
>>> ship = next(iter(agent.fleet))
>>> markets_data = [market.data for market in ship.markets]
Note how the first version, using ship.markets()
, checked if a ship
was located at the Market
before getting the Market
's data. This is unnecessary as calling either ship.markets()
or market.data
will return the default Market
data if no ship
or probe
is located at the Market
Waypoint
.
The default Market
data will contain what the Market
exports, imports, or exchanges but will not contain recent transactions, trade volumes, or prices.
Once you've parked enough probe
s at enough Markets
, you can begin trading on arbitrage and making credits to buy more probe
s, etc. There are some helper functions located in the snisp.markets
file but I'll leave that exercise to the reader.
Fuel Stations
It is worth pointing out there is a Markets.fuel_stations
method that is meant to be a convenient lookup for Markets
that export or exchange FUEL. For instance, you can find the closest Market
that allows refueling by calling
>>> ship = next(iter(agent.fleet))
>>> closest_fuel = ship.closet(ship.markets.fuel_stations())
>>> ship.navigate(closest_fuel)
>>> ship.refuel()
The fuel_stations
method will take a considerable amount of time to run initially but all subsequent calls in the same session will cached. The internal SnakesInSpace cache is covered more in depth later on.
Shipyards
Like Markets
, all Shipyards
are Waypoints
but not all Waypoints
are Shipyards
. All Shipyards
are Markets
but not all Markets
are Shipyards
.
You can find all Shipyards
in a ship
s System by iterating over the ship.shipyards
>>> ship = next(iter(agent.fleet))
>>> shipyards = list(ship.shipyards)
Like a Market
, a Shipyard
with a ship
or probe
located at it can reveal additional information by calling .data
.
>>> ship = next(iter(agent.fleet))
>>> shipyard = next(iter(ship.shipyards)) # Assumes a Ship or Drone is located at the Waypoint
>>> shipyard.data # the good stuff
To purchase additional Ships
, Probes
, Drones
, and Shuttles
, call the .purchase
method on the Shipyard
object with an acceptable ship_type
. You can see all available ship_type
s in snisp.utils.SHIP_TYPES
.
>>> ship = next(iter(agent.fleet))
>>> shipyard = ship.closest(ship.shipyards)
>>> ship.autopilot(shipyard)
>>> snisp.utils.ilen(agent.fleet)
2
>>> probe = ship.purchase('SHIP_PROBE')
>>> snisp.utils.ilen(agent.fleet)
3
If you attempt to purchase a ship
at a Shipyard
that does not have any Probes
or Ships
located at it, a snisp.exceptions.NoShipAtLocationError
will be raised. If you attempt to purchase a ship_type
that is not sold at the Shipyard
, an Exception will be raised.
The Shipyards
class does support autopurchase
, like in Markets
. This is a convenience method to purchase additional Ships
by type. autopurchase
requires a ship_type
kwarg and can also take optional kwargs of max_units
and buffer
, with defaults of 1 and 300,000, respectively. The buffer
works like the buffer in ship.autopurchase
in that you will be able to purchase up to max_units
so long as your current agent.data.credits
- buffer
>= purchase price. To remove the buffer
, just pass a 0.
>>> ship = next(iter(agent.fleet))
>>> snisp.utils.ilen(agent.fleet)
2
>>> transactions = ship.shipyards.autopurchase(ship_type='SHIP_PROBE', max_units=5)
>>> for ship in transactions:
... print(ship.frame.symbol)
FRAME_PROBE
FRAME_PROBE
>>> snisp.utils.ilen(agent.fleet)
4
You can see in the above example the user could only afford two Probes
before hitting the buffer limit.
ship.shipyards.autopurchase
does come with a caveat, namely, it relies on a ship
or probe
being at the Shipyard
Waypoint
in order to be able to access the Shipyard
's Market
data to get available ships and ship price. This means if you don't have any probes
or ships
at a Shipyard
that sells SHIP_PROBES
and you call ship.shipyards.autopurchase(ship_type="SHIP_PROBE")
, nothing will happen.
As a convenience, you can see the available ship
s in a Shipyard
by calling the .available_ships
method. The method accepts an optional ship_type
if you wanted to check if the Shipyard
sold Probes
, for instance.
>>> ship = next(iter(agent.fleet))
>>> shipyard = next(iter(ship.shipyards)) # Assumes a Ship or Drone is located at the Waypoint
>>> all_available_ships = list(shipyard.available_ships())
>>> available_probes = list(shipyard.available_ships('SHIP_PROBE))
SpaceTraders does not limit the supply of ships available for purchase in a Shipyard
. As long as one ship of a ship_type
is available, you can purchase as many as your credits will allow.
The ship
objects returned by .available_ships
do have a convenience method of purchase
, which works like Shipyard.purchase
except the ship_type
is the ship
s type by default. Meaning, if the type of the ship
returned by .available_ships
is a probe
and you call .purchase()
on it, a probe
will be purchased. This can be handy for manually iterating over the ship
s being sold at the Shipyard
and purchasing them on demand.
>>> ship = next(iter(agent.fleet))
>>> shipyard = next(iter(ship.shipyards)) # Assumes a Ship or Drone is located at the Waypoint
>>> for ship in shipyard.available_ships():
... if ship.purchase_price < 10_000:
... ship.purchase()
Construction Sites
Select Waypoints
will need construction materials delivered to them before they'll function correctly. Currently, all JumpGate
s in new Systems will need to be completed before you'll be able to use them to jump
between Systems.
The ConstructionSite
will list the required materials in its .materials
.
>>> ship = next(iter(agent.fleet))
>>> construction_sites = list(ship.waypoints.construction_sites())
>>> all(not construction_site.is_complete for construction_site in construction_sites)
True
>>> construction_sites[0]
ConstructionSite({'symbol': 'X1-CC27-I56', 'materials': [{'tradeSymbol': 'FAB_MATS', 'required': 4000, 'fulfilled': 0}, {'tradeSymbol': 'ADVANCED_CIRCUITRY', 'required': 1200, 'fulfilled': 0}, {'tradeSymbol': 'QUANTUM_STABILIZERS', 'required': 1, 'fulfilled': 1}], 'isComplete': False, 'systemSymbol': 'X1-CC27'})
>>> jump_gate = ship.waypoints.get(waypoint_symbol=construction_sites[0].symbol)
>>> jump_gate
JumpGate({'systemSymbol': 'X1-CC27', 'symbol': 'X1-CC27-I56', 'type': 'JUMP_GATE', 'x': -335, 'y': 298, 'orbitals': [], 'traits': [{'symbol': 'MARKETPLACE', 'name': 'Marketplace', 'description': 'A thriving center of commerce where traders from across the galaxy gather to buy, sell, and exchange goods.'}], 'modifiers': [], 'chart': {'submittedBy': 'COSMIC', 'submittedOn': '2024-03-10T02:51:05.063Z'}, 'faction': {'symbol': 'COSMIC'}, 'isUnderConstruction': True})
>>> jump_gate.is_under_construction
True
>>> jump_gate.symbol == construction_sites[0].symbol
True
This particular JumpGate
won't become functional until 4,000 Units of FAB_MATS
and 1,200 Units of ADVANCED_CIRCUITRY
has been delivered to it.
You can supply materials to a ConstructionSite
in much the same way as you can deliver materials to a Contract
. Diffferences being, a ConstructionSite
uses .supply
instead of .deliver
and a ConstructionSite
requires kwargs of ship
, trade_symbol
, and units
.
>>> ship = next(iter(agent.fleet))
>>> construction_site = next(iter(ship.waypoints.construction_sites()))
>>> construction_site
ConstructionSite({'symbol': 'X1-CC27-I56', 'materials': [{'tradeSymbol': 'FAB_MATS', 'required': 4000, 'fulfilled': 0}, {'tradeSymbol': 'ADVANCED_CIRCUITRY', 'required': 1200, 'fulfilled': 0}, {'tradeSymbol': 'QUANTUM_STABILIZERS', 'required': 1, 'fulfilled': 1}], 'isComplete': False, 'systemSymbol': 'X1-CC27'})
>>> ship.autopilot(construction_site)
>>> ship.cargo.inventory
[Inventory({'symbol': 'FAB_MATS', 'name': 'Fab Mats', 'description': 'DESCRIPTION', 'units': 40})]
>>> construction_site.supply(ship=ship, trade_symbol='FAB_MATS', units=40)
>>> construction_site
ConstructionSite({'symbol': 'X1-CC27-I56', 'materials': [{'tradeSymbol': 'FAB_MATS', 'required': 4000, 'fulfilled': 40}, {'tradeSymbol': 'ADVANCED_CIRCUITRY', 'required': 1200, 'fulfilled': 0}, {'tradeSymbol': 'QUANTUM_STABILIZERS', 'required': 1, 'fulfilled': 1}], 'isComplete': False, 'systemSymbol': 'X1-CC27'})
>>> ship.cargo
Cargo({'capacity': 40, 'units': 0, 'inventory': []})
Once all of the materials have been supplied to the ConstructionSite
, it will no longer be returned by ship.waypoints.construction_sites()
as it is no longer .is_under_construction
.
Systems
Systems
in SpaceTraders are connected by JumpGates
and by Ships
that can warp
between them.
You can see all of the Systems
in the current SpaceTraders system by iterating over the agent
's .systems
>>> agent = Agent(symbol='your_symbol_here', faction='COSMIC', email='[email protected]')
>>> systems = list(agent.systems)
...but I wouldn't do it. There are a lot of Systems
in SpaceTraders. A lot.
Each ship
will contain the system
the ship
is located in it's respective .system
property. You can scan for nearby Systems
with ship.systems.scan()
method.
>>> ship = next(iter(agent.fleet))
>>> scans = list(ship.system.scan())
>>> scans[0]
StarSystem({'symbol': 'X1-HD87', 'sectorSymbol': 'X1', 'type': 'ORANGE_STAR', 'x': -22731, 'y': -8129, 'distance': 300}), StarSystem({'symbol': 'X1-MR62', 'sectorSymbol': 'X1', 'type': 'BLUE_STAR', 'x': -23151, 'y': -8498, 'distance': 761})
Threads and Blocking
SnakesInSpace will take care of all calls to .dock
, .orbit
, as well as handeling Cooldowns and making sure no actions are peformed while the ship
is in transit. The convenience of this does come at a cost: Blocking.
If you start one action in a thread and attempt to perform another action on that ship
in another thread, the Library will automatically block until the previous action has completed.
>>> from threading import Thread
>>> ship = next(iter(agent.fleet))
>>> waypoint = ship.farthest(ship.waypoints)
>>> t = Thread(target=ship.autopilot, args=(waypoint,))
>>> t.start()
>>> ship.dock()
The call to ship.dock()
will block until the ship
has reached the waypoint
, which, depending on how far away the waypoint
is from the starting location, may be seconds, minutes, or hours.
To avoid unnecessary blocking, make sure to perform any ship
action in its own thread. To expand on the original example at the top of this page, you could combine the navigation, extraction, and selling into one function that can be passed off to a thread.
>>> def extract_all(ship, asteroid):
... while True:
... ship.autopilot(asteroid)
... while ship.cargo.units < ship.cargo.capacity:
... extraction = ship.extract() # ship.extract will handle Cooldowns automatically
... if extraction.units == 0:
... # Asteroid has been stripped
... return
... if extraction.symbol == 'ICE_WATER':
... ship.jettison(extraction.symbol, exctraction.units)
... ship.sell_off_cargo()
>>>
>>> command_ship = next(iter(agent.fleet))
>>> for asteroid in command_ship.waypoints.asteroids():
... threads = []
... for ship in agent.fleet.ships():
... if ship.can_mine:
... t = Thread(target=extract_all, args=(ship, asteroid))
... t.start()
... threads.append(t)
... for thread in threads:
... thread.join()
>>>
This is a simple and inefficent example but it shows how well each ship
can mantain itself. Each thread will have its own ship
which will
- Navigate to the
asteroid
- Extract until the
ship
'sship.cargo
is full - Sell whatever has been extracted at the closest
markets
- Navigate back to the
asteroid
- Repeat until the
asteroid
has been depeleted, upon which it will exit the loop and return to be joined and continue on to the nextasteroid
Congratulations, you just stripped all of the asteroids
in a system
in less than 20 LOC.
Ratelimiting
SpaceTraders, a FREE game, allows two requests per second per IP with additional "bursts." The SnakesInSpace Library will automatically restrict you to two request per second per active instance. Meaning, if you run multiple clients in multiple terminals, you may run in to issues with SpaceTraders rate-limiting your IP. The SnakesInSpace rate-limiter will automatically handle these overages on your behalf, but, given this is a FREE resource, please take care to only run one to two clients at a time.
Cache
SnakesInSpace uses a rudimentary cache with a SQLite database to try and prevent any unnecessary calls to the SpaceTraders API. The current database will be located at SnakesInSpace/snisps/data/cache.db.
The cache will be reset on every login.
The cache can be ignored for now by the end user.
Tests
You can run pytest
in the current working directory for which SnakesInSpace
is located. You will also need respx
installed to run the tests.