Compare commits
2 Commits
5179d425fc
...
a31fd760d9
| Author | SHA1 | Date | |
|---|---|---|---|
| a31fd760d9 | |||
| 51e45d7600 |
@ -22,6 +22,7 @@ Header, .header {
|
|||||||
color: #ed7d3a;
|
color: #ed7d3a;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
|
border: solid #3a9bed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Buttons === */
|
/* === Buttons === */
|
||||||
@ -33,6 +34,7 @@ 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 {
|
||||||
@ -136,11 +138,81 @@ Input:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm_button {
|
|
||||||
align-horizontal: center;
|
.game_card {
|
||||||
align-vertical: top;
|
|
||||||
margin-top: 1;
|
|
||||||
width: auto;
|
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;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game_info {
|
.game_info {
|
||||||
@ -148,3 +220,7 @@ Input:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
color: #ed7d3a;
|
color: #ed7d3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#game_info_box {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
@ -27,11 +27,13 @@ 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 = []
|
||||||
|
|
||||||
@ -51,6 +53,8 @@ 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
|
||||||
@ -68,6 +72,7 @@ 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)
|
||||||
@ -84,13 +89,32 @@ 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,11 +1,16 @@
|
|||||||
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
|
from textual.containers import Vertical, Horizontal, HorizontalScroll
|
||||||
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
|
||||||
import asyncio
|
from rich.panel import Panel
|
||||||
from math import ceil
|
from rich.console import Group
|
||||||
|
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
|
||||||
@ -24,27 +29,62 @@ class ProgressWidget(Widget):
|
|||||||
return f"[bold cyan]{self.message}[/bold cyan]"
|
return f"[bold cyan]{self.message}[/bold cyan]"
|
||||||
|
|
||||||
|
|
||||||
class GameAccordion(Vertical):
|
class GameAccordion(Static):
|
||||||
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("-", "_")
|
||||||
|
|
||||||
def compose(self):
|
image_path = os.path.abspath(self.metadata.get("image_path", ""))
|
||||||
yield Button(f"{self.title}", id=f"btn_{self.safe_id}", classes="game_button")
|
self.image_renderable = self.load_image_scaled(image_path)
|
||||||
|
|
||||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
super().__init__(id=f"accordion_{self.safe_id}", classes="game_card")
|
||||||
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):
|
||||||
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
|
|
||||||
meta = self.metadata
|
meta = self.metadata
|
||||||
|
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
|
||||||
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']}",
|
||||||
@ -53,7 +93,6 @@ class GameAccordion(Vertical):
|
|||||||
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
|
||||||
@ -62,6 +101,12 @@ class GameAccordion(Vertical):
|
|||||||
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"
|
||||||
@ -69,10 +114,10 @@ class HomeView(Screen):
|
|||||||
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
||||||
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
||||||
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
||||||
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║ ██║\n"
|
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║\n"
|
||||||
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
||||||
classes="header",
|
classes="header",
|
||||||
expand=True,
|
expand=False,
|
||||||
)
|
)
|
||||||
self.progress_text = ProgressWidget()
|
self.progress_text = ProgressWidget()
|
||||||
|
|
||||||
@ -111,35 +156,33 @@ 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(
|
||||||
Static("🎮 Welcome to Agent M", classes="header"),
|
self.logo,
|
||||||
|
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 i, row in enumerate(rows, start=1):
|
for rom in verified_roms:
|
||||||
rom_row = self.query_one(f"#rom_row_{i}", Horizontal)
|
await self.rom_scroll_row.mount(GameAccordion(
|
||||||
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,
|
||||||
@ -149,4 +192,3 @@ 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,3 +8,5 @@ stable-baselines3
|
|||||||
tensorboard
|
tensorboard
|
||||||
requests
|
requests
|
||||||
sqlite3
|
sqlite3
|
||||||
|
rich-pixels
|
||||||
|
pillow
|
||||||
Loading…
Reference in New Issue
Block a user