""" =================================================================================================== 🏓 MRPONG: PUBLIC HUGGING FACE INFERENCE & INTERACTIVE GAMEPLAY SCRIPT =================================================================================================== Model Repository: https://huggingface.co/fromziro/MrPong Zero-dependency, standalone script for the public to: 1. Play against MrPong live in the terminal (Real-time hold-to-move keyboard control) 2. Run match simulations against built-in AI baseline opponents Quickstart: pip install torch transformers python hf_inference.py --mode play python hf_inference.py --mode simulate --opponent realistic_hard --matches 20 """ import os import sys import time import math import random import argparse from dataclasses import dataclass from typing import Optional, Tuple, Dict, Any, List import numpy as np import torch try: from transformers import AutoModel, AutoConfig except ImportError: print("[!] Error: 'transformers' is required. Run: pip install transformers torch") sys.exit(1) # Platform-specific real-time non-blocking keyboard input IS_WINDOWS = sys.platform.startswith("win") if IS_WINDOWS: import ctypes HAS_KEYBOARD = True else: try: import select import tty import termios HAS_KEYBOARD = True except ImportError: HAS_KEYBOARD = False # ================================================================================================= # STANDALONE PING PONG PHYSICS & ENVIRONMENT # ================================================================================================= @dataclass class PhysicsConfig: table_width: float = 800.0 table_height: float = 500.0 paddle_width: float = 14.0 paddle_height: float = 80.0 paddle_speed: float = 8.0 paddle_smoothing: float = 0.70 ball_radius: float = 8.0 ball_speed_initial: float = 8.0 ball_speed_max: float = 16.0 ball_acceleration: float = 1.035 frame_skip: int = 3 max_rally_steps: int = 1500 class StandalonePongEnv: """Self-contained table tennis physics environment for public standalone execution.""" def __init__(self, phys: Optional[PhysicsConfig] = None, seed: Optional[int] = None): self.phys = phys or PhysicsConfig() self.rng = random.Random(seed) self.ego_paddle_h = self.phys.paddle_height self.opp_paddle_h = self.phys.paddle_height self.reset() def reset(self, serve_direction: Optional[int] = None, initial_speed: Optional[float] = None) -> np.ndarray: self.ego_y = self.phys.table_height / 2.0 self.opp_y = self.phys.table_height / 2.0 self.ego_vy = 0.0 self.opp_vy = 0.0 self.prev_ego_action = 0 self.ball_x = self.phys.table_width / 2.0 self.ball_y = self.phys.table_height / 2.0 if serve_direction is None: serve_direction = 1 if self.rng.random() < 0.5 else -1 serve_angle = self.rng.uniform(-math.pi / 7.0, math.pi / 7.0) speed = initial_speed or self.phys.ball_speed_initial self.ball_vx = serve_direction * speed * math.cos(serve_angle) self.ball_vy = speed * math.sin(serve_angle) self.rally_count = 0 self.step_count = 0 return self.get_ego_observation() def _get_action_velocity(self, action: int) -> float: if action == 1: return -self.phys.paddle_speed elif action == 2: return self.phys.paddle_speed return 0.0 def physics_substep(self, ego_action: int, opp_action: int) -> Tuple[bool, Dict[str, Any]]: """Executes a single continuous physics sub-step (fluid momentum + Continuous Collision Detection).""" info = {"winner": None} done = False prev_ego_y = self.ego_y prev_opp_y = self.opp_y ego_target_v = self._get_action_velocity(ego_action) opp_target_v = self._get_action_velocity(opp_action) alpha = self.phys.paddle_smoothing self.ego_vy = alpha * self.ego_vy + (1.0 - alpha) * ego_target_v self.opp_vy = alpha * self.opp_vy + (1.0 - alpha) * opp_target_v ego_half_h = self.ego_paddle_h / 2.0 opp_half_h = self.opp_paddle_h / 2.0 self.ego_y = float(np.clip(self.ego_y + self.ego_vy, ego_half_h, self.phys.table_height - ego_half_h)) self.opp_y = float(np.clip(self.opp_y + self.opp_vy, opp_half_h, self.phys.table_height - opp_half_h)) prev_ball_x = self.ball_x prev_ball_y = self.ball_y r = self.phys.ball_radius ego_paddle_x = self.phys.paddle_width opp_paddle_x = self.phys.table_width - self.phys.paddle_width ego_impact_plane = ego_paddle_x + r opp_impact_plane = opp_paddle_x - r next_ball_x = prev_ball_x + self.ball_vx next_ball_y = prev_ball_y + self.ball_vy hit_occurred = False # Left (Ego / Human) Paddle Hit Check (Continuous Collision Detection) if self.ball_vx < 0 and prev_ball_x >= ego_impact_plane and next_ball_x <= ego_impact_plane: t = float(np.clip((prev_ball_x - ego_impact_plane) / max(1e-6, -self.ball_vx), 0.0, 1.0)) y_ball_at_impact = prev_ball_y + t * self.ball_vy y_ego_at_impact = prev_ego_y + t * (self.ego_y - prev_ego_y) if abs(y_ball_at_impact - y_ego_at_impact) <= (ego_half_h + r * 0.6): hit_occurred = True self.rally_count += 1 offset = float(np.clip((y_ball_at_impact - y_ego_at_impact) / ego_half_h, -1.0, 1.0)) bounce_angle = offset * (math.pi / 3.0) current_speed = math.hypot(self.ball_vx, self.ball_vy) new_speed = min(current_speed * self.phys.ball_acceleration, self.phys.ball_speed_max) new_vx = new_speed * math.cos(bounce_angle) new_vy = new_speed * math.sin(bounce_angle) + 0.25 * self.ego_vy rem_dt = 1.0 - t self.ball_x = ego_impact_plane + rem_dt * new_vx self.ball_y = y_ball_at_impact + rem_dt * new_vy self.ball_vx = new_vx self.ball_vy = new_vy # Right (Opponent / AI) Paddle Hit Check elif self.ball_vx > 0 and prev_ball_x <= opp_impact_plane and next_ball_x >= opp_impact_plane: t = float(np.clip((opp_impact_plane - prev_ball_x) / max(1e-6, self.ball_vx), 0.0, 1.0)) y_ball_at_impact = prev_ball_y + t * self.ball_vy y_opp_at_impact = prev_opp_y + t * (self.opp_y - prev_opp_y) if abs(y_ball_at_impact - y_opp_at_impact) <= (opp_half_h + r * 0.6): hit_occurred = True self.rally_count += 1 offset = float(np.clip((y_ball_at_impact - y_opp_at_impact) / opp_half_h, -1.0, 1.0)) bounce_angle = offset * (math.pi / 3.0) current_speed = math.hypot(self.ball_vx, self.ball_vy) new_speed = min(current_speed * self.phys.ball_acceleration, self.phys.ball_speed_max) new_vx = -new_speed * math.cos(bounce_angle) new_vy = new_speed * math.sin(bounce_angle) + 0.25 * self.opp_vy rem_dt = 1.0 - t self.ball_x = opp_impact_plane + rem_dt * new_vx self.ball_y = y_ball_at_impact + rem_dt * new_vy self.ball_vx = new_vx self.ball_vy = new_vy if not hit_occurred: self.ball_x = next_ball_x self.ball_y = next_ball_y # Top / Bottom Wall Collisions if self.ball_y - r <= 0: self.ball_y = r + abs(r - self.ball_y) self.ball_vy = abs(self.ball_vy) elif self.ball_y + r >= self.phys.table_height: self.ball_y = (self.phys.table_height - r) - abs(self.ball_y + r - self.phys.table_height) self.ball_vy = -abs(self.ball_vy) # Goal boundary check if self.ball_x - r < 0: done = True info["winner"] = "opponent" elif self.ball_x + r > self.phys.table_width: done = True info["winner"] = "ego" self.prev_ego_action = ego_action return done, info def step(self, ego_action: int, opp_action: int) -> Tuple[np.ndarray, bool, Dict[str, Any]]: self.step_count += 1 done = False info = {"winner": None} for _ in range(self.phys.frame_skip): d, sub_info = self.physics_substep(ego_action, opp_action) if d: done = True info = sub_info break if not done and self.step_count >= self.phys.max_rally_steps: done = True info["winner"] = "draw" return self.get_ego_observation(), done, info def calculate_intercept_y(self, target_x: float, ball_x: float, ball_y: float, ball_vx: float, ball_vy: float) -> float: if (target_x > ball_x and ball_vx <= 0) or (target_x < ball_x and ball_vx >= 0): return self.phys.table_height / 2.0 bx, by = float(ball_x), float(ball_y) bvx, bvy = float(ball_vx), float(ball_vy) h = self.phys.table_height r = self.phys.ball_radius for _ in range(10): dt_x = (target_x - bx) / bvx if bvx != 0 else float('inf') if dt_x <= 0: break if bvy > 0: dt_y = (h - r - by) / bvy elif bvy < 0: dt_y = (r - by) / bvy else: dt_y = float('inf') if dt_x <= dt_y: by += bvy * dt_x break else: bx += bvx * dt_y by += bvy * dt_y bvy = -bvy return float(np.clip(by, r, h - r)) def get_ego_observation(self) -> np.ndarray: w, h = self.phys.table_width, self.phys.table_height v_max = self.phys.ball_speed_max pv_max = self.phys.paddle_speed half_h = self.ego_paddle_h / 2.0 ego_x = self.phys.paddle_width pred_intercept_y = self.calculate_intercept_y(ego_x, self.ball_x, self.ball_y, self.ball_vx, self.ball_vy) rel_pred_y = (pred_intercept_y - self.ego_y) / h pred_norm_y = pred_intercept_y / h opp_y_norm = self.opp_y / h opp_open_top = (self.opp_y - half_h) / h opp_open_bottom = (h - (self.opp_y + half_h)) / h speed_norm = math.hypot(self.ball_vx, self.ball_vy) / v_max return np.array([ (self.ball_y - self.ego_y) / h, (self.ball_x - ego_x) / w, self.ball_vx / v_max, self.ball_vy / v_max, self.ego_y / h, self.ego_vy / pv_max, (self.opp_y - self.ego_y) / h, self.opp_vy / pv_max, self.ball_y / h, self.ball_x / w, rel_pred_y, pred_norm_y, opp_y_norm, opp_open_top, opp_open_bottom, speed_norm ], dtype=np.float32) def get_opp_observation(self) -> np.ndarray: w, h = self.phys.table_width, self.phys.table_height v_max = self.phys.ball_speed_max pv_max = self.phys.paddle_speed half_h = self.opp_paddle_h / 2.0 opp_x = self.phys.table_width - self.phys.paddle_width pred_intercept_y = self.calculate_intercept_y(opp_x, self.ball_x, self.ball_y, self.ball_vx, self.ball_vy) rel_pred_y = (pred_intercept_y - self.opp_y) / h pred_norm_y = pred_intercept_y / h ego_y_norm = self.ego_y / h ego_open_top = (self.ego_y - half_h) / h ego_open_bottom = (h - (self.ego_y + half_h)) / h speed_norm = math.hypot(self.ball_vx, self.ball_vy) / v_max return np.array([ (self.ball_y - self.opp_y) / h, (opp_x - self.ball_x) / w, -self.ball_vx / v_max, self.ball_vy / v_max, self.opp_y / h, self.opp_vy / pv_max, (self.ego_y - self.opp_y) / h, self.ego_vy / pv_max, self.ball_y / h, (w - self.ball_x) / w, rel_pred_y, pred_norm_y, ego_y_norm, ego_open_top, ego_open_bottom, speed_norm ], dtype=np.float32) # ================================================================================================= # BUILT-IN AI OPPONENT BASELINES # ================================================================================================= def smooth_aim_action(current_y: float, target_y: float, prev_action: int, deadzone: float = 6.0) -> int: diff = target_y - current_y if abs(diff) < deadzone: return 0 return 2 if diff > 0 else 1 class RealisticHardOpponent: def __init__(self, commit_x_ratio: float = 0.60): self.commit_x_ratio = commit_x_ratio self.prev_action = 0 self.perceptual_noise = 0.0 def act(self, env: StandalonePongEnv) -> int: if env.ball_vx <= 0: target_y = env.phys.table_height / 2.0 self.perceptual_noise = random.uniform(-12.0, 12.0) elif env.ball_x < env.phys.table_width * self.commit_x_ratio: target_y = env.phys.table_height / 2.0 + (env.ball_y - env.phys.table_height / 2.0) * 0.40 else: target_x = env.phys.table_width - env.phys.paddle_width exact_y = env.calculate_intercept_y(target_x, env.ball_x, env.ball_y, env.ball_vx, env.ball_vy) target_y = float(np.clip(exact_y + self.perceptual_noise, 8.0, env.phys.table_height - 8.0)) action = smooth_aim_action(env.opp_y, target_y, self.prev_action, deadzone=7.0) self.prev_action = action return action class MediumOpponent: def __init__(self): self.prev_action = 0 def act(self, env: StandalonePongEnv) -> int: if env.ball_vx <= 0: target_y = env.phys.table_height / 2.0 else: dt = (env.phys.table_width - env.phys.paddle_width - env.ball_x) / max(1.0, env.ball_vx) target_y = env.ball_y + env.ball_vy * dt target_y = float(np.clip(target_y, 0, env.phys.table_height)) action = smooth_aim_action(env.opp_y, target_y, self.prev_action, deadzone=14.0) self.prev_action = action return action class EasyOpponent: def __init__(self): self.prev_action = 0 def act(self, env: StandalonePongEnv) -> int: if env.ball_vx <= 0 or env.ball_x < env.phys.table_width * 0.45: target_y = env.phys.table_height / 2.0 else: target_y = env.ball_y + random.uniform(-30.0, 30.0) action = smooth_aim_action(env.opp_y, target_y, self.prev_action, deadzone=25.0) self.prev_action = action return action class ImpossibleHardOpponent: def __init__(self): self.prev_action = 0 def act(self, env: StandalonePongEnv) -> int: if env.ball_vx <= 0: target_y = env.phys.table_height / 2.0 else: target_x = env.phys.table_width - env.phys.paddle_width target_y = env.calculate_intercept_y(target_x, env.ball_x, env.ball_y, env.ball_vx, env.ball_vy) action = smooth_aim_action(env.opp_y, target_y, self.prev_action, deadzone=2.0) self.prev_action = action return action class RandomOpponent: def act(self, env: StandalonePongEnv) -> int: return random.randint(0, 2) # ================================================================================================= # REAL-TIME HOLD-TO-MOVE KEYBOARD CONTROLLER # ================================================================================================= class KeyboardController: """ Direct hardware key state listener. Holding W/Up moves UP. Holding S/Down moves DOWN. Releasing stops (STAY). """ def __init__(self): self.is_windows = IS_WINDOWS if self.is_windows: self.user32 = ctypes.windll.user32 # Virtual Key Codes self.VK_W = 0x57 self.VK_S = 0x53 self.VK_UP = 0x26 self.VK_DOWN = 0x28 self.VK_Q = 0x51 self.VK_ESCAPE = 0x1B else: self.decay_frames = 0 self.current_act = 0 if HAS_KEYBOARD: self.old_settings = termios.tcgetattr(sys.stdin) tty.setcbreak(sys.stdin.fileno()) def get_action(self) -> int: """ Returns: 1: UP (while W / Up Arrow is held) 2: DOWN (while S / Down Arrow is held) 0: STAY (when released) -1: QUIT (when Q / ESC is pressed) """ if self.is_windows: # Check Quit if (self.user32.GetAsyncKeyState(self.VK_Q) & 0x8000) or (self.user32.GetAsyncKeyState(self.VK_ESCAPE) & 0x8000): return -1 # Direct hardware physical key state check (0 latency) w_held = bool((self.user32.GetAsyncKeyState(self.VK_W) & 0x8000) or (self.user32.GetAsyncKeyState(self.VK_UP) & 0x8000)) s_held = bool((self.user32.GetAsyncKeyState(self.VK_S) & 0x8000) or (self.user32.GetAsyncKeyState(self.VK_DOWN) & 0x8000)) if w_held and not s_held: return 1 elif s_held and not w_held: return 2 else: return 0 else: # Unix non-blocking input with key-release decay if not HAS_KEYBOARD: return 0 rlist, _, _ = select.select([sys.stdin], [], [], 0) if rlist: ch = sys.stdin.read(1) if ch in ['w', 'W']: self.current_act = 1 self.decay_frames = 5 elif ch in ['s', 'S']: self.current_act = 2 self.decay_frames = 5 elif ch in ['q', 'Q']: return -1 if self.decay_frames > 0: self.decay_frames -= 1 return self.current_act else: self.current_act = 0 return 0 def close(self): if not self.is_windows and HAS_KEYBOARD: try: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings) except Exception: pass # ================================================================================================= # MAIN INFERENCE & GAME ENGINE # ================================================================================================= def resolve_model_path(path_or_id: Optional[str] = None) -> str: if path_or_id and os.path.exists(path_or_id): return os.path.abspath(path_or_id) local_candidates = [ os.path.abspath("./MrPong"), os.path.abspath("C:/Users/harley/MrPong"), os.path.abspath("./mrpong_hf") ] for lp in local_candidates: if os.path.exists(lp) and os.path.exists(os.path.join(lp, "config.json")): if path_or_id is None or path_or_id == "fromziro/MrPong": return lp return path_or_id or "fromziro/MrPong" class PublicMrPongRunner: def __init__(self, model_id_or_path: Optional[str] = None, device: str = "cpu"): self.model_path = resolve_model_path(model_id_or_path) self.device = torch.device(device if torch.cuda.is_available() else "cpu") print(f"[*] Loading MrPong from: {self.model_path} ...") self.config = AutoConfig.from_pretrained(self.model_path, trust_remote_code=True) self.model = AutoModel.from_pretrained(self.model_path, trust_remote_code=True).to(self.device) self.model.eval() self.obs_dim = getattr(self.config, "obs_dim", 12) print(f"[OK] MrPong ready! (obs_dim={self.obs_dim}, hidden_dims={self.config.hidden_dims})\n") def predict_action(self, obs: np.ndarray, deterministic: bool = True) -> int: obs_input = obs[:self.obs_dim] if len(obs) >= self.obs_dim else np.pad(obs, (0, self.obs_dim - len(obs))) return self.model.act(obs_input, deterministic=deterministic) # --------------------------------------------------------------------------------------------- # PLAY INTERACTIVELY IN TERMINAL (HOLD TO MOVE, RELEASE TO STAY) # --------------------------------------------------------------------------------------------- def play(self, points_to_win: int = 5, difficulty: str = "normal"): phys = PhysicsConfig() if difficulty == "easy": phys.paddle_speed = 9.0 initial_ball_speed = 3.5 ego_paddle_h = 110.0 frame_delay = 0.028 elif difficulty == "hard": phys.paddle_speed = 8.0 initial_ball_speed = 6.0 ego_paddle_h = 80.0 frame_delay = 0.022 else: # normal phys.paddle_speed = 8.5 initial_ball_speed = 4.2 ego_paddle_h = 95.0 frame_delay = 0.025 env = StandalonePongEnv(phys) env.ego_paddle_h = ego_paddle_h kbd = KeyboardController() human_score = 0 ai_score = 0 max_rally = 0 print("=" * 64) print(f" 🏓 PLAY AGAINST MRPONG (ARCADE MODE - {difficulty.upper()})") print("=" * 64) print(" Controls:") print(" Hold [W] / [Up Arrow] : Move UP") print(" Hold [S] / [Down Arrow] : Move DOWN") print(" Release key : STAY still") print(" [Q] / [Esc] : Quit match") print(f"\n First player to {points_to_win} points wins!") input("\n Press [ENTER] to start match...") sys.stdout.write("\033[?25l") sys.stdout.flush() serve_dir = 1 ai_act = 0 substep_count = 0 try: while human_score < points_to_win and ai_score < points_to_win: obs = env.reset(serve_direction=serve_dir, initial_speed=initial_ball_speed) for cd in [3, 2, 1]: self._render_terminal_court(env, human_score, ai_score, 0, ai_act, points_to_win, banner=f"GET READY: SERVING IN {cd}...") time.sleep(0.6) done = False while not done: t_start = time.perf_counter() # 1. Real-time physical key check (Hold = Move, Release = Stay) human_act = kbd.get_action() if human_act == -1: print("\n[!] Match aborted by player.") return # 2. Query AI policy every frame_skip sub-steps if substep_count % env.phys.frame_skip == 0: opp_obs = env.get_opp_observation() ai_act = self.predict_action(opp_obs, deterministic=True) substep_count += 1 # 3. Advance continuous physics sub-step done, info = env.physics_substep(ego_action=human_act, opp_action=ai_act) if env.rally_count > max_rally: max_rally = env.rally_count # 4. Render smooth flicker-free frame self._render_terminal_court(env, human_score, ai_score, human_act, ai_act, points_to_win) # 5. Precise frame timing t_elapsed = time.perf_counter() - t_start if t_elapsed < frame_delay: time.sleep(frame_delay - t_elapsed) # Point completed winner = info.get("winner") if winner == "ego": human_score += 1 serve_dir = 1 banner = ">>> POINT TO YOU! <<<" elif winner == "opponent": ai_score += 1 serve_dir = -1 banner = ">>> POINT TO MRPONG! <<<" else: banner = ">>> RALLY DRAW <<<" self._render_terminal_court(env, human_score, ai_score, 0, ai_act, points_to_win, banner=banner) time.sleep(1.2) sys.stdout.write("\033[?25h\033[H\033[J") print("\n" + "=" * 64) if human_score >= points_to_win: print(f" 🏆 CONGRATULATIONS! YOU WON {human_score} - {ai_score}!") else: print(f" 🤖 MRPONG WON {ai_score} - {human_score}!") print(f" Longest Rally: {max_rally} hits") print("=" * 64 + "\n") finally: sys.stdout.write("\033[?25h") sys.stdout.flush() kbd.close() def _render_terminal_court(self, env: StandalonePongEnv, s1: int, s2: int, a1: int, a2: int, target: int, banner: str = ""): """Atomic flicker-free frame buffer rendering.""" cols, rows = 60, 18 bx = int(np.clip((env.ball_x / env.phys.table_width) * (cols - 1), 0, cols - 1)) by = int(np.clip((env.ball_y / env.phys.table_height) * (rows - 1), 0, rows - 1)) p1_y = int(np.clip((env.ego_y / env.phys.table_height) * (rows - 1), 0, rows - 1)) p2_y = int(np.clip((env.opp_y / env.phys.table_height) * (rows - 1), 0, rows - 1)) p1_ph = max(1, int((env.ego_paddle_h / env.phys.table_height) * (rows - 1) / 2)) p2_ph = max(1, int((env.opp_paddle_h / env.phys.table_height) * (rows - 1) / 2)) act_names = ["STAY", " UP ", "DOWN"] buf = [] buf.append("\033[H") buf.append("+" + "-" * (cols + 2) + "+\n") buf.append(f"| YOU [P1]: {s1}/{target} ({act_names[a1]})" + " " * (cols - 41) + f"MRPONG [AI]: {s2}/{target} ({act_names[a2]}) |\n") buf.append("+" + "-" * (cols + 2) + "+\n") for r in range(rows): line = ["#" if abs(r - p1_y) <= p1_ph else " "] for c in range(cols): if r == by and c == bx: line.append("O") elif c == cols // 2: line.append(":") else: line.append(" ") line.append("|" if abs(r - p2_y) <= p2_ph else " ") buf.append("|" + "".join(line) + "|\n") buf.append("+" + "-" * (cols + 2) + "+\n") speed = math.hypot(env.ball_vx, env.ball_vy) if banner: buf.append(f"| {banner:^60} |\n") else: buf.append(f"| Rally: {env.rally_count:2d} hits | Ball Speed: {speed:4.1f} px/f" + " " * (cols - 33) + "|\n") buf.append("+" + "-" * (cols + 2) + "+\n") sys.stdout.write("".join(buf)) sys.stdout.flush() # --------------------------------------------------------------------------------------------- # SIMULATE MATCHES # --------------------------------------------------------------------------------------------- def simulate(self, opponent_type: str = "realistic_hard", num_matches: int = 20): opp_dict = { "realistic_hard": ("Realistic Hard Pro", RealisticHardOpponent()), "medium": ("Medium Logic", MediumOpponent()), "easy": ("Easy Logic", EasyOpponent()), "impossible_hard": ("Impossible Hard Wall", ImpossibleHardOpponent()), "random": ("Random Agent", RandomOpponent()) } if opponent_type not in opp_dict: print(f"[!] Invalid opponent. Choose from: {list(opp_dict.keys())}") return name, opponent = opp_dict[opponent_type] print("=" * 72) print(f" SIMULATING {num_matches} MATCHES: MRPONG (P1) vs {name.upper()} (P2)") print("=" * 72) env = StandalonePongEnv() wins, draws, losses = 0, 0, 0 rallies = [] t0 = time.time() for match_i in range(1, num_matches + 1): obs = env.reset(serve_direction=1 if match_i % 2 == 0 else -1) done = False while not done: ego_act = self.predict_action(obs, deterministic=True) opp_act = opponent.act(env) obs, done, info = env.step(ego_action=ego_act, opp_action=opp_act) winner = info.get("winner") rallies.append(env.rally_count) if winner == "ego": wins += 1 res = "WIN (MrPong Scored)" elif winner == "opponent": losses += 1 res = f"LOSS ({name} Scored)" else: draws += 1 res = "DRAW (Max Rally Limit)" print(f" Match {match_i:3d}/{num_matches:3d} | Result: {res:<25} | Rally: {env.rally_count:3d} hits") elapsed = time.time() - t0 print("\n" + "=" * 72) print(" SIMULATION SUMMARY") print("=" * 72) print(f" Opponent : {name}") print(f" Record (W / D / L): {wins} Wins / {draws} Draws / {losses} Losses") print(f" Win Rate : {(wins / num_matches) * 100.0:.1f}%") print(f" Draw Rate : {(draws / num_matches) * 100.0:.1f}%") print(f" Loss Rate : {(losses / num_matches) * 100.0:.1f}%") print(f" Average Rally : {float(np.mean(rallies)):.1f} hits (Max: {max(rallies)} hits)") print(f" Total Time : {elapsed:.2f}s ({num_matches / elapsed:.1f} matches/sec)") print("=" * 72 + "\n") def main(): parser = argparse.ArgumentParser(description="MrPong Hugging Face Public Inference Runner (fromziro/MrPong)") parser.add_argument("--model", type=str, default="fromziro/MrPong", help="Hugging Face repo ID or local path (default: fromziro/MrPong)") parser.add_argument("--mode", type=str, choices=["play", "simulate"], default="play", help="Mode: 'play' to play against AI, 'simulate' for AI vs AI match simulations") parser.add_argument("--difficulty", type=str, choices=["easy", "normal", "hard"], default="normal", help="Difficulty preset for human play mode (default: normal)") parser.add_argument("--points", type=int, default=5, help="Points to win in play mode (default: 5)") parser.add_argument("--opponent", type=str, default="realistic_hard", choices=["realistic_hard", "medium", "easy", "impossible_hard", "random"], help="Opponent type in simulate mode") parser.add_argument("--matches", type=int, default=20, help="Number of matches to simulate (default: 20)") args = parser.parse_args() runner = PublicMrPongRunner(model_id_or_path=args.model) if args.mode == "play": runner.play(points_to_win=args.points, difficulty=args.difficulty) elif args.mode == "simulate": runner.simulate(opponent_type=args.opponent, num_matches=args.matches) if __name__ == "__main__": main()