Compare commits
No commits in common. "a31fd760d9d47c569592073f992c1679120e0288" and "5179d425fc6c64e44c88f8414aa15cc6f0b9b2b1" have entirely different histories.
a31fd760d9
...
5179d425fc
@ -22,7 +22,6 @@ Header, .header {
|
||||
color: #ed7d3a;
|
||||
text-style: bold;
|
||||
padding: 1 2;
|
||||
border: solid #3a9bed;
|
||||
}
|
||||
|
||||
/* === Buttons === */
|
||||
@ -34,7 +33,6 @@ Button {
|
||||
padding: 1 2;
|
||||
margin: 1;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
@ -138,81 +136,11 @@ Input:focus {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.game_card {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 60;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
background: #1f1f1f;
|
||||
color: #ed7d3a;
|
||||
border: solid #3a9bed;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
.game_card:hover {
|
||||
background: #2a2a2a;
|
||||
color: #ed7d3a;
|
||||
}
|
||||
|
||||
.game_card:focus {
|
||||
background: #2a2a2a;
|
||||
border: solid #ed7d3a;
|
||||
}
|
||||
|
||||
.game_card:disabled {
|
||||
background: #1f1f1f;
|
||||
color: #3a9bed;
|
||||
}
|
||||
|
||||
.game_card_clicked {
|
||||
background: #ed7d3a;
|
||||
color: #0e0e0e;
|
||||
}
|
||||
|
||||
|
||||
.offset_card {
|
||||
width: 11;
|
||||
height: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* ✅ Keep this for consistent row layout */
|
||||
.rom_row {
|
||||
layout: horizontal;
|
||||
.confirm_button {
|
||||
align-horizontal: center;
|
||||
padding: 1 1;
|
||||
}
|
||||
|
||||
.rom_row {
|
||||
layout: horizontal;
|
||||
align-horizontal: center;
|
||||
padding: 1 2; /* maybe increase vertical padding */
|
||||
}
|
||||
|
||||
|
||||
.info_confirm_row {
|
||||
layout: horizontal;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
align-vertical: top;
|
||||
}
|
||||
|
||||
|
||||
#confirm_button {
|
||||
width: 20%;
|
||||
height: auto;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.confirm_button:hover {
|
||||
background: #ed7d3a;
|
||||
color: #0e0e0e;
|
||||
margin-top: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.game_info {
|
||||
@ -220,7 +148,3 @@ Input:focus {
|
||||
width: 100%;
|
||||
color: #ed7d3a;
|
||||
}
|
||||
|
||||
#game_info_box {
|
||||
width: 80%;
|
||||
}
|
||||
@ -27,13 +27,11 @@ GAME_FILES = {
|
||||
"X-Men vs. Street Fighter": "xmvsf.zip",
|
||||
}
|
||||
|
||||
|
||||
def get_image_path(rom_file: str) -> str:
|
||||
"""Returns relative image path for a given ROM zip."""
|
||||
base_name = os.path.splitext(rom_file)[0]
|
||||
return f"agentm/assets/game_images/{base_name}.jpg"
|
||||
|
||||
|
||||
def get_verified_roms():
|
||||
verified = []
|
||||
|
||||
@ -53,8 +51,6 @@ def get_verified_roms():
|
||||
|
||||
cached = get_cached_rom(rom_file)
|
||||
if cached and cached["verified"]:
|
||||
# Add image path to runtime version even if not stored in DB
|
||||
cached["image_path"] = get_image_path(rom_file)
|
||||
log_with_caller("info", f"✓ Cached ROM is valid: {title} ({rom_file})")
|
||||
verified.append(cached)
|
||||
continue
|
||||
@ -72,7 +68,6 @@ def get_verified_roms():
|
||||
sha256_match = re.search(r"sha256\s*=\s*([a-f0-9]{64})", result.stdout, re.IGNORECASE)
|
||||
sha256 = sha256_match.group(1) if sha256_match else ""
|
||||
game_id = rom_file.replace(".zip", "")
|
||||
image_path = get_image_path(rom_file)
|
||||
|
||||
block = extract_game_block(full_list_output, game_id)
|
||||
difficulty_min = extract_line_int(block, "Difficulty levels", index=0)
|
||||
@ -89,32 +84,13 @@ def get_verified_roms():
|
||||
difficulty_max=difficulty_max,
|
||||
characters=characters,
|
||||
keywords=keywords
|
||||
# Note: if your DB schema supports image_path, include it here
|
||||
)
|
||||
|
||||
verified.append({
|
||||
"title": title,
|
||||
"rom_file": rom_file,
|
||||
"game_id": game_id,
|
||||
"sha256": sha256,
|
||||
"difficulty_min": difficulty_min,
|
||||
"difficulty_max": difficulty_max,
|
||||
"characters": characters,
|
||||
"keywords": keywords,
|
||||
"verified": True,
|
||||
"image_path": image_path,
|
||||
})
|
||||
|
||||
log_with_caller("info", f"✓ Verified and cached: {title} ({rom_file})")
|
||||
else:
|
||||
log_with_caller("error", f"✗ Invalid ROM: {title} ({rom_file})")
|
||||
|
||||
# If using DB records, append image paths before returning
|
||||
if not verified:
|
||||
verified = get_all_verified_roms()
|
||||
for v in verified:
|
||||
v["image_path"] = get_image_path(v["rom_file"])
|
||||
|
||||
verified = get_all_verified_roms()
|
||||
log_with_caller("info", f"ROM verification completed: {len(verified)} valid game(s)")
|
||||
return verified
|
||||
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Static, Button
|
||||
from textual.containers import Vertical, Horizontal, HorizontalScroll
|
||||
from textual.containers import Vertical, Horizontal
|
||||
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 PIL import Image, ImageEnhance, ImageFilter
|
||||
import os
|
||||
|
||||
from rich_pixels import Pixels
|
||||
from rich_pixels._renderer import HalfcellRenderer
|
||||
import asyncio
|
||||
from math import ceil
|
||||
|
||||
from agentm.utils.logger import log_with_caller
|
||||
from agentm.logic.roms import get_verified_roms, GAME_FILES
|
||||
@ -29,62 +24,27 @@ class ProgressWidget(Widget):
|
||||
return f"[bold cyan]{self.message}[/bold cyan]"
|
||||
|
||||
|
||||
class GameAccordion(Static):
|
||||
class GameAccordion(Vertical):
|
||||
def __init__(self, title: str, rom_file: str, metadata: dict, parent_view):
|
||||
safe_id = rom_file.replace(".", "_").replace("-", "_")
|
||||
super().__init__(id=f"accordion_{safe_id}")
|
||||
self.title = title
|
||||
self.rom_file = rom_file
|
||||
self.safe_id = safe_id
|
||||
self.metadata = metadata
|
||||
self.parent_view = parent_view
|
||||
self.safe_id = rom_file.replace(".", "_").replace("-", "_")
|
||||
|
||||
image_path = os.path.abspath(self.metadata.get("image_path", ""))
|
||||
self.image_renderable = self.load_image_scaled(image_path)
|
||||
def compose(self):
|
||||
yield Button(f"{self.title}", id=f"btn_{self.safe_id}", classes="game_button")
|
||||
|
||||
super().__init__(id=f"accordion_{self.safe_id}", classes="game_card")
|
||||
|
||||
def load_image_scaled(self, path: str):
|
||||
try:
|
||||
with Image.open(path) as img:
|
||||
if img.mode != "RGBA":
|
||||
img = img.convert("RGBA")
|
||||
scale_factor = 0.10
|
||||
target_width = int(img.width * scale_factor)
|
||||
target_height = int(img.height * scale_factor)
|
||||
resized = img.resize(
|
||||
(target_width, target_height),
|
||||
resample=Image.Resampling.LANCZOS
|
||||
)
|
||||
resized = resized.filter(ImageFilter.UnsharpMask(radius=1, percent=150, threshold=3))
|
||||
self._render_width = target_width
|
||||
self._render_height = target_height // 2
|
||||
return Pixels.from_image(
|
||||
resized,
|
||||
renderer=HalfcellRenderer(default_color="black"),
|
||||
)
|
||||
except Exception as e:
|
||||
self._render_width = 24
|
||||
self._render_height = 16
|
||||
return f"[red]Failed to load image[/red]\n[dim]{e}]"
|
||||
|
||||
def render(self):
|
||||
return Panel(
|
||||
Group(
|
||||
self.image_renderable,
|
||||
"", # spacer
|
||||
f"[center][bold orange1][u]{self.title.upper()}[/u][/bold orange1][/center]\n",
|
||||
),
|
||||
border_style="bright_magenta",
|
||||
padding=(1, 2),
|
||||
expand=False
|
||||
)
|
||||
|
||||
async def on_click(self):
|
||||
await self.display_info()
|
||||
self.parent_view.highlight_selected(self)
|
||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == f"btn_{self.safe_id}":
|
||||
await self.display_info()
|
||||
|
||||
async def display_info(self):
|
||||
meta = self.metadata
|
||||
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
|
||||
meta = self.metadata
|
||||
|
||||
info_lines = [
|
||||
f"[b]Title:[/b] {meta['title']}",
|
||||
f"[b]Game ID:[/b] {meta['game_id']}",
|
||||
@ -93,6 +53,7 @@ class GameAccordion(Static):
|
||||
f"[b]Keywords:[/b] {', '.join(meta.get('keywords', []))}",
|
||||
f"[b]SHA256:[/b] {meta['sha256']}",
|
||||
]
|
||||
|
||||
self.parent_view.shared_info_box.update("\n".join(info_lines))
|
||||
self.parent_view.shared_confirm_button.label = f"✅ Confirm {meta['title']}"
|
||||
self.parent_view.selected_game = meta
|
||||
@ -101,12 +62,6 @@ class GameAccordion(Static):
|
||||
class HomeView(Screen):
|
||||
BINDINGS = [("escape", "app.quit", "Quit")]
|
||||
|
||||
def highlight_selected(self, selected_widget: GameAccordion):
|
||||
for card in self.rom_scroll_row.children:
|
||||
if isinstance(card, GameAccordion):
|
||||
card.remove_class("game_card_clicked")
|
||||
selected_widget.add_class("game_card_clicked")
|
||||
|
||||
def compose(self):
|
||||
self.logo = Static(
|
||||
"[b bright_magenta]\n\n"
|
||||
@ -114,10 +69,10 @@ class HomeView(Screen):
|
||||
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
||||
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
||||
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
||||
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║\n"
|
||||
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
||||
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║ ██║\n"
|
||||
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
||||
classes="header",
|
||||
expand=False,
|
||||
expand=True,
|
||||
)
|
||||
self.progress_text = ProgressWidget()
|
||||
|
||||
@ -156,39 +111,42 @@ class HomeView(Screen):
|
||||
self.shared_info_box = Static("", id="game_info_box", classes="game_info")
|
||||
self.shared_confirm_button = Button("✅ Confirm", id="confirm_button", classes="confirm_button")
|
||||
|
||||
self.rom_scroll_row = HorizontalScroll(id="rom_scroll_row", classes="rom_row")
|
||||
|
||||
await self.mount(
|
||||
Vertical(
|
||||
self.logo,
|
||||
Static("🎮 Welcome to Agent M", classes="header", expand=False),
|
||||
Static("🎮 Welcome to Agent M", classes="header"),
|
||||
Static(
|
||||
"This is an unofficial DIAMBRA launcher to help you easily train, evaluate, and submit RL agents for fighting games.\n\n"
|
||||
"Verified ROMs are shown below. Click one to view game info and confirm selection.",
|
||||
classes="body",
|
||||
expand=False
|
||||
classes="body"
|
||||
),
|
||||
self.rom_scroll_row,
|
||||
Horizontal(
|
||||
self.shared_info_box,
|
||||
self.shared_confirm_button,
|
||||
id="info_row",
|
||||
classes="info_confirm_row",
|
||||
Static(f"✅ Found {len(verified_roms)} valid ROM(s).", id="status_message"),
|
||||
Vertical(
|
||||
Horizontal(id="rom_row_1", classes="rom_row"),
|
||||
Horizontal(id="rom_row_2", classes="rom_row"),
|
||||
id="rom_rows",
|
||||
classes="rom_rows_container"
|
||||
),
|
||||
self.shared_info_box,
|
||||
self.shared_confirm_button,
|
||||
id="home_screen_container",
|
||||
classes="centered_layout"
|
||||
)
|
||||
)
|
||||
|
||||
per_row = ceil(len(verified_roms) / 2)
|
||||
rows = [verified_roms[:per_row], verified_roms[per_row:]]
|
||||
|
||||
for rom in verified_roms:
|
||||
await self.rom_scroll_row.mount(GameAccordion(
|
||||
title=rom["title"],
|
||||
rom_file=rom["rom_file"],
|
||||
metadata=rom,
|
||||
parent_view=self
|
||||
))
|
||||
for i, row in enumerate(rows, start=1):
|
||||
rom_row = self.query_one(f"#rom_row_{i}", Horizontal)
|
||||
for rom in row:
|
||||
await rom_row.mount(GameAccordion(
|
||||
title=rom["title"],
|
||||
rom_file=rom["rom_file"],
|
||||
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)
|
||||
|
||||
|
||||
@ -8,5 +8,3 @@ stable-baselines3
|
||||
tensorboard
|
||||
requests
|
||||
sqlite3
|
||||
rich-pixels
|
||||
pillow
|
||||
Loading…
Reference in New Issue
Block a user