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)