feat(lan-client): triggers panel + CMD DMG score badges + fix encoding project-wide

Left action panel revised to show triggers list + Create Token/Auto-Arrange.
CMD DMG badges added to center life-panel score areas (click opens matrix).
Removed renderEnhancedPanels calls that split the battlefield on damage adjust.
Added encoding='utf-8' to all bare open() calls across 17 Python files to
prevent cp1252 crashes on MTG card text with smart quotes/em-dashes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
rgriffithandClaude Opus 4.6 committed 2026-04-05 22:14:56 -04:00
1 parent 45ca961064
commit 9b79d18201
24 files changed
+442 -55

No files matched your search

+4 -4
View File
@@ -77,7 +77,7 @@ class CoachService:
logger.warning("Deck report not found: %s", deck_id)
return None
try:
with open(report_path, "r") as f:
with open(report_path, "r", encoding="utf-8") as f:
data = json.load(f)
return DeckReport(**data)
except Exception as e:
@@ -95,7 +95,7 @@ class CoachService:
"""Persist a coaching session to disk and SQLite."""
COACH_SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
path = COACH_SESSIONS_DIR / f"{session.sessionId}.json"
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
f.write(session.model_dump_json(indent=2))
if _DB_AVAILABLE:
try:
@@ -147,7 +147,7 @@ class CoachService:
if not path.exists():
return None
try:
with open(path, "r") as f:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return CoachSession(**data)
except Exception as e:
@@ -168,7 +168,7 @@ class CoachService:
sessions = []
for f in sorted(COACH_SESSIONS_DIR.glob("*.json"), reverse=True):
try:
with open(f, "r") as fh:
with open(f, "r", encoding="utf-8") as fh:
data = json.load(fh)
if deck_id and data.get("deckId", "") != deck_id:
continue
+4 -6
View File
@@ -5286,8 +5286,7 @@ function setLife(playerId, newLife) {
eliminatePlayer(playerId, 'life total reached 0');
}
checkStateBasedActions();
// Sync enhanced player panels
if (typeof renderEnhancedPanels === 'function') renderEnhancedPanels();
if (typeof renderCmdDmgInline === 'function') renderCmdDmgInline();
}
// ============================================================
@@ -5322,8 +5321,7 @@ function adjustCommanderDamage(targetPlayerId, sourcePlayerId, amount) {
panelContainer.replaceWith(newPanel);
}
checkStateBasedActions();
// Sync enhanced player panels
if (typeof renderEnhancedPanels === 'function') renderEnhancedPanels();
if (typeof renderCmdDmgInline === 'function') renderCmdDmgInline();
}
// ============================================================
@@ -5356,8 +5354,6 @@ function adjustPoison(playerId, amount) {
panelContainer.replaceWith(newPanel);
}
if (gameState.rulesEngine && gameState.rulesEngine.sbaChecks) checkStateBasedActions();
// Sync enhanced player panels
if (typeof renderEnhancedPanels === 'function') renderEnhancedPanels();
}
// ============================================================
@@ -13049,6 +13045,8 @@ function startGame() {
renderHandArea();
renderPhaseTracker();
updateTurnDisplay();
if (typeof renderCmdDmgInline === 'function') renderCmdDmgInline();
if (typeof refreshLeftPanelTriggers === 'function') refreshLeftPanelTriggers();
// Set default search focus
setTimeout(() => {
+137 -4
View File
@@ -269,7 +269,7 @@
<button onclick="adjustLife(0,1)" aria-label="Increase P1 life">+</button>
</div>
</div>
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-0">
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-0" onclick="openCmdDmgMatrix()" title="Open Commander Damage matrix">
<span class="life-panel-label">Cmd Dmg</span>
<div class="cmd-dmg-inline-row">&#8212;</div>
</div>
@@ -288,7 +288,7 @@
<button onclick="adjustLife(1,1)" aria-label="Increase P2 life">+</button>
</div>
</div>
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-1">
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-1" onclick="openCmdDmgMatrix()" title="Open Commander Damage matrix">
<span class="life-panel-label">Cmd Dmg</span>
<div class="cmd-dmg-inline-row">&#8212;</div>
</div>
@@ -307,7 +307,7 @@
<button onclick="adjustLife(2,1)" aria-label="Increase P3 life">+</button>
</div>
</div>
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-2">
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-2" onclick="openCmdDmgMatrix()" title="Open Commander Damage matrix">
<span class="life-panel-label">Cmd Dmg</span>
<div class="cmd-dmg-inline-row">&#8212;</div>
</div>
@@ -326,7 +326,7 @@
<button onclick="adjustLife(3,1)" aria-label="Increase P4 life">+</button>
</div>
</div>
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-3">
<div class="life-panel-box cmd-dmg-box" id="cmd-dmg-inline-3" onclick="openCmdDmgMatrix()" title="Open Commander Damage matrix">
<span class="life-panel-label">Cmd Dmg</span>
<div class="cmd-dmg-inline-row">&#8212;</div>
</div>
@@ -369,6 +369,24 @@
<!-- Sidebar edge tab -->
<div id="sidebar-edge-tab" class="sidebar-edge-tab" title="Toggle card search">&#x2039;</div>
<!-- LEFT ACTION PANEL (triggers + quick actions) -->
<div id="left-action-panel" class="collapsed">
<div class="left-panel-header">
<span class="left-panel-title">Triggers <span id="left-panel-trigger-count" class="left-panel-count">0</span></span>
<button class="sidebar-toggle" onclick="toggleLeftPanel()" title="Close panel">&#x2715;</button>
</div>
<div class="left-panel-actions">
<button class="left-panel-btn" onclick="openTokenModal((typeof gameState !== 'undefined' && gameState) ? (gameState.currentPlayerIndex || 0) : 0)" title="Create token">&#x2728; Create Token</button>
<button class="left-panel-btn" onclick="autoArrangeBattlefield()" title="Auto-arrange battlefield">&#x21C4; Auto-Arrange</button>
</div>
<div class="left-panel-body" id="left-panel-triggers">
<div class="left-panel-empty">No pending triggers.</div>
</div>
</div>
<!-- Left panel edge tab -->
<div id="left-panel-edge-tab" class="left-panel-edge-tab" onclick="toggleLeftPanel()" title="Toggle quick actions">&#x203A;</div>
</div><!-- /.table-surface / #game-battlefield -->
<!-- ====================================================
@@ -763,6 +781,121 @@ function toggleCollectionPanel() {
if (panel) panel.classList.toggle('hidden');
}
function toggleLeftPanel() {
var panel = document.getElementById('left-action-panel');
if (panel) {
panel.classList.toggle('collapsed');
if (!panel.classList.contains('collapsed') && typeof refreshLeftPanelTriggers === 'function') {
refreshLeftPanelTriggers();
}
}
}
// Render the triggers list inside the left action panel.
// Mirrors #trigger-stack-body rendering but stays embedded in the panel.
function refreshLeftPanelTriggers() {
var body = document.getElementById('left-panel-triggers');
var countEl = document.getElementById('left-panel-trigger-count');
if (!body) return;
if (typeof gameState === 'undefined' || !gameState) {
if (countEl) countEl.textContent = '0';
body.innerHTML = '<div class="left-panel-empty">No pending triggers.</div>';
return;
}
var pending = (gameState.triggerStack || []).filter(function(t){ return !t.resolved; });
if (countEl) countEl.textContent = String(pending.length);
if (pending.length === 0) {
body.innerHTML = '<div class="left-panel-empty">No pending triggers.</div>';
return;
}
var icons = (typeof TRIGGER_TYPE_ICONS !== 'undefined') ? TRIGGER_TYPE_ICONS : {};
var labels = (typeof TRIGGER_TYPE_LABELS !== 'undefined') ? TRIGGER_TYPE_LABELS : {};
var html = '';
pending.forEach(function(t) {
var icon = icons[t.type] || '\u2747';
var label = labels[t.type] || t.type || '';
var owner = (gameState.players && gameState.players[t.ownerIndex]) || {};
var color = owner.color || '#888';
var name = (t.cardName || 'Unknown').replace(/</g,'&lt;');
var snip = (t.oracleSnippet || '').replace(/</g,'&lt;');
html +=
'<div class="lpt-item">' +
'<div class="lpt-icon">' + icon + '</div>' +
'<div class="lpt-body">' +
'<div class="lpt-title">' + name + '</div>' +
'<div class="lpt-type">' + label + '</div>' +
'<div class="lpt-text">' + snip + '</div>' +
'</div>' +
'<div class="lpt-dot" style="background:' + color + '"></div>' +
'<button class="lpt-dismiss" title="Dismiss" onclick="if(typeof dismissTrigger===\'function\')dismissTrigger(' + t.id + ');refreshLeftPanelTriggers();">&#x2715;</button>' +
'</div>';
});
body.innerHTML = html;
}
// Populate each seat's Cmd Dmg score-area box with per-opponent badges.
// Skips the seat's own player number from the display.
function renderCmdDmgInline() {
if (typeof gameState === 'undefined' || !gameState || !gameState.players) return;
var players = gameState.players;
for (var i = 0; i < players.length; i++) {
var box = document.getElementById('cmd-dmg-inline-' + i);
if (!box) continue;
var row = box.querySelector('.cmd-dmg-inline-row');
if (!row) continue;
var me = players[i];
if (!me) continue;
var html = '';
for (var j = 0; j < players.length; j++) {
if (j === i) continue; // leave own player number out
var op = players[j];
if (!op) continue;
var dmg = (me.commanderDamage && me.commanderDamage[op.id]) || 0;
var danger = dmg >= 21 ? ' danger' : dmg >= 15 ? ' crit' : dmg >= 8 ? ' warn' : '';
html +=
'<span class="cdi-badge' + danger + '" title="Commander damage from ' + (op.name||'P'+(j+1)) + '">' +
'<span class="cdi-dot" style="background:' + (op.color||'#888') + '"></span>' +
'P' + (j + 1) + ' <b>' + dmg + '</b>' +
'</span>';
}
row.innerHTML = html || '&#8212;';
}
}
// Hook into the trigger-stack badge updater so the left panel stays in sync.
(function() {
if (typeof window === 'undefined') return;
var tryHook = function() {
if (typeof window.updateTriggerBadge === 'function' && !window.updateTriggerBadge._leftPanelHooked) {
var orig = window.updateTriggerBadge;
window.updateTriggerBadge = function() {
var r = orig.apply(this, arguments);
try { refreshLeftPanelTriggers(); } catch(e) {}
return r;
};
window.updateTriggerBadge._leftPanelHooked = true;
}
// Also wrap adjustCommanderDamage so badges refresh after matrix clicks.
if (typeof window.adjustCommanderDamage === 'function' && !window.adjustCommanderDamage._cdmInlineHooked) {
var origAdj = window.adjustCommanderDamage;
window.adjustCommanderDamage = function() {
var r = origAdj.apply(this, arguments);
try { renderCmdDmgInline(); } catch(e) {}
return r;
};
window.adjustCommanderDamage._cdmInlineHooked = true;
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryHook);
} else {
tryHook();
}
// Also attempt the hook again a tick later (engine.js loads after this script in some orderings)
setTimeout(tryHook, 0);
setTimeout(tryHook, 500);
})();
// Listen for deck exports from the deck builder iframe
window.addEventListener('message', function(e) {
if (!e.data || e.data.type !== 'DECK_BUILDER_EXPORT') return;
+256
View File
@@ -3705,3 +3705,259 @@ kbd {
font-size: 18px;
line-height: 1;
}
/* ==========================================================
LEFT ACTION PANEL — triggers list + quick actions
(mirrors #card-search-sidebar on the right)
========================================================== */
:root {
--left-panel-w: 240px;
}
#left-action-panel {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: var(--left-panel-w);
max-height: 80vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, rgba(24,20,32,0.97) 0%, rgba(16,14,22,0.97) 100%);
border: 1px solid rgba(139,92,246,0.35);
border-left: none;
border-radius: 0 10px 10px 0;
box-shadow: 0 8px 28px rgba(0,0,0,0.55), 0 0 24px rgba(139,92,246,0.18);
z-index: 45;
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.28s ease;
overflow: hidden;
}
#left-action-panel.collapsed {
transform: translateY(-50%) translateX(calc(-1 * var(--left-panel-w) - 8px));
opacity: 0;
pointer-events: none;
}
.left-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px 8px 12px;
background: rgba(139,92,246,0.14);
border-bottom: 1px solid rgba(139,92,246,0.28);
flex-shrink: 0;
}
.left-panel-title {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #c084fc;
text-shadow: 0 0 6px rgba(192,132,252,0.5);
display: flex;
align-items: center;
gap: 6px;
}
.left-panel-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
border-radius: 8px;
background: rgba(168,85,247,0.4);
color: #fff;
font-size: 10px;
font-weight: 700;
letter-spacing: 0;
text-shadow: none;
}
.left-panel-actions {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
border-bottom: 1px solid rgba(139,92,246,0.2);
flex-shrink: 0;
}
.left-panel-body {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
overflow-y: auto;
flex: 1 1 auto;
}
.left-panel-empty {
text-align: center;
font-size: 11px;
color: rgba(200,190,220,0.4);
padding: 16px 8px;
font-style: italic;
}
.left-panel-btn {
appearance: none;
-webkit-appearance: none;
display: block;
width: 100%;
padding: 8px 10px;
text-align: left;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
color: rgba(240,235,250,0.92);
background: rgba(40,34,56,0.85);
border: 1px solid rgba(139,92,246,0.25);
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;
}
.left-panel-btn:hover {
background: rgba(124,58,237,0.32);
border-color: rgba(168,85,247,0.6);
color: #ffffff;
box-shadow: 0 0 10px rgba(139,92,246,0.35), inset 0 0 8px rgba(139,92,246,0.15);
}
.left-panel-btn:active {
transform: translateY(1px);
background: rgba(109,40,217,0.45);
}
/* Trigger items in the left panel */
.lpt-item {
display: grid;
grid-template-columns: 20px 1fr 8px 18px;
gap: 6px;
align-items: flex-start;
padding: 6px 8px;
background: rgba(40,34,56,0.55);
border: 1px solid rgba(139,92,246,0.15);
border-radius: 5px;
font-size: 10px;
}
.lpt-icon {
font-size: 14px;
line-height: 1;
text-align: center;
color: #c084fc;
}
.lpt-body { min-width: 0; overflow: hidden; }
.lpt-title {
font-weight: 700;
font-size: 11px;
color: rgba(240,235,250,0.95);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lpt-type {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: rgba(192,132,252,0.75);
margin-top: 1px;
}
.lpt-text {
font-size: 9px;
color: rgba(200,190,220,0.55);
margin-top: 2px;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.lpt-dot {
width: 8px; height: 8px;
border-radius: 50%;
margin-top: 2px;
}
.lpt-dismiss {
width: 18px; height: 18px;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.1);
background: transparent;
color: rgba(200,190,220,0.55);
font-size: 10px;
line-height: 1;
cursor: pointer;
padding: 0;
}
.lpt-dismiss:hover {
background: rgba(211,32,42,0.2);
color: #ff9090;
border-color: rgba(211,32,42,0.4);
}
/* Edge tab (always visible; click to toggle panel) */
.left-panel-edge-tab {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 22px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, rgba(139,92,246,0.85) 0%, rgba(109,40,217,0.85) 100%);
color: #fff;
font-size: 18px;
font-weight: 700;
border: 1px solid rgba(168,85,247,0.6);
border-left: none;
border-radius: 0 8px 8px 0;
cursor: pointer;
z-index: 44;
box-shadow: 2px 0 10px rgba(0,0,0,0.5), 0 0 12px rgba(139,92,246,0.3);
transition: background 0.15s ease, transform 0.15s ease, left 0.28s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.left-panel-edge-tab:hover {
background: linear-gradient(90deg, rgba(168,85,247,0.95) 0%, rgba(124,58,237,0.95) 100%);
box-shadow: 2px 0 14px rgba(0,0,0,0.6), 0 0 18px rgba(168,85,247,0.5);
}
/* When panel is open, push tab to panel's right edge and flip arrow */
#left-action-panel:not(.collapsed) ~ .left-panel-edge-tab {
left: var(--left-panel-w);
transform: translateY(-50%) rotate(180deg);
}
/* ==========================================================
CMD DMG score-area badges (inline per-opponent readout)
========================================================== */
.cmd-dmg-box {
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.cmd-dmg-box:hover {
background: rgba(139,92,246,0.12);
border-color: rgba(139,92,246,0.35);
}
.cdi-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 1px 5px;
border-radius: 3px;
background: rgba(255,255,255,0.05);
color: rgba(255,255,255,0.75);
font-size: 9px;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1.3;
}
.cdi-badge b { font-size: 10px; color: #fff; }
.cdi-badge.warn { background: rgba(241,196,15,0.12); color: #f1c40f; }
.cdi-badge.crit { background: rgba(230,126,34,0.15); color: #e67e22; }
.cdi-badge.danger { background: rgba(211,32,42,0.2); color: #ff6b6b; }
.cdi-dot {
width: 5px; height: 5px;
border-radius: 50%;
display: inline-block;
}
+1 -1
View File
@@ -330,7 +330,7 @@ def label_decisions_file(jsonl_path: str) -> Tuple[np.ndarray, List[dict]]:
labels = []
raw = []
with open(jsonl_path, "r") as f:
with open(jsonl_path, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
line = line.strip()
if not line:
+1 -1
View File
@@ -266,7 +266,7 @@ def build_dataset(
logger.info("Processing %s [%s] ...", jsonl_file.name, source)
file_count = 0
with open(jsonl_file, "r") as f:
with open(jsonl_file, "r", encoding="utf-8") as f:
for line_num, line in enumerate(f):
line = line.strip()
if not line:
+2 -2
View File
@@ -523,7 +523,7 @@ class QualityGate:
report_path = os.path.join(
self.config.reports_dir, f"quality_{timestamp}.json"
)
with open(report_path, "w") as f:
with open(report_path, "w", encoding="utf-8") as f:
json.dump(metrics, f, indent=2)
logger.info("Quality report saved: %s", report_path)
@@ -582,6 +582,6 @@ class QualityGate:
path = os.path.join(self.config.reports_dir, "verdict_history.json")
history = [v.to_dict() for v in self.verdicts]
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
logger.info("Verdict history saved: %s (%d entries)", path, len(history))
+1 -1
View File
@@ -415,7 +415,7 @@ def encode_decisions_file(
states = []
raw = []
with open(jsonl_path, "r") as f:
with open(jsonl_path, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if max_samples and i >= max_samples:
break
+2 -2
View File
@@ -130,7 +130,7 @@ class EloHistory:
def _load(self):
if os.path.exists(self.path):
try:
with open(self.path, "r") as f:
with open(self.path, "r", encoding="utf-8") as f:
data = json.load(f)
self.entries = data.get("entries", [])
except Exception:
@@ -138,7 +138,7 @@ class EloHistory:
def save(self):
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
with open(self.path, "w") as f:
with open(self.path, "w", encoding="utf-8") as f:
json.dump({"entries": self.entries}, f, indent=2)
def append(self, generation: int, ratings: Dict[str, float]):
+2 -2
View File
@@ -280,7 +280,7 @@ class ForgeEvaluator:
if not Path(batch_path).exists():
logger.warning("[Eval] Batch JSON not found at %s — falling back to synthetic", batch_path)
return self._run_synthetic_eval(service)
with open(batch_path) as f:
with open(batch_path, encoding="utf-8") as f:
batch = json.load(f)
games_list = batch.get("games", [])
if not games_list:
@@ -543,6 +543,6 @@ class ForgeEvaluator:
"games": [asdict(r) for r in self._records],
}
os.makedirs(self.cfg.results_dir, exist_ok=True)
with open(self._result_path, "w") as f:
with open(self._result_path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
logger.info("[Eval %s] Results written to %s", self.cfg.run_id, self._result_path)
+2 -2
View File
@@ -280,7 +280,7 @@ class ForgeEvaluator:
if not Path(batch_path).exists():
logger.warning("[Eval] Batch JSON not found at %s — falling back to synthetic", batch_path)
return self._run_synthetic_eval(service)
with open(batch_path) as f:
with open(batch_path, encoding="utf-8") as f:
batch = json.load(f)
games_list = batch.get("games", [])
if not games_list:
@@ -543,6 +543,6 @@ class ForgeEvaluator:
"games": [asdict(r) for r in self._records],
}
os.makedirs(self.cfg.results_dir, exist_ok=True)
with open(self._result_path, "w") as f:
with open(self._result_path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
logger.info("[Eval %s] Results written to %s", self.cfg.run_id, self._result_path)
+1 -1
View File
@@ -417,7 +417,7 @@ def main():
# Save results
output_path = args.output or os.path.join(ckpt_dir, "tournament_results.json")
with open(output_path, "w") as f:
with open(output_path, "w", encoding="utf-8") as f:
json.dump(result.to_dict(), f, indent=2)
logger.info("Results saved: %s", output_path)
+2 -2
View File
@@ -38,7 +38,7 @@ def load_precon_cards(precon_dir: str) -> dict:
cards = {"commander": None, "creatures": [], "spells": [], "lands": [], "artifacts": [], "all": []}
section = "Main"
with open(dck_file, "r") as f:
with open(dck_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("//") or line.startswith("Name="):
@@ -380,7 +380,7 @@ def main():
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
total_decisions = 0
with open(args.output, "w") as f:
with open(args.output, "w", encoding="utf-8") as f:
for game_idx in range(args.games):
# Pick two random decks
d1_name, d2_name = random.sample(deck_names, 2)
+3 -3
View File
@@ -68,7 +68,7 @@ def cmd_status(args):
for f in jsonl_files:
lines = 0
try:
with open(f) as fh:
with open(f, encoding="utf-8") as fh:
lines = sum(1 for _ in fh)
except Exception:
pass
@@ -130,7 +130,7 @@ def cmd_inspect(args):
print(f"\n Inspecting: {filepath.name}\n")
count = 0
with open(filepath) as f:
with open(filepath, encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= args.limit:
break
@@ -150,7 +150,7 @@ def cmd_inspect(args):
except Exception as e:
print(f" [{i:>4}] ERROR: {e}")
with open(filepath) as f:
with open(filepath, encoding="utf-8") as f:
total = sum(1 for _ in f)
print(f"\n Showing {count} of {total:,} total decisions.\n")
+1 -1
View File
@@ -253,7 +253,7 @@ class DecisionExporter:
filename = f"ml-decisions-ppo-{self._batch_id}.jsonl"
filepath = os.path.join(self.config.output_dir, filename)
with open(filepath, "w") as f:
with open(filepath, "w", encoding="utf-8") as f:
for record in self._records:
f.write(json.dumps(record) + "\n")
+2 -2
View File
@@ -759,7 +759,7 @@ class DistillationLoop:
"""Save generation history to JSON."""
path = os.path.join(self.config.history_dir, "generations.json")
history = [g.to_dict() for g in self.generations]
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
logger.info(" Generation history saved: %s", path)
@@ -793,7 +793,7 @@ class DistillationLoop:
def _save_summary(self, summary: Dict):
"""Save final summary to JSON."""
path = os.path.join(self.config.history_dir, "summary.json")
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
json.dump(summary, f, indent=2, default=str)
logger.info(" Summary saved: %s", path)
+1 -1
View File
@@ -91,7 +91,7 @@ def parse_forge_jsonl(path: Path) -> List[Dict[str, Any]]:
game_id, turn, phase, active_seat, state, action, outcome
"""
snapshots = []
with open(path, "r") as f:
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.strip()
if not line:
+1 -1
View File
@@ -696,7 +696,7 @@ class PPOTrainer:
self.save_checkpoint()
history_path = os.path.join(cfg.checkpoint_dir, "ppo_history.json")
with open(history_path, "w") as f:
with open(history_path, "w", encoding="utf-8") as f:
json.dump(self.history, f, indent=2, default=str)
summary = {
+3 -3
View File
@@ -322,7 +322,7 @@ class SupervisedTrainer:
}
serializable_history.append(entry)
with open(history_path, "w") as f:
with open(history_path, "w", encoding="utf-8") as f:
json.dump(serializable_history, f, indent=2)
logger.info(" History saved: %s", history_path)
@@ -644,7 +644,7 @@ def main():
# Save evaluation results
eval_path = os.path.join(ckpt_dir, "eval_results.json")
with open(eval_path, "w") as f:
with open(eval_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
logger.info(f" Results saved: {eval_path}")
return
@@ -702,7 +702,7 @@ def main():
print_evaluation(results)
eval_path = os.path.join(ckpt_dir, "eval_results.json")
with open(eval_path, "w") as f:
with open(eval_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
logger.info(" Eval results saved: %s", eval_path)
else:
+1 -1
View File
@@ -22,7 +22,7 @@ new_method = '''
if not Path(batch_path).exists():
logger.warning("[Eval] Batch JSON not found at %s -- falling back to synthetic", batch_path)
return self._run_synthetic_eval(service)
with open(batch_path) as f:
with open(batch_path, encoding='utf-8') as f:
batch = json.load(f)
games_list = batch.get("games", [])
if not games_list:
+1 -1
View File
@@ -226,7 +226,7 @@ def _finish_sim(sim_id, summary, game_results, engine=None):
results_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'results')
os.makedirs(results_dir, exist_ok=True)
batch_path = os.path.join(results_dir, f'ml-decision-sim-{sim_id}.json')
with open(batch_path, 'w') as f:
with open(batch_path, 'w', encoding='utf-8') as f:
json.dump(batch_data, f, indent=2, default=str)
logger.info(f'Saved batch results to {batch_path}')
+11 -11
View File
@@ -130,7 +130,7 @@ async def ml_status():
for f in sorted(results_dir.glob("ml-decisions-*.jsonl")):
lines = 0
try:
with open(f) as fh:
with open(f, encoding="utf-8") as fh:
lines = sum(1 for _ in fh)
except Exception:
pass
@@ -168,7 +168,7 @@ async def ml_get_decisions(filename: str, limit: int = 100, offset: int = 0):
raise HTTPException(404, f"ML decisions file not found: {filename}")
decisions = []
try:
with open(filepath) as f:
with open(filepath, encoding="utf-8") as f:
for i, line in enumerate(f):
if i < offset:
continue
@@ -436,7 +436,7 @@ def _run_training_pipeline(
best_model, _ = load_checkpoint(summary["checkpoint_path"], device)
eval_results = evaluate_model(best_model, test_states, test_labels, device)
eval_path = os.path.join(ckpt_dir, "eval_results.json")
with open(eval_path, "w") as f:
with open(eval_path, "w", encoding="utf-8") as f:
json.dump(eval_results, f, indent=2)
# Done
with _training_lock:
@@ -530,7 +530,7 @@ async def ml_data_status():
for f in sorted(results_dir.glob("ml-decisions-*.jsonl")):
line_count = 0
try:
with open(f, "r") as fh:
with open(f, "r", encoding="utf-8") as fh:
line_count = sum(1 for _ in fh)
except Exception:
pass
@@ -561,7 +561,7 @@ async def ml_data_status():
eval_results = None
if eval_path.exists():
try:
with open(eval_path, "r") as f:
with open(eval_path, "r", encoding="utf-8") as f:
eval_results = json.load(f)
except Exception:
pass
@@ -705,7 +705,7 @@ def _run_tournament_pipeline(episodes: int, playstyle: str):
playstyle=playstyle,
)
output_path = os.path.join(ckpt_dir, "tournament_results.json")
with open(output_path, "w") as f:
with open(output_path, "w", encoding="utf-8") as f:
json.dump(result.to_dict(), f, indent=2)
with _tournament_lock:
_tournament_state.phase = "done"
@@ -758,7 +758,7 @@ async def ml_tournament_results():
project_root = Path(__file__).resolve().parent.parent
results_path = project_root / "ml" / "models" / "checkpoints" / "tournament_results.json"
if results_path.exists():
with open(results_path, "r") as f:
with open(results_path, "r", encoding="utf-8") as f:
return json.load(f)
return {"error": "No tournament results found. Run a tournament first."}
@@ -798,7 +798,7 @@ async def ml_stats():
eval_path = ckpt_dir / "eval_results.json"
if eval_path.exists():
try:
with open(eval_path, "r") as f:
with open(eval_path, "r", encoding="utf-8") as f:
eval_results = json.load(f)
except Exception:
pass
@@ -1057,14 +1057,14 @@ async def ml_distillation_history():
if history_path.exists():
try:
with open(history_path, "r") as f:
with open(history_path, "r", encoding="utf-8") as f:
result["generations"] = json.load(f)
except Exception:
pass
if summary_path.exists():
try:
with open(summary_path, "r") as f:
with open(summary_path, "r", encoding="utf-8") as f:
result["summary"] = json.load(f)
except Exception:
pass
@@ -1203,7 +1203,7 @@ async def ml_elo_history():
history_path = project_root / "data" / "elo_history.json"
if history_path.exists():
try:
with open(history_path, "r") as f:
with open(history_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
+2 -2
View File
@@ -113,7 +113,7 @@ def load_result(path: str) -> dict:
print(f"ERROR: File not found: {path}", file=sys.stderr)
sys.exit(2)
try:
with open(p) as f:
with open(p, encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON in {path}: {e}", file=sys.stderr)
@@ -289,7 +289,7 @@ def main() -> int:
"passed": delta >= args.min_delta and pval < args.alpha,
}
Path(args.json_out).parent.mkdir(parents=True, exist_ok=True)
with open(args.json_out, "w") as jf:
with open(args.json_out, "w", encoding="utf-8") as jf:
json.dump(out, jf, indent=2)
print(f"Comparison JSON written to: {args.json_out}")
+1 -1
View File
@@ -499,7 +499,7 @@ def _run_deepseek_batch_thread(
for fname in sorted(os.listdir(results_dir), reverse=True):
if fname.startswith('ml-decision-') and fname.endswith('.json'):
try:
with open(os.path.join(results_dir, fname), 'r') as rf:
with open(os.path.join(results_dir, fname), 'r', encoding='utf-8') as rf:
past = json.loads(rf.read())
for dd in past.get('decks', []):
dname = dd.get('deckName', '')