- In arena.py, widened RESULT_MODAL_MAX_POLLS to 200 to accommodate real battle durations and added early exit check in _wait_for_result to prevent stuck screens. - Updated _fight_one to properly handle _wait_for_result's return value, ensuring failures stop the fight loop. - In bounty.py, implemented OCR verification in _confirm_dialog_is_sweep to ensure the correct confirmation dialog is displayed before proceeding with sweeps, preventing unintended battles. - Documented hazards and fixes in plan.md, detailing incidents from 2026-07-17 that led to these changes.
440 lines
22 KiB
Python
440 lines
22 KiB
Python
"""Arena / Tactical Challenge. Reference: baas-reference/module/arena.py.
|
|
|
|
Ports `implement`'s flow: open Tactical Challenge, OCR the ticket count, stop
|
|
early (collecting rewards only) if there are no tickets or -- per
|
|
config.ARENA_STOP_FIGHT_WHEN_RANK1 -- rank 1 is already reached, otherwise
|
|
reroll the chosen opponent slot by level (`choose_enemy`), commit to a fight
|
|
(`攻撃編成`), confirm the skip toggle, sortie, wait for the result, and
|
|
collect both reward slots (`collect_tactical_challenge_reward`).
|
|
|
|
This client differs from the reference in a few confirmed ways:
|
|
|
|
- Tactical Challenge is a card inside the お仕事 (Work) hub
|
|
(config.ARENA_WORK_HUB_CARD), not a bottom-nav icon on the main page.
|
|
- The reference's separate opponent-info and formation-edit ("攻撃編成")
|
|
screens are merged into one modal here, showing the matchup and the
|
|
attack-formation button together with a live ticket-count preview
|
|
(e.g. "5→4") confirming it's the real fight-commit step.
|
|
- `navigation.is_modal_open`'s shared darkness probe reads INVERTED on this
|
|
screen specifically (confirmed live: the list's own background art is
|
|
darker than the modal's white card at that exact point) -- this module
|
|
has its own `_is_modal_open` using config.ARENA_MODAL_PROBE instead.
|
|
- Originally (2026-07-09) this fought exactly ONE battle per invocation,
|
|
matching the reference's own per-call pacing (the reference relies on its
|
|
always-running background thread rescheduling itself 55 minutes later via
|
|
`self.next_time = 55`, which this project's one-shot CLI has no
|
|
equivalent for). Per explicit user direction (2026-07-12), this now loops
|
|
internally instead: `_fight_one` fights a single battle, and `run()` calls
|
|
it repeatedly until the OCR'd ticket count reaches 0 (or
|
|
config.ARENA_MAX_FIGHTS_PER_RUN, a defensive bound only -- not a
|
|
hardcoded assumption of the account's real daily ticket count, which is
|
|
read live every run like everything else in this project), waiting
|
|
config.ARENA_POST_BATTLE_COOLDOWN (30s, per the user) between fights for a
|
|
real in-game lockout after a battle finishes before the next one can be
|
|
queued. `config.ARENA_STOP_FIGHT_WHEN_RANK1` is now re-checked before
|
|
every fight in the loop, not just once before the first -- rank can
|
|
change mid-run from fighting. Reward collection still runs unconditionally
|
|
once at the end regardless of how the loop exits (ticket exhaustion, a
|
|
rank-1 stop, or a fight that didn't complete cleanly) rather than the
|
|
reference's "only if this was the last ticket" rule -- both reward slots
|
|
are idempotent/harmless to check regardless.
|
|
|
|
Live-discovered gotchas around the post-fight result (see config.py's
|
|
ARENA_RESULT_CONFIRM_KEY comment for the full writeup -- three real bugs
|
|
across three of this session's five real arena tickets): the "対戦結果"
|
|
WIN/LOSE modal can render later than a naive fixed delay would catch, and
|
|
its confirm button's position varies between WIN and LOSE (different modal
|
|
heights) in a way that made a color-region search unreliable -- a false
|
|
match there once clicked into a completely unrelated opponent's info
|
|
modal. `_wait_for_result` now just presses Enter in a bounded blind-retry
|
|
loop (matching lesson.py's own pattern for "a variable sequence of
|
|
post-action screens"), gated by a hard safety check against the one modal
|
|
where Enter would actually be dangerous (the opponent-info modal's own
|
|
attack-formation button, also Enter-bound, spends a real ticket).
|
|
|
|
Deliberately out of scope for v1 (see mapping.md's Arena row): the
|
|
reference's "no ticket" popup race (get_tickets going stale between the
|
|
initial read and the attack-formation click) and the LOSE variant of the
|
|
result modal, since neither has been seen live yet.
|
|
|
|
Consecutive-invocation navigation fix (2026-07-11): this task never
|
|
returned home at the end, unlike lesson.py/shop_*.py/event_sweep.py. Live
|
|
bug report: a first invocation completed normally and left the game
|
|
sitting on the Tactical Challenge screen; a second invocation's
|
|
_open_tactical_challenge then failed all 3 retries, because its
|
|
WORK_ICON/ARENA_WORK_HUB_CARD clicks are home-screen-relative coordinates
|
|
that mean nothing from wherever the previous run left the game. Fixed by
|
|
calling the shared navigation.return_to_home(driver) at the very start of
|
|
run(), before _open_tactical_challenge -- the same generic Escape-based
|
|
recovery primitive event_sweep.py uses for its own wrong-page recovery.
|
|
|
|
Real hazard found via a real unattended `daily` cron run (2026-07-17),
|
|
reported live by the user: the log showed "battle skip already on" printed
|
|
for all 5 fights and a clean "fought 5 battle(s) this run", but the account
|
|
was left showing a real "Battle Complete" result screen with a ~3-minute
|
|
combat timer -- battles had actually run in full, not skipped -- and every
|
|
task that ran for the rest of that `daily` invocation failed to open its
|
|
own screen. Two compounding bugs, both fixed: (1) `_ensure_skip_on`'s "on"
|
|
detection has never actually been verified to reject a real "off" state
|
|
(see config.py's ARENA_SKIP_ON_RGB comment -- skip happened to already be
|
|
on every session calibrated so far), so a wrong "already on" reading was
|
|
never caught and the toggle was never actually clicked; (2)
|
|
independently, and more directly responsible for the stuck screen,
|
|
`_wait_for_result` had no early-exit signal at all and always ran its full
|
|
fixed budget (10*1.5=15s) before unconditionally declaring success -- far
|
|
too short for a real, non-skipped battle, and `_fight_one` on top of that
|
|
discarded `_wait_for_result`'s return value entirely, so even a correctly
|
|
*detected* failure (including the dangerous opponent-info-modal case its
|
|
own hard safety gate exists to catch) never actually stopped `run()`'s
|
|
fight loop from proceeding to the next ticket. Fixed by widening
|
|
`RESULT_MODAL_MAX_POLLS` substantially (to tolerate a real battle's full
|
|
duration regardless of whether the skip-mode detection bug gets fully
|
|
root-caused) while adding a new `_back_on_challenge_list` early exit (so
|
|
the normal fast-skip-mode case doesn't also slow down), and by making
|
|
`_fight_one` actually stop the loop when `_wait_for_result` reports
|
|
failure. The skip-toggle's own "off" detection was NOT independently
|
|
re-verified (would require spending a real ticket at a moment the account
|
|
had none left) -- this fix is deliberately structured to make the
|
|
consequences safe even if that underlying detection is still wrong, rather
|
|
than depending on fully root-causing it first.
|
|
"""
|
|
|
|
from ba_auto import detector, navigation
|
|
|
|
OPEN_RETRIES = 3
|
|
# Widened from 10 (real hazard found live 2026-07-17, reported by the user):
|
|
# this loop originally always ran its full fixed budget with no early exit,
|
|
# so at the old 10*1.5=15s bound it silently assumed success even when
|
|
# _ensure_skip_on's "already on" check was wrong and a fight actually ran
|
|
# as a real, non-skipped battle (confirmed live: a "Battle Complete" result
|
|
# screen with a ~3-minute combat timer was found on the account afterward,
|
|
# and every task that ran for the rest of that `daily` invocation failed to
|
|
# open its own screen). _ensure_skip_on's own "off" detection has never
|
|
# actually been verified live (see config.py's ARENA_SKIP_ON_RGB comment --
|
|
# it was only ever confirmed to recognize "on", ON by default every session
|
|
# so far), so this bound is widened generously to tolerate a real battle
|
|
# regardless of whether that gets fixed, rather than trusting skip mode to
|
|
# always engage. The new _back_on_challenge_list early-exit (see
|
|
# _wait_for_result) means this larger bound doesn't slow down the normal,
|
|
# already-working case -- a fast skip-mode fight still exits within the
|
|
# first couple of polls, same as before.
|
|
RESULT_MODAL_MAX_POLLS = 200
|
|
RESULT_POLL_INTERVAL = 1.5
|
|
|
|
|
|
def _color_in_range(rgb, rgb_range):
|
|
lo, hi = rgb_range
|
|
r, g, b = rgb
|
|
return lo[0] <= r <= hi[0] and lo[1] <= g <= hi[1] and lo[2] <= b <= hi[2]
|
|
|
|
|
|
def _read_ticket_count(driver, config):
|
|
text = detector.read_text(config.ARENA_TICKET_OCR_RECT, whitelist="0123456789/", psm=7)
|
|
head = text.split("/")[0] if "/" in text else text
|
|
digits = "".join(ch for ch in head if ch.isdigit())
|
|
return int(digits) if digits else None
|
|
|
|
|
|
def _read_rank(driver, config):
|
|
return detector.read_int(config.ARENA_RANK_OCR_RECT)
|
|
|
|
|
|
def _open_tactical_challenge(driver, config):
|
|
for attempt in range(1, OPEN_RETRIES + 1):
|
|
driver.click(*config.WORK_ICON)
|
|
driver.wait(2)
|
|
if navigation.is_on_subscreen(driver):
|
|
break
|
|
print(f"[arena] work hub not detected after click (attempt {attempt}/{OPEN_RETRIES})")
|
|
else:
|
|
# Known recurring environment gotcha (see Handoff.md): the game's
|
|
# UI-hide/photo-mode toggle can leave the screen showing full-art
|
|
# with no icons, so every icon click above silently misses. A
|
|
# background click clears it -- try that once, then retry the
|
|
# whole click sequence rather than repeating the same blind click.
|
|
print("[arena] work hub still not detected -- trying the known UI-hide-toggle recovery click")
|
|
driver.click(960, 600)
|
|
driver.wait(1)
|
|
driver.click(*config.WORK_ICON)
|
|
driver.wait(2)
|
|
if not navigation.is_on_subscreen(driver):
|
|
return False
|
|
|
|
for attempt in range(1, OPEN_RETRIES + 1):
|
|
driver.click(*config.ARENA_WORK_HUB_CARD)
|
|
driver.wait(2)
|
|
if navigation.is_on_subscreen(driver):
|
|
return True
|
|
print(f"[arena] tactical challenge screen not detected after click (attempt {attempt}/{OPEN_RETRIES})")
|
|
return False
|
|
|
|
|
|
def _reward_claimable(driver, config, probe):
|
|
return _color_in_range(driver.color_at(*probe), config.ARENA_REWARD_CLAIMABLE_RGB)
|
|
|
|
|
|
def _collect_rewards(driver, config):
|
|
if _reward_claimable(driver, config, config.ARENA_TIME_REWARD_PROBE):
|
|
print("[arena] claiming time reward")
|
|
driver.click(*config.ARENA_TIME_REWARD_BUTTON)
|
|
driver.wait(1)
|
|
driver.keypress("Return")
|
|
driver.wait(1.2)
|
|
else:
|
|
print("[arena] time reward not currently claimable")
|
|
|
|
if _reward_claimable(driver, config, config.ARENA_DAILY_REWARD_PROBE):
|
|
print("[arena] claiming daily reward")
|
|
driver.click(*config.ARENA_DAILY_REWARD_BUTTON)
|
|
driver.wait(1)
|
|
driver.keypress("Return")
|
|
driver.wait(1.2)
|
|
else:
|
|
print("[arena] daily reward not currently claimable")
|
|
|
|
|
|
def _is_modal_open(driver, config):
|
|
# navigation.is_modal_open's shared probe reads inverted here -- see
|
|
# module docstring and config.py's ARENA_MODAL_PROBE comment.
|
|
r, g, b = driver.color_at(*config.ARENA_MODAL_PROBE)
|
|
threshold = config.ARENA_MODAL_OPEN_MIN_CHANNEL
|
|
return r > threshold and g > threshold and b > threshold
|
|
|
|
|
|
def _open_opponent_modal(driver, config, slot_index):
|
|
driver.click(config.ARENA_OPPONENT_ROW_X, config.ARENA_OPPONENT_ROW_Y[slot_index])
|
|
driver.wait(1.5)
|
|
return _is_modal_open(driver, config)
|
|
|
|
|
|
def _refresh_opponents(driver, config):
|
|
driver.click(*config.ARENA_REFRESH_LIST_BUTTON)
|
|
driver.wait(1.5)
|
|
|
|
|
|
def _choose_enemy(driver, config, slot_index):
|
|
"""Port of choose_enemy: reroll the shown opponents (this client has one
|
|
"リスト更新" button that refreshes all 3 slots at once, matching the
|
|
reference's own single-click-for-3-fixed-slots behavior rather than a
|
|
per-slot reroll) until the chosen slot's level is within
|
|
config.ARENA_LEVEL_DIFF of self, or config.ARENA_MAX_REFRESH_TIMES is
|
|
exhausted. Both level reads are read directly from the list screen, the
|
|
same way the reference does before opening any modal.
|
|
"""
|
|
self_level = detector.read_int_white_on_dark(config.ARENA_SELF_LEVEL_OCR_RECT)
|
|
if self_level is None:
|
|
print("[arena] could not OCR self level -- skipping reroll, using current opponent as-is")
|
|
return
|
|
|
|
rect = config.ARENA_OPPONENT_LEVEL_OCR_RECTS[slot_index]
|
|
for refresh in range(config.ARENA_MAX_REFRESH_TIMES + 1):
|
|
opponent_level = detector.read_int(rect)
|
|
if opponent_level is None:
|
|
print("[arena] could not OCR opponent level -- skipping reroll, using current opponent as-is")
|
|
return
|
|
if opponent_level + config.ARENA_LEVEL_DIFF <= self_level:
|
|
print(f"[arena] opponent level {opponent_level} acceptable (self {self_level}, diff {config.ARENA_LEVEL_DIFF})")
|
|
return
|
|
if refresh >= config.ARENA_MAX_REFRESH_TIMES:
|
|
print(f"[arena] giving up rerolling after {config.ARENA_MAX_REFRESH_TIMES} refreshes, opponent level {opponent_level}")
|
|
return
|
|
print(f"[arena] opponent level {opponent_level} too high (self {self_level}) -- refreshing ({refresh + 1}/{config.ARENA_MAX_REFRESH_TIMES})")
|
|
_refresh_opponents(driver, config)
|
|
|
|
|
|
def _skip_is_on(driver, config):
|
|
return _color_in_range(driver.color_at(*config.ARENA_SKIP_TOGGLE_PROBE), config.ARENA_SKIP_ON_RGB)
|
|
|
|
|
|
def _ensure_skip_on(driver, config):
|
|
if _skip_is_on(driver, config):
|
|
print("[arena] battle skip already on")
|
|
return
|
|
print("[arena] battle skip appears off -- toggling on")
|
|
driver.click(*config.ARENA_SKIP_TOGGLE_CLICK)
|
|
driver.wait(0.8)
|
|
if _skip_is_on(driver, config):
|
|
print("[arena] battle skip confirmed on")
|
|
else:
|
|
print("[arena] warning: could not confirm battle skip is on -- proceeding anyway")
|
|
|
|
|
|
def _opponent_info_modal_showing(driver, config):
|
|
# The opponent-info modal's own gold 攻撃編成 button at this fixed spot
|
|
# is a reliable, position-based signal -- unlike hunting for the result
|
|
# modal's confirm button by color (see below), this doesn't depend on
|
|
# modal height and isn't at risk of matching stray portrait-art pixels.
|
|
return _color_in_range(driver.color_at(*config.ARENA_ATTACK_FORMATION_BUTTON), config.ARENA_REWARD_CLAIMABLE_RGB)
|
|
|
|
|
|
def _back_on_challenge_list(driver, config):
|
|
# The natural "nothing left to dismiss, the fight genuinely wrapped up"
|
|
# signal -- the same navigation.is_on_subscreen check
|
|
# _open_tactical_challenge itself trusts to confirm this exact screen,
|
|
# combined with this module's own (inverted-reading) _is_modal_open to
|
|
# rule out a dialog still open on top of it. Added alongside widening
|
|
# RESULT_MODAL_MAX_POLLS (2026-07-17) so a real, non-skipped battle's
|
|
# much longer wait doesn't also slow down the normal fast-skip-mode
|
|
# case -- lets _wait_for_result's loop exit as soon as a fight is
|
|
# actually done instead of always blindly running its full budget.
|
|
return navigation.is_on_subscreen(driver) and not _is_modal_open(driver, config)
|
|
|
|
|
|
def _wait_for_result(driver, config):
|
|
"""Dismiss whatever unpredictable sequence of screens follows Sortie:
|
|
the "対戦結果" WIN/LOSE modal, and occasionally an unrelated "list
|
|
refresh expired" notice if the season list's own countdown lapses
|
|
mid-fight.
|
|
|
|
A first version tried to precisely locate each one's confirm button by
|
|
color -- but WIN and LOSE modals aren't the same height (WIN shows a
|
|
reward showcase, LOSE doesn't), so their confirm buttons sit at
|
|
different y-positions, and a live test found that searching a region
|
|
wide enough to cover both also risked matching stray cyan-ish pixels in
|
|
the opponent list's own (highly variable, per-refresh) portrait art --
|
|
a false-positive click there opened a completely unrelated opponent's
|
|
info modal instead of confirming anything.
|
|
|
|
This instead follows the same bounded-blind-Enter-press pattern
|
|
lesson.py's _run_one_schedule already uses for its own "variable
|
|
sequence of post-action screens" problem: press Enter, which is the
|
|
universal safe dismiss/confirm for every popup actually involved here,
|
|
and stop once no further popup is showing.
|
|
|
|
Hard safety gate, checked before every single press: the opponent-info
|
|
modal's OWN attack-formation button also responds to Enter and spends a
|
|
real ticket. That modal should never legitimately be showing at this
|
|
point in the flow -- if it is (confirmed live to be reachable via a
|
|
stray click elsewhere), this stops immediately WITHOUT pressing Enter,
|
|
rather than risk an unintended second ticket spend the way a mistimed
|
|
keypress caused a real hazard elsewhere in this project (see CLAUDE.md's
|
|
story_sweep writeup).
|
|
|
|
Real hazard found live (2026-07-17, reported by the user): this loop
|
|
originally had no early exit at all -- it always ran its full fixed
|
|
budget (10*1.5=15s), assuming success once that budget elapsed. When
|
|
_ensure_skip_on's "battle skip already on" check was wrong (see
|
|
config.py's ARENA_SKIP_ON_RGB comment -- its "off" detection was never
|
|
actually verified live), a fight ran as a real, non-skipped battle
|
|
(confirmed via a screenshot: a "Battle Complete" result screen with a
|
|
~3-minute combat timer), and this loop declared the result handled
|
|
after 15s regardless -- while the real battle was still playing. Every
|
|
task that ran for the rest of that `daily` invocation then failed to
|
|
open its own screen, because the game was never actually back at a
|
|
known state. Fixed two ways: RESULT_MODAL_MAX_POLLS widened
|
|
substantially to tolerate a real battle's full duration regardless of
|
|
whether the skip-mode bug gets fixed separately, and a new
|
|
_back_on_challenge_list early exit so this widened budget doesn't also
|
|
slow down the normal fast-skip-mode case -- the loop now exits as soon
|
|
as a fight is genuinely confirmed over, not after a fixed guess.
|
|
"""
|
|
for _ in range(RESULT_MODAL_MAX_POLLS):
|
|
if _opponent_info_modal_showing(driver, config):
|
|
print("[arena] warning: opponent-info modal unexpectedly showing during result wait -- stopping without pressing Enter to avoid an unintended ticket spend")
|
|
return False
|
|
if _back_on_challenge_list(driver, config):
|
|
return True
|
|
driver.keypress(config.ARENA_RESULT_CONFIRM_KEY)
|
|
driver.wait(RESULT_POLL_INTERVAL)
|
|
|
|
if _opponent_info_modal_showing(driver, config):
|
|
print("[arena] warning: opponent-info modal showing after result-wait timeout -- leaving as-is without pressing Enter")
|
|
return False
|
|
return _back_on_challenge_list(driver, config)
|
|
|
|
|
|
def _fight_one(driver, config):
|
|
"""Fight exactly one battle: reroll to an acceptable opponent, commit to
|
|
attack formation (spends a ticket), sortie, and wait out the result.
|
|
Returns True once through to the result being dismissed, False if any
|
|
step along the way couldn't be confirmed -- callers should stop looping
|
|
on False rather than guess whether it's safe to try again immediately.
|
|
"""
|
|
slot_index = config.ARENA_COMPONENT_NUMBER - 1
|
|
_choose_enemy(driver, config, slot_index)
|
|
|
|
if not _open_opponent_modal(driver, config, slot_index):
|
|
print("[arena] opponent-info modal not detected, aborting without spending a ticket")
|
|
return False
|
|
|
|
print("[arena] committing to attack formation (spends a ticket)")
|
|
driver.click(*config.ARENA_ATTACK_FORMATION_BUTTON)
|
|
driver.wait(2)
|
|
|
|
if not navigation.is_on_subscreen(driver):
|
|
print("[arena] attack-formation screen not detected after commit -- ticket may already be spent, check manually. Aborting without pressing further keys")
|
|
return False
|
|
|
|
_ensure_skip_on(driver, config)
|
|
|
|
print("[arena] sortie")
|
|
driver.keypress(config.ARENA_SORTIE_CONFIRM_KEY)
|
|
driver.wait(2)
|
|
|
|
# Real bug found alongside the _wait_for_result fixes above
|
|
# (2026-07-17): this used to discard _wait_for_result's return value
|
|
# and always report success regardless, so even a detected problem --
|
|
# including the dangerous opponent-info-modal case its own hard safety
|
|
# gate exists to catch -- never actually stopped run()'s fight loop
|
|
# from proceeding to the next ticket.
|
|
if not _wait_for_result(driver, config):
|
|
print("[arena] result not confirmed after the fight -- stopping without attempting further fights")
|
|
return False
|
|
return True
|
|
|
|
|
|
def run(driver, config):
|
|
driver.focus_game()
|
|
|
|
# This task never returns home at the end (unlike lesson.py/shop_*.py/
|
|
# event_sweep.py) -- confirmed live: a second invocation starting from
|
|
# wherever the previous run left the game (the arena list, a leftover
|
|
# modal, etc.) sent _open_tactical_challenge's WORK_ICON/ARENA_WORK_HUB_
|
|
# CARD clicks to the wrong place, since those coordinates only mean
|
|
# anything from the home screen, and it failed to reopen tactical
|
|
# challenge at all. Reset to a known state first via the shared
|
|
# navigation.return_to_home primitive (built for exactly this purpose
|
|
# during event_sweep.py's own navigation debugging) -- Escape-based,
|
|
# verifies before every press, safe across this project's screens.
|
|
if not navigation.return_to_home(driver):
|
|
print("[arena] warning: could not confirm return to home screen -- attempting to open tactical challenge anyway")
|
|
|
|
if not _open_tactical_challenge(driver, config):
|
|
print("[arena] could not confirm tactical challenge screen is open, aborting without pressing further keys")
|
|
return
|
|
|
|
tickets = _read_ticket_count(driver, config)
|
|
if tickets is None:
|
|
print("[arena] could not OCR ticket count, aborting without pressing further keys")
|
|
return
|
|
print(f"[arena] tickets: {tickets}")
|
|
|
|
fights = 0
|
|
while tickets > 0 and fights < config.ARENA_MAX_FIGHTS_PER_RUN:
|
|
if config.ARENA_STOP_FIGHT_WHEN_RANK1:
|
|
rank = _read_rank(driver, config)
|
|
if rank == 1:
|
|
print("[arena] already rank 1 -- not fighting")
|
|
break
|
|
print(f"[arena] current rank: {rank}")
|
|
|
|
if not _fight_one(driver, config):
|
|
print("[arena] fight did not complete cleanly -- stopping without attempting further fights")
|
|
break
|
|
fights += 1
|
|
|
|
new_tickets = _read_ticket_count(driver, config)
|
|
if new_tickets is None:
|
|
print("[arena] could not re-read ticket count after the fight -- stopping rather than guess whether more remain")
|
|
break
|
|
tickets = new_tickets
|
|
print(f"[arena] tickets remaining: {tickets}")
|
|
|
|
if tickets > 0 and fights < config.ARENA_MAX_FIGHTS_PER_RUN:
|
|
print(f"[arena] waiting {config.ARENA_POST_BATTLE_COOLDOWN}s for the post-battle cooldown before the next fight")
|
|
driver.wait(config.ARENA_POST_BATTLE_COOLDOWN)
|
|
|
|
print(f"[arena] fought {fights} battle(s) this run")
|
|
_collect_rewards(driver, config)
|
|
print("[arena] Done.")
|