Filling waves¶
client.attack.fill_attack fills an attack the way the game's own "Fill
waves" button does. Give it your castle, a target and a commander; it reads
everything else itself.
client.load_game_data() # optional: takes the first-use download up front
castle = client.castle.get_all()[0]
commander = client.commanders.get_commanders()[1]
attack = client.attack.fill_attack(
castle.castle_id,
target_x=624, target_y=247, # a target is all it needs
commander=commander,
)
client.attack.send_attack(
source_x=castle.x, source_y=castle.y,
target_x=624, target_y=247,
waves=attack.waves, yard_wave=attack.yard,
commander_id=commander.commander_id,
min_soldiers=attack.min_soldiers,
)
fill_attack returns a FilledAttack: the waves, the courtyard wave
yard, and min_soldiers, the fewest units the waves must carry.
What it reads¶
From the coordinates it reads the target's area type and structures, the defenders each flank holds and the castellan holding it, the area effects that widen your flanks, your general's skills, and your own legend and Hall of Legends skills. A camp's level comes from the victory count in its map row; a player's from the owner records beside it.
Every one of those can be passed instead, and passing one skips the request
that would have found it. The keyword arguments of
fill_attack
list them all.
The reads are best effort against a refusal: the server refuses the
pre-calculation for a target you may not hit, and the fill goes on with what
the map tile says. Each refused read is named in FilledAttack.unread, keyed
by TargetRead with the server's CommandError, and logged under
empire_core.attack: a warning, or info for the pre-calculation's
INVALID_AREA. A timeout, a dropped connection or an unreadable reply raises,
and so does a target whose area type has no pre-calculation modelled
(ValueError).
attack = client.attack.fill_attack(castle_id, target_x=700, target_y=710)
for read, refusal in attack.unread.items():
print("filled without", read.value, refusal.error) # e.g. precalculation GGEError.INVALID_AREA
flowchart LR
T([Target x, y]) --> R[Attack pre-calculation:<br/>map row, defenders,<br/>area effects]
S[General and player<br/>skills: gie, skl] --> C
R --> C[Wave capacity<br/>from the target owner's level]
R --> D[Defenders, wall, gate<br/>and moat per flank]
C --> F
D --> F
subgraph F [Each flank]
direction TB
F1[Tools first] --> F2[Then units, against<br/>the defense left]
end
F --> Y[Courtyard wave]
Y --> A([FilledAttack])
How the waves are sized¶
Each wave is sized the way the game sizes it, which is by the target owner's level rather than the attacker's: a level 13 castle holds far fewer troops than a level 70 one, whatever the attacker's level. Some targets defend at a level of their own; a monument is built for level 70 however low its owner is. On top come the commander's own equipment, its general's unit-limit skills, the Hall of Legends skills, and the legend skills when both sides are at the level cap.
How each flank is filled¶
Each flank takes tools first and then units, because a placed tool reduces the defense the units are then chosen against.
- Units are picked to counter whichever of the target's defenses is proportionally weaker.
- Tools are picked to cancel the target's wall, gate, moat and defender bonuses in as few units as possible, and are skipped entirely where the commander's own reductions already erase them. A flank that ends up with tools but no units gives the tools back.
- Fortification is per flank, not per castle: a defending tool raises only the flank it stands on, and only the middle flank meets the gate at all.
- Tools are filtered by the target: many may only be carried against particular kingdoms and area types, or not against camps.
- Global effects buff units: the fill methods read the running ones from
state, the timers of the
Event.GLOBAL_EFFECTevent, or take your own asglobal_effects(GlobalEffectTimers). A timer that has ended counts for nothing. The booster's boost to the effectsbielists is added from state: each such effect's strength plus itsGlobalEffectBuffEvent.boost_value.
The courtyard wave¶
Alongside the waves comes the courtyard wave, the final assault that rides in the same request. It holds units only, is sized from both levels rather than the target's alone, and is filled against the defenders of the keep.
The minimum¶
An attack's waves must carry at least a tenth of what one wave holds at the
target's level (8 units against a level 16 castle, 32 from level 70), as the
game client requires. The server refused fewer on a player's castle with
MOVEMENT_HAS_NO_UNITS (100); whether it enforces this on NPC camps is not yet
known.
fill_attackraisesAttackBelowMinimumErrorwhen the castle cannot reach it.send_attackchecks it before sending when givenmin_soldiersor acapacity.empire_core.combat.min_attack_soldiers(target_owner_level, area_type)works it out for any target.
Runnable example¶
"""Fill an attack the way the game's own "Fill Waves" button does, and send it.
Give it a target's coordinates. Everything else is read for you: the target's
area type and structures, the defenders each flank holds, the castellan holding
it, the area effects that widen your flanks, your general's skills and your own.
export GGE_USERNAME=your_user
export GGE_PASSWORD=your_pass
python examples/fill_waves.py 700 710 [--send]
The script prints what it would send and only sends with ``--send``.
"""
import logging
import os
import sys
from empire_core import EmpireClient, EmpireError
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
logging.getLogger("websocket").setLevel(logging.WARNING)
def main() -> int:
username = os.getenv("GGE_USERNAME")
password = os.getenv("GGE_PASSWORD")
if not username or not password:
print("Set GGE_USERNAME and GGE_PASSWORD first.", file=sys.stderr)
return 2
args = [a for a in sys.argv[1:] if a != "--send"]
if len(args) != 2:
print("Usage: fill_waves.py TARGET_X TARGET_Y [--send]", file=sys.stderr)
return 2
target_x, target_y = int(args[0]), int(args[1])
really_send = "--send" in sys.argv
client = EmpireClient(username=username, password=password)
try:
client.login()
# The items payload is a large download, so it is never implicit.
client.load_game_data()
castles = client.castle.get_all()
commanders = [c for c in client.commanders.get_commanders() if c.commander_id >= 0]
if not castles or not commanders:
print("Need at least one castle and one commander.", file=sys.stderr)
return 1
source, leader = castles[0], commanders[0]
# Coordinates are enough: the pre-calculation and the map supply the
# rest. Anything passed here would skip the request that finds it.
filled = client.attack.fill_attack(
source.castle_id,
target_x=target_x,
target_y=target_y,
commander=leader,
)
print(f"\n{len(filled.waves)} wave(s), {filled.unit_count()} units, led by {leader.commander_id}:")
for index, wave in enumerate(filled.waves):
for name, flank in (("left", wave.left), ("middle", wave.middle), ("right", wave.right)):
units = [(unit, amount) for unit, amount in flank.units if unit is not None]
tools = [(tool, amount) for tool, amount in flank.tools if tool is not None]
if units or tools:
print(f" wave {index} {name}: units {units} tools {tools}")
placed = [(unit, amount) for unit, amount in filled.yard if unit is not None]
print(f" courtyard: {placed or 'empty'}")
if not really_send:
print("\nDry run. Re-run with --send to actually send it.")
return 0
accepted = client.attack.send_attack(
source_x=source.x,
source_y=source.y,
target_x=target_x,
target_y=target_y,
waves=filled.waves,
yard_wave=filled.yard,
kingdom_id=source.kingdom_id,
commander_id=leader.commander_id,
)
print("Attack sent." if accepted else "Server rejected the attack.")
except EmpireError as e:
print(f"{type(e).__name__}: {e}", file=sys.stderr)
return 1
finally:
client.close()
return 0
if __name__ == "__main__":
sys.exit(main())