feat: add page map recognition and grouped task planning
This commit is contained in:
1 parent
386198e8e0
commit
f7628ab41e
76 files changed
+12437
-49
No files matched your search
BIN
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,107 @@
|
||||
import copy
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from automation.pages import PageMap
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class PageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.page_map = PageMap(ROOT)
|
||||
|
||||
def frame(self, *page_ids):
|
||||
image = Image.new('RGB', (562, 1002), (35, 50, 70))
|
||||
for page_id in page_ids:
|
||||
for spec in self.page_map.pages[page_id]['recognition'][0]:
|
||||
with Image.open(ROOT / 'assets' / spec['template']) as template:
|
||||
x, y = spec['search'][:2]
|
||||
image.paste(template.convert('RGB'), (x + 4, y + 4))
|
||||
return np.asarray(image)
|
||||
|
||||
def test_primary_pages_are_distinct(self):
|
||||
for page_id in ('home', 'shop', 'heroes', 'king', 'territory', 'guild'):
|
||||
with self.subTest(page=page_id):
|
||||
result = self.page_map.identify(self.frame(page_id))
|
||||
self.assertEqual(result['status'], 'identified')
|
||||
self.assertEqual(result['page']['id'], page_id)
|
||||
|
||||
def test_white_or_darkened_frame_is_not_treated_as_home(self):
|
||||
self.assertEqual(self.page_map.identify(np.full((1002, 562, 3), 255, dtype=np.uint8))['status'], 'unknown')
|
||||
dark = (self.frame('home').astype(float) * .35).astype(np.uint8)
|
||||
self.assertEqual(self.page_map.identify(dark)['status'], 'unknown')
|
||||
|
||||
def test_overlay_has_priority_and_does_not_guess_its_parent(self):
|
||||
result = self.page_map.identify(self.frame('shop', 'reward'))
|
||||
self.assertEqual(result['page']['id'], 'reward')
|
||||
self.assertIsNone(self.page_map.pages['reward']['parent'])
|
||||
|
||||
def test_duplicate_identity_reports_ambiguity(self):
|
||||
duplicate = copy.deepcopy(self.page_map.pages['territory'])
|
||||
duplicate['id'] = 'another_territory'
|
||||
self.page_map.pages[duplicate['id']] = duplicate
|
||||
result = self.page_map.identify(self.frame('territory'))
|
||||
self.assertEqual(result['status'], 'ambiguous')
|
||||
self.assertIsNone(result['page'])
|
||||
|
||||
def test_specific_tab_supersedes_matching_overlay_and_background(self):
|
||||
container = copy.deepcopy(self.page_map.pages['reward'])
|
||||
container['id'] = 'test.container'
|
||||
self.page_map.pages[container['id']] = container
|
||||
# Keep the generic reward out of this fixture: it has no known parent.
|
||||
self.page_map.pages['reward']['recognition'] = []
|
||||
tab = copy.deepcopy(container)
|
||||
tab.update(id='test.tab', parent='test.container', kind='tab')
|
||||
self.page_map.pages[tab['id']] = tab
|
||||
result = self.page_map.identify(self.frame('shop', 'test.tab'))
|
||||
self.assertEqual(result['page']['id'], 'test.tab')
|
||||
self.assertEqual(result['status'], 'identified')
|
||||
|
||||
def test_territory_to_recruit_route_includes_home_and_shop(self):
|
||||
route = self.page_map.route('territory', 'recruit.lamp')
|
||||
self.assertEqual([edge['to'] for edge in route], ['home', 'shop', 'recruit.lamp'])
|
||||
self.assertIn('condition', route[-1])
|
||||
|
||||
def test_unverified_paths_are_excluded(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.page_map.route('home', 'guild.boss')
|
||||
with self.assertRaises(ValueError):
|
||||
self.page_map.route('reward', 'home')
|
||||
|
||||
def test_shared_progress_popup_requires_known_origin(self):
|
||||
frame = self.frame('planet.progress')
|
||||
self.assertEqual(self.page_map.identify(frame)['status'], 'ambiguous')
|
||||
self.assertEqual(self.page_map.identify(frame, previous_page='territory.rank.board')['page']['id'],
|
||||
'territory.rank.progress')
|
||||
self.assertEqual(self.page_map.identify(frame, previous_page='planet.rank')['page']['id'],
|
||||
'planet.progress')
|
||||
self.assertEqual(self.page_map.identify(frame, previous_page='home')['status'], 'ambiguous')
|
||||
|
||||
def test_mode_selection_uses_its_distinct_card_label(self):
|
||||
for page_id in ('coop.selection', 'arena.selection'):
|
||||
with self.subTest(page=page_id):
|
||||
result = self.page_map.identify(self.frame(page_id))
|
||||
self.assertEqual(result['page']['id'], page_id)
|
||||
|
||||
def test_pet_weekly_challenge_is_not_home_weekly_tasks(self):
|
||||
# Actual audit pixels retain the similar task title that previously
|
||||
# matched home weekly tasks; the selected navigation label differs.
|
||||
frame = np.asarray(Image.open(ROOT / 'tests/fixtures/pet_challenge_identity.png').convert('RGB'))
|
||||
result = self.page_map.identify(frame)
|
||||
self.assertEqual(result['page']['id'], 'coop.pet.challenge.weekly')
|
||||
self.assertNotIn('tasks.weekly', [p['id'] for p in result['candidates']])
|
||||
|
||||
def test_history_does_not_resolve_unrelated_matches(self):
|
||||
duplicate = copy.deepcopy(self.page_map.pages['territory'])
|
||||
duplicate.update(id='test.unknown_territory', parent='home')
|
||||
self.page_map.pages[duplicate['id']] = duplicate
|
||||
result = self.page_map.identify(self.frame('territory'), previous_page='home')
|
||||
self.assertEqual(result['status'], 'ambiguous')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,59 @@
|
||||
import copy
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from automation.planning import PAGE_ORDER, TaskPlanner
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class PlanningTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.data = json.loads((ROOT / 'workflows/page_map.json').read_text())
|
||||
self.planner = TaskPlanner(self.data, ROOT)
|
||||
|
||||
def test_map_and_task_references_are_valid(self):
|
||||
self.assertTrue(self.planner.audit()['valid'], self.planner.audit()['issues'])
|
||||
|
||||
def test_disabled_ancestor_and_cycles_are_rejected(self):
|
||||
data = copy.deepcopy(self.data)
|
||||
page = next(p for p in data['pages'] if p['id'] == 'guild.store')
|
||||
page['automation'] = {'enabled': False}
|
||||
issues = TaskPlanner(data, ROOT).audit()['issues']
|
||||
self.assertTrue(any('disabled ancestor guild.store' in issue for issue in issues))
|
||||
page['parent'] = 'guild.store.contribution'
|
||||
self.assertTrue(any('cyclic parent' in issue for issue in TaskPlanner(data, ROOT).audit()['issues']))
|
||||
data['planned_tasks'][0]['group_page'] = 'missing_group'
|
||||
self.assertTrue(any('unknown group page missing_group' in issue
|
||||
for issue in TaskPlanner(data, ROOT).audit()['issues']))
|
||||
|
||||
def test_manual_and_deferred_tasks_are_not_in_default_plan(self):
|
||||
plan = self.planner.plan()
|
||||
ids = {row['id'] for row in plan['tasks']}
|
||||
self.assertNotIn('sweep', ids)
|
||||
self.assertNotIn('wall_command_build', ids)
|
||||
self.assertNotIn('movie_daily_rewards', ids)
|
||||
expanded = self.planner.plan(True, True)
|
||||
self.assertTrue({'sweep', 'wall_command_build', 'movie_daily_rewards'} <= {r['id'] for r in expanded['tasks']})
|
||||
|
||||
def test_shared_store_keeps_two_tasks_and_legacy_home_contract(self):
|
||||
plan = self.planner.plan()
|
||||
hub = next(group for group in plan['visit_groups'] if group['hub'] == 'guild.store')
|
||||
self.assertEqual(set(hub['tasks']), {'guild_contribution_store_purchases', 'guild_dragon_scale_store_purchases'})
|
||||
legacy = next(row for row in plan['tasks'] if row['id'] == 'recruit')
|
||||
self.assertEqual(legacy['script_start'], 'home')
|
||||
self.assertEqual(legacy['navigation_mode'], 'legacy_script_owns_navigation')
|
||||
|
||||
def test_page_order_blockers_and_config_are_preserved(self):
|
||||
before = copy.deepcopy(self.data)
|
||||
rows = self.planner.plan()['tasks']
|
||||
groups = [PAGE_ORDER.index(row['group_page']) for row in rows]
|
||||
self.assertEqual(groups, sorted(groups))
|
||||
self.assertTrue(all('任务执行器尚未实现' in row['blockers'] for row in rows if row['implementation'] == 'planned'))
|
||||
self.assertEqual(self.data, before)
|
||||
self.assertTrue(all(r['group_page'] == 'guild' for r in self.planner.plan(group_page='guild')['tasks']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user