Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added simulator/badge_frame.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 86 additions & 34 deletions simulator/badge_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,14 +806,50 @@ def transformed_point(self, x: float, y: float):


class Screen(_SurfaceTarget):
def __init__(self, width: int = 160, height: int = 120, scale: int = 4, screenshot_dir: str = None) -> None:
# Badge frame LCD coordinates (in badge_frame.png at 856x1280)
_FRAME_LCD_X = 130
_FRAME_LCD_Y = 715
_FRAME_LCD_W = 560
_FRAME_LCD_H = 430

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LCD target rectangle is 560×430, but the simulator framebuffer is 160×120 (4:3). Scaling 160×120 into a 560×430 rect will always distort the image (~2.3% aspect error). Consider either adjusting _FRAME_LCD_H to preserve 4:3 for the cutout, or letterboxing/pillarboxing by scaling to fit while preserving aspect and centering within the cutout rect.

Suggested change
_FRAME_LCD_H = 430
_FRAME_LCD_H = 420 # 4:3 aspect with width 560 to match 160x120 framebuffer

Copilot uses AI. Check for mistakes.
_FRAME_BUTTONS = [
(240, 1145, 62, 55, "A"),
(384, 1145, 62, 55, "B"),
(528, 1145, 62, 55, "C"),
(688, 858, 44, 54, "^"),
(688, 1002, 44, 54, "v"),
]

def __init__(self, width: int = 160, height: int = 120, scale: int = 4, screenshot_dir: str = None, frame: bool = False) -> None:
self.width = width
self.height = height
self.scale = scale
self.screenshot_dir = screenshot_dir
self._screenshot_counter = 0
# Add space below for keyboard hints (30 pixels)
self._window = pygame.display.set_mode((width * scale, height * scale + 30))
self._badge_frame = None

if frame:
frame_path = os.path.join(os.path.dirname(__file__), "badge_frame.png")
if os.path.exists(frame_path):
try:
raw = pygame.image.load(frame_path)
max_h = 800
frame_scale = max_h / 1280
fw = int(856 * frame_scale)
fh = int(1280 * frame_scale)
self._badge_frame = pygame.transform.smoothscale(raw, (fw, fh))
self._frame_lcd_x = int(self._FRAME_LCD_X * frame_scale)
self._frame_lcd_y = int(self._FRAME_LCD_Y * frame_scale)
self._frame_lcd_w = int(self._FRAME_LCD_W * frame_scale)
self._frame_lcd_h = int(self._FRAME_LCD_H * frame_scale)
self._frame_scale = frame_scale
self._window = pygame.display.set_mode((fw, fh))
except Exception as e:
print(f"Failed to load badge frame: {e}")
self._badge_frame = None

if self._badge_frame is None:
self._window = pygame.display.set_mode((width * scale, height * scale + 30))

pygame.display.set_caption("Badge Local Simulator")
surface = pygame.Surface((width, height), pygame.SRCALPHA)
super().__init__(surface)
Expand Down Expand Up @@ -862,36 +898,47 @@ def take_screenshot(self) -> None:
print(f"Screenshot saved: {filepath}")

def present(self) -> None:
# Scale and blit the game screen to a temporary surface
scaled_game = pygame.transform.scale(
self._surface, (self.width * self.scale, self.height * self.scale)
)

# Blit the scaled game to the window
self._window.blit(scaled_game, (0, 0))

# Draw keyboard hints below the screen
y_offset = self.height * self.scale
hint_bg = (40, 40, 40)
hint_text = (200, 200, 200)

# Fill the hint area with dark background
hint_rect = pygame.Rect(0, y_offset, self.width * self.scale, 30)
pygame.draw.rect(self._window, hint_bg, hint_rect)

# Draw the hint text
hints = [
("Z/A: A", 10),
("X/B: B", 100),
("Space/C: C", 180),
("Arrows: D-pad", 300),
("H/Esc: Home", 450)
]

for hint, x_pos in hints:
text_surf = self._hint_font.render(hint, True, hint_text)
self._window.blit(text_surf, (x_pos, y_offset + 8))

if self._badge_frame is not None:
# Badge frame mode: render LCD content inside the frame
self._window.blit(self._badge_frame, (0, 0))
pygame.draw.rect(self._window, (0, 0, 0),
(self._frame_lcd_x, self._frame_lcd_y,
self._frame_lcd_w, self._frame_lcd_h))
scaled_game = pygame.transform.scale(
self._surface, (self._frame_lcd_w, self._frame_lcd_h))
self._window.blit(scaled_game,
(self._frame_lcd_x, self._frame_lcd_y))
# Draw button labels over white button squares
btn_font = pygame.font.Font(None, int(24 * self._frame_scale))

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btn_font = pygame.font.Font(...) is created inside present(), so a new font object is allocated every frame in --frame mode. This can add avoidable overhead and churn. Cache the font on self (e.g., create it once in __init__ after _frame_scale is known) and reuse it each frame.

Suggested change
btn_font = pygame.font.Font(None, int(24 * self._frame_scale))
if not hasattr(self, "_btn_font"):
self._btn_font = pygame.font.Font(None, int(24 * self._frame_scale))
btn_font = self._btn_font

Copilot uses AI. Check for mistakes.
for bx, by, bw, bh, label in self._FRAME_BUTTONS:
sx = int(bx * self._frame_scale)
sy = int(by * self._frame_scale)
sw = int(bw * self._frame_scale)
sh = int(bh * self._frame_scale)
pygame.draw.rect(self._window, (50, 50, 50), (sx, sy, sw, sh))
pygame.draw.rect(self._window, (80, 80, 80), (sx, sy, sw, sh), 1)
text = btn_font.render(label, True, (180, 180, 180))
tx = sx + (sw - text.get_width()) // 2
ty = sy + (sh - text.get_height()) // 2
self._window.blit(text, (tx, ty))
else:
# Default mode: scaled screen with keyboard hints
scaled_game = pygame.transform.scale(
self._surface, (self.width * self.scale, self.height * self.scale))
self._window.blit(scaled_game, (0, 0))
y_offset = self.height * self.scale
hint_bg = (40, 40, 40)
hint_text = (200, 200, 200)
hint_rect = pygame.Rect(0, y_offset, self.width * self.scale, 30)
pygame.draw.rect(self._window, hint_bg, hint_rect)
hints = [
("Z/A: A", 10), ("X/B: B", 100), ("Space/C: C", 180),
("Arrows: D-pad", 300), ("H/Esc: Home", 450)
]
for hint, x_pos in hints:
text_surf = self._hint_font.render(hint, True, hint_text)
self._window.blit(text_surf, (x_pos, y_offset + 8))

pygame.display.flip()


Expand Down Expand Up @@ -1789,6 +1836,11 @@ def main() -> None:
action="store_true",
help="Show live performance metrics (CPU and memory usage) in terminal.",
)
parser.add_argument(
"--frame",
action="store_true",
help="Show badge hardware frame around the screen.",
)
args = parser.parse_args()

# Clean temporary files if requested
Expand Down Expand Up @@ -1820,7 +1872,7 @@ def main() -> None:
pygame.init()

global screen, io, SIM_ROOT
screen = Screen(scale=args.scale, screenshot_dir=args.screenshot_dir)
screen = Screen(scale=args.scale, screenshot_dir=args.screenshot_dir, frame=args.frame)
io = IO()

# Set system root with default to ./badge relative to simulator
Expand Down
Loading