Getting started¶
This page takes you from an empty virtual environment to a script that logs in, reads your castles, scans a kingdom and handles the errors it can meet on the way.
Install¶
EmpireCore supports Python 3.10 and newer and needs pydantic 2.5 or newer. It
ships a py.typed marker, so mypy and pyright check your calls against its
models.
Working on the library itself
git clone https://github.com/eschnitzler/EmpireCore.git
cd EmpireCore
uv sync --extra dev # a plain `uv sync` has no pytest, ruff or mypy
uv run pytest
To build these docs locally, uv run --group docs mkdocs serve.
Log in¶
from empire_core import EmpireClient
client = EmpireClient(username="your_user", password="your_pass")
client.login()
login() runs the game client's handshake: the WebSocket connection, the
SmartFox version check and zone login, joining the lobby room, the game's own
version check (vck) and finally the account login (lli). It returns once the
server has sent the login data (gbd), which fills client.state with your
player and castles. The login and requests
page draws the whole exchange.
Every failure raises. A wrong password raises LoginError, too many attempts
LoginCooldownError, a banned account AccountBannedError, and an account that
lives on another server WrongServerError. The connection and its threads are
closed before the error reaches you, so a failed login leaks nothing.
Choose a server and client version¶
The default configuration connects to the EmpireEx_21 zone. Each world lives
on its own server, so another world needs that server's address as well as its
zone; instance_for_zone looks both up in the game's server list:
from empire_core import EmpireClient, EmpireConfig, instance_for_zone
config = EmpireConfig.for_instance(instance_for_zone("EmpireEx_2"))
client = EmpireClient(username="your_user", password="your_pass", config=config)
An account in accounts.json with "world": "EmpireEx_2" does this for you.
fetch_network_instances() lists every world of a network.
Each login sends the version of the live game client, read from the client's
files, so a game update needs no library release. Set client_version (on
EmpireConfig, or on an account in accounts.json) only to pin a version.
Log in again without the password¶
After a login the server pushes a login token, which the client keeps in
client.login_token. The push arrives just after the login data, so read it a
moment after login() returns:
token = client.login_token
later = EmpireClient(username="your_user", login_token=token)
later.login()
login(recaptcha_token=...) sends a reCAPTCHA v3 token, or calls a function
for one, the way the browser does. The library cannot make one itself; logins
are accepted without it today.
Close the client¶
A logged-in client runs a receive thread, a keepalive thread and a callback thread. Use the client as a context manager so all of them stop however your code exits:
Without the with block, call client.close() yourself, in a finally. The
context manager does not log in for you, so the errors of login() stay
visible where you call it.
Read your castles¶
castles = client.castle.get_all()
for castle in castles:
print(castle.castle_id, castle.castle_name, castle.kingdom_id)
resources = client.castle.get_resources(castle_id=castles[0].castle_id)
print(resources.wood, resources.stone)
Methods that act on one of your castles take its id alone: the kingdom comes
from the castle list the server sent at login. An id that is not one of your
castles raises UnknownCastleError, and one listed in several of your kingdoms
raises AmbiguousCastleError.
client.castle.get_all() asks the server. What the login data already told the
client is in client.state, readable without a request:
player = client.state.get_local_player() # None until the login data arrives
for castle in client.state.get_castles():
print(castle.id, castle.name)
State is only as fresh as the last packet that carried it; see State and freshness before trusting resources read from it.
Game data¶
Unit stats, building prices and the other static tables come from the game's
items file. An API that needs them loads it on first use, from the disk cache
or a download, so nothing has to be loaded first. That first download is
large; call client.load_game_data() up front to take it before
timing-sensitive work. See Game data.
Scan the map¶
from empire_core import Kingdom, MapItemType
result = client.map.scan_kingdom(Kingdom.GREEN, item_types=[MapItemType.CASTLE])
print(f"{len(result.items)} castles, {len(result.failed_chunks)} chunks failed")
for item in result.items[:5]:
print(item.name, item.x, item.y, item.owner_id)
A full scan walks the kingdom breadth-first from your castle and can take a few
minutes. Always look at failed_chunks: a partial scan is not an empty kingdom.
Map scanning shows how to re-scan cheaply.
Handle errors¶
Everything the library raises derives from EmpireError:
from empire_core import CommandError, EmpireError, EmpireTimeoutError
try:
castles = client.castle.get_all()
except CommandError as e:
print(f"the server refused {e.command} with code {e.code}")
except EmpireTimeoutError:
print("no reply in time")
except EmpireError as e:
print(f"{type(e).__name__}: {e}")
Error handling lists every error and when you meet it.
A complete script¶
The repository's examples run end to end. This one logs in, lists your castles and prints the movements in flight:
"""Log in, list your castles, and read live troop movements.
The smallest useful end-to-end script: connect, read state, disconnect.
Run it with credentials in the environment (never inline them in a file that
might get committed)::
export GGE_USERNAME=your_user
export GGE_PASSWORD=your_pass
python examples/login_and_castles.py
"""
import logging
import os
import sys
from empire_core import EmpireClient, EmpireError
# INFO is the right level for an application. DEBUG additionally dumps every
# protocol frame the library sends and receives, which is useful when reverse
# engineering a command but far too noisy otherwise.
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
client = EmpireClient(username=username, password=password)
try:
# login() opens the connection and raises on failure - LoginError for
# bad credentials, LoginCooldownError when the server is rate limiting,
# EmpireTimeoutError when it never answers.
client.login()
castles = client.castle.get_all()
print(f"\n{len(castles)} castle(s):")
for castle in castles:
print(
f" [{castle.castle_id}] {castle.castle_name!r} at ({castle.x}, {castle.y}) type {castle.castle_type}"
)
if castles:
first = castles[0]
resources = client.castle.get_resources(castle_id=first.castle_id)
print(f"\nResources in {first.castle_name!r}: {resources}")
movements = client.movements.get_movements()
print(f"\n{len(movements)} movement(s) in flight:")
for movement in movements:
print(f" {movement}")
except EmpireError as e:
# Every failure this library raises derives from EmpireError.
print(f"{type(e).__name__}: {e}", file=sys.stderr)
return 1
finally:
# close() stops the receive and keepalive threads. Skipping it leaves
# the process alive with a connected socket.
client.close()
return 0
if __name__ == "__main__":
sys.exit(main())
Next steps¶
-
One page per part of the game: alliance, castle, army, attacks, spies, the map and more.
-
Every service, model and enum, with field descriptions and wire keys.