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,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
|