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,124 @@
1
+ """
2
+ Defense management and automation for castles.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Dict, Optional
9
+
10
+ if TYPE_CHECKING:
11
+ from empire_core.client.client import EmpireClient
12
+ from empire_core.state.models import Castle
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class DefensePreset:
19
+ """A predefined defense configuration."""
20
+
21
+ name: str
22
+ wall_left_tools: Dict[int, int] = field(default_factory=dict)
23
+ wall_middle_tools: Dict[int, int] = field(default_factory=dict)
24
+ wall_right_tools: Dict[int, int] = field(default_factory=dict)
25
+ wall_left_units_up: int = 0
26
+ wall_left_units_count: int = 0
27
+ wall_middle_units_up: int = 0
28
+ wall_middle_units_count: int = 0
29
+ wall_right_units_up: int = 0
30
+ wall_right_units_count: int = 0
31
+ moat_left_slots: Dict[int, int] = field(default_factory=dict)
32
+ moat_middle_slots: Dict[int, int] = field(default_factory=dict)
33
+ moat_right_slots: Dict[int, int] = field(default_factory=dict)
34
+
35
+
36
+ class DefenseManager:
37
+ """
38
+ Manages defense configurations for castles.
39
+
40
+ Features:
41
+ - Apply predefined defense presets to castles.
42
+ - Monitor current defense and apply changes.
43
+ """
44
+
45
+ def __init__(self, client: "EmpireClient"):
46
+ self.client = client
47
+ self.presets: Dict[str, DefensePreset] = {}
48
+
49
+ def add_preset(self, preset: DefensePreset):
50
+ """Add a defense preset."""
51
+ self.presets[preset.name] = preset
52
+ logger.info(f"Defense preset '{preset.name}' added.")
53
+
54
+ def get_preset(self, name: str) -> Optional[DefensePreset]:
55
+ """Get a defense preset by name."""
56
+ return self.presets.get(name)
57
+
58
+ async def apply_defense_preset(self, castle_id: int, preset_name: str) -> bool:
59
+ """
60
+ Apply a named defense preset to a specific castle.
61
+
62
+ Args:
63
+ castle_id: The ID of the castle to apply the preset to.
64
+ preset_name: The name of the defense preset.
65
+
66
+ Returns:
67
+ bool: True if the preset was successfully applied, False otherwise.
68
+ """
69
+ preset = self.get_preset(preset_name)
70
+ if not preset:
71
+ logger.warning(f"Defense preset '{preset_name}' not found.")
72
+ return False
73
+
74
+ castle: Optional["Castle"] = (
75
+ self.client.state.local_player.castles.get(castle_id) if self.client.state.local_player else None
76
+ )
77
+ if not castle:
78
+ logger.error(f"Castle {castle_id} not found in state.")
79
+ return False
80
+
81
+ logger.info(f"Applying defense preset '{preset_name}' to castle {castle.name}...")
82
+
83
+ # Apply wall defense
84
+ wall_success = await self.client.defense.set_wall_defense(
85
+ castle_id=castle.id,
86
+ castle_x=castle.x,
87
+ castle_y=castle.y,
88
+ left_tools=preset.wall_left_tools,
89
+ middle_tools=preset.wall_middle_tools,
90
+ right_tools=preset.wall_right_tools,
91
+ left_units_up=preset.wall_left_units_up,
92
+ left_units_count=preset.wall_left_units_count,
93
+ middle_units_up=preset.wall_middle_units_up,
94
+ middle_units_count=preset.wall_middle_units_count,
95
+ right_units_up=preset.wall_right_units_up,
96
+ right_units_count=preset.wall_right_units_count,
97
+ wait_for_response=True, # Always wait for critical defense commands
98
+ )
99
+ if not wall_success:
100
+ logger.error(f"Failed to apply wall defense for castle {castle_id}.")
101
+ return False
102
+
103
+ await asyncio.sleep(0.5) # Rate limit
104
+
105
+ # Apply moat defense
106
+ moat_success = await self.client.defense.set_moat_defense(
107
+ castle_id=castle.id,
108
+ castle_x=castle.x,
109
+ castle_y=castle.y,
110
+ left_slots=preset.moat_left_slots,
111
+ middle_slots=preset.moat_middle_slots,
112
+ right_slots=preset.moat_right_slots,
113
+ wait_for_response=True, # Always wait for critical defense commands
114
+ )
115
+ if not moat_success:
116
+ logger.error(f"Failed to apply moat defense for castle {castle_id}.")
117
+ return False
118
+
119
+ logger.info(f"Defense preset '{preset_name}' successfully applied to castle {castle.name}.")
120
+ return True
121
+
122
+ # TODO: Add methods to read current defense configuration for verification
123
+ # TODO: Add logic for 'auto-defense' based on incoming attacks or threats
124
+ # TODO: Implement dfk (keep defense) if protocol is found
@@ -0,0 +1,370 @@
1
+ """
2
+ Map scanning and exploration automation with asynchronous database persistence.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from dataclasses import dataclass, field
9
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple
10
+
11
+ from empire_core.events.base import PacketEvent
12
+ from empire_core.state.world_models import MapObject
13
+ from empire_core.utils.calculations import calculate_distance
14
+ from empire_core.utils.enums import MapObjectType
15
+
16
+ if TYPE_CHECKING:
17
+ from empire_core.client.client import EmpireClient
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # The step between chunks is exactly 13 tiles (index 0-12 inclusive)
22
+ MAP_STEP = 13
23
+
24
+
25
+ @dataclass
26
+ class ScanResult:
27
+ """Result of a map scan."""
28
+
29
+ kingdom_id: int
30
+ chunks_scanned: int
31
+ objects_found: int
32
+ duration: float
33
+ targets_by_type: Dict[str, int] = field(default_factory=dict)
34
+
35
+
36
+ @dataclass
37
+ class ScanProgress:
38
+ """Progress of an ongoing scan."""
39
+
40
+ total_chunks: int
41
+ completed_chunks: int
42
+ current_x: int
43
+ current_y: int
44
+ objects_found: int
45
+
46
+ @property
47
+ def percent_complete(self) -> float:
48
+ if self.total_chunks == 0:
49
+ return 0.0
50
+ return (self.completed_chunks / self.total_chunks) * 100
51
+
52
+
53
+ class MapScanner:
54
+ """
55
+ Automated map scanning with intelligent chunk management and persistence.
56
+
57
+ Features:
58
+ - High-speed spiral scan pattern aligned to 13x13 grid
59
+ - Persistent chunk caching in SQLite database (async)
60
+ - Real-time database persistence via packet events
61
+ - Progress callbacks for UI updates
62
+ - Database-backed target discovery
63
+ """
64
+
65
+ def __init__(self, client: "EmpireClient"):
66
+ self.client = client
67
+ self._scanned_chunks: Dict[int, Set[Tuple[int, int]]] = {} # kingdom -> chunk_coordinates
68
+ self._progress_callbacks: List[Callable[[ScanProgress], None]] = []
69
+ self._running = False
70
+ self._stop_event = asyncio.Event()
71
+
72
+ async def initialize(self):
73
+ """Initialize scanner and load cache from database."""
74
+ try:
75
+ for kid in [0, 1, 2, 3, 4]:
76
+ chunks = await self.client.db.get_scanned_chunks(kid)
77
+ if chunks:
78
+ self._scanned_chunks[kid] = chunks
79
+ logger.debug(f"MapScanner: Loaded cache for {len(self._scanned_chunks)} kingdoms from DB")
80
+
81
+ # Register real-time persistence handler
82
+ self.client.events.listen(self._on_gaa_packet)
83
+ except Exception as e:
84
+ logger.warning(f"MapScanner: Failed to initialize: {e}")
85
+
86
+ async def _on_gaa_packet(self, event: PacketEvent):
87
+ """Handle incoming map data and persist immediately."""
88
+ if event.command_id == "gaa":
89
+ try:
90
+ objects = list(self.client.state.map_objects.values())
91
+ if objects:
92
+ await self.client.db.save_map_objects(objects)
93
+ except Exception as e:
94
+ logger.error(f"MapScanner: Failed to persist map objects from packet: {e}")
95
+
96
+ @property
97
+ def map_objects(self) -> Dict[int, MapObject]:
98
+ """Get all discovered map objects in current session memory."""
99
+ return self.client.state.map_objects
100
+
101
+ def get_scanned_chunk_count(self, kingdom_id: int = 0) -> int:
102
+ """Get number of scanned chunks in a kingdom."""
103
+ return len(self._scanned_chunks.get(kingdom_id, set()))
104
+
105
+ def is_chunk_scanned(self, kingdom_id: int, x: int, y: int) -> bool:
106
+ """Check if a coordinate belongs to a scanned chunk."""
107
+ chunk_x = x // MAP_STEP
108
+ chunk_y = y // MAP_STEP
109
+ return (chunk_x, chunk_y) in self._scanned_chunks.get(kingdom_id, set())
110
+
111
+ def on_progress(self, callback: Callable[[ScanProgress], None]):
112
+ """Register callback for scan progress updates."""
113
+ self._progress_callbacks.append(callback)
114
+
115
+ async def scan_area(
116
+ self,
117
+ center_x: int,
118
+ center_y: int,
119
+ radius: int = 5,
120
+ kingdom_id: int = 0,
121
+ rescan: bool = False,
122
+ quit_on_empty: Optional[int] = None,
123
+ ) -> ScanResult:
124
+ """
125
+ Scan an area around a center point at maximum speed.
126
+ """
127
+ start_time = time.time()
128
+ objects_before = len(self.map_objects)
129
+
130
+ chunks = self._generate_spiral_pattern(center_x, center_y, radius)
131
+ total_chunks = len(chunks)
132
+ completed = 0
133
+ consecutive_empty = 0
134
+
135
+ if kingdom_id not in self._scanned_chunks:
136
+ self._scanned_chunks[kingdom_id] = set()
137
+
138
+ self._running = True
139
+ self._stop_event.clear()
140
+
141
+ for chunk_x, chunk_y in chunks:
142
+ if self._stop_event.is_set():
143
+ logger.info("Scan cancelled")
144
+ break
145
+
146
+ chunk_key = (chunk_x, chunk_y)
147
+
148
+ if not rescan and chunk_key in self._scanned_chunks[kingdom_id]:
149
+ completed += 1
150
+ continue
151
+
152
+ # Calculate top-left tile coordinates for this chunk
153
+ tile_x = chunk_x * MAP_STEP
154
+ tile_y = chunk_y * MAP_STEP
155
+
156
+ try:
157
+ # Ensure we are connected
158
+ if not self.client.connection.connected or not self.client.is_logged_in:
159
+ await self.client.wait_until_ready()
160
+
161
+ objs_before_chunk = len(self.map_objects)
162
+
163
+ # GAA command uses AX1, AY1, AX2, AY2.
164
+ # To match 13-tile step, we request size 12 chunks (step-1).
165
+ await self.client.get_map_chunk(kingdom_id, tile_x, tile_y)
166
+
167
+ # Small yield
168
+ await asyncio.sleep(0)
169
+
170
+ new_in_chunk = len(self.map_objects) - objs_before_chunk
171
+ if new_in_chunk > 0:
172
+ consecutive_empty = 0
173
+ else:
174
+ consecutive_empty += 1
175
+
176
+ # Update cache and DB for chunk
177
+ self._scanned_chunks[kingdom_id].add(chunk_key)
178
+ await self.client.db.mark_chunk_scanned(kingdom_id, chunk_x, chunk_y)
179
+
180
+ if quit_on_empty and consecutive_empty >= quit_on_empty:
181
+ logger.info(f"MapScanner: Stopping early after {consecutive_empty} empty chunks.")
182
+ break
183
+
184
+ except Exception as e:
185
+ logger.warning(f"Failed to scan chunk ({chunk_x}, {chunk_y}): {e}")
186
+
187
+ completed += 1
188
+
189
+ # Notify progress
190
+ progress = ScanProgress(
191
+ total_chunks=total_chunks,
192
+ completed_chunks=completed,
193
+ current_x=tile_x,
194
+ current_y=tile_y,
195
+ objects_found=len(self.map_objects) - objects_before,
196
+ )
197
+ for callback in self._progress_callbacks:
198
+ try:
199
+ callback(progress)
200
+ except Exception as e:
201
+ logger.error(f"Progress callback error: {e}")
202
+
203
+ self._running = False
204
+ await asyncio.sleep(1.0)
205
+
206
+ duration = time.time() - start_time
207
+ objects_found = len(self.map_objects) - objects_before
208
+ summary = await self.get_scan_summary()
209
+
210
+ result = ScanResult(
211
+ kingdom_id=kingdom_id,
212
+ chunks_scanned=completed,
213
+ objects_found=objects_found,
214
+ duration=duration,
215
+ targets_by_type=summary["objects_by_type"],
216
+ )
217
+
218
+ logger.info(f"High-speed scan complete: {completed} chunks in {duration:.1f}s")
219
+ return result
220
+
221
+ async def scan_around_castles(
222
+ self, radius: int = 5, rescan: bool = False, quit_on_empty: Optional[int] = None
223
+ ) -> List[ScanResult]:
224
+ """Scan areas around all player castles."""
225
+ results: List[ScanResult] = []
226
+ player = self.client.state.local_player
227
+ if not player or not player.castles:
228
+ return results
229
+
230
+ for _castle_id, castle in player.castles.items():
231
+ result = await self.scan_area(
232
+ center_x=castle.x,
233
+ center_y=castle.y,
234
+ radius=radius,
235
+ kingdom_id=castle.KID,
236
+ rescan=rescan,
237
+ quit_on_empty=quit_on_empty,
238
+ )
239
+ results.append(result)
240
+
241
+ return results
242
+
243
+ async def find_nearby_targets(
244
+ self,
245
+ origin_x: int,
246
+ origin_y: int,
247
+ max_distance: float = 50.0,
248
+ target_types: Optional[List[MapObjectType]] = None,
249
+ max_level: int = 999,
250
+ exclude_player_ids: Optional[List[int]] = None,
251
+ use_db: bool = True,
252
+ ) -> List[Tuple[Any, float]]:
253
+ """
254
+ Find targets near a point, searching both memory and database.
255
+ """
256
+ targets_dict: Dict[int, Tuple[Any, float]] = {}
257
+ exclude_ids = set(exclude_player_ids or [])
258
+
259
+ # 1. Search Database
260
+ if use_db:
261
+ db_types = [int(t) for t in target_types] if target_types else None
262
+ db_results = await self.client.db.find_targets(0, max_level=max_level, types=db_types)
263
+ for record in db_results:
264
+ if record.owner_id in exclude_ids:
265
+ continue
266
+ dist = calculate_distance(origin_x, origin_y, record.x, record.y)
267
+ if dist <= max_distance:
268
+ targets_dict[record.area_id] = (record, dist)
269
+
270
+ # 2. Search Memory
271
+ for obj in self.map_objects.values():
272
+ if target_types and obj.type not in target_types:
273
+ continue
274
+ if obj.level > max_level:
275
+ continue
276
+ if obj.owner_id in exclude_ids:
277
+ continue
278
+ dist = calculate_distance(origin_x, origin_y, obj.x, obj.y)
279
+ if dist <= max_distance:
280
+ targets_dict[obj.area_id] = (obj, dist)
281
+
282
+ results = list(targets_dict.values())
283
+ results.sort(key=lambda t: t[1])
284
+ return results
285
+
286
+ async def find_npc_targets(self, x: int, y: int, dist: float = 30.0) -> List[Tuple[Any, float]]:
287
+ """Find permanent NPC targets (Robber Barons, etc)."""
288
+ npc_types = [MapObjectType.ROBBER_BARON_CASTLE, MapObjectType.DUNGEON, MapObjectType.BOSS_DUNGEON]
289
+ return await self.find_nearby_targets(x, y, max_distance=dist, target_types=npc_types)
290
+
291
+ async def find_player_targets(self, x: int, y: int, dist: float = 50.0) -> List[Tuple[Any, float]]:
292
+ """Find player targets (Castles, Outposts)."""
293
+ player_types = [MapObjectType.CASTLE, MapObjectType.OUTPOST, MapObjectType.CAPITAL]
294
+ return await self.find_nearby_targets(x, y, max_distance=dist, target_types=player_types)
295
+
296
+ async def find_event_targets(self, x: int, y: int, dist: float = 40.0) -> List[Tuple[Any, float]]:
297
+ """Find active event targets (Nomads, Samurai, Aliens)."""
298
+ event_types = [
299
+ MapObjectType.NOMAD_CAMP,
300
+ MapObjectType.SAMURAI_CAMP,
301
+ MapObjectType.ALIEN_CAMP,
302
+ MapObjectType.RED_ALIEN_CAMP,
303
+ ]
304
+ return await self.find_nearby_targets(x, y, max_distance=dist, target_types=event_types)
305
+
306
+ async def get_scan_summary(self) -> Dict[str, Any]:
307
+ """Get summary including database stats."""
308
+ mem_summary = {kid: len(chunks) for kid, chunks in self._scanned_chunks.items()}
309
+ db_count = await self.client.db.get_object_count()
310
+ db_types = await self.client.db.get_object_counts_by_type()
311
+
312
+ readable_types = {}
313
+ category_counts = {"Player": 0, "NPC": 0, "Event": 0, "Resource": 0, "Other": 0}
314
+
315
+ for type_id, count in db_types.items():
316
+ try:
317
+ enum_type = MapObjectType(type_id)
318
+ name = enum_type.name
319
+ if enum_type.is_player:
320
+ category_counts["Player"] += count
321
+ elif enum_type.is_npc:
322
+ category_counts["NPC"] += count
323
+ elif enum_type.is_event:
324
+ category_counts["Event"] += count
325
+ elif enum_type.is_resource:
326
+ category_counts["Resource"] += count
327
+ else:
328
+ category_counts["Other"] += count
329
+ except ValueError:
330
+ name = f"Unknown({type_id})"
331
+ category_counts["Other"] += count
332
+
333
+ readable_types[name] = count
334
+
335
+ return {
336
+ "memory_objects": len(self.map_objects),
337
+ "database_objects": db_count,
338
+ "objects_by_type": readable_types,
339
+ "objects_by_category": category_counts,
340
+ "chunks_by_kingdom": mem_summary,
341
+ "total_chunks_scanned": sum(mem_summary.values()),
342
+ }
343
+
344
+ def _generate_spiral_pattern(self, center_x: int, center_y: int, radius: int) -> List[Tuple[int, int]]:
345
+ """Generate spiral scan pattern from center outward, ensuring coordinates are positive."""
346
+ # Using MAP_STEP (13) for chunk grid
347
+ center_chunk_x = max(0, center_x // MAP_STEP)
348
+ center_chunk_y = max(0, center_y // MAP_STEP)
349
+
350
+ chunks = [(center_chunk_x, center_chunk_y)]
351
+
352
+ for r in range(1, radius + 1):
353
+ # Top row
354
+ for x in range(center_chunk_x - r, center_chunk_x + r + 1):
355
+ if x >= 0 and (center_chunk_y - r) >= 0:
356
+ chunks.append((x, center_chunk_y - r))
357
+ # Bottom row
358
+ for x in range(center_chunk_x - r, center_chunk_x + r + 1):
359
+ if x >= 0:
360
+ chunks.append((x, center_chunk_y + r))
361
+ # Left column
362
+ for y in range(center_chunk_y - r + 1, center_chunk_y + r):
363
+ if (center_chunk_x - r) >= 0 and y >= 0:
364
+ chunks.append((center_chunk_x - r, y))
365
+ # Right column
366
+ for y in range(center_chunk_y - r + 1, center_chunk_y + r):
367
+ if y >= 0:
368
+ chunks.append((center_chunk_x + r, y))
369
+
370
+ return chunks