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,719 @@
1
+ """
2
+ Unit production and army management automation.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from enum import IntEnum
9
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
10
+
11
+ from empire_core.state.models import Castle
12
+
13
+ if TYPE_CHECKING:
14
+ from empire_core.client.client import EmpireClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class UnitType(IntEnum):
20
+ """Common unit type IDs."""
21
+
22
+ VETERANSABERSLASHER = 5
23
+ VETERANSLINGSHOTMARKSMAN = 6
24
+ OGERMACE_2 = 7
25
+ OGERCROSSBOW_2 = 8
26
+ ELITERANKREWARDMELEE = 9
27
+ ELITERANKREWARDRANGE = 10
28
+ ELITEFLAMETHROWER = 11
29
+ ELITEARROWTHROWER = 12
30
+ AUXILIARYMELEE = 13
31
+ AUXILIARYRANGE = 14
32
+ BERIMONDREWARDMELEE = 18
33
+ BERIMONDREWARDRANGE = 19
34
+ ELITEBERIMONDREWARDMELEE = 20
35
+ ELITEBERIMONDREWARDRANGE = 21
36
+ VALKYRIEMILEE = 22
37
+ VALKYRIERANGE = 23
38
+ SAMURAIATTACKERMELEE = 34
39
+ SAMURAIATTACKERRANGE = 35
40
+ SAMURAIDEFENDERMELEE = 36
41
+ SAMURAIDEFENDERRANGE = 37
42
+ SAMURAIDEFENDERMELEENPC = 38
43
+ SAMURAIDEFENDERRANGENPC = 39
44
+ RENEGADESKELETONSPEERMAN = 40
45
+ RENEGADESKELETONBOWMAN = 41
46
+ HALLOWEENMELEE = 42
47
+ HALLOWEENRANGE = 43
48
+ WINTERATTACKERMELEE = 48
49
+ ELITETINOSWOLVES = 49
50
+ WINTERATTACKERRANGE = 50
51
+ ELITEWINTERATTACKERMELEE = 51
52
+ ELITEWINTERATTACKERRANGE = 52
53
+ RANKREWARDMELEEUSA = 58
54
+ RANKREWARDRANGEUSA = 59
55
+ OGERMACE = 68
56
+ OGERCROSSBOW = 74
57
+ ELITEKINGSMACE = 75
58
+ ELITEKINGSCROSSBOWMAN = 76
59
+ RENEGADEOGERMACE = 78
60
+ RENEGADEOGERCROSSBOW = 79
61
+ ELITESPRINGATTACKERMELEE = 83
62
+ ELITESPRINGATTACKERRANGE = 84
63
+ ELITESPRINGDEFENDERMELEE = 85
64
+ ELITESPRINGDEFENDERRANGE = 86
65
+ SHADOWRANKREWARDMELEE = 92
66
+ SHADOWRANKREWARDRANGE = 93
67
+ STPATRICKSDEFENDERMELEE = 100
68
+ STPATRICKSDEFENDERRANGE = 101
69
+ EASTERDEFENDERMELEE = 102
70
+ EASTERDEFENDERRANGE = 103
71
+ ALIENREROLLEDEFENDERMELEE = 146
72
+ ALIENREROLLEDEFENDERRANGE = 147
73
+ RELICAXE = 148
74
+ RELICSHORTBOW = 149
75
+ RELICHAMMER = 150
76
+ RELICLONGBOW = 151
77
+ MAYAMELEE = 183
78
+ MAYARANGE = 184
79
+ MAYAELITEMELEEE = 185
80
+ MAYAELITERANGE = 186
81
+ RENEGADEMAYAMELEE = 187
82
+ RENEGADEMAYARANGE = 188
83
+ RENEGADEMAYAELITEMELEEE = 189
84
+ RENEGADEMAYAELITERANGE = 190
85
+ CORRUPTEDASSASSIN = 191
86
+ CORRUPTEDCROSSBOWMAN = 192
87
+ CORRUPTEDELITEHALBERDIER = 193
88
+ CORRUPTEDELITELONGBOWMAN = 194
89
+ MEADSHIELDMAIDEN = 195
90
+ MEADSHIELDMAIDEN_2 = 196
91
+ MEADSHIELDMAIDEN_3 = 197
92
+ MEADSHIELDMAIDEN_4 = 198
93
+ MEADSHIELDMAIDEN_5 = 199
94
+ MEADSHIELDMAIDEN_6 = 200
95
+ MEADSHIELDMAIDEN_7 = 201
96
+ MEADSHIELDMAIDEN_8 = 202
97
+ MEADSHIELDMAIDEN_9 = 203
98
+ MEADSHIELDMAIDEN_10 = 204
99
+ MEADRANGER = 205
100
+ MEADRANGER_2 = 206
101
+ MEADRANGER_3 = 207
102
+ MEADRANGER_4 = 208
103
+ MEADRANGER_5 = 209
104
+ MEADRANGER_6 = 210
105
+ MEADRANGER_7 = 211
106
+ MEADRANGER_8 = 212
107
+ MEADRANGER_9 = 213
108
+ MEADRANGER_10 = 214
109
+ MEADSHIELDMAIDEN_11 = 215
110
+ MEADRANGER_11 = 216
111
+ MEADMACE = 217
112
+ MEADMACE_2 = 218
113
+ MEADMACE_3 = 219
114
+ MEADMACE_4 = 220
115
+ MEADMACE_5 = 221
116
+ MEADMACE_6 = 222
117
+ MEADMACE_7 = 223
118
+ MEADMACE_8 = 224
119
+ MEADMACE_9 = 225
120
+ MEADMACE_10 = 226
121
+ MEADMACE_11 = 227
122
+ MEADBOW = 228
123
+ MEADBOW_2 = 229
124
+ MEADBOW_3 = 230
125
+ MEADBOW_4 = 231
126
+ MEADBOW_5 = 232
127
+ MEADBOW_6 = 233
128
+ MEADBOW_7 = 234
129
+ MEADBOW_8 = 235
130
+ MEADBOW_9 = 236
131
+ MEADBOW_10 = 237
132
+ MEADBOW_11 = 238
133
+ ELITEHALBERD = 308
134
+ ELITETWOHANDEDSWORD = 309
135
+ ELITELONGBOWMAN = 311
136
+ ELITEHEAVYCROSSBOWMAN = 312
137
+ SWORDMAN = 601
138
+ SPEERMAN = 602
139
+ MACE = 603
140
+ HALBERD = 604
141
+ TWOHANDEDSWORD = 605
142
+ ARCHER = 606
143
+ CROSSBOWMAN = 607
144
+ BOWMAN = 608
145
+ HEAVYCROSSBOWMAN = 609
146
+ LONGBOWMAN = 610
147
+ SHADOWMACE = 612
148
+ SHADOWCROSSBOWMAN = 613
149
+ MILITIA = 620
150
+ SWORDSMAN = 621
151
+ SPEARMAN = 622
152
+ KNIGHT = 623
153
+ LIGHT_CAVALRY = 640
154
+ HEAVY_CAVALRY = 641
155
+ LANCER = 642
156
+ PEASANT = 652
157
+ BATTERING_RAM = 650
158
+ CATAPULT = 651
159
+ SIEGE_TOWER = 652
160
+ EVENTKNIGHT = 655
161
+ EVENTCROSSBOWMAN = 656
162
+ NATIVEMELEE = 657
163
+ NATIVERANGE = 658
164
+ PRINCENOOB = 659
165
+ MANTLET = 660
166
+ WALL_DEFENDER = 661
167
+ SKELETONSPEERMAN = 662
168
+ SKELETONBOWMAN = 663
169
+ KINGSCROSSBOWMAN = 664
170
+ SOLDIERS = 665
171
+ SHADOWTWOHANDEDSWORD = 667
172
+ SHADOWHEAVYCROSSBOWMAN = 668
173
+ SPY = 670
174
+ AXEVIKING = 670
175
+ COMMANDER = 680
176
+ BOWVIKING = 671
177
+ KINGSMACE = 672
178
+ DRAGONCLAWS = 673
179
+ DRAGONJAW = 674
180
+ DESERTMELEE = 675
181
+ DESERTRANGE = 676
182
+ ICEMELEE = 677
183
+ ICERANGE = 678
184
+ FIREMELEE = 679
185
+ FIRERANGE = 680
186
+ MARAUDER = 684
187
+ FIREDEVIL = 685
188
+ KINGSSPEERMAN = 686
189
+ KINGSBOWMAN = 687
190
+ DESERTEVENTMELEE = 688
191
+ DESERTEVENTRANGE = 689
192
+ ICEEVENTMELEE = 690
193
+ ICEEVENTRANGE = 691
194
+ FIREEVENTMELEE = 692
195
+ FIREEVENTRANGE = 693
196
+ COWHALBERD = 698
197
+ COWBOWMAN = 699
198
+ BLUEMELEE = 710
199
+ BLUERANGE = 711
200
+ REDMELEE = 712
201
+ REDRANGE = 713
202
+ RANKREWARDMELEE = 714
203
+ RANKREWARDRANGE = 715
204
+ PIRATESPEERMAN = 716
205
+ PIRATEBOWMAN = 717
206
+ TENTACLE = 718
207
+ OCTOPUSHEAD = 719
208
+ TINOSWOLVES = 720
209
+ CONAN = 721
210
+ SLINGSHOTMARKSMAN = 727
211
+ RENEGADEPIKEMAN = 728
212
+ RENEGADESPEARTHROWER = 729
213
+ CRUSADEMILEE = 753
214
+ CRUSADERANGE = 754
215
+ RENEGADEPIRATEMILEE = 759
216
+ RENEGADEPIRATERANGE = 760
217
+ ELITEMARAUDER = 765
218
+ ELITEFIREDEVIL = 766
219
+ RENEGADENATIVEMELEE = 767
220
+ RENEGADENATIVERANGE = 768
221
+ SAMURAIDEFENDERMELEENPC_2 = 820
222
+ SAMURAIDEFENDERMELEENPC_3 = 821
223
+ SAMURAIDEFENDERMELEENPC_4 = 822
224
+ SAMURAIDEFENDERMELEENPC_5 = 823
225
+ SAMURAIDEFENDERMELEENPC_6 = 824
226
+ SAMURAIDEFENDERMELEENPC_7 = 825
227
+ SAMURAIDEFENDERMELEENPC_8 = 826
228
+ SAMURAIDEFENDERMELEENPC_9 = 827
229
+ SAMURAIDEFENDERMELEENPC_10 = 828
230
+ SAMURAIDEFENDERMELEENPC_11 = 829
231
+ SAMURAIDEFENDERRANGENPC_2 = 830
232
+ SAMURAIDEFENDERRANGENPC_3 = 831
233
+ SAMURAIDEFENDERRANGENPC_4 = 832
234
+ SAMURAIDEFENDERRANGENPC_5 = 833
235
+ SAMURAIDEFENDERRANGENPC_6 = 834
236
+ SAMURAIDEFENDERRANGENPC_7 = 835
237
+ SAMURAIDEFENDERRANGENPC_8 = 836
238
+ SAMURAIDEFENDERRANGENPC_9 = 837
239
+ SAMURAIDEFENDERRANGENPC_10 = 838
240
+ SAMURAIDEFENDERRANGENPC_11 = 839
241
+ SAMURAIATTACKERMELEENPC = 860
242
+ SAMURAIATTACKERMELEENPC_2 = 861
243
+ SAMURAIATTACKERMELEENPC_3 = 862
244
+ SAMURAIATTACKERMELEENPC_4 = 863
245
+ SAMURAIATTACKERMELEENPC_5 = 864
246
+ SAMURAIATTACKERMELEENPC_6 = 865
247
+ SAMURAIATTACKERMELEENPC_7 = 866
248
+ SAMURAIATTACKERMELEENPC_8 = 867
249
+ SAMURAIATTACKERMELEENPC_9 = 868
250
+ SAMURAIATTACKERMELEENPC_10 = 869
251
+ SAMURAIATTACKERRANGENPC = 870
252
+ SAMURAIATTACKERRANGENPC_2 = 871
253
+ SAMURAIATTACKERRANGENPC_3 = 872
254
+ SAMURAIATTACKERRANGENPC_4 = 873
255
+ SAMURAIATTACKERRANGENPC_5 = 874
256
+ SAMURAIATTACKERRANGENPC_6 = 875
257
+ SAMURAIATTACKERRANGENPC_7 = 876
258
+ SAMURAIATTACKERRANGENPC_8 = 877
259
+ SAMURAIATTACKERRANGENPC_9 = 878
260
+ SAMURAIATTACKERRANGENPC_10 = 879
261
+ PIKEMAN = 900
262
+ PIKEMAN_2 = 901
263
+ PIKEMAN_3 = 902
264
+ PIKEMAN_4 = 903
265
+ PIKEMAN_5 = 904
266
+ PIKEMAN_6 = 905
267
+ PIKEMAN_7 = 906
268
+ PIKEMAN_8 = 907
269
+ PIKEMAN_9 = 908
270
+ PIKEMAN_10 = 909
271
+ SPEARTHROWER = 910
272
+ SPEARTHROWER_2 = 911
273
+ SPEARTHROWER_3 = 912
274
+ SPEARTHROWER_4 = 913
275
+ SPEARTHROWER_5 = 914
276
+ SPEARTHROWER_6 = 915
277
+ SPEARTHROWER_7 = 916
278
+ SPEARTHROWER_8 = 917
279
+ SPEARTHROWER_9 = 918
280
+ SPEARTHROWER_10 = 919
281
+ VETERANSABERSLASHER_2 = 940
282
+ VETERANSABERSLASHER_3 = 941
283
+ VETERANSABERSLASHER_4 = 942
284
+ VETERANSABERSLASHER_5 = 943
285
+ VETERANSABERSLASHER_6 = 944
286
+ VETERANSABERSLASHER_7 = 945
287
+ VETERANSABERSLASHER_8 = 946
288
+ VETERANSABERSLASHER_9 = 947
289
+ VETERANSABERSLASHER_10 = 948
290
+ VETERANSABERSLASHER_11 = 949
291
+ VETERANSLINGSHOTMARKSMAN_2 = 950
292
+ VETERANSLINGSHOTMARKSMAN_3 = 951
293
+ VETERANSLINGSHOTMARKSMAN_4 = 952
294
+ VETERANSLINGSHOTMARKSMAN_5 = 953
295
+ VETERANSLINGSHOTMARKSMAN_6 = 954
296
+ VETERANSLINGSHOTMARKSMAN_7 = 955
297
+ VETERANSLINGSHOTMARKSMAN_8 = 956
298
+ VETERANSLINGSHOTMARKSMAN_9 = 957
299
+ VETERANSLINGSHOTMARKSMAN_10 = 958
300
+ VETERANSLINGSHOTMARKSMAN_11 = 959
301
+ ELITESPRINGATTACKERMELEE_2 = 960
302
+ ELITESPRINGATTACKERRANGE_2 = 961
303
+ RENEGADEPIRATEMILEE_2 = 962
304
+ RENEGADEPIRATERANGE_2 = 963
305
+ QUICKATTACK = 1337
306
+ ELITEHALBERD_2 = 2000
307
+ ELITEHALBERD_3 = 2001
308
+ ELITEHALBERD_4 = 2002
309
+ ELITEHALBERD_5 = 2003
310
+ ELITEHALBERD_6 = 2004
311
+ ELITEHALBERD_7 = 2005
312
+ ELITEHALBERD_8 = 2006
313
+ ELITEHALBERD_9 = 2007
314
+ ELITEHALBERD_10 = 2008
315
+ ELITEHALBERD_11 = 2009
316
+ ELITETWOHANDEDSWORD_2 = 2010
317
+ ELITETWOHANDEDSWORD_3 = 2011
318
+ ELITETWOHANDEDSWORD_4 = 2012
319
+ ELITETWOHANDEDSWORD_5 = 2013
320
+ ELITETWOHANDEDSWORD_6 = 2014
321
+ ELITETWOHANDEDSWORD_7 = 2015
322
+ ELITETWOHANDEDSWORD_8 = 2016
323
+ ELITETWOHANDEDSWORD_9 = 2017
324
+ ELITETWOHANDEDSWORD_10 = 2018
325
+ ELITETWOHANDEDSWORD_11 = 2019
326
+ RELICAXE_2 = 2020
327
+ RELICAXE_3 = 2021
328
+ RELICAXE_4 = 2022
329
+ RELICAXE_5 = 2023
330
+ RELICAXE_6 = 2024
331
+ RELICAXE_7 = 2025
332
+ RELICAXE_8 = 2026
333
+ RELICAXE_9 = 2027
334
+ RELICAXE_10 = 2028
335
+ RELICAXE_11 = 2029
336
+ RELICHAMMER_2 = 2030
337
+ RELICHAMMER_3 = 2031
338
+ RELICHAMMER_4 = 2032
339
+ RELICHAMMER_5 = 2033
340
+ RELICHAMMER_6 = 2034
341
+ RELICHAMMER_7 = 2035
342
+ RELICHAMMER_8 = 2036
343
+ RELICHAMMER_9 = 2037
344
+ RELICHAMMER_10 = 2038
345
+ RELICHAMMER_11 = 2039
346
+ ELITELONGBOWMAN_2 = 2040
347
+ ELITELONGBOWMAN_3 = 2041
348
+ ELITELONGBOWMAN_4 = 2042
349
+ ELITELONGBOWMAN_5 = 2043
350
+ ELITELONGBOWMAN_6 = 2044
351
+ ELITELONGBOWMAN_7 = 2045
352
+ ELITELONGBOWMAN_8 = 2046
353
+ ELITELONGBOWMAN_9 = 2047
354
+ ELITELONGBOWMAN_10 = 2048
355
+ ELITELONGBOWMAN_11 = 2049
356
+ ELITEHEAVYCROSSBOWMAN_2 = 2050
357
+ ELITEHEAVYCROSSBOWMAN_3 = 2051
358
+ ELITEHEAVYCROSSBOWMAN_4 = 2052
359
+ ELITEHEAVYCROSSBOWMAN_5 = 2053
360
+ ELITEHEAVYCROSSBOWMAN_6 = 2054
361
+ ELITEHEAVYCROSSBOWMAN_7 = 2055
362
+ ELITEHEAVYCROSSBOWMAN_8 = 2056
363
+ ELITEHEAVYCROSSBOWMAN_9 = 2057
364
+ ELITEHEAVYCROSSBOWMAN_10 = 2058
365
+ ELITEHEAVYCROSSBOWMAN_11 = 2059
366
+ RELICSHORTBOW_2 = 2060
367
+ RELICSHORTBOW_3 = 2061
368
+ RELICSHORTBOW_4 = 2062
369
+ RELICSHORTBOW_5 = 2063
370
+ RELICSHORTBOW_6 = 2064
371
+ RELICSHORTBOW_7 = 2065
372
+ RELICSHORTBOW_8 = 2066
373
+ RELICSHORTBOW_9 = 2067
374
+ RELICSHORTBOW_10 = 2068
375
+ RELICSHORTBOW_11 = 2069
376
+ RELICLONGBOW_2 = 2070
377
+ RELICLONGBOW_3 = 2071
378
+ RELICLONGBOW_4 = 2072
379
+ RELICLONGBOW_5 = 2073
380
+ RELICLONGBOW_6 = 2074
381
+ RELICLONGBOW_7 = 2075
382
+ RELICLONGBOW_8 = 2076
383
+ RELICLONGBOW_9 = 2077
384
+ RELICLONGBOW_10 = 2078
385
+ RELICLONGBOW_11 = 2079
386
+
387
+
388
+ @dataclass
389
+ class RecruitmentTask:
390
+ """A unit recruitment task."""
391
+
392
+ castle_id: int
393
+ unit_type: int
394
+ count: int
395
+ priority: int = 0
396
+
397
+
398
+ @dataclass
399
+ class UnitProductionTarget:
400
+ """Target unit counts for a castle."""
401
+
402
+ castle_id: int
403
+ targets: Dict[int, int] = field(default_factory=dict) # unit_type -> target_count
404
+
405
+
406
+ @dataclass
407
+ class ArmyStatus:
408
+ """Current army status for a castle."""
409
+
410
+ castle_id: int
411
+ units: Dict[int, int] = field(default_factory=dict) # unit_type -> count
412
+ total_units: int = 0
413
+ in_production: Dict[int, int] = field(default_factory=dict)
414
+
415
+
416
+ class UnitManager:
417
+ """
418
+ Manages unit production and army composition.
419
+
420
+ Features:
421
+ - Production queue management
422
+ - Auto-recruitment based on targets
423
+ - Army composition recommendations
424
+ - Multi-castle coordination
425
+ """
426
+
427
+ def __init__(self, client: "EmpireClient"):
428
+ self.client = client
429
+ self.queue: List[RecruitmentTask] = []
430
+ self.targets: Dict[int, UnitProductionTarget] = {} # castle_id -> targets
431
+ self._auto_recruit_enabled = False
432
+ self._running = False
433
+
434
+ @property
435
+ def castles(self) -> Dict[int, Castle]:
436
+ """Get player's castles."""
437
+ player = self.client.state.local_player
438
+ if player:
439
+ return player.castles
440
+ return {}
441
+
442
+ def set_target(self, castle_id: int, unit_type: int, count: int):
443
+ """
444
+ Set target unit count for a castle.
445
+
446
+ Args:
447
+ castle_id: Castle ID
448
+ unit_type: Unit type ID
449
+ count: Target number of units
450
+ """
451
+ if castle_id not in self.targets:
452
+ self.targets[castle_id] = UnitProductionTarget(castle_id=castle_id)
453
+
454
+ self.targets[castle_id].targets[unit_type] = count
455
+ logger.info(f"Set target: Castle {castle_id}, Unit {unit_type}, Count {count}")
456
+
457
+ def set_army_composition(
458
+ self,
459
+ castle_id: int,
460
+ composition: Dict[int, int],
461
+ ):
462
+ """
463
+ Set complete army composition target.
464
+
465
+ Args:
466
+ castle_id: Castle ID
467
+ composition: Dict of {unit_type: count}
468
+ """
469
+ self.targets[castle_id] = UnitProductionTarget(
470
+ castle_id=castle_id,
471
+ targets=composition.copy(),
472
+ )
473
+ logger.info(f"Set army composition for castle {castle_id}")
474
+
475
+ def clear_targets(self, castle_id: Optional[int] = None):
476
+ """Clear production targets."""
477
+ if castle_id:
478
+ self.targets.pop(castle_id, None)
479
+ else:
480
+ self.targets.clear()
481
+
482
+ def add_task(
483
+ self,
484
+ castle_id: int,
485
+ unit_type: int,
486
+ count: int,
487
+ priority: int = 0,
488
+ ) -> RecruitmentTask:
489
+ """Add a recruitment task to the queue."""
490
+ task = RecruitmentTask(
491
+ castle_id=castle_id,
492
+ unit_type=unit_type,
493
+ count=count,
494
+ priority=priority,
495
+ )
496
+ self.queue.append(task)
497
+ self._sort_queue()
498
+ logger.info(f"Added recruitment: {count}x Unit {unit_type} in Castle {castle_id}")
499
+ return task
500
+
501
+ def remove_task(self, castle_id: int, unit_type: int) -> bool:
502
+ """Remove a task from the queue."""
503
+ for i, task in enumerate(self.queue):
504
+ if task.castle_id == castle_id and task.unit_type == unit_type:
505
+ self.queue.pop(i)
506
+ return True
507
+ return False
508
+
509
+ def clear_queue(self, castle_id: Optional[int] = None):
510
+ """Clear recruitment queue."""
511
+ if castle_id:
512
+ self.queue = [t for t in self.queue if t.castle_id != castle_id]
513
+ else:
514
+ self.queue.clear()
515
+
516
+ def get_army_status(self, castle_id: int) -> Optional[ArmyStatus]:
517
+ """Get current army status for a castle."""
518
+ # Get from state manager
519
+ army = self.client.state.armies.get(castle_id)
520
+ if not army:
521
+ return ArmyStatus(castle_id=castle_id)
522
+
523
+ units = army.units.copy()
524
+ total = sum(units.values())
525
+
526
+ return ArmyStatus(
527
+ castle_id=castle_id,
528
+ units=units,
529
+ total_units=total,
530
+ )
531
+
532
+ def get_deficit(self, castle_id: int) -> Dict[int, int]:
533
+ """
534
+ Get unit deficit compared to targets.
535
+
536
+ Returns:
537
+ Dict of {unit_type: deficit_count}
538
+ """
539
+ deficit: Dict[int, int] = {}
540
+ target = self.targets.get(castle_id)
541
+ if not target:
542
+ return deficit
543
+
544
+ status = self.get_army_status(castle_id)
545
+ current_units = status.units if status else {}
546
+
547
+ for unit_type, target_count in target.targets.items():
548
+ current = current_units.get(unit_type, 0)
549
+ if current < target_count:
550
+ deficit[unit_type] = target_count - current
551
+
552
+ return deficit
553
+
554
+ def calculate_recruitment_tasks(self, castle_id: Optional[int] = None) -> List[RecruitmentTask]:
555
+ """
556
+ Calculate recruitment tasks needed to meet targets.
557
+
558
+ Args:
559
+ castle_id: Specific castle or None for all
560
+
561
+ Returns:
562
+ List of RecruitmentTask objects
563
+ """
564
+ tasks = []
565
+ castle_ids = [castle_id] if castle_id else list(self.targets.keys())
566
+
567
+ for cid in castle_ids:
568
+ deficit = self.get_deficit(cid)
569
+ for unit_type, count in deficit.items():
570
+ if count > 0:
571
+ tasks.append(
572
+ RecruitmentTask(
573
+ castle_id=cid,
574
+ unit_type=unit_type,
575
+ count=count,
576
+ )
577
+ )
578
+
579
+ return tasks
580
+
581
+ async def execute_task(self, task: RecruitmentTask) -> bool:
582
+ """Execute a recruitment task."""
583
+ try:
584
+ success = await self.client.recruit_units(
585
+ castle_id=task.castle_id,
586
+ unit_id=task.unit_type,
587
+ count=task.count,
588
+ )
589
+
590
+ if success:
591
+ # Remove from queue
592
+ self.queue = [
593
+ t for t in self.queue if not (t.castle_id == task.castle_id and t.unit_type == task.unit_type)
594
+ ]
595
+ logger.info(f"Started recruitment: {task.count}x Unit {task.unit_type} in Castle {task.castle_id}")
596
+
597
+ return bool(success)
598
+ except Exception as e:
599
+ logger.error(f"Recruitment failed: {e}")
600
+ return False
601
+
602
+ async def process_queue(self) -> int:
603
+ """Process the recruitment queue."""
604
+ if not self.queue:
605
+ return 0
606
+
607
+ executed = 0
608
+ for task in self.queue[:]: # Copy to allow modification
609
+ success = await self.execute_task(task)
610
+ if success:
611
+ executed += 1
612
+ await asyncio.sleep(0.5) # Rate limit
613
+
614
+ return executed
615
+
616
+ async def auto_recruit(self) -> int:
617
+ """
618
+ Automatically recruit units to meet targets.
619
+
620
+ Returns:
621
+ Number of recruitment tasks executed
622
+ """
623
+ # Calculate needed recruitments
624
+ tasks = self.calculate_recruitment_tasks()
625
+
626
+ if not tasks:
627
+ logger.debug("No recruitment needed")
628
+ return 0
629
+
630
+ # Execute tasks
631
+ executed = 0
632
+ for task in tasks:
633
+ success = await self.execute_task(task)
634
+ if success:
635
+ executed += 1
636
+ await asyncio.sleep(0.5)
637
+
638
+ logger.info(f"Auto-recruit: executed {executed} tasks")
639
+ return executed
640
+
641
+ async def start_auto_recruit(self, interval: int = 120):
642
+ """
643
+ Start automatic recruitment.
644
+
645
+ Args:
646
+ interval: Check interval in seconds
647
+ """
648
+ self._auto_recruit_enabled = True
649
+ self._running = True
650
+
651
+ logger.info(f"Auto-recruit started (interval: {interval}s)")
652
+
653
+ while self._running and self._auto_recruit_enabled:
654
+ try:
655
+ await self.auto_recruit()
656
+ except Exception as e:
657
+ logger.error(f"Auto-recruit error: {e}")
658
+
659
+ await asyncio.sleep(interval)
660
+
661
+ def stop_auto_recruit(self):
662
+ """Stop automatic recruitment."""
663
+ self._auto_recruit_enabled = False
664
+ self._running = False
665
+ logger.info("Auto-recruit stopped")
666
+
667
+ def get_summary(self) -> Dict[str, Any]:
668
+ """Get recruitment summary."""
669
+ return {
670
+ "queue_length": len(self.queue),
671
+ "castles_with_targets": len(self.targets),
672
+ "total_deficit": sum(sum(self.get_deficit(cid).values()) for cid in self.targets),
673
+ }
674
+
675
+ def recommend_composition(self, focus: str = "balanced", size: int = 500) -> Dict[int, int]:
676
+ """
677
+ Recommend army composition.
678
+
679
+ Args:
680
+ focus: "balanced", "attack", "defense", "farming"
681
+ size: Total army size
682
+
683
+ Returns:
684
+ Dict of {unit_type: count}
685
+ """
686
+ if focus == "attack":
687
+ return {
688
+ UnitType.SWORDSMAN: int(size * 0.3),
689
+ UnitType.KNIGHT: int(size * 0.2),
690
+ UnitType.CROSSBOWMAN: int(size * 0.2),
691
+ UnitType.HEAVY_CAVALRY: int(size * 0.2),
692
+ UnitType.CATAPULT: int(size * 0.1),
693
+ }
694
+ elif focus == "defense":
695
+ return {
696
+ UnitType.SPEARMAN: int(size * 0.3),
697
+ UnitType.CROSSBOWMAN: int(size * 0.3),
698
+ UnitType.WALL_DEFENDER: int(size * 0.2),
699
+ UnitType.MANTLET: int(size * 0.2),
700
+ }
701
+ elif focus == "farming":
702
+ return {
703
+ UnitType.MILITIA: int(size * 0.5),
704
+ UnitType.SWORDSMAN: int(size * 0.3),
705
+ UnitType.BOWMAN: int(size * 0.2),
706
+ }
707
+ else: # balanced
708
+ return {
709
+ UnitType.SWORDSMAN: int(size * 0.25),
710
+ UnitType.SPEARMAN: int(size * 0.15),
711
+ UnitType.CROSSBOWMAN: int(size * 0.2),
712
+ UnitType.KNIGHT: int(size * 0.15),
713
+ UnitType.LIGHT_CAVALRY: int(size * 0.15),
714
+ UnitType.CATAPULT: int(size * 0.1),
715
+ }
716
+
717
+ def _sort_queue(self):
718
+ """Sort queue by priority."""
719
+ self.queue.sort(key=lambda t: t.priority, reverse=True)