PublicLogMemory: the observation's fair fields from a timed log of card plays¶
royaleimitate.public_log.PublicLogMemory gives you one seat's fair observation fields with no
engine running. You give it a timed log of card plays and the seat's dealt deck order. It gives you
the numbers the env would show that seat at the same tick. Log the ability presses too: see
Ability presses.
What it fills¶
Both elixir bars, your own hand and cycle, the cards the opponent has shown, and the time since the
last play. fields() lists the names in vector order: RoyaleGym's FAIR_FIELDS, plus
enemy_last_card when you pass enemy_last_card=True.
What it cannot fill¶
The board: tower hitpoints, crowns and which kings are awake (BOARD_FIELDS). A log of plays does
not say what the plays did.
Ability presses¶
A press of an ability button (a champion's, a hero's) plays no card, and it is paid from the bar: 1
elixir for a Golden Knight's, 3 for a hero Musketeer's, 2 for a hero Ice Golem's. Log each one with
own_press(tick, elixir) or enemy_press(tick, elixir), dated like a play. Both counted bars then
charge it, as the env's MatchMemory does from RoyaleGym 276c3e9 on. The env reads a press off the
public ability rows.
A press missing from the log leaves that side's counted bar high by its cost, until the bar is full again. A log with presses needs RoyaleGym 276c3e9 or later. A log without them runs on older ones as before.
The env makes presses only when RoyaleGym's action parser has its opt-in ability buttons on. A log
of real matches has one wherever a champion's or hero's ability was used. A battle in
tests/test_public_log.py in which both seats press a Golden Knight checks every field against the
env at every step.
The command delay¶
With a command delay (RoyaleGym's command_delay_ticks, on an engine that has one), a play or a
press is accepted on one tick and runs some ticks later. It is paid, and the hand moves, only when
it runs, as in the client. Log each one at the tick it runs.
A seat knows its own taps, so give its own commands the tick they were accepted too: own_play(tick,
card, accepted=...) and own_press(tick, elixir, accepted=...). From the tick after that through
the tick it runs, the fields show the command waiting, as the env's do: the waiting card is flagged
in own_hand_pending, own_pending_cost shows what it holds, and the hand is priced from the bar
less that. A waiting Mirror holds the price it had when accepted. The opponent's waiting commands
are never an input.
A log without accepted ticks shows nothing waiting, and runs on a RoyaleGym before 5565645 as
before. Battles in tests/test_public_log.py with the two seats' commands waiting 21 and 22 ticks
check every field against the env at every step, with and without the Mirror in both decks.
Use¶
from royalegym.mock_engine import MockEngine
from royaleimitate.public_log import PublicLogMemory
CARD_NAMES = ["Knight", "Archer", "Goblins", "Giant", "Musketeer",
"Skeletons", "Minions", "Valkyrie", "Fireball", "Arrows"]
cards = MockEngine(card_names=CARD_NAMES).cards()
memory = PublicLogMemory(cards, list(range(8)), card_names=CARD_NAMES)
memory.own_play(168, 0) # the Knight in the first hand slot
memory.enemy_play(170, 9) # the opponent plays Arrows
fields = memory.observe(200)
print(fields["own_elixir"], fields["enemy_cards_seen"])
cardsis the run's catalogue, in the run's order. Card ids are positions in it.- The deck order is the seat's eight card ids as the match dealt them: the first four are the hand, slot by slot, then the queue, next card first.
start_tick,own_elixir_milliandenemy_elixir_millistart the memory mid-match.calibrationsets the elixir rules and the match clock. Pass the one your run's engine uses, or leave it out for the default.- Feed plays in any amount ahead of time.
observe(tick)sees exactly the plays made before that tick. Ticks only move forward, and a play dated before an observed tick is refused. own_pressandenemy_presslog ability presses the same way: see Ability presses.own_ticks_since_playcounts from the observation that first showed the play, as the env does.unaffordablecounts plays the counted bar could not pay, as (own, enemy). It stays (0, 0) on a log an engine produced. Anything else means a play is missing from the log or the elixir law is different, and from then on the elixir fields are estimates.
The card_names pin¶
Card ids are positions in the catalogue, so making one more card loadable renumbers every later id.
card_names is required. The names in cards must equal it exactly, in order, or the constructor
raises ValueError. The message names the first id that differs, the pinned name and the loaded
name there, and both lengths. Any difference refuses, including one extra card at the end.
Pass the list your run pins, the one its config names. Do not read it back from the catalogue: the check would then compare the catalogue with itself and could never fail.
One set of formulas¶
It is a thin wrapper over RoyaleGym's MatchMemory (bind, start, advance, show_own_hand)
and fair_fields. The env keeps its own MatchMemory with the same calls and reads its vector with
the same fair_fields. Of the calls, only the clock differs: the env reads it from the engine, and
this class works it out from the tick with MatchClock.at. Apart from the card_names check, this
class adds only where the inputs come from: plays from the log, the own hand from the deck order,
and what each hand slot costs (hand_costs()), which for a Mirror is more than its listed elixir.
The assumptions, and where each is checked¶
- A played card's hand slot takes the next card, and the played card goes to the back of the queue.
- A play at tick p is paid before tick p runs, and shows in any observation after tick p.
- A match still running at the end of regulation is in overtime.
- The elixir rate at a tick is
ElixirLaw.rate_at. - A Mirror in the hand costs the card it would copy plus one. The card it copies is its side's
last play that was not a Mirror. This one is checked against the engine's own price for each
hand slot (
PlayerState.hand_costs), in battles where both seats hold the Mirror.
The elixir counts charge a Mirror play the same price, the card it copied plus one. That count is
RoyaleGym's MatchMemory, which does it from RoyaleGym c6a36b0 on; before that it charged one
elixir, and a counted bar read too high after every Mirror play. The env writes a Mirror slot at
its price from RoyaleGym 74c3852 on. So the fields here match the env's with RoyaleGym 74c3852 or
later, as long as it can load your engine: a RoyaleGym older than its engine refuses to build a
RustEngine. On RoyaleSim 1e6a6a5 the oldest RoyaleGym that loads it and passes these tests is
600bfc8. A battle in tests/test_public_log.py with the Mirror in both decks checks every field
against the env at every step.
tests/test_public_log.py checks each one against MockEngine and RustEngine. It plays battles on
both, logs every accepted play, and compares every field with the env's vector at every step, for
both seats. The field names and their order must match too. The battles cover a full turn of both
queues, the switch to 2x and the tick regulation ends. A log dated one tick late must fail. The
RustEngine rows skip, with the reason, when the RoyaleSim core cannot be imported.