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.
Files changed (67) hide show
  1. empire_core/__init__.py +36 -0
  2. empire_core/_archive/actions.py +511 -0
  3. empire_core/_archive/automation/__init__.py +24 -0
  4. empire_core/_archive/automation/alliance_tools.py +266 -0
  5. empire_core/_archive/automation/battle_reports.py +196 -0
  6. empire_core/_archive/automation/building_queue.py +242 -0
  7. empire_core/_archive/automation/defense_manager.py +124 -0
  8. empire_core/_archive/automation/map_scanner.py +370 -0
  9. empire_core/_archive/automation/multi_account.py +296 -0
  10. empire_core/_archive/automation/quest_automation.py +94 -0
  11. empire_core/_archive/automation/resource_manager.py +380 -0
  12. empire_core/_archive/automation/target_finder.py +153 -0
  13. empire_core/_archive/automation/tasks.py +224 -0
  14. empire_core/_archive/automation/unit_production.py +719 -0
  15. empire_core/_archive/cli.py +68 -0
  16. empire_core/_archive/client_async.py +469 -0
  17. empire_core/_archive/commands.py +201 -0
  18. empire_core/_archive/connection_async.py +228 -0
  19. empire_core/_archive/defense.py +156 -0
  20. empire_core/_archive/events/__init__.py +35 -0
  21. empire_core/_archive/events/base.py +153 -0
  22. empire_core/_archive/events/manager.py +85 -0
  23. empire_core/accounts.py +190 -0
  24. empire_core/client/__init__.py +0 -0
  25. empire_core/client/client.py +459 -0
  26. empire_core/config.py +87 -0
  27. empire_core/exceptions.py +42 -0
  28. empire_core/network/__init__.py +0 -0
  29. empire_core/network/connection.py +378 -0
  30. empire_core/protocol/__init__.py +0 -0
  31. empire_core/protocol/models/__init__.py +339 -0
  32. empire_core/protocol/models/alliance.py +186 -0
  33. empire_core/protocol/models/army.py +444 -0
  34. empire_core/protocol/models/attack.py +229 -0
  35. empire_core/protocol/models/auth.py +216 -0
  36. empire_core/protocol/models/base.py +403 -0
  37. empire_core/protocol/models/building.py +455 -0
  38. empire_core/protocol/models/castle.py +317 -0
  39. empire_core/protocol/models/chat.py +150 -0
  40. empire_core/protocol/models/defense.py +300 -0
  41. empire_core/protocol/models/map.py +269 -0
  42. empire_core/protocol/packet.py +104 -0
  43. empire_core/services/__init__.py +31 -0
  44. empire_core/services/alliance.py +222 -0
  45. empire_core/services/base.py +107 -0
  46. empire_core/services/castle.py +221 -0
  47. empire_core/state/__init__.py +0 -0
  48. empire_core/state/manager.py +398 -0
  49. empire_core/state/models.py +215 -0
  50. empire_core/state/quest_models.py +60 -0
  51. empire_core/state/report_models.py +115 -0
  52. empire_core/state/unit_models.py +75 -0
  53. empire_core/state/world_models.py +269 -0
  54. empire_core/storage/__init__.py +1 -0
  55. empire_core/storage/database.py +237 -0
  56. empire_core/utils/__init__.py +0 -0
  57. empire_core/utils/battle_sim.py +172 -0
  58. empire_core/utils/calculations.py +170 -0
  59. empire_core/utils/crypto.py +8 -0
  60. empire_core/utils/decorators.py +69 -0
  61. empire_core/utils/enums.py +111 -0
  62. empire_core/utils/helpers.py +252 -0
  63. empire_core/utils/response_awaiter.py +153 -0
  64. empire_core/utils/troops.py +93 -0
  65. empire_core-0.7.3.dist-info/METADATA +197 -0
  66. empire_core-0.7.3.dist-info/RECORD +67 -0
  67. 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