Attack¶
client.attack.send_attack sends an attack from a castle to a map position.
This page builds the waves by hand; Filling waves has the
library fill them the way the game's own button does.
from empire_core import AttackWave, WaveFlank
from empire_core.gamedata import Tool, Unit
commanders = client.commanders.get_commanders()
accepted = client.attack.send_attack(
source_x=500,
source_y=510,
target_x=700,
target_y=710,
waves=[
AttackWave(
left=WaveFlank(units={Unit.SWORDMAN: 100}, tools={Tool.RAM: 5})
)
],
commander_id=commanders[0].commander_id,
)
An AttackWave has a left, middle and right flank, and each
WaveFlank holds its units and tools slots as WodAmount pairs, one per
slot in order. Pass a mapping (units={Unit.SWORDMAN: 100}) and each entry
becomes a slot; an empty slot is EMPTY_SLOT ([-1, 0] on the wire). To
assign slots to an existing flank, WodAmount.slots({...}) builds them. Waves
go front to back.
The courtyard wave, the support tools and collector boosters read the same
way: yard_wave={Unit.SWORDMAN: 300}, support_tools=(Tool.X, None, None)
with None for an empty slot, collector_booster={CurrencyId.SAMURAI_MEDAL_BOOSTER: 5}.
The attack is sent in the kingdom of your area at the source position, looked
up in your castle list; pass kingdom_id to name it yourself. With no area of
yours there it raises UnknownCastleError, and with areas of yours at that
position in several kingdoms, AmbiguousCastleError.
The commander¶
commander_id is required. Every id get_commanders() returns leads an
attack, 0 included, so there is no value that means "no commander". The
server echoes the chosen one back in its reply (CreateAttackResponse.leader).
-14 with use_premium_commander=True leads with the premium commander, which
uses one of your free premium commanders or costs rubies. Where it may cost
rubies send_attack raises PremiumCommanderCostError unless you pass
spend_rubies=True; see the premium commander.
A conquer attack is checked the same way, although the game client sends most
of them without the premium commander's rubies: the guide there has the gap.
What the client checks, and so does the library¶
- Waves without units are dropped before sending, as the game client does.
If no wave carries any,
send_attackraisesValueError. feathers=Trueforces the horse field to -1, exactly as the client does.- With one of your attacks already on its way to the same target,
send_attackraisesAttackInProgressError, carrying that attack's arrival time and size. Passsend_anyway=Trueto send regardless, as the client's confirmation dialog does. - Given
min_soldiersor acapacity, it raisesAttackBelowMinimumErrorbefore sending an attack below the minimum the client requires; see the minimum.
send_attack returns True when the server accepts the attack and False
when it refuses it, for example with MOVEMENT_HAS_NO_UNITS (100).
Before you attack¶
client.attack.get_attack_info(target_x, target_y, source_x, source_y) asks
for the pre-calculation the game's attack dialog asks for: the target's map
row, your inventory and commanders, and your effects scoped to this target.
Pass the target's area_type so the right command is asked, as the client
picks it.
With a spy report on the target, info.spy_army holds its defenders by
position, None without a report. Each position is a tuple of WodAmount
stacks in the order the report lists them. SpyArmySection names the
positions and iterates in wire order:
from empire_core.army import SpyArmySection
army = info.spy_army
if army is not None:
for unit, amount in army.section(SpyArmySection.KEEP):
print(unit, amount)
print(army.wall_total(), [section.value for section in SpyArmySection if section.is_wall])
Presets¶
The game's attack presets are saved armies, one per slot. get_presets()
lists your unlocked slots; save_preset stores a wave in one and
rename_preset names it:
for preset in client.attack.get_presets():
army = preset.army() # PresetArmy, or None for an empty slot
print(preset.index, preset.name, army.to_wave() if army else None)
client.attack.save_preset(0, AttackWave(left=WaveFlank(units={Unit.SWORDMAN: 100})))
client.attack.rename_preset(0, "Farm")
A wave is saved the way the game saves one: its filled slots, without support
tools. A preset name is at most 15 characters and none of the characters the
game refuses in names (SMARTFOX_INVALID_CHARS); rename_preset raises
ValueError for any other, as the game's rename dialog refuses it. Both
return True when the server accepts and False when it refuses. The game
saves only into unlocked slots.
Unlocking a slot is left out: every slot after the first costs rubies, from 2,200 for the second to 72,000 for the thirtieth.
Runnable example¶
This script lists your commanders and prints the attack it would send; it
only sends with --send:
"""List your commanders and send an attack led by one of them.
The script prints the attack it would send and only sends it when you pass
``--send``, so you can inspect the payload before any troops leave.
Pick a real barracks unit for the wave: an inventory also holds tools and
event boosts, and an army built from those is rejected with
MOVEMENT_HAS_NO_UNITS.
Coordinates are absolute map positions, not castle IDs. Unit and tool IDs are
the game's WOD IDs, the same ones ``client.army.get_units()`` returns.
export GGE_USERNAME=your_user
export GGE_PASSWORD=your_pass
python examples/commanders_and_attack.py 700 710 [--send]
"""
import logging
import os
import sys
from empire_core import AttackWave, EmpireClient, EmpireError, WaveFlank
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: commanders_and_attack.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()
# Commanders and castellans come back from the same command; the
# commander is the one that leads an attack.
commanders = client.commanders.get_commanders()
print(f"\n{len(commanders)} commander(s):")
for commander in commanders:
equipment = commander.equipment
print(
f" [{commander.commander_id}] {commander.name or '(unnamed)'} "
f"- {commander.wins}W/{commander.defeats}L, "
f"spree {commander.win_spree}, {len(equipment)} item(s)"
)
castellans = client.commanders.get_castellans()
print(f"{len(castellans)} castellan(s)")
# Commander 0 is the free starting one and leads attacks like any other;
# only a negative id is a sentinel rather than a commander.
leaders = [c for c in commanders if c.commander_id >= 0]
castles = client.castle.get_all()
if not castles or not leaders:
print("Need at least one castle and one commander.", file=sys.stderr)
return 1
source = castles[0]
leader = leaders[0]
units = client.army.get_units(castle_id=source.castle_id)
if not units:
print(f"No units in {source.castle_name!r}.", file=sys.stderr)
return 1
# One wave, everything on the left flank. A real attack spreads units
# and tools across the left, middle and right flanks of several waves;
# waves without units are dropped before sending.
strongest = max(units, key=lambda unit: units[unit])
waves = [AttackWave(left=WaveFlank(units={strongest: min(units[strongest], 10)}))]
print(
f"\nAttack from {source.castle_name!r} ({source.x}, {source.y}) "
f"to ({target_x}, {target_y}) led by commander {leader.commander_id}:"
)
print(f" {waves[0].model_dump(by_alias=True)}")
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=waves,
kingdom_id=source.kingdom_id,
commander_id=leader.commander_id,
)
# send_attack returns False when the server rejects the attack by rule
# (no troops, target out of range, commander already in a movement).
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())
API: AttackService