199 lines
7.6 KiB
Python
199 lines
7.6 KiB
Python
from textual.screen import Screen
|
|
from textual.widgets import Static, Button
|
|
from textual.containers import Vertical, Horizontal, VerticalScroll, Grid
|
|
from textual.message import Message
|
|
from textual.reactive import reactive
|
|
from textual.widget import Widget
|
|
from rich.panel import Panel
|
|
from rich.console import Group
|
|
from rich.table import Table
|
|
from rich.rule import Rule
|
|
from rich.text import Text
|
|
|
|
from agentm.utils.logger import log_with_caller
|
|
from agentm.logic.roms import get_verified_roms, GAME_FILES
|
|
from agentm.theme.palette import get_theme
|
|
from agentm.components.footer import AgentMFooter
|
|
|
|
palette = get_theme()
|
|
|
|
|
|
class GameSelected(Message):
|
|
def __init__(self, sender: Widget, metadata: dict):
|
|
self.metadata = metadata
|
|
super().__init__(sender)
|
|
|
|
|
|
class ProgressWidget(Widget):
|
|
message = reactive("Loading...")
|
|
|
|
def render(self) -> str:
|
|
return f"[bold {palette.ACCENT}]{self.message}[/]"
|
|
|
|
|
|
class GameCardButton(Button):
|
|
def __init__(self, metadata: dict, parent_view):
|
|
self.metadata = metadata
|
|
self.parent_view = parent_view
|
|
safe_id = metadata["rom_file"].replace(".", "_").replace("-", "_")
|
|
label = Text(metadata["title"], style=f"bold {palette.ACCENT}")
|
|
|
|
super().__init__(
|
|
label=label,
|
|
id=f"game_btn_{safe_id}",
|
|
classes="game_button"
|
|
)
|
|
self.styles.min_height = 3 # Ensures buttons stay visible even in constrained space
|
|
|
|
async def on_click(self):
|
|
await self.display_info()
|
|
self.parent_view.highlight_selected(self)
|
|
|
|
async def display_info(self):
|
|
meta = self.metadata
|
|
|
|
table = Table.grid(padding=(0, 1))
|
|
table.add_column("Key", style="bold underline", no_wrap=True)
|
|
table.add_column("Value", style=palette.ACCENT, overflow="fold")
|
|
|
|
table.add_row("Title", meta["title"])
|
|
table.add_row("Game ID", meta["game_id"])
|
|
table.add_row("Difficulty", f"{meta.get('difficulty_min')} - {meta.get('difficulty_max')}")
|
|
table.add_row("Characters", ", ".join(meta.get("characters", [])))
|
|
table.add_row("Keywords", ", ".join(meta.get("keywords", [])))
|
|
table.add_row("SHA256", meta["sha256"])
|
|
|
|
self.parent_view.shared_info_content.update(
|
|
Panel(Group(table, Rule(style="dim")), title="Game Info", border_style=palette.BORDER, expand=True)
|
|
)
|
|
self.parent_view.shared_confirm_button.label = f"✅ Confirm {meta['title']}"
|
|
self.parent_view.shared_confirm_button.disabled = False
|
|
self.parent_view.selected_game = meta
|
|
|
|
|
|
class HomeView(Screen):
|
|
BINDINGS = [("escape", "app.quit", "Quit")]
|
|
|
|
def highlight_selected(self, selected_widget: GameCardButton):
|
|
for card in self.rom_grid.children:
|
|
if isinstance(card, GameCardButton):
|
|
card.remove_class("game_card_clicked")
|
|
selected_widget.add_class("game_card_clicked")
|
|
|
|
def compose(self):
|
|
self.logo = Static(
|
|
f"[bold {palette.ACCENT}]\n\n"
|
|
" █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ███╗ ███╗\n"
|
|
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
|
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
|
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
|
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║\n"
|
|
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
|
classes="header",
|
|
expand=False,
|
|
)
|
|
|
|
self.welcome_text = Static(
|
|
"This is an unofficial DIAMBRA launcher to help you easily train, evaluate, and submit RL agents for fighting games.",
|
|
classes="body",
|
|
expand=False
|
|
)
|
|
|
|
self.progress_text = ProgressWidget()
|
|
|
|
self.loading_container = Vertical(
|
|
Static(f"[bold {palette.SUCCESS}]LOADING...[/]", expand=True),
|
|
self.progress_text,
|
|
id="loading_container"
|
|
)
|
|
|
|
self.dynamic_container = Vertical(self.loading_container, id="dynamic_content")
|
|
|
|
yield Vertical(
|
|
self.logo,
|
|
self.welcome_text,
|
|
self.dynamic_container,
|
|
AgentMFooter(compact=True),
|
|
id="home_screen_container",
|
|
classes="centered_layout"
|
|
)
|
|
|
|
async def on_mount(self):
|
|
log_with_caller("debug", "HomeView mounted. Starting ROM verification.")
|
|
self.selected_game = None
|
|
self.run_worker(self.run_verification, thread=True, exclusive=True, name="rom-verification")
|
|
|
|
def run_verification(self):
|
|
total = len(GAME_FILES)
|
|
verified_roms = get_verified_roms()
|
|
|
|
for idx, rom in enumerate(verified_roms, start=1):
|
|
self.app.call_from_thread(
|
|
lambda title=rom["title"], idx=idx: setattr(
|
|
self.progress_text, "message",
|
|
f"Processing {title} ({idx}/{total})"
|
|
)
|
|
)
|
|
import time
|
|
time.sleep(0.01)
|
|
|
|
self.app.call_from_thread(lambda: self.display_verified_roms(verified_roms))
|
|
|
|
async def display_verified_roms(self, verified_roms):
|
|
log_with_caller("info", f"ROM verification complete. Total: {len(verified_roms)}")
|
|
|
|
self.shared_info_content = Static(
|
|
Panel(
|
|
"[dim]Select a Game From the Grid Below to Start[/dim]",
|
|
title="Game Info",
|
|
border_style=palette.BORDER,
|
|
expand=True
|
|
),
|
|
id="info_panel_static"
|
|
)
|
|
|
|
self.shared_info_box = VerticalScroll(
|
|
self.shared_info_content,
|
|
id="game_info_box",
|
|
classes="game_info"
|
|
)
|
|
self.shared_info_box.styles.height = 7 # Around 5 visible rows
|
|
|
|
self.shared_confirm_button = Button(
|
|
"✅ Confirm",
|
|
id="confirm_button",
|
|
classes="confirm_button",
|
|
disabled=True
|
|
)
|
|
|
|
self.rom_grid = Grid(id="rom_grid", classes="rom_grid")
|
|
self.rom_grid.styles.grid_columns = ["1fr"] * 5
|
|
self.rom_grid.styles.grid_gap = (0, 1)
|
|
self.rom_grid.styles.width = "100%"
|
|
|
|
rom_grid_scroll = VerticalScroll(self.rom_grid, id="rom_grid_scroll")
|
|
rom_grid_scroll.styles.max_height = "30vh"
|
|
|
|
new_content = Vertical(
|
|
rom_grid_scroll,
|
|
Horizontal(
|
|
self.shared_info_box,
|
|
Vertical(self.shared_confirm_button, id="confirm_button_container"),
|
|
id="info_row",
|
|
classes="info_confirm_row"
|
|
),
|
|
id="game_content_layout",
|
|
classes="game_content_layout"
|
|
)
|
|
|
|
dynamic_container = self.query_one("#dynamic_content")
|
|
await dynamic_container.remove_children()
|
|
await dynamic_container.mount(new_content)
|
|
|
|
for rom in verified_roms:
|
|
await self.rom_grid.mount(GameCardButton(metadata=rom, parent_view=self))
|
|
|
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "confirm_button" and self.selected_game:
|
|
await self.app.push_screen("training", self.selected_game)
|