The battle model
One row per battle, both sides of it, and the words the numbers are built from.
Every battle tool reads the same corpus and the same row shape. This page is the vocabulary those tools share: what a row holds, how modes are grouped, what "decided" means, how a deck gets its identity, and how a war week is counted. The tools themselves are on Tools; the statistics on top of them are on How the numbers are made.
What a battle record holds
A battle is one row, seen from both sides. The game's battle log shows a
battle from the player whose log it is; Elixir MCP records both logs when it
has them and folds them into one battle_id (a SHA-256 of the canonical
battle time, the sorted participant tags and the battle class), so the same
battle read from two logs is one record with two perspectives. battles_query
answers from the perspective of the tag you asked about:
| Field | Meaning |
|---|---|
battle_id |
the record's identity; battles_query({ battle_id }) returns it from every side |
battle_time |
when it was played, ISO 8601 UTC. Never when it was captured |
battle_time_local |
the same instant as ISO 8601 with a UTC offset (2026-09-09T23:31:47-05:00), present when a timezone applies (the account's, or the call's timezone) |
type |
the API's battle type, exactly as the game names it (PvP, riverRacePvP, boatBattle, ...) |
game_mode |
{ id, name } of the game mode, in the game's own naming, event modes included |
arena |
the arena id the battle was fought in |
league_number |
the Path of Legends league when the battle was ranked; null otherwise |
me |
the asked-about participant: outcome (win, loss, draw or unresolved), crowns, trophy_change, starting_trophies, deck_hash, deck, elixir_leaked, tower_hp |
teammates, opponents |
the other participants, each with player_tag, name, name_known, crowns, deck_hash, clan_tag, deck, tower_hp |
name_known |
false when no observation ever carried a name for that tag; players_names resolves the ones the corpus knows |
rounds_played |
present on duel rows only: how many games the row collapses |
deck holds the cards as played, with levels on the in-game 1 to 16 scale
and each card's form (see Deck identity and forms).
elixir_leaked is the game's own leaked-elixir counter for the asked-about
side; null when the game did not report it.
tower_hp is hitpoints remaining at the end of the battle, not tower
level: { king, princess: [a, b] } per side. A destroyed princess tower reads
0, and the array is always padded to length 2 (the API omits a destroyed
tower on head-to-head rows and writes 0 on duel rows, so array length was
never a tower count; position carries no meaning). null means the game did
not report tower state for that side. verbosity: "compact" drops deck,
elixir_leaked and tower_hp and keeps deck_hash.
Mode groups
The mode argument on every battle tool takes one of six groups. Each folds
one or more of the API's battle types; omitting mode pools every group.
This table is generated from the contract, so it is what the tools accept.
| Group | In the game | API battle types folded |
|---|---|---|
casual |
2v2, friendly and trail battles | trail, clanMate2v2, friendly |
challenge |
challenges and events | challenge |
ladder |
Trophy Road | PvP |
ranked |
Path of Legends | pathOfLegend |
tournament |
tournaments | tournament |
war |
river race battles, duels and boat battles | riverRacePvP, riverRaceDuel, riverRaceDuelColosseum, boatBattle |
game_mode.name is finer than the group: an event mode such as a Chaos or
Crazy Mode battle is a challenge-group battle with its own mode name, which
battles_query({ mode_name }) can filter by substring and
battles_performance({ group_by: "mode" }) lists.
Duels and boat battles
Two war battle types are shaped differently from a head-to-head battle.
A duel (riverRaceDuel, riverRaceDuelColosseum) is one row for up to
three games. crowns is summed across the rounds, tower_hp describes the
final round only, deck_hash is null because there is no single deck, the
decks sit under deck.rounds[] one per round, and rounds_played says how
many rounds the row holds. battles_cards and the meta tools exclude duels
for exactly this reason; battles_opponents counts a duel once however many
rounds it held.
A boat battle (boatBattle) is an attack on a static defense, not a
head-to-head match. The record classes every battle as type_class pvp
(head-to-head) or boat, and the tools branch on it. Boat battles are
outside every decided-battle denominator; a boat win still counts in wins.
Decided battles and denominators
A decided battle is a head-to-head battle whose outcome is a win or a
loss. Draws, unresolved outcomes (the game reported no winner) and boat
battles are not decided. Every rate a battle tool serves names its
denominator:
win_rate = decided_wins / decided_battles, wheredecided_battles = decided_wins + decided_losses. Boat battles and draws are outside both sides of that fraction.winsandlossesare the plain counts and do include boat wins, sowinscan exceeddecided_wins.three_crown_rate = three-crown wins / head_to_head_battles. Duels and boat battles are excluded from both sides.battlesis every recorded battle in the window, whatever its kind, sobattlesminusdecided_battlesis draws plus unresolved plus boat.- Duel crowns are summed over rounds, so
crowns_forandcrowns_againstmix units when a window holds duels;duel_battlessays how many did.
The meta tools (battles_meta_decks, battles_meta_cards, battles_trends,
cards_synergy) count decided head-to-head player-battle observations,
both participants of a match when both are in the segment, and itemize what
the window held and left out in excluded. The formulas, priors and floors
are on How the numbers are made.
Deck identity and forms
A deck's identity is deck_hash: the SHA-256 hex of
sort(cards.map(c => id + ":" + (evolutionLevel ?? 0))).join(",") + "|" + (towerTroopId ?? 0).
It is built from card ids, each card's form and the tower troop, never
levels. Two decks with the same eight names but a different form on one
card, or a different tower troop, are two decks; the same deck at two
different card levels is one. Some event modes field more or fewer than
eight cards; the identity is the exact set played.
A card's form is a bit field the API calls evolutionLevel: 1 is the
Evolution form, 2 the Hero form, 3 both, absent or 0 the base card. It
is a form discriminator, never a level or a progress counter, and forms are
never merged: battles_cards and battles_meta_cards carry one row per form.
On a collection, maxEvolutionLevel says which forms exist for the card and
evolutionLevel which the player holds; players_collection and
cards_catalog decode them into forms_available and forms_unlocked.
Cards are recorded as rows, not only as the deck's JSON: every card a
participant played is a fact of its own, so card questions are indexed
lookups rather than scans of every deck. battles_query takes with_card
(one id in your deck), with_cards (several ids, all present) and
against_card (one id in an opponent's deck); battles_cards,
battles_meta_cards and cards_synergy count from the same rows, and a
deck's cards in battles_decks and battles_meta_decks are the identity's
own (ordered by card id, named from the catalog), not one player's copy.
Card filters match the deck's cards, not the tower troop, and not the
separate rounds of a duel. An empty cards list - some event formats
disclose no deck - has no deck_hash.
Levels are served on the in-game 1 to 16 scale everywhere in the recorded
tools: a level-16 card is maxed whatever its rarity. The API itself counts
levels relative to rarity (a maxed legendary reads 8 of 8), and
cards_catalog carries both maxima as maxLevel and maxLevelRarityScale;
live_fetch returns the raw payload, so levels there are rarity-relative.
War weeks, points and fame
A river race is scored twice, and the two numbers are not interchangeable.
Points are what each member contributes: war_current and war_history
list them per member as points, with decks_used. Fame belongs to the
boat, the clan as a whole: the standings show each clan's fame and the
week's finish line is a fame total. Dividing a clan's fame among its members
is not a computation the record supports, and it is never done here.
During a war day, war_current.standings also carries period_points: the
clan's score in the day currently being fought. fame is the cumulative boat
score banked when a day closes. They deliberately do not move together, so on
war day 1 a clan can have non-zero member points and period_points while its
banked fame is still zero.
Rows restored only from finished race history have period_points: null
because that endpoint does not report the former current-day value.
war_history returns one row per recorded week with:
in_progress, true while the week is still being fought; on older weeks anullour_rankorour_famemeans the week was observed without a standings capture, a capture gap rather than a zero.finished_early, true when the boat reached the 10,000-fame line before the week ended. Decks used after the finish earn zero points, so per-deck arithmetic over such a week is invalid.history_starts_at, the recording horizon: fewer seasons than requested is coverage, not absence.member_weeks(withplayer_tag) for one member week by week:war_days_battledcounts the days they fought andwar_dayslists the day indices;nullwar_days_battledmeans per-day attendance is unknown for that week, not zero.
Supply season_id and section_index together to select one exact week.
Without player_tag, member_weeks then contains every recorded participant
for that week, including their tag, name, points, decks, boat attacks and the
same per-day attendance fields. This is the one-call closed-week roster path.
Which day it is, and why a member can appear with more decks than a day holds, is on Time and clocks.