from typing import TYPE_CHECKING, Any, Callable, List, Optional, Protocol import json import math if TYPE_CHECKING: class _PyScript(Protocol): document: Any pyscript: _PyScript else: import pyscript from solver_core import MultisetGraph KG_UNITS_PER_KG = 4 LB_PER_KG = 2.2046226218 BAR_KG = 20.0 BAR_UNITS = int(BAR_KG * KG_UNITS_PER_KG) def lbs_to_kg_units(lbs: float) -> int: # total weight, in quarter-kg units, rounded up kg = lbs / LB_PER_KG return int(math.ceil(kg * KG_UNITS_PER_KG)) def reachable_table(nums: List[int]) -> List[bool]: total = sum(nums) reachable = [False] * (total + 1) reachable[0] = True for x in nums: for t in range(total, x - 1, -1): if reachable[t - x]: reachable[t] = True return reachable def ceil_reachable(reachable: List[bool], target: int) -> int: if target <= 0: return 0 if target >= len(reachable): return len(reachable) - 1 for t in range(target, len(reachable)): if reachable[t]: return t return len(reachable) - 1 def format_kg_plate_side(pair_units: int) -> str: q = pair_units // 2 # quarter-kg units per side whole = q // 4 rem = q % 4 if rem == 0: s = str(whole) # interpunct-pad 1-digit integers (mainly "5") if len(s) == 1: s = s + "·" return s elif rem == 1: return f"{whole}¼" if whole else "¼" elif rem == 2: return f"{whole}½" if whole else "½" else: # rem == 3 return f"{whole}¾" if whole else "¾" def format_kg_total(total_units: int) -> str: kg = total_units / 4.0 return f"{kg:5.1f} kg" # e.g. " 92.5 kg", "115.0 kg" def doit( plates, targets, reps, *, bar_units: int = 45, format_plate_side: Optional[Callable[[int], str]] = None, format_total: Optional[Callable[[int], str]] = None, hints: Optional[List[str]] = None, unit_mode: str = "lb", ): graph = MultisetGraph(plates, targets) graph.build_layers() _, shortest_path = graph.dijkstra() lines = [] # Default lb formatting (matches your current behavior) def default_format_plate_side(n: int) -> str: if n == 5: return "2½" elif n == 10: return "5·" else: return str(n // 2) def default_format_total(total_units: int) -> str: return str(total_units) if format_plate_side is None: format_plate_side = default_format_plate_side if format_total is None: format_total = default_format_total lines.append("") for prev_node, node in zip(shortest_path, shortest_path[1:-1]): if node.layer == 0: continue # Find the longest common prefix list_a = prev_node.elements list_b = node.elements p = 0 while p < len(list_a) and p < len(list_b) and list_a[p] == list_b[p]: p += 1 pops = list_a[p:][::-1] pushes = list_b[p:] set_lines = [] # Keep existing coloring behavior for lb mode. # For kg mode later, you can either accept string plates (no color) or tweak this. def _parse_plate_value(s: str) -> Optional[float]: if s.endswith("·"): try: return float(s[:-1]) except ValueError: pass # "2½" is ambiguous: 2.5 lb (small-green) vs 2.5 kg (amber bucket) if s == "2½": return 2.5 # kg fraction glyphs like "1¼", "12½", etc. frac_map = {"¼": 0.25, "½": 0.5, "¾": 0.75} if len(s) >= 1 and s[-1] in frac_map: try: whole = int(s[:-1]) if s[:-1] != "" else 0 return whole + frac_map[s[-1]] except ValueError: pass try: return float(s) except ValueError: return None def color_plate(plate_str: str) -> str: v = _parse_plate_value(plate_str) if v is None: return plate_str # Mode-specific color maps (per-side values) LB_COLORS = { 2.5: "#2dc84d", 5.0: "#ffb703", 10.0: "#00b4d8", 25.0: "#e63946", 35.0: "#3fae2a", 45.0: "#0077b6", } # Map kg plates to "closest lb family" colors: # 1.25kg ≈ 2.5lb (green) # 2.5kg ≈ 5lb (amber) # 5kg ≈ 10lb (cyan) # 10kg ≈ 25lb (red) (closer than 35lb) # 15kg ≈ 35lb (green2) # 20kg ≈ 45lb (blue) # 25kg ≈ 55lb (no direct lb class; pick blue or red—I'll pick blue-ish as "big") KG_COLORS = { 1.25: "#2dc84d", 2.5: "#ffb703", 5.0: "#00b4d8", 10.0: "#e63946", 15.0: "#3fae2a", 20.0: "#0077b6", 25.0: "#0077b6", } # Choose map cmap = KG_COLORS if unit_mode == "kg" else LB_COLORS # Float safety: compare with tolerance for key, color in cmap.items(): if abs(v - key) < 1e-9: return f"{plate_str}" return plate_str plate_side_strs = [format_plate_side(x) for x in node.elements] plate_list = "–".join( [color_plate(s) for s in plate_side_strs] ) set_lines.append("