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,266 @@
1
+ """
2
+ Alliance management and chat tools.
3
+ """
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
8
+
9
+ from empire_core.protocol.packet import Packet
10
+
11
+ if TYPE_CHECKING:
12
+ from empire_core.client.client import EmpireClient
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class AllianceMember:
19
+ """Alliance member info."""
20
+
21
+ player_id: int
22
+ name: str
23
+ level: int = 0
24
+ rank: str = "member"
25
+ online: bool = False
26
+ last_seen: int = 0
27
+ castle_count: int = 0
28
+
29
+
30
+ @dataclass
31
+ class ChatMessage:
32
+ """A chat message."""
33
+
34
+ timestamp: int
35
+ player_name: str
36
+ player_id: int
37
+ message: str
38
+ channel: str = "global"
39
+
40
+
41
+ class AllianceService:
42
+ """Service for alliance operations."""
43
+
44
+ def __init__(self, client: "EmpireClient"):
45
+ self.client = client
46
+ self.alliance_members: Dict[int, AllianceMember] = {}
47
+ self._alliance_callbacks: List[Callable[[List[AllianceMember]], None]] = []
48
+
49
+ @property
50
+ def alliance(self):
51
+ """Get current alliance info from player state."""
52
+ if self.client.state.local_player:
53
+ return self.client.state.local_player.alliance
54
+ return None
55
+
56
+ @property
57
+ def alliance_id(self) -> Optional[int]:
58
+ """Get alliance ID."""
59
+ if self.client.state.local_player:
60
+ return self.client.state.local_player.AID
61
+ return None
62
+
63
+ @property
64
+ def is_in_alliance(self) -> bool:
65
+ """Check if player is in an alliance."""
66
+ return self.alliance_id is not None and self.alliance_id > 0
67
+
68
+ async def refresh_members(self) -> bool:
69
+ """Refresh alliance member list from server."""
70
+ if not self.is_in_alliance:
71
+ logger.warning("Not in an alliance")
72
+ return False
73
+
74
+ packet = Packet.build_xt(self.client.config.default_zone, "gal", {})
75
+ await self.client.connection.send(packet)
76
+ return True
77
+
78
+ async def send_alliance_chat(self, message: str) -> bool:
79
+ """Send message to alliance chat."""
80
+ if not self.is_in_alliance:
81
+ logger.warning("Not in an alliance")
82
+ return False
83
+
84
+ packet = Packet.build_xt(self.client.config.default_zone, "sam", {"M": message})
85
+ await self.client.connection.send(packet)
86
+ logger.info(f"Sent alliance message: {message[:50]}...")
87
+ return True
88
+
89
+ async def coordinate_attack(self, target_x: int, target_y: int, target_name: str = "Target") -> bool:
90
+ """Send attack coordination message to alliance."""
91
+ message = f"⚔️ Attack: {target_name} at ({target_x}, {target_y})"
92
+ return await self.send_alliance_chat(message)
93
+
94
+ async def request_support(self, castle_id: int, castle_name: str, reason: str = "under attack") -> bool:
95
+ """Request support from alliance members."""
96
+ message = f"🛡️ Need support at {castle_name} (ID: {castle_id}) - {reason}"
97
+ return await self.send_alliance_chat(message)
98
+
99
+ async def donate_resources(
100
+ self,
101
+ alliance_id: int,
102
+ kingdom_id: int = 0,
103
+ wood: int = 0,
104
+ stone: int = 0,
105
+ food: int = 0,
106
+ wait_for_response: bool = False,
107
+ timeout: float = 5.0,
108
+ ) -> bool:
109
+ """
110
+ Donate resources to the alliance.
111
+
112
+ Args:
113
+ alliance_id: ID of the alliance to donate to.
114
+ kingdom_id: Kingdom ID (KID, default 0).
115
+ wood: Amount of wood to donate.
116
+ stone: Amount of stone to donate.
117
+ food: Amount of food to donate.
118
+ wait_for_response: Whether to wait for server confirmation.
119
+ timeout: Response timeout in seconds.
120
+
121
+ Returns:
122
+ bool: True if donation sent successfully.
123
+ """
124
+ if wood <= 0 and stone <= 0 and food <= 0:
125
+ raise ValueError("Must specify at least one resource to donate.")
126
+
127
+ logger.info(f"Donating {wood}W/{stone}S/{food}F to alliance {alliance_id} in K{kingdom_id}")
128
+
129
+ payload = {
130
+ "AID": alliance_id,
131
+ "KID": kingdom_id,
132
+ "RV": {
133
+ "O": wood, # Wood
134
+ "G": stone, # Stone
135
+ "C": food, # Food
136
+ },
137
+ }
138
+
139
+ response = await self.client._send_command_generic(
140
+ "ado", payload, "Alliance Donation", wait_for_response, timeout
141
+ )
142
+
143
+ return response
144
+
145
+ def get_online_members(self) -> List[AllianceMember]:
146
+ """Get online alliance members."""
147
+ return [m for m in self.alliance_members.values() if m.online]
148
+
149
+ def get_members_by_rank(self, rank: str) -> List[AllianceMember]:
150
+ """Get members by rank."""
151
+ return [m for m in self.alliance_members.values() if m.rank.lower() == rank.lower()]
152
+
153
+ def get_member(self, player_id: int) -> Optional[AllianceMember]:
154
+ """Get specific member by ID."""
155
+ return self.alliance_members.get(player_id)
156
+
157
+ def get_member_count(self) -> int:
158
+ """Get total member count."""
159
+ return len(self.alliance_members)
160
+
161
+ def update_members(self, members_data: List[Dict[str, Any]]):
162
+ """Update member list from server data."""
163
+ self.alliance_members.clear()
164
+ for m_data in members_data:
165
+ member = AllianceMember(
166
+ player_id=m_data.get("PID", 0),
167
+ name=m_data.get("N", "Unknown"),
168
+ level=m_data.get("L", 0),
169
+ rank=m_data.get("R", "member"),
170
+ online=m_data.get("O", False),
171
+ last_seen=m_data.get("LS", 0),
172
+ castle_count=m_data.get("CC", 0),
173
+ )
174
+ self.alliance_members[member.player_id] = member
175
+
176
+ logger.info(f"Updated {len(self.alliance_members)} alliance members")
177
+
178
+ # Notify callbacks
179
+ for callback in self._alliance_callbacks:
180
+ try:
181
+ callback(list(self.alliance_members.values()))
182
+ except Exception as e:
183
+ logger.error(f"Member callback error: {e}")
184
+
185
+ def on_members_updated(self, callback: Callable[[List[AllianceMember]], None]):
186
+ """Register callback for when member list updates."""
187
+ self._alliance_callbacks.append(callback)
188
+
189
+
190
+ class ChatService:
191
+ """Service for chat functionality."""
192
+
193
+ MAX_HISTORY = 100
194
+
195
+ def __init__(self, client: "EmpireClient"):
196
+ self.client = client
197
+ self.chat_history: List[ChatMessage] = []
198
+ self._chat_callbacks: List[Callable[[ChatMessage], None]] = []
199
+
200
+ async def send_global_chat(self, message: str) -> bool:
201
+ """Send message to global chat."""
202
+ return await self._send_chat(message, "global")
203
+
204
+ async def send_kingdom_chat(self, message: str, kingdom_id: int = 0) -> bool:
205
+ """Send message to kingdom chat."""
206
+ return await self._send_chat(message, f"kingdom_{kingdom_id}")
207
+
208
+ async def send_private_message(self, player_id: int, message: str) -> bool:
209
+ """Send private message to a player."""
210
+ packet = Packet.build_xt(
211
+ self.client.config.default_zone,
212
+ "spm",
213
+ {"RID": player_id, "M": message},
214
+ )
215
+ await self.client.connection.send(packet)
216
+ logger.info(f"Sent PM to {player_id}")
217
+ return True
218
+
219
+ async def _send_chat(self, message: str, channel: str) -> bool:
220
+ """Send chat message to channel."""
221
+ packet = Packet.build_xt(
222
+ self.client.config.default_zone,
223
+ "sct",
224
+ {"M": message, "C": channel},
225
+ )
226
+ await self.client.connection.send(packet)
227
+ logger.debug(f"Sent to {channel}: {message[:50]}...")
228
+ return True
229
+
230
+ def on_chat_message(self, callback: Callable[[ChatMessage], None]):
231
+ """Register callback for incoming messages."""
232
+ self._chat_callbacks.append(callback)
233
+
234
+ def handle_incoming_chat(self, data: Dict[str, Any]):
235
+ """Handle incoming chat message from server."""
236
+ msg = ChatMessage(
237
+ timestamp=data.get("T", 0),
238
+ player_name=data.get("N", ""),
239
+ player_id=data.get("PID", 0),
240
+ message=data.get("M", ""),
241
+ channel=data.get("C", "global"),
242
+ )
243
+ self.chat_history.append(msg)
244
+
245
+ # Trim history
246
+ if len(self.chat_history) > self.MAX_HISTORY:
247
+ self.chat_history = self.chat_history[-self.MAX_HISTORY :]
248
+
249
+ # Notify callbacks
250
+ for callback in self._chat_callbacks:
251
+ try:
252
+ callback(msg)
253
+ except Exception as e:
254
+ logger.error(f"Chat callback error: {e}")
255
+
256
+ def get_chat_history(self, channel: Optional[str] = None, limit: int = 50) -> List[ChatMessage]:
257
+ """Get chat history, optionally filtered by channel."""
258
+ messages = self.chat_history
259
+ if channel:
260
+ messages = [m for m in messages if m.channel == channel]
261
+ return messages[-limit:]
262
+
263
+ def search_chat_history(self, keyword: str) -> List[ChatMessage]:
264
+ """Search chat history for keyword."""
265
+ keyword_lower = keyword.lower()
266
+ return [m for m in self.chat_history if keyword_lower in m.message.lower()]
@@ -0,0 +1,196 @@
1
+ """
2
+ Battle report automation for analyzing combat outcomes.
3
+ """
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any, Dict, List
7
+
8
+ from empire_core.state.report_models import BattleReport, ReportManager
9
+
10
+ if TYPE_CHECKING:
11
+ from empire_core.client.client import EmpireClient
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class BattleReportService:
17
+ """Service for battle report fetching and analysis."""
18
+
19
+ def __init__(self, client: "EmpireClient"):
20
+ self.client = client
21
+
22
+ @property
23
+ def reports_manager(self) -> ReportManager:
24
+ """Get the report manager from state."""
25
+ return self.client.state.reports
26
+
27
+ async def fetch_recent_reports(self, count: int = 10) -> bool:
28
+ """Fetch recent battle reports from server."""
29
+ # Using client method
30
+ return await self.client.get_battle_reports(count)
31
+
32
+ async def fetch_report_details(self, report_id: int) -> bool:
33
+ """Fetch detailed data for a specific battle report."""
34
+ # Using client method
35
+ return await self.client.get_battle_report_details(report_id)
36
+
37
+ def get_recent_reports(self, count: int = 10) -> List[BattleReport]:
38
+ """Get most recent battle reports."""
39
+ return self.reports_manager.get_recent_reports(count)
40
+
41
+ def get_unread_reports(self) -> List[BattleReport]:
42
+ """Get all unread battle reports."""
43
+ return self.reports_manager.get_unread_reports()
44
+
45
+ def mark_report_read(self, report_id: int):
46
+ """Mark a battle report as read."""
47
+ self.reports_manager.mark_as_read(report_id)
48
+
49
+ def get_report_summary(self, report: BattleReport) -> Dict[str, Any]:
50
+ """Get a summary of a battle report."""
51
+ summary = {
52
+ "report_id": report.report_id,
53
+ "timestamp": report.timestamp,
54
+ "datetime": report.datetime.isoformat(),
55
+ "is_read": report.is_read,
56
+ "report_type": report.report_type,
57
+ "target": {
58
+ "name": report.target_name,
59
+ "x": report.target_x,
60
+ "y": report.target_y,
61
+ },
62
+ "winner": report.winner,
63
+ "loot": report.loot,
64
+ }
65
+
66
+ # Add participant info if available
67
+ if report.attacker:
68
+ summary["attacker"] = {
69
+ "player_id": report.attacker.player_id,
70
+ "player_name": report.attacker.player_name,
71
+ "alliance_name": report.attacker.alliance_name,
72
+ "units_before": report.attacker.units_before,
73
+ "units_after": report.attacker.units_after,
74
+ "losses": report.attacker.losses,
75
+ }
76
+
77
+ if report.defender:
78
+ summary["defender"] = {
79
+ "player_id": report.defender.player_id,
80
+ "player_name": report.defender.player_name,
81
+ "alliance_name": report.defender.alliance_name,
82
+ "units_before": report.defender.units_before,
83
+ "units_after": report.defender.units_after,
84
+ "losses": report.defender.losses,
85
+ }
86
+
87
+ return summary
88
+
89
+ def analyze_battle_efficiency(self, report: BattleReport) -> Dict[str, Any]:
90
+ """Analyze the efficiency of a battle."""
91
+ loot_total = sum(report.loot.values())
92
+ analysis = {
93
+ "report_id": report.report_id,
94
+ "victory": report.winner == "attacker",
95
+ "loot_total": loot_total,
96
+ "loot_breakdown": report.loot,
97
+ }
98
+
99
+ # Calculate losses if we have participant data
100
+ if report.attacker and report.defender:
101
+ attacker_losses = sum(report.attacker.losses.values())
102
+ defender_losses = sum(report.defender.losses.values())
103
+
104
+ analysis.update(
105
+ {
106
+ "attacker_losses": attacker_losses,
107
+ "defender_losses": defender_losses,
108
+ "total_losses": attacker_losses + defender_losses,
109
+ }
110
+ )
111
+
112
+ # Calculate efficiency metrics
113
+ if attacker_losses > 0:
114
+ loot_per_loss = loot_total / attacker_losses
115
+ analysis["loot_per_attacker_loss"] = loot_per_loss
116
+
117
+ # Simple efficiency rating (higher is better)
118
+ if loot_per_loss > 100:
119
+ analysis["efficiency"] = "excellent"
120
+ elif loot_per_loss > 50:
121
+ analysis["efficiency"] = "good"
122
+ elif loot_per_loss > 20:
123
+ analysis["efficiency"] = "fair"
124
+ else:
125
+ analysis["efficiency"] = "poor"
126
+
127
+ return analysis
128
+
129
+ def get_battle_stats(self, reports: List[BattleReport]) -> Dict[str, Any]:
130
+ """Get aggregate statistics from multiple battle reports."""
131
+ if not reports:
132
+ return {"total_battles": 0}
133
+
134
+ total_battles = len(reports)
135
+ victories = 0
136
+ defeats = 0
137
+ total_loot = {"wood": 0, "stone": 0, "food": 0}
138
+ total_losses = 0
139
+
140
+ for report in reports:
141
+ if report.winner == "attacker":
142
+ victories += 1
143
+ else:
144
+ defeats += 1
145
+
146
+ # Sum loot
147
+ for resource, amount in report.loot.items():
148
+ if resource in total_loot:
149
+ total_loot[resource] += amount
150
+
151
+ # Sum losses (if available)
152
+ if report.attacker:
153
+ total_losses += sum(report.attacker.losses.values())
154
+
155
+ win_rate = victories / total_battles if total_battles > 0 else 0
156
+
157
+ return {
158
+ "total_battles": total_battles,
159
+ "victories": victories,
160
+ "defeats": defeats,
161
+ "total_loot": total_loot,
162
+ "total_losses": total_losses,
163
+ "win_rate": win_rate,
164
+ }
165
+
166
+ async def auto_fetch_and_analyze(self, count: int = 10, wait_time: float = 1.0) -> Dict[str, Any]:
167
+ """
168
+ Fetch recent reports and return analysis.
169
+
170
+ Args:
171
+ count: Number of reports to fetch
172
+ wait_time: Time to wait for server response
173
+
174
+ Returns:
175
+ Dict with stats and analyses
176
+ """
177
+ import asyncio
178
+
179
+ # Fetch reports
180
+ await self.fetch_recent_reports(count)
181
+ await asyncio.sleep(wait_time) # Wait for response
182
+
183
+ # Get reports
184
+ reports = self.get_recent_reports(count)
185
+
186
+ # Analyze each report
187
+ analyses = [self.analyze_battle_efficiency(report) for report in reports]
188
+
189
+ # Get aggregate stats
190
+ stats = self.get_battle_stats(reports)
191
+
192
+ return {
193
+ "stats": stats,
194
+ "reports_analyzed": len(analyses),
195
+ "analyses": analyses[:5], # Return top 5 analyses
196
+ }
@@ -0,0 +1,242 @@
1
+ """
2
+ Building queue management and automation.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from enum import IntEnum
9
+ from typing import TYPE_CHECKING, Any, Dict, List
10
+
11
+ if TYPE_CHECKING:
12
+ from empire_core.client.client import EmpireClient
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BuildingType(IntEnum):
18
+ """Common building type IDs."""
19
+
20
+ # Resource production
21
+ WOODCUTTER = 1
22
+ QUARRY = 2
23
+ FARM = 3
24
+
25
+ # Storage
26
+ WAREHOUSE = 4
27
+
28
+ # Military
29
+ BARRACKS = 5
30
+ ARCHERY_RANGE = 6
31
+ STABLE = 7
32
+ SIEGE_WORKSHOP = 8
33
+ DEFENSE_WORKSHOP = 9
34
+
35
+ # Other
36
+ KEEP = 10
37
+ TAVERN = 11
38
+ MARKETPLACE = 12
39
+ HOSPITAL = 13
40
+ WALL = 14
41
+
42
+
43
+ @dataclass
44
+ class BuildingTask:
45
+ """A building task in the queue."""
46
+
47
+ castle_id: int
48
+ building_id: int
49
+ target_level: int = 1
50
+ priority: int = 1
51
+ description: str = ""
52
+
53
+
54
+ class BuildingManager:
55
+ """
56
+ Manages building and upgrade queues for castles.
57
+
58
+ Features:
59
+ - Queue buildings for automatic upgrade.
60
+ - Priority-based build order.
61
+ - Monitor build progress.
62
+ - Building recommendations.
63
+ """
64
+
65
+ def __init__(self, client: "EmpireClient"):
66
+ self.client = client
67
+ self.queue: List[BuildingTask] = []
68
+ self.in_progress: Dict[int, BuildingTask] = {} # castle_id -> current task
69
+ self.is_running = False
70
+
71
+ def queue_upgrade(self, castle_id: int, building_id: int, target_level: int = 1, priority: int = 1):
72
+ """Add a building to the upgrade queue."""
73
+ task = BuildingTask(
74
+ castle_id=castle_id,
75
+ building_id=building_id,
76
+ target_level=target_level,
77
+ priority=priority,
78
+ description=f"Upgrade building {building_id} to level {target_level}",
79
+ )
80
+ self.queue.append(task)
81
+ self._sort_queue()
82
+ logger.info(f"Queued building {building_id} for castle {castle_id} (priority: {priority})")
83
+
84
+ def cancel_task(self, castle_id: int, building_id: int):
85
+ """Remove a task from the queue."""
86
+ self.queue = [t for t in self.queue if not (t.castle_id == castle_id and t.building_id == building_id)]
87
+ logger.info(f"Cancelled build task for building {building_id} in castle {castle_id}")
88
+
89
+ async def process_queue(self):
90
+ """Check queue and start next available build tasks."""
91
+ if not self.queue:
92
+ return
93
+
94
+ # Check each castle's availability
95
+ # Note: In this game, usually one build slot per castle (unless rubies/premium)
96
+ active_castles = set(self.in_progress.keys())
97
+
98
+ for task in list(self.queue):
99
+ if task.castle_id in active_castles:
100
+ continue
101
+
102
+ # Attempt to start build
103
+ success = await self._start_build(task)
104
+ if success:
105
+ self.in_progress[task.castle_id] = task
106
+ self.queue.remove(task)
107
+ logger.info(f"Started building {task.building_id} in castle {task.castle_id}")
108
+
109
+ async def _start_build(self, task: BuildingTask) -> bool:
110
+ """Internal: Send build command to server."""
111
+ try:
112
+ # Command: bui (Build)
113
+ # Needs implementation in GameActionsMixin/EmpireClient
114
+ success = await self.client.upgrade_building(
115
+ castle_id=task.castle_id,
116
+ building_id=task.building_id,
117
+ )
118
+ return bool(success)
119
+ except Exception as e:
120
+ logger.error(f"Failed to start build: {e}")
121
+ return False
122
+
123
+ async def refresh_status(self):
124
+ """Update status of in-progress builds."""
125
+ # Refresh state from server
126
+ await self.client.get_detailed_castle_info()
127
+
128
+ # Check if in-progress buildings are finished
129
+ player = self.client.state.local_player
130
+ if not player:
131
+ return
132
+
133
+ finished_castles = []
134
+ for castle_id, task in self.in_progress.items():
135
+ castle = player.castles.get(castle_id)
136
+ if not castle:
137
+ continue
138
+
139
+ # Check if building reached target level
140
+ # In this game, buildings list in dcl contains current level
141
+ is_finished = False
142
+ for building in castle.buildings:
143
+ if building.id == task.building_id and building.level >= task.target_level:
144
+ is_finished = True
145
+ break
146
+
147
+ if is_finished:
148
+ finished_castles.append(castle_id)
149
+ logger.info(f"Build finished: {task.description} in castle {castle_id}")
150
+
151
+ for cid in finished_castles:
152
+ del self.in_progress[cid]
153
+
154
+ async def start_automation(self, interval: int = 60):
155
+ """Start automatic build queue processing."""
156
+ self.is_running = True
157
+ logger.info("Building automation started")
158
+
159
+ while self.is_running:
160
+ try:
161
+ await self.refresh_status()
162
+ await self.process_queue()
163
+ except Exception as e:
164
+ logger.error(f"Building automation error: {e}")
165
+
166
+ await asyncio.sleep(interval)
167
+
168
+ def stop_automation(self):
169
+ """Stop automatic build queue processing."""
170
+ self.is_running = False
171
+ logger.info("Building automation stopped")
172
+
173
+ def get_status(self) -> Dict[str, Any]:
174
+ """Get current status of queue and active builds."""
175
+ return {
176
+ "queue_size": len(self.queue),
177
+ "queue_by_castle": {
178
+ castle_id: len([t for t in self.queue if t.castle_id == castle_id])
179
+ for castle_id in set(t.castle_id for t in self.queue)
180
+ },
181
+ "in_progress": list(self.in_progress.keys()),
182
+ }
183
+
184
+ def _sort_queue(self):
185
+ """Sort queue by priority (highest first)."""
186
+ self.queue.sort(key=lambda t: t.priority, reverse=True)
187
+
188
+ def get_recommendations(self, castle_id: int, focus: str = "balanced") -> List[BuildingTask]:
189
+ """Get recommended buildings to build/upgrade."""
190
+ recommendations: List[BuildingTask] = []
191
+ castle = self.client.state.castles.get(castle_id)
192
+ if not castle:
193
+ return recommendations
194
+
195
+ # Get current building levels
196
+ buildings = {b.id: b.level for b in castle.buildings}
197
+
198
+ # Define priority based on focus
199
+ priority_types: List[int] = []
200
+ if focus == "military":
201
+ priority_types = [
202
+ BuildingType.BARRACKS,
203
+ BuildingType.STABLE,
204
+ BuildingType.ARCHERY_RANGE,
205
+ ]
206
+ elif focus == "economy":
207
+ priority_types = [
208
+ BuildingType.WOODCUTTER,
209
+ BuildingType.QUARRY,
210
+ BuildingType.FARM,
211
+ BuildingType.WAREHOUSE,
212
+ ]
213
+ elif focus == "defense":
214
+ priority_types = [
215
+ BuildingType.WALL,
216
+ BuildingType.DEFENSE_WORKSHOP,
217
+ BuildingType.KEEP,
218
+ ]
219
+ else: # balanced
220
+ priority_types = [
221
+ BuildingType.KEEP,
222
+ BuildingType.WOODCUTTER,
223
+ BuildingType.QUARRY,
224
+ BuildingType.FARM,
225
+ BuildingType.BARRACKS,
226
+ ]
227
+
228
+ # Find buildings that need upgrading
229
+ for i, building_type in enumerate(priority_types):
230
+ if building_type in buildings:
231
+ current_level = buildings[building_type]
232
+ if current_level < 10: # Simple threshold
233
+ recommendations.append(
234
+ BuildingTask(
235
+ castle_id=castle_id,
236
+ building_id=building_type,
237
+ target_level=current_level + 1,
238
+ priority=len(priority_types) - i, # Higher priority first
239
+ )
240
+ )
241
+
242
+ return recommendations