McRogueFace/tests/cookbook/apps/calculator.py
John McCardle 6d5e99a114 Remove legacy string enum comparisons from InputState/Key/MouseButton, closes #306
Removed custom __eq__/__ne__ that allowed comparing enums to legacy string
names (e.g., Key.ESCAPE == "Escape"). Removed _legacy_names dicts and
to_legacy_string() functions. Kept from_legacy_string() in PyKey.cpp as
it's used by C++ event dispatch. Updated ~50 Python test/demo/cookbook
files to use enum members instead of string comparisons. Also updates
grid.position -> grid.pos in files that had both types of changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:19:02 -04:00

319 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Calculator App - Boss-key calculator with scene switching
Interactive controls:
0-9: Number input
+, -, *, /: Operations
Enter/=: Calculate result
C: Clear
ESC: Toggle between game and calculator scenes
Backspace: Delete last digit
This demonstrates:
- Scene switching (boss key pattern)
- Button grid layout
- State management
- Click handlers
"""
import mcrfpy
import sys
import os
# Add parent to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lib.button import Button
class Calculator:
"""A simple calculator with GUI."""
def __init__(self):
self.scene = mcrfpy.Scene("calculator")
self.ui = self.scene.children
self.expression = ""
self.result = ""
self.setup()
def setup(self):
"""Build the calculator UI."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(30, 30, 35)
)
self.ui.append(bg)
# Calculator frame
calc_frame = mcrfpy.Frame(
pos=(312, 100),
size=(400, 550),
fill_color=mcrfpy.Color(40, 40, 50),
outline_color=mcrfpy.Color(80, 80, 100),
outline=2
)
self.ui.append(calc_frame)
# Title
title = mcrfpy.Caption(
text="Calculator",
pos=(512, 130),
font_size=24,
fill_color=mcrfpy.Color(200, 200, 200)
)
self.ui.append(title)
# Display frame
display_bg = mcrfpy.Frame(
pos=(332, 170),
size=(360, 80),
fill_color=mcrfpy.Color(20, 25, 30),
outline_color=mcrfpy.Color(60, 60, 80),
outline=1
)
self.ui.append(display_bg)
# Expression display
self.expr_display = mcrfpy.Caption(
text="",
pos=(682, 180), # Right-aligned
font_size=18,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(self.expr_display)
# Result display
self.result_display = mcrfpy.Caption(
text="0",
pos=(682, 210), # Right-aligned
font_size=32,
fill_color=mcrfpy.Color(255, 255, 255)
)
self.ui.append(self.result_display)
# Button layout
button_layout = [
["C", "(", ")", "/"],
["7", "8", "9", "*"],
["4", "5", "6", "-"],
["1", "2", "3", "+"],
["0", ".", "DEL", "="],
]
start_x = 342
start_y = 270
btn_width = 80
btn_height = 50
spacing = 8
for row_idx, row in enumerate(button_layout):
for col_idx, label in enumerate(row):
x = start_x + col_idx * (btn_width + spacing)
y = start_y + row_idx * (btn_height + spacing)
# Different colors for different button types
if label in "0123456789.":
fill = mcrfpy.Color(60, 60, 70)
hover = mcrfpy.Color(80, 80, 95)
elif label == "=":
fill = mcrfpy.Color(80, 120, 80)
hover = mcrfpy.Color(100, 150, 100)
elif label == "C":
fill = mcrfpy.Color(120, 60, 60)
hover = mcrfpy.Color(150, 80, 80)
else:
fill = mcrfpy.Color(70, 70, 90)
hover = mcrfpy.Color(90, 90, 115)
btn = Button(
label,
pos=(x, y),
size=(btn_width, btn_height),
fill_color=fill,
hover_color=hover,
callback=lambda l=label: self.on_button(l),
font_size=20
)
self.ui.append(btn.frame)
# Instructions
instr = mcrfpy.Caption(
text="Press ESC to switch to game | Click buttons or use keyboard",
pos=(512, 580),
font_size=14,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(instr)
def on_button(self, label):
"""Handle button press."""
if label == "C":
self.expression = ""
self.result = "0"
elif label == "DEL":
self.expression = self.expression[:-1]
elif label == "=":
self.calculate()
else:
self.expression += label
self._update_display()
def calculate(self):
"""Evaluate the expression."""
if not self.expression:
return
try:
# Safe evaluation (only math operations)
allowed = set("0123456789+-*/.()")
if all(c in allowed for c in self.expression):
self.result = str(eval(self.expression))
if self.result.endswith('.0'):
self.result = self.result[:-2]
else:
self.result = "Error"
except Exception:
self.result = "Error"
def _update_display(self):
"""Update the display captions."""
self.expr_display.text = self.expression or ""
self.result_display.text = self.result or "0"
def on_key(self, key, state):
"""Handle keyboard input."""
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.ESCAPE:
# Switch to game scene
mcrfpy.current_scene = game_scene
return
# Map keys to buttons
key_map = {
mcrfpy.Key.NUM_0: "0", mcrfpy.Key.NUM_1: "1", mcrfpy.Key.NUM_2: "2", mcrfpy.Key.NUM_3: "3",
mcrfpy.Key.NUM_4: "4", mcrfpy.Key.NUM_5: "5", mcrfpy.Key.NUM_6: "6", mcrfpy.Key.NUM_7: "7",
mcrfpy.Key.NUM_8: "8", mcrfpy.Key.NUM_9: "9",
mcrfpy.Key.PERIOD: ".", mcrfpy.Key.ADD: "+", mcrfpy.Key.SUBTRACT: "-",
mcrfpy.Key.MULTIPLY: "*", mcrfpy.Key.DIVIDE: "/",
mcrfpy.Key.ENTER: "=",
mcrfpy.Key.C: "C", mcrfpy.Key.BACKSPACE: "DEL",
}
if key in key_map:
self.on_button(key_map[key])
def activate(self):
"""Activate the calculator scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
class GamePlaceholder:
"""Placeholder game scene to switch to/from."""
def __init__(self):
self.scene = mcrfpy.Scene("game_placeholder")
self.ui = self.scene.children
self.setup()
def setup(self):
"""Build the game placeholder UI."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 40, 20)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="GAME SCENE",
pos=(512, 350),
font_size=48,
fill_color=mcrfpy.Color(100, 200, 100)
)
title.outline = 3
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Subtitle
subtitle = mcrfpy.Caption(
text="Press ESC for Calculator (Boss Key!)",
pos=(512, 420),
font_size=20,
fill_color=mcrfpy.Color(150, 200, 150)
)
self.ui.append(subtitle)
# Fake game elements
for i in range(5):
fake_element = mcrfpy.Frame(
pos=(100 + i * 180, 550),
size=(150, 100),
fill_color=mcrfpy.Color(40 + i * 10, 60 + i * 5, 40),
outline_color=mcrfpy.Color(80, 120, 80),
outline=1
)
self.ui.append(fake_element)
label = mcrfpy.Caption(
text=f"Game Element {i+1}",
pos=(175 + i * 180, 590),
font_size=12,
fill_color=mcrfpy.Color(200, 200, 200)
)
self.ui.append(label)
def on_key(self, key, state):
"""Handle keyboard input."""
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.ESCAPE:
# Switch to calculator
calculator.activate()
def activate(self):
"""Activate the game scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
# Global instances
calculator = None
game_scene = None
def main():
"""Run the calculator app."""
global calculator, game_scene
calculator = Calculator()
game_scene = GamePlaceholder()
# Start with the game scene
game_scene.activate()
# Headless mode: capture screenshot and exit
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
# Show calculator for screenshot
calculator.activate()
calculator.expression = "7+8"
calculator._update_display()
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/apps/calculator.png"),
sys.exit(0)
), 100)
except AttributeError:
pass
if __name__ == "__main__":
main()