Compare commits
No commits in common. "a31fd760d9d47c569592073f992c1679120e0288" and "5179d425fc6c64e44c88f8414aa15cc6f0b9b2b1" have entirely different histories.
a31fd760d9
...
5179d425fc
@ -22,7 +22,6 @@ Header, .header {
|
|||||||
color: #ed7d3a;
|
color: #ed7d3a;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
border: solid #3a9bed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Buttons === */
|
/* === Buttons === */
|
||||||
@ -34,7 +33,6 @@ Button {
|
|||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
margin: 1;
|
margin: 1;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
text-style: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button:hover {
|
Button:hover {
|
||||||
@ -138,81 +136,11 @@ Input:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm_button {
|
||||||
.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;
|
|
||||||
align-horizontal: center;
|
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;
|
align-vertical: top;
|
||||||
}
|
margin-top: 1;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
#confirm_button {
|
|
||||||
width: 20%;
|
|
||||||
height: auto;
|
|
||||||
content-align: center middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm_button:hover {
|
|
||||||
background: #ed7d3a;
|
|
||||||
color: #0e0e0e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.game_info {
|
.game_info {
|
||||||
@ -220,7 +148,3 @@ Input:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
color: #ed7d3a;
|
color: #ed7d3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
#game_info_box {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
@ -27,13 +27,11 @@ GAME_FILES = {
|
|||||||
"X-Men vs. Street Fighter": "xmvsf.zip",
|
"X-Men vs. Street Fighter": "xmvsf.zip",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_image_path(rom_file: str) -> str:
|
def get_image_path(rom_file: str) -> str:
|
||||||
"""Returns relative image path for a given ROM zip."""
|
"""Returns relative image path for a given ROM zip."""
|
||||||
base_name = os.path.splitext(rom_file)[0]
|
base_name = os.path.splitext(rom_file)[0]
|
||||||
return f"agentm/assets/game_images/{base_name}.jpg"
|
return f"agentm/assets/game_images/{base_name}.jpg"
|
||||||
|
|
||||||
|
|
||||||
def get_verified_roms():
|
def get_verified_roms():
|
||||||
verified = []
|
verified = []
|
||||||
|
|
||||||
@ -53,8 +51,6 @@ def get_verified_roms():
|
|||||||
|
|
||||||
cached = get_cached_rom(rom_file)
|
cached = get_cached_rom(rom_file)
|
||||||
if cached and cached["verified"]:
|
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})")
|
log_with_caller("info", f"✓ Cached ROM is valid: {title} ({rom_file})")
|
||||||
verified.append(cached)
|
verified.append(cached)
|
||||||
continue
|
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_match = re.search(r"sha256\s*=\s*([a-f0-9]{64})", result.stdout, re.IGNORECASE)
|
||||||
sha256 = sha256_match.group(1) if sha256_match else ""
|
sha256 = sha256_match.group(1) if sha256_match else ""
|
||||||
game_id = rom_file.replace(".zip", "")
|
game_id = rom_file.replace(".zip", "")
|
||||||
image_path = get_image_path(rom_file)
|
|
||||||
|
|
||||||
block = extract_game_block(full_list_output, game_id)
|
block = extract_game_block(full_list_output, game_id)
|
||||||
difficulty_min = extract_line_int(block, "Difficulty levels", index=0)
|
difficulty_min = extract_line_int(block, "Difficulty levels", index=0)
|
||||||
@ -89,32 +84,13 @@ def get_verified_roms():
|
|||||||
difficulty_max=difficulty_max,
|
difficulty_max=difficulty_max,
|
||||||
characters=characters,
|
characters=characters,
|
||||||
keywords=keywords
|
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})")
|
log_with_caller("info", f"✓ Verified and cached: {title} ({rom_file})")
|
||||||
else:
|
else:
|
||||||
log_with_caller("error", f"✗ Invalid ROM: {title} ({rom_file})")
|
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()
|
verified = get_all_verified_roms()
|
||||||
for v in verified:
|
|
||||||
v["image_path"] = get_image_path(v["rom_file"])
|
|
||||||
|
|
||||||
log_with_caller("info", f"ROM verification completed: {len(verified)} valid game(s)")
|
log_with_caller("info", f"ROM verification completed: {len(verified)} valid game(s)")
|
||||||
return verified
|
return verified
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,11 @@
|
|||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Static, Button
|
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.message import Message
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from rich.panel import Panel
|
import asyncio
|
||||||
from rich.console import Group
|
from math import ceil
|
||||||
from PIL import Image, ImageEnhance, ImageFilter
|
|
||||||
import os
|
|
||||||
|
|
||||||
from rich_pixels import Pixels
|
|
||||||
from rich_pixels._renderer import HalfcellRenderer
|
|
||||||
|
|
||||||
from agentm.utils.logger import log_with_caller
|
from agentm.utils.logger import log_with_caller
|
||||||
from agentm.logic.roms import get_verified_roms, GAME_FILES
|
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]"
|
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):
|
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.title = title
|
||||||
self.rom_file = rom_file
|
self.rom_file = rom_file
|
||||||
|
self.safe_id = safe_id
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.parent_view = parent_view
|
self.parent_view = parent_view
|
||||||
self.safe_id = rom_file.replace(".", "_").replace("-", "_")
|
|
||||||
|
|
||||||
image_path = os.path.abspath(self.metadata.get("image_path", ""))
|
def compose(self):
|
||||||
self.image_renderable = self.load_image_scaled(image_path)
|
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")
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == f"btn_{self.safe_id}":
|
||||||
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()
|
await self.display_info()
|
||||||
self.parent_view.highlight_selected(self)
|
|
||||||
|
|
||||||
async def display_info(self):
|
async def display_info(self):
|
||||||
meta = self.metadata
|
|
||||||
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
|
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
|
||||||
|
meta = self.metadata
|
||||||
|
|
||||||
info_lines = [
|
info_lines = [
|
||||||
f"[b]Title:[/b] {meta['title']}",
|
f"[b]Title:[/b] {meta['title']}",
|
||||||
f"[b]Game ID:[/b] {meta['game_id']}",
|
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]Keywords:[/b] {', '.join(meta.get('keywords', []))}",
|
||||||
f"[b]SHA256:[/b] {meta['sha256']}",
|
f"[b]SHA256:[/b] {meta['sha256']}",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.parent_view.shared_info_box.update("\n".join(info_lines))
|
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.shared_confirm_button.label = f"✅ Confirm {meta['title']}"
|
||||||
self.parent_view.selected_game = meta
|
self.parent_view.selected_game = meta
|
||||||
@ -101,12 +62,6 @@ class GameAccordion(Static):
|
|||||||
class HomeView(Screen):
|
class HomeView(Screen):
|
||||||
BINDINGS = [("escape", "app.quit", "Quit")]
|
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):
|
def compose(self):
|
||||||
self.logo = Static(
|
self.logo = Static(
|
||||||
"[b bright_magenta]\n\n"
|
"[b bright_magenta]\n\n"
|
||||||
@ -114,10 +69,10 @@ class HomeView(Screen):
|
|||||||
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
||||||
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
||||||
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
||||||
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║\n"
|
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║ ██║\n"
|
||||||
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
||||||
classes="header",
|
classes="header",
|
||||||
expand=False,
|
expand=True,
|
||||||
)
|
)
|
||||||
self.progress_text = ProgressWidget()
|
self.progress_text = ProgressWidget()
|
||||||
|
|
||||||
@ -156,33 +111,35 @@ class HomeView(Screen):
|
|||||||
self.shared_info_box = Static("", id="game_info_box", classes="game_info")
|
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.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(
|
await self.mount(
|
||||||
Vertical(
|
Vertical(
|
||||||
self.logo,
|
Static("🎮 Welcome to Agent M", classes="header"),
|
||||||
Static("🎮 Welcome to Agent M", classes="header", expand=False),
|
|
||||||
Static(
|
Static(
|
||||||
"This is an unofficial DIAMBRA launcher to help you easily train, evaluate, and submit RL agents for fighting games.\n\n"
|
"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.",
|
"Verified ROMs are shown below. Click one to view game info and confirm selection.",
|
||||||
classes="body",
|
classes="body"
|
||||||
expand=False
|
),
|
||||||
|
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.rom_scroll_row,
|
|
||||||
Horizontal(
|
|
||||||
self.shared_info_box,
|
self.shared_info_box,
|
||||||
self.shared_confirm_button,
|
self.shared_confirm_button,
|
||||||
id="info_row",
|
|
||||||
classes="info_confirm_row",
|
|
||||||
),
|
|
||||||
id="home_screen_container",
|
id="home_screen_container",
|
||||||
classes="centered_layout"
|
classes="centered_layout"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
per_row = ceil(len(verified_roms) / 2)
|
||||||
|
rows = [verified_roms[:per_row], verified_roms[per_row:]]
|
||||||
|
|
||||||
for rom in verified_roms:
|
for i, row in enumerate(rows, start=1):
|
||||||
await self.rom_scroll_row.mount(GameAccordion(
|
rom_row = self.query_one(f"#rom_row_{i}", Horizontal)
|
||||||
|
for rom in row:
|
||||||
|
await rom_row.mount(GameAccordion(
|
||||||
title=rom["title"],
|
title=rom["title"],
|
||||||
rom_file=rom["rom_file"],
|
rom_file=rom["rom_file"],
|
||||||
metadata=rom,
|
metadata=rom,
|
||||||
@ -192,3 +149,4 @@ class HomeView(Screen):
|
|||||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
if event.button.id == "confirm_button" and self.selected_game:
|
if event.button.id == "confirm_button" and self.selected_game:
|
||||||
await self.app.push_screen("training", self.selected_game)
|
await self.app.push_screen("training", self.selected_game)
|
||||||
|
|
||||||
|
|||||||
@ -8,5 +8,3 @@ stable-baselines3
|
|||||||
tensorboard
|
tensorboard
|
||||||
requests
|
requests
|
||||||
sqlite3
|
sqlite3
|
||||||
rich-pixels
|
|
||||||
pillow
|
|
||||||
Loading…
Reference in New Issue
Block a user