-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_roles.py
More file actions
196 lines (137 loc) · 8.37 KB
/
Copy pathtest_roles.py
File metadata and controls
196 lines (137 loc) · 8.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""Fleet role-classification (roles.py).
The engine used to split the fleet on cargo capacity alone — "has a hold ⇒
trade/contract, else scout" — which mis-cast a mining drone (15-unit hold) as a hauler
and never ran it on the survey→extract loop. These cover the capability-based split that
fixes it, including the capital-base guard so the starting fleet's behaviour is preserved.
``roles`` has no relative imports, so it tests host-free (``import roles``) like
``client.py`` — no plugin package, no network.
"""
import roles
def _ship(symbol, *, cap, mounts=(), ):
return {"symbol": symbol, "cargo": {"capacity": cap},
"mounts": [{"symbol": m} for m in mounts]}
# Representative hulls (the mounts that matter for classification).
DRONE = _ship("DRONE-1", cap=15, mounts=["MOUNT_MINING_LASER_I"])
HAULER = _ship("HAULER-1", cap=40, mounts=["MOUNT_CARGO_HOLD_II"])
FRIGATE = _ship("FRIGATE-1", cap=40, mounts=["MOUNT_MINING_LASER_I", "MOUNT_SURVEYOR_I"])
PROBE = _ship("PROBE-1", cap=0, mounts=["MOUNT_SENSOR_ARRAY_I"])
SIPHON = _ship("SIPHON-1", cap=15, mounts=["MOUNT_GAS_SIPHON_I"])
# --- can_mine ----------------------------------------------------------------------
def test_can_mine_true_for_mining_laser():
assert roles.can_mine(DRONE) is True
assert roles.can_mine(FRIGATE) is True
def test_can_mine_false_for_hauler_probe_and_siphon():
assert roles.can_mine(HAULER) is False
assert roles.can_mine(PROBE) is False
assert roles.can_mine(SIPHON) is False # gas siphon ≠ ore mining
def test_can_mine_tolerates_missing_mounts():
assert roles.can_mine({"symbol": "X"}) is False
assert roles.can_mine({"symbol": "X", "mounts": None}) is False
# --- assign_roles ------------------------------------------------------------------
def test_probe_is_classified_by_zero_hold():
r = roles.assign_roles([PROBE])
assert [s["symbol"] for s in r["probes"]] == ["PROBE-1"]
assert r["miners"] == [] and r["traders"] == []
def test_dedicated_drone_mines_when_a_hauler_covers_trade():
# The original bug, fixed: a hauler is present for contracts/trade, so the drone
# is free to MINE instead of being swept into the trade rotation.
r = roles.assign_roles([HAULER, DRONE])
assert [s["symbol"] for s in r["traders"]] == ["HAULER-1"]
assert [s["symbol"] for s in r["miners"]] == ["DRONE-1"]
def test_frigate_plus_drone_keeps_frigate_on_contracts():
# The live scenario (PROTOTRADERS): both hulls carry a mining laser. The roomier
# frigate is drafted onto contracts (capital base); the dedicated drone mines.
r = roles.assign_roles([FRIGATE, DRONE])
assert [s["symbol"] for s in r["traders"]] == ["FRIGATE-1"]
assert [s["symbol"] for s in r["miners"]] == ["DRONE-1"]
def test_lone_mine_haul_frigate_still_works_contracts():
# Fresh agent: the only ship is the mine+haul frigate. Capital-base guard promotes
# it to trader (today's proven behaviour) rather than mining with nothing on trade.
r = roles.assign_roles([FRIGATE])
assert [s["symbol"] for s in r["traders"]] == ["FRIGATE-1"]
assert r["miners"] == []
def test_first_trader_is_the_contract_worker():
# trader[0] (the contract worker) preserves acquisition order — first hauler leads.
h2 = _ship("HAULER-2", cap=40)
r = roles.assign_roles([HAULER, h2, DRONE])
assert [s["symbol"] for s in r["traders"]] == ["HAULER-1", "HAULER-2"]
assert [s["symbol"] for s in r["miners"]] == ["DRONE-1"]
def test_full_fleet_partitions_every_ship_once():
fleet = [PROBE, HAULER, FRIGATE, DRONE]
r = roles.assign_roles(fleet)
classified = [s["symbol"] for grp in r.values() for s in grp]
assert sorted(classified) == sorted(s["symbol"] for s in fleet)
# --- mining_enabled (the trade-max strategy lever) ---------------------------------
def test_mining_disabled_sends_every_hold_to_trade():
r = roles.assign_roles([FRIGATE, DRONE, HAULER], mining_enabled=False)
assert r["miners"] == []
assert sorted(s["symbol"] for s in r["traders"]) == ["DRONE-1", "FRIGATE-1", "HAULER-1"]
def test_mining_disabled_still_scouts_probes():
r = roles.assign_roles([PROBE, DRONE], mining_enabled=False)
assert [s["symbol"] for s in r["probes"]] == ["PROBE-1"]
assert [s["symbol"] for s in r["traders"]] == ["DRONE-1"]
# --- per-ship overrides (st_assign) ------------------------------------------------
def test_mine_pin_on_a_laserless_ship_routes_to_trade():
# A "mine" pin can't make a laser-less HAULER mine (no extraction mount) — it would just
# burn the rate budget on failed extracts (the J58 bug). It's routed to trade instead;
# the DRONE (a real miner) mines.
r = roles.assign_roles([HAULER, DRONE], overrides={"HAULER-1": "mine"})
assert "HAULER-1" in [s["symbol"] for s in r["traders"]]
assert "HAULER-1" not in [s["symbol"] for s in r["miners"]]
assert [s["symbol"] for s in r["miners"]] == ["DRONE-1"]
def test_mine_pin_on_a_mining_capable_ship_is_honoured():
# FRIGATE carries a mining laser, so its "mine" pin stands.
r = roles.assign_roles([FRIGATE, HAULER], overrides={"FRIGATE-1": "mine"})
assert [s["symbol"] for s in r["miners"]] == ["FRIGATE-1"]
def test_both_pinned_to_mine_only_the_laser_ship_mines():
# Pin both: the DRONE (laser) mines; the HAULER (no laser) is routed to trade, not mining.
r = roles.assign_roles([HAULER, DRONE], overrides={"HAULER-1": "mine", "DRONE-1": "mine"})
assert [s["symbol"] for s in r["miners"]] == ["DRONE-1"]
assert [s["symbol"] for s in r["traders"]] == ["HAULER-1"]
def test_override_idle_parks_a_ship():
r = roles.assign_roles([HAULER, DRONE], overrides={"DRONE-1": "idle"})
syms = [s["symbol"] for grp in r.values() for s in grp]
assert "DRONE-1" not in syms # parked — no job this window
assert [s["symbol"] for s in r["traders"]] == ["HAULER-1"]
def test_override_contract_leads_the_trader_list():
h2 = _ship("HAULER-2", cap=40)
r = roles.assign_roles([HAULER, h2], overrides={"HAULER-2": "contract"})
assert r["traders"][0]["symbol"] == "HAULER-2" # pinned contract worker leads
def test_override_scout_parks_a_hauler_as_price_feed():
r = roles.assign_roles([HAULER, DRONE], overrides={"HAULER-1": "scout"})
assert [s["symbol"] for s in r["probes"]] == ["HAULER-1"]
def test_explicit_mine_pin_is_not_drafted_for_capital_base():
# Pin the only cargo ship to mine: the capital-base guard must RESPECT it (no draft),
# unlike the auto case where a lone frigate is drafted onto contracts.
r = roles.assign_roles([FRIGATE], overrides={"FRIGATE-1": "mine"})
assert [s["symbol"] for s in r["miners"]] == ["FRIGATE-1"]
assert r["traders"] == []
# --- gas siphoning (auto for dedicated drones + st_assign siphon) ------------------
def test_can_siphon_reads_the_gas_siphon_mount():
assert roles.can_siphon(SIPHON) is True
assert roles.can_siphon(HAULER) is False
assert roles.can_siphon(DRONE) is False # a mining laser is not a gas siphon
def test_dedicated_siphon_drone_auto_siphons():
# A gas siphon + NO mining laser ⇒ a dedicated SIPHON_DRONE — auto-classified as a siphoner.
r = roles.assign_roles([SIPHON, HAULER])
assert [s["symbol"] for s in r["siphoners"]] == ["SIPHON-1"]
assert "SIPHON-1" not in [s["symbol"] for s in r["traders"]]
assert "HAULER-1" in [s["symbol"] for s in r["traders"]]
def test_frigate_with_both_mounts_is_not_auto_siphoned():
# The COMMAND frigate has a laser AND a siphon — it stays a miner/contract draftee, never
# auto-siphoned (so it isn't stolen from the capital base).
r = roles.assign_roles([FRIGATE, HAULER])
assert "FRIGATE-1" not in [s["symbol"] for s in r["siphoners"]]
assert "FRIGATE-1" in [s["symbol"] for s in r["miners"]]
def test_mining_disabled_turns_off_auto_siphon_too():
r = roles.assign_roles([SIPHON, HAULER], mining_enabled=False)
assert r["siphoners"] == []
assert "SIPHON-1" in [s["symbol"] for s in r["traders"]]
def test_siphon_pin_on_a_capable_ship():
r = roles.assign_roles([SIPHON, HAULER], overrides={"SIPHON-1": "siphon"})
assert [s["symbol"] for s in r["siphoners"]] == ["SIPHON-1"]
assert "SIPHON-1" not in [s["symbol"] for s in r["traders"]]
def test_siphon_pin_on_a_non_siphon_ship_routes_to_trade():
r = roles.assign_roles([HAULER, DRONE], overrides={"HAULER-1": "siphon"})
assert r["siphoners"] == []
assert "HAULER-1" in [s["symbol"] for s in r["traders"]]