empire-core 0.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- empire_core/__init__.py +36 -0
- empire_core/_archive/actions.py +511 -0
- empire_core/_archive/automation/__init__.py +24 -0
- empire_core/_archive/automation/alliance_tools.py +266 -0
- empire_core/_archive/automation/battle_reports.py +196 -0
- empire_core/_archive/automation/building_queue.py +242 -0
- empire_core/_archive/automation/defense_manager.py +124 -0
- empire_core/_archive/automation/map_scanner.py +370 -0
- empire_core/_archive/automation/multi_account.py +296 -0
- empire_core/_archive/automation/quest_automation.py +94 -0
- empire_core/_archive/automation/resource_manager.py +380 -0
- empire_core/_archive/automation/target_finder.py +153 -0
- empire_core/_archive/automation/tasks.py +224 -0
- empire_core/_archive/automation/unit_production.py +719 -0
- empire_core/_archive/cli.py +68 -0
- empire_core/_archive/client_async.py +469 -0
- empire_core/_archive/commands.py +201 -0
- empire_core/_archive/connection_async.py +228 -0
- empire_core/_archive/defense.py +156 -0
- empire_core/_archive/events/__init__.py +35 -0
- empire_core/_archive/events/base.py +153 -0
- empire_core/_archive/events/manager.py +85 -0
- empire_core/accounts.py +190 -0
- empire_core/client/__init__.py +0 -0
- empire_core/client/client.py +459 -0
- empire_core/config.py +87 -0
- empire_core/exceptions.py +42 -0
- empire_core/network/__init__.py +0 -0
- empire_core/network/connection.py +378 -0
- empire_core/protocol/__init__.py +0 -0
- empire_core/protocol/models/__init__.py +339 -0
- empire_core/protocol/models/alliance.py +186 -0
- empire_core/protocol/models/army.py +444 -0
- empire_core/protocol/models/attack.py +229 -0
- empire_core/protocol/models/auth.py +216 -0
- empire_core/protocol/models/base.py +403 -0
- empire_core/protocol/models/building.py +455 -0
- empire_core/protocol/models/castle.py +317 -0
- empire_core/protocol/models/chat.py +150 -0
- empire_core/protocol/models/defense.py +300 -0
- empire_core/protocol/models/map.py +269 -0
- empire_core/protocol/packet.py +104 -0
- empire_core/services/__init__.py +31 -0
- empire_core/services/alliance.py +222 -0
- empire_core/services/base.py +107 -0
- empire_core/services/castle.py +221 -0
- empire_core/state/__init__.py +0 -0
- empire_core/state/manager.py +398 -0
- empire_core/state/models.py +215 -0
- empire_core/state/quest_models.py +60 -0
- empire_core/state/report_models.py +115 -0
- empire_core/state/unit_models.py +75 -0
- empire_core/state/world_models.py +269 -0
- empire_core/storage/__init__.py +1 -0
- empire_core/storage/database.py +237 -0
- empire_core/utils/__init__.py +0 -0
- empire_core/utils/battle_sim.py +172 -0
- empire_core/utils/calculations.py +170 -0
- empire_core/utils/crypto.py +8 -0
- empire_core/utils/decorators.py +69 -0
- empire_core/utils/enums.py +111 -0
- empire_core/utils/helpers.py +252 -0
- empire_core/utils/response_awaiter.py +153 -0
- empire_core/utils/troops.py +93 -0
- empire_core-0.7.3.dist-info/METADATA +197 -0
- empire_core-0.7.3.dist-info/RECORD +67 -0
- empire_core-0.7.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource management and auto-balancing between castles.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from empire_core.state.models import Castle
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from empire_core.client.client import EmpireClient
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ResourceTransfer:
|
|
20
|
+
"""A pending resource transfer."""
|
|
21
|
+
|
|
22
|
+
source_castle_id: int
|
|
23
|
+
target_castle_id: int
|
|
24
|
+
wood: int = 0
|
|
25
|
+
stone: int = 0
|
|
26
|
+
food: int = 0
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def total(self) -> int:
|
|
30
|
+
return self.wood + self.stone + self.food
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_empty(self) -> bool:
|
|
34
|
+
return self.total == 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CastleResourceStatus:
|
|
39
|
+
"""Resource status of a castle."""
|
|
40
|
+
|
|
41
|
+
castle_id: int
|
|
42
|
+
castle_name: str
|
|
43
|
+
wood: int
|
|
44
|
+
stone: int
|
|
45
|
+
food: int
|
|
46
|
+
wood_cap: int
|
|
47
|
+
stone_cap: int
|
|
48
|
+
food_cap: int
|
|
49
|
+
wood_rate: float
|
|
50
|
+
stone_rate: float
|
|
51
|
+
food_rate: float
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def wood_percent(self) -> float:
|
|
55
|
+
return (self.wood / self.wood_cap * 100) if self.wood_cap > 0 else 0
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def stone_percent(self) -> float:
|
|
59
|
+
return (self.stone / self.stone_cap * 100) if self.stone_cap > 0 else 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def food_percent(self) -> float:
|
|
63
|
+
return (self.food / self.food_cap * 100) if self.food_cap > 0 else 0
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_near_capacity(self) -> bool:
|
|
67
|
+
"""Check if any resource is above 90% capacity."""
|
|
68
|
+
return self.wood_percent > 90 or self.stone_percent > 90 or self.food_percent > 90
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_low(self) -> bool:
|
|
72
|
+
"""Check if any resource is below 20% capacity."""
|
|
73
|
+
return self.wood_percent < 20 or self.stone_percent < 20 or self.food_percent < 20
|
|
74
|
+
|
|
75
|
+
def get_excess(self, threshold_percent: float = 70) -> Dict[str, int]:
|
|
76
|
+
"""Get excess resources above threshold."""
|
|
77
|
+
excess = {}
|
|
78
|
+
threshold_wood = int(self.wood_cap * threshold_percent / 100)
|
|
79
|
+
threshold_stone = int(self.stone_cap * threshold_percent / 100)
|
|
80
|
+
threshold_food = int(self.food_cap * threshold_percent / 100)
|
|
81
|
+
|
|
82
|
+
if self.wood > threshold_wood:
|
|
83
|
+
excess["wood"] = self.wood - threshold_wood
|
|
84
|
+
if self.stone > threshold_stone:
|
|
85
|
+
excess["stone"] = self.stone - threshold_stone
|
|
86
|
+
if self.food > threshold_food:
|
|
87
|
+
excess["food"] = self.food - threshold_food
|
|
88
|
+
|
|
89
|
+
return excess
|
|
90
|
+
|
|
91
|
+
def get_deficit(self, threshold_percent: float = 50) -> Dict[str, int]:
|
|
92
|
+
"""Get resource deficit below threshold."""
|
|
93
|
+
deficit = {}
|
|
94
|
+
threshold_wood = int(self.wood_cap * threshold_percent / 100)
|
|
95
|
+
threshold_stone = int(self.stone_cap * threshold_percent / 100)
|
|
96
|
+
threshold_food = int(self.food_cap * threshold_percent / 100)
|
|
97
|
+
|
|
98
|
+
if self.wood < threshold_wood:
|
|
99
|
+
deficit["wood"] = threshold_wood - self.wood
|
|
100
|
+
if self.stone < threshold_stone:
|
|
101
|
+
deficit["stone"] = threshold_stone - self.stone
|
|
102
|
+
if self.food < threshold_food:
|
|
103
|
+
deficit["food"] = threshold_food - self.food
|
|
104
|
+
|
|
105
|
+
return deficit
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ResourceManager:
|
|
109
|
+
"""
|
|
110
|
+
Manages resources across multiple castles.
|
|
111
|
+
|
|
112
|
+
Features:
|
|
113
|
+
- Monitor resource levels across all castles
|
|
114
|
+
- Auto-balance resources between castles
|
|
115
|
+
- Priority-based resource allocation
|
|
116
|
+
- Overflow protection (send resources before cap)
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# Default thresholds
|
|
120
|
+
OVERFLOW_THRESHOLD = 85 # Send resources when above this %
|
|
121
|
+
LOW_THRESHOLD = 30 # Castle needs resources when below this %
|
|
122
|
+
TARGET_THRESHOLD = 60 # Target level after balancing
|
|
123
|
+
|
|
124
|
+
def __init__(self, client: "EmpireClient"):
|
|
125
|
+
self.client = client
|
|
126
|
+
self._auto_balance_enabled = False
|
|
127
|
+
self._balance_interval = 300 # 5 minutes
|
|
128
|
+
self._running = False
|
|
129
|
+
self._priority_castles: List[int] = [] # Castles that get resources first
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def castles(self) -> Dict[int, Castle]:
|
|
133
|
+
"""Get player's castles."""
|
|
134
|
+
player = self.client.state.local_player
|
|
135
|
+
if player:
|
|
136
|
+
return player.castles
|
|
137
|
+
return {}
|
|
138
|
+
|
|
139
|
+
def set_priority_castle(self, castle_id: int):
|
|
140
|
+
"""Set a castle as high priority for receiving resources."""
|
|
141
|
+
if castle_id not in self._priority_castles:
|
|
142
|
+
self._priority_castles.append(castle_id)
|
|
143
|
+
|
|
144
|
+
def remove_priority_castle(self, castle_id: int):
|
|
145
|
+
"""Remove castle from priority list."""
|
|
146
|
+
if castle_id in self._priority_castles:
|
|
147
|
+
self._priority_castles.remove(castle_id)
|
|
148
|
+
|
|
149
|
+
def get_castle_status(self, castle_id: int) -> Optional[CastleResourceStatus]:
|
|
150
|
+
"""Get resource status for a specific castle."""
|
|
151
|
+
castle = self.castles.get(castle_id)
|
|
152
|
+
if not castle:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
r = castle.resources
|
|
156
|
+
return CastleResourceStatus(
|
|
157
|
+
castle_id=castle_id,
|
|
158
|
+
castle_name=castle.name,
|
|
159
|
+
wood=r.wood,
|
|
160
|
+
stone=r.stone,
|
|
161
|
+
food=r.food,
|
|
162
|
+
wood_cap=r.wood_cap,
|
|
163
|
+
stone_cap=r.stone_cap,
|
|
164
|
+
food_cap=r.food_cap,
|
|
165
|
+
wood_rate=r.wood_rate,
|
|
166
|
+
stone_rate=r.stone_rate,
|
|
167
|
+
food_rate=r.food_rate,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def get_all_status(self) -> List[CastleResourceStatus]:
|
|
171
|
+
"""Get resource status for all castles."""
|
|
172
|
+
statuses = []
|
|
173
|
+
for castle_id in self.castles:
|
|
174
|
+
status = self.get_castle_status(castle_id)
|
|
175
|
+
if status:
|
|
176
|
+
statuses.append(status)
|
|
177
|
+
return statuses
|
|
178
|
+
|
|
179
|
+
def get_overflow_castles(self, threshold: float = OVERFLOW_THRESHOLD) -> List[CastleResourceStatus]:
|
|
180
|
+
"""Get castles with resources above threshold."""
|
|
181
|
+
return [
|
|
182
|
+
s
|
|
183
|
+
for s in self.get_all_status()
|
|
184
|
+
if s.wood_percent > threshold or s.stone_percent > threshold or s.food_percent > threshold
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
def get_low_castles(self, threshold: float = LOW_THRESHOLD) -> List[CastleResourceStatus]:
|
|
188
|
+
"""Get castles with resources below threshold."""
|
|
189
|
+
return [
|
|
190
|
+
s
|
|
191
|
+
for s in self.get_all_status()
|
|
192
|
+
if s.wood_percent < threshold or s.stone_percent < threshold or s.food_percent < threshold
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
def calculate_transfers(
|
|
196
|
+
self,
|
|
197
|
+
overflow_threshold: float = OVERFLOW_THRESHOLD,
|
|
198
|
+
low_threshold: float = LOW_THRESHOLD,
|
|
199
|
+
target_threshold: float = TARGET_THRESHOLD,
|
|
200
|
+
) -> List[ResourceTransfer]:
|
|
201
|
+
"""
|
|
202
|
+
Calculate optimal resource transfers between castles.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of ResourceTransfer objects
|
|
206
|
+
"""
|
|
207
|
+
transfers: List[ResourceTransfer] = []
|
|
208
|
+
statuses = self.get_all_status()
|
|
209
|
+
|
|
210
|
+
if len(statuses) < 2:
|
|
211
|
+
return transfers # Need at least 2 castles to balance
|
|
212
|
+
|
|
213
|
+
# Find sources (overflow) and targets (low)
|
|
214
|
+
sources = []
|
|
215
|
+
targets = []
|
|
216
|
+
|
|
217
|
+
for status in statuses:
|
|
218
|
+
excess = status.get_excess(overflow_threshold)
|
|
219
|
+
deficit = status.get_deficit(low_threshold)
|
|
220
|
+
|
|
221
|
+
if excess:
|
|
222
|
+
sources.append((status, excess))
|
|
223
|
+
if deficit:
|
|
224
|
+
targets.append((status, deficit))
|
|
225
|
+
|
|
226
|
+
# Prioritize targets
|
|
227
|
+
for castle_id in self._priority_castles:
|
|
228
|
+
for i, (status, deficit) in enumerate(targets):
|
|
229
|
+
if status.castle_id == castle_id:
|
|
230
|
+
targets.insert(0, targets.pop(i))
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
# Match sources to targets
|
|
234
|
+
for target_status, deficit in targets:
|
|
235
|
+
for source_status, excess in sources:
|
|
236
|
+
if source_status.castle_id == target_status.castle_id:
|
|
237
|
+
continue # Can't transfer to self
|
|
238
|
+
|
|
239
|
+
transfer = ResourceTransfer(
|
|
240
|
+
source_castle_id=source_status.castle_id,
|
|
241
|
+
target_castle_id=target_status.castle_id,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Calculate transfer amounts
|
|
245
|
+
for resource in ["wood", "stone", "food"]:
|
|
246
|
+
if resource in deficit and resource in excess:
|
|
247
|
+
# Transfer minimum of excess and deficit
|
|
248
|
+
amount = min(deficit[resource], excess[resource])
|
|
249
|
+
setattr(transfer, resource, amount)
|
|
250
|
+
# Update tracking
|
|
251
|
+
deficit[resource] -= amount
|
|
252
|
+
excess[resource] -= amount
|
|
253
|
+
|
|
254
|
+
if not transfer.is_empty:
|
|
255
|
+
transfers.append(transfer)
|
|
256
|
+
|
|
257
|
+
return transfers
|
|
258
|
+
|
|
259
|
+
async def execute_transfer(self, transfer: ResourceTransfer) -> bool:
|
|
260
|
+
"""Execute a resource transfer."""
|
|
261
|
+
if transfer.is_empty:
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
success = await self.client.send_transport(
|
|
266
|
+
origin_castle_id=transfer.source_castle_id,
|
|
267
|
+
target_area_id=transfer.target_castle_id,
|
|
268
|
+
wood=transfer.wood,
|
|
269
|
+
stone=transfer.stone,
|
|
270
|
+
food=transfer.food,
|
|
271
|
+
)
|
|
272
|
+
if success:
|
|
273
|
+
logger.info(
|
|
274
|
+
f"Transferred {transfer.wood}W/{transfer.stone}S/{transfer.food}F "
|
|
275
|
+
f"from {transfer.source_castle_id} to {transfer.target_castle_id}"
|
|
276
|
+
)
|
|
277
|
+
return bool(success)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Transfer failed: {e}")
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
async def auto_balance(self) -> int:
|
|
283
|
+
"""
|
|
284
|
+
Automatically balance resources across castles.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Number of transfers executed
|
|
288
|
+
"""
|
|
289
|
+
# Refresh castle data first
|
|
290
|
+
await self.client.get_detailed_castle_info()
|
|
291
|
+
await asyncio.sleep(1) # Wait for response
|
|
292
|
+
|
|
293
|
+
transfers = self.calculate_transfers()
|
|
294
|
+
|
|
295
|
+
if not transfers:
|
|
296
|
+
logger.debug("No resource transfers needed")
|
|
297
|
+
return 0
|
|
298
|
+
|
|
299
|
+
executed = 0
|
|
300
|
+
for transfer in transfers:
|
|
301
|
+
success = await self.execute_transfer(transfer)
|
|
302
|
+
if success:
|
|
303
|
+
executed += 1
|
|
304
|
+
await asyncio.sleep(0.5) # Rate limit
|
|
305
|
+
|
|
306
|
+
logger.info(f"Executed {executed}/{len(transfers)} resource transfers")
|
|
307
|
+
return executed
|
|
308
|
+
|
|
309
|
+
async def start_auto_balance(self, interval: int = 300):
|
|
310
|
+
"""
|
|
311
|
+
Start automatic resource balancing.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
interval: Balance check interval in seconds
|
|
315
|
+
"""
|
|
316
|
+
self._auto_balance_enabled = True
|
|
317
|
+
self._balance_interval = interval
|
|
318
|
+
self._running = True
|
|
319
|
+
|
|
320
|
+
logger.info(f"Auto-balance started (interval: {interval}s)")
|
|
321
|
+
|
|
322
|
+
while self._running and self._auto_balance_enabled:
|
|
323
|
+
try:
|
|
324
|
+
await self.auto_balance()
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"Auto-balance error: {e}")
|
|
327
|
+
|
|
328
|
+
await asyncio.sleep(self._balance_interval)
|
|
329
|
+
|
|
330
|
+
def stop_auto_balance(self):
|
|
331
|
+
"""Stop automatic resource balancing."""
|
|
332
|
+
self._auto_balance_enabled = False
|
|
333
|
+
self._running = False
|
|
334
|
+
logger.info("Auto-balance stopped")
|
|
335
|
+
|
|
336
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
337
|
+
"""Get summary of resource status across all castles."""
|
|
338
|
+
statuses = self.get_all_status()
|
|
339
|
+
|
|
340
|
+
if not statuses:
|
|
341
|
+
return {"castle_count": 0}
|
|
342
|
+
|
|
343
|
+
total_wood = sum(s.wood for s in statuses)
|
|
344
|
+
total_stone = sum(s.stone for s in statuses)
|
|
345
|
+
total_food = sum(s.food for s in statuses)
|
|
346
|
+
total_wood_cap = sum(s.wood_cap for s in statuses)
|
|
347
|
+
total_stone_cap = sum(s.stone_cap for s in statuses)
|
|
348
|
+
total_food_cap = sum(s.food_cap for s in statuses)
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"castle_count": len(statuses),
|
|
352
|
+
"total_wood": total_wood,
|
|
353
|
+
"total_stone": total_stone,
|
|
354
|
+
"total_food": total_food,
|
|
355
|
+
"total_capacity": {
|
|
356
|
+
"wood": total_wood_cap,
|
|
357
|
+
"stone": total_stone_cap,
|
|
358
|
+
"food": total_food_cap,
|
|
359
|
+
},
|
|
360
|
+
"overall_percent": {
|
|
361
|
+
"wood": (total_wood / total_wood_cap * 100) if total_wood_cap > 0 else 0,
|
|
362
|
+
"stone": (total_stone / total_stone_cap * 100) if total_stone_cap > 0 else 0,
|
|
363
|
+
"food": (total_food / total_food_cap * 100) if total_food_cap > 0 else 0,
|
|
364
|
+
},
|
|
365
|
+
"overflow_castles": len(self.get_overflow_castles()),
|
|
366
|
+
"low_castles": len(self.get_low_castles()),
|
|
367
|
+
"priority_castles": self._priority_castles.copy(),
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
def format_status(self) -> str:
|
|
371
|
+
"""Format resource status as readable string."""
|
|
372
|
+
lines = ["Resource Status:"]
|
|
373
|
+
for status in self.get_all_status():
|
|
374
|
+
lines.append(
|
|
375
|
+
f" {status.castle_name}:"
|
|
376
|
+
f" W:{status.wood:,}/{status.wood_cap:,} ({status.wood_percent:.0f}%)"
|
|
377
|
+
f" S:{status.stone:,}/{status.stone_cap:,} ({status.stone_percent:.0f}%)"
|
|
378
|
+
f" F:{status.food:,}/{status.food_cap:,} ({status.food_percent:.0f}%)"
|
|
379
|
+
)
|
|
380
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Target finding and world scanning.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from empire_core.state.world_models import MapObject
|
|
9
|
+
from empire_core.utils.calculations import calculate_distance
|
|
10
|
+
from empire_core.utils.enums import MapObjectType
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TargetFilter:
|
|
16
|
+
"""Filter criteria for targets."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.max_distance: Optional[float] = None
|
|
20
|
+
self.min_level: int = 0
|
|
21
|
+
self.max_level: int = 999
|
|
22
|
+
self.object_types: List[MapObjectType] = []
|
|
23
|
+
self.exclude_alliances: List[int] = []
|
|
24
|
+
self.only_inactive: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TargetFinder:
|
|
28
|
+
"""Find and evaluate targets for attacks."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, map_objects: Dict[int, MapObject]):
|
|
31
|
+
self.map_objects = map_objects
|
|
32
|
+
|
|
33
|
+
def find_targets(
|
|
34
|
+
self,
|
|
35
|
+
origin_x: int,
|
|
36
|
+
origin_y: int,
|
|
37
|
+
max_distance: float = 50.0,
|
|
38
|
+
target_type: MapObjectType = MapObjectType.CASTLE,
|
|
39
|
+
max_level: int = 10,
|
|
40
|
+
) -> List[Tuple[MapObject, float]]:
|
|
41
|
+
"""
|
|
42
|
+
Find targets near origin.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of (map_object, distance) tuples sorted by distance
|
|
46
|
+
"""
|
|
47
|
+
targets = []
|
|
48
|
+
|
|
49
|
+
for obj in self.map_objects.values():
|
|
50
|
+
if obj.type != target_type:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if obj.level > max_level:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
distance = calculate_distance(origin_x, origin_y, obj.x, obj.y)
|
|
57
|
+
|
|
58
|
+
if distance <= max_distance:
|
|
59
|
+
targets.append((obj, distance))
|
|
60
|
+
|
|
61
|
+
# Sort by distance
|
|
62
|
+
targets.sort(key=lambda t: t[1])
|
|
63
|
+
|
|
64
|
+
logger.info(f"Found {len(targets)} targets within {max_distance} distance")
|
|
65
|
+
return targets
|
|
66
|
+
|
|
67
|
+
def find_npc_camps(self, origin_x: int, origin_y: int, max_distance: float = 30.0) -> List[Tuple[MapObject, float]]:
|
|
68
|
+
"""Find NPC camps (robber camps, etc.)."""
|
|
69
|
+
npc_types = [
|
|
70
|
+
MapObjectType.NOMAD_CAMP,
|
|
71
|
+
MapObjectType.SAMURAI_CAMP,
|
|
72
|
+
MapObjectType.ALIEN_CAMP,
|
|
73
|
+
MapObjectType.FACTION_CAMP,
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
targets = []
|
|
77
|
+
for obj in self.map_objects.values():
|
|
78
|
+
if obj.type in npc_types:
|
|
79
|
+
distance = calculate_distance(origin_x, origin_y, obj.x, obj.y)
|
|
80
|
+
if distance <= max_distance:
|
|
81
|
+
targets.append((obj, distance))
|
|
82
|
+
|
|
83
|
+
targets.sort(key=lambda t: t[1])
|
|
84
|
+
return targets
|
|
85
|
+
|
|
86
|
+
def find_resources(self, origin_x: int, origin_y: int, max_distance: float = 20.0) -> List[Tuple[MapObject, float]]:
|
|
87
|
+
"""Find resource locations."""
|
|
88
|
+
targets = []
|
|
89
|
+
for obj in self.map_objects.values():
|
|
90
|
+
if obj.type == MapObjectType.ISLE_RESOURCE:
|
|
91
|
+
distance = calculate_distance(origin_x, origin_y, obj.x, obj.y)
|
|
92
|
+
if distance <= max_distance:
|
|
93
|
+
targets.append((obj, distance))
|
|
94
|
+
|
|
95
|
+
targets.sort(key=lambda t: t[1])
|
|
96
|
+
return targets
|
|
97
|
+
|
|
98
|
+
def evaluate_target(self, target: MapObject, player_level: int) -> Dict[str, Any]:
|
|
99
|
+
"""Evaluate target profitability/safety."""
|
|
100
|
+
score = 0
|
|
101
|
+
risk = "low"
|
|
102
|
+
|
|
103
|
+
# Level difference
|
|
104
|
+
level_diff = player_level - target.level
|
|
105
|
+
if level_diff > 5:
|
|
106
|
+
score += 50
|
|
107
|
+
risk = "low"
|
|
108
|
+
elif level_diff > 0:
|
|
109
|
+
score += 30
|
|
110
|
+
risk = "medium"
|
|
111
|
+
else:
|
|
112
|
+
score += 10
|
|
113
|
+
risk = "high"
|
|
114
|
+
|
|
115
|
+
# Target type bonuses
|
|
116
|
+
if target.type == MapObjectType.NOMAD_CAMP:
|
|
117
|
+
score += 40 # Good loot
|
|
118
|
+
elif target.type == MapObjectType.CASTLE:
|
|
119
|
+
score += 20 # Variable loot
|
|
120
|
+
|
|
121
|
+
return {"score": score, "risk": risk, "level_diff": level_diff, "recommended": score > 40 and risk != "high"}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class WorldScanner:
|
|
125
|
+
"""Scan and map the world."""
|
|
126
|
+
|
|
127
|
+
def __init__(self):
|
|
128
|
+
self.scanned_chunks: set = set()
|
|
129
|
+
|
|
130
|
+
def generate_scan_pattern(self, center_x: int, center_y: int, radius: int = 10) -> List[Tuple[int, int]]:
|
|
131
|
+
"""Generate spiral scan pattern."""
|
|
132
|
+
coords = []
|
|
133
|
+
|
|
134
|
+
# Spiral outward
|
|
135
|
+
for r in range(1, radius + 1):
|
|
136
|
+
for x in range(center_x - r, center_x + r + 1):
|
|
137
|
+
coords.append((x, center_y - r))
|
|
138
|
+
coords.append((x, center_y + r))
|
|
139
|
+
for y in range(center_y - r + 1, center_y + r):
|
|
140
|
+
coords.append((center_x - r, y))
|
|
141
|
+
coords.append((center_x + r, y))
|
|
142
|
+
|
|
143
|
+
return coords
|
|
144
|
+
|
|
145
|
+
def mark_scanned(self, kingdom_id: int, chunk_x: int, chunk_y: int):
|
|
146
|
+
"""Mark chunk as scanned."""
|
|
147
|
+
key = f"{kingdom_id}:{chunk_x}:{chunk_y}"
|
|
148
|
+
self.scanned_chunks.add(key)
|
|
149
|
+
|
|
150
|
+
def is_scanned(self, kingdom_id: int, chunk_x: int, chunk_y: int) -> bool:
|
|
151
|
+
"""Check if chunk is scanned."""
|
|
152
|
+
key = f"{kingdom_id}:{chunk_x}:{chunk_y}"
|
|
153
|
+
return key in self.scanned_chunks
|