import init, { WasmEngine } from '../pkg/engine_rust.js'; export class WasmAdapter { constructor() { this.engine = null; this.initialized = false; this.cardDbRaw = null; this.cardDb = null; this.initPromise = null; } async init() { if (this.initialized) return; if (this.initPromise) return this.initPromise; this.initPromise = (async () => { console.log("[WASM] Initializing..."); try { await init(); console.log("[WASM] Loaded."); // Load Card DB const base = getAppBaseUrl(); const res = await fetch(`${base}data/cards_compiled.json`); const text = await res.text(); this.cardDbRaw = text; this.cardDb = JSON.parse(text); // Keep a JS copy for lookups this.engine = new WasmEngine(this.cardDbRaw); this.initialized = true; console.log("[WASM] Engine Ready."); // Create a default game state this.createOfflineGame(); } catch (e) { console.error("[WASM] Init failed:", e); throw e; } })(); return this.initPromise; } createOfflineGame() { // Default init with blank boards this.engine.init_game( new Uint32Array([]), new Uint32Array([]), new Uint32Array([]), new Uint32Array([]), new Uint32Array([]), new Uint32Array([]), BigInt(Date.now()) ); } createGameWithDecks(p0, p1) { if (!this.engine) return { success: false, error: "Engine not initialized" }; console.log("[WASM] Init game with decks:", p0, p1); this.engine.init_game( new Uint32Array(p0.deck || []), new Uint32Array(p1.deck || []), new Uint32Array(p0.energy || []), new Uint32Array(p1.energy || []), new Uint32Array(p0.lives || []), new Uint32Array(p1.lives || []), BigInt(Date.now()) ); return { success: true }; } // --- API Replacements --- async fetchState() { if (!this.initialized) await this.init(); try { const json = this.engine.get_state_json(); const state = JSON.parse(json); // Augment state state.mode = "pve"; state.is_pvp = false; state.my_player_id = 0; // Generate enriched legal actions state.legal_actions = this.enrichLegalActions(state); return { success: true, state: state }; } catch (e) { console.error(e); return { success: false, error: e.toString() }; } } async doAction(actionId) { if (!this.initialized) return { success: false, error: "Not initialized" }; try { this.engine.step(actionId); return await this.fetchState(); } catch (e) { return { success: false, error: e.toString() }; } } async resetGame() { if (!this.initialized) return; // Reuse current decks if possible, or clear? // In Python reset uses stored decks. // We should store decks in this adapter. if (this.lastDecks) { this.engine.init_game( new Uint32Array(this.lastDecks.p0.deck), new Uint32Array(this.lastDecks.p1.deck), new Uint32Array(this.lastDecks.p0.energy), new Uint32Array(this.lastDecks.p1.energy), new Uint32Array(this.lastDecks.p0.lives), new Uint32Array(this.lastDecks.p1.lives), BigInt(Date.now()) ); } else { this.createOfflineGame(); } return await this.fetchState(); } async aiSuggest(sims) { if (!this.initialized) return { success: false }; try { const actionId = this.engine.ai_suggest(sims || 500); // Map ID to description for UI const enriched = this.enrichAction(actionId, this.getLastState()); const suggestions = [{ action_id: actionId, desc: enriched.desc || ("Action " + actionId), value: 0.5, // Dummy value visits: sims }]; return { success: true, suggestions: suggestions }; } catch (e) { return { success: false, error: e.toString() }; } } // --- Deck Management --- async uploadDeck(playerId, content) { // content is either raw HTML or JSON list of IDs let deckList = []; try { // Try JSON first deckList = JSON.parse(content); } catch { // Parse HTML (Deck Log) deckList = this.parseDeckLogHtml(content); } if (!deckList || deckList.length === 0) return { success: false, error: "Invalid deck content" }; const config = this.resolveDeckList(deckList); if (!this.lastDecks) this.lastDecks = { p0: { deck: [], energy: [], lives: [] }, p1: { deck: [], energy: [], lives: [] } }; this.lastDecks[playerId === 0 ? 'p0' : 'p1'] = config; // Re-init game with new decks this.createGameWithDecks(this.lastDecks.p0, this.lastDecks.p1); return { success: true, message: `Loaded ${config.deck.length} members, ${config.lives.length} lives, ${config.energy.length} energy.` }; } async loadNamedDeck(deckName) { try { // Try relative path first (GitHub Pages / Static) const res = await fetch(`decks/${deckName}.txt`); if (!res.ok) throw new Error(`Status ${res.status}`); const text = await res.text(); // Extract PL! IDs (simple regex parsing for the txt format) const matches = text.match(/(PL![A-Za-z0-9\-]+)/g); if (!matches) throw new Error("No card IDs found"); return this.resolveDeckList(matches); } catch (e) { console.error(`Failed to load named deck ${deckName}:`, e); return null; } } async resolveDeckList(deckList) { if (!this.initialized) await this.init(); if (!this.cardDb) throw new Error("Card database not loaded"); if (!this.cardMap) this.buildCardMap(); const deck = []; const energy = []; const lives = []; if (!deckList || !Array.isArray(deckList)) return { deck, energy, lives }; deckList.forEach(rawId => { let info = null; if (typeof rawId === 'number') { info = this.cardDb.member_db[rawId] || this.cardDb.energy_db[rawId]; if (!info && this.cardDb.live_db) info = this.cardDb.live_db[rawId]; } else { info = this.cardMap[rawId]; } if (info) { const id = info.card_id; if (id >= 20000) energy.push(id); else if (id >= 10000) lives.push(id); else deck.push(id); } }); return { deck, energy, lives }; } buildCardMap() { if (!this.cardDb) return; this.cardMap = {}; const dbs = [this.cardDb.member_db, this.cardDb.live_db, this.cardDb.energy_db]; for (const db of dbs) { if (!db) continue; for (const key in db) { const card = db[key]; if (card.card_no) this.cardMap[card.card_no] = card; this.cardMap[card.card_id] = card; // Also map ID } } } parseDeckLogHtml(html) { const regex = /title="([^"]+?) :[^"]*"[^>]*>.*?class="num">(\d+)<\/span>/gs; const cards = []; let match; while ((match = regex.exec(html)) !== null) { const cardNo = match[1].trim(); const qty = parseInt(match[2], 10); for (let i = 0; i < qty; i++) cards.push(cardNo); } return cards; } // --- Helpers --- getLastState() { // Helper to get state without parsing everything if possible, // but we need it for context. return JSON.parse(this.engine.get_state_json()); } enrichLegalActions(state) { const rawIds = this.engine.get_legal_actions(); // Uint32Array return Array.from(rawIds).map(id => this.enrichAction(id, state)); } enrichAction(id, state) { // Logic to reverse-engineer action details from ID and State const p = state.players[state.current_player]; if (id === 0) return { id, desc: "Pass / Confirm" }; // 1-180: Play Member (HandIdx 0-59 * 3 + Slot 0-2) if (id >= 1 && id <= 180) { const adj = id - 1; const handIdx = Math.floor(adj / 3); const slotIdx = adj % 3; const cardId = p.hand[handIdx]; const card = this.getCard(cardId); return { id, type: 'PLAY', hand_idx: handIdx, area_idx: slotIdx, name: card ? card.name : "Unknown", img: card ? (card.img_path.startsWith('img/') ? card.img_path : 'img/' + card.img_path) : null, cost: card ? card.cost : 0, // Should calc reduced cost desc: `Play ${card ? card.name : 'Card'} to Slot ${slotIdx}` }; } // 200-399: Ability (Slot 0-2 * 10 + AbIdx 0-9) // Wait, logic.rs mask is: 200 + slot_idx * 10 + ab_idx. // Assuming max 10 abilities per card (safe assumption). if (id >= 200 && id < 400) { const adj = id - 200; const slotIdx = Math.floor(adj / 10); const abIdx = adj % 10; const cardId = p.stage[slotIdx]; const card = this.getCard(cardId); // We don't have ability text easily available unless we look in DB // compiled DB structure: member_db -> abilities (array) -> text let abText = "Ability"; if (card && card.abilities && card.abilities[abIdx]) { abText = card.abilities[abIdx].raw_text || "Ability"; } return { id, type: 'ABILITY', area_idx: slotIdx, name: card ? card.name : "Unknown", img: card ? (card.img_path.startsWith('img/') ? card.img_path : 'img/' + card.img_path) : null, desc: `Activate ${card ? card.name : 'Card'}`, text: abText }; } // 400-499: Live Set (HandIdx 0-99) if (id >= 400 && id < 500) { const handIdx = id - 400; const cardId = p.hand[handIdx]; const card = this.getCard(cardId); return { id, type: 'LIVE_SET', hand_idx: handIdx, name: card ? card.name : "Unknown", img: card ? (card.img_path.startsWith('img/') ? card.img_path : 'img/' + card.img_path) : null, desc: `Set Live: ${card ? card.name : 'Card'}` }; } // 300-399: Mulligan ?? // In Rust `step`: `action >> i`. This means Action IS the mask. // But `get_legal_actions`: `mask[i] = true` where i is 0..2^N. // So ID is the bitmask. if (state.phase === -1 || state.phase === 0) { // Mulligan phases return { id, type: 'MULLIGAN', desc: `Mulligan Pattern ${id}` }; } // 560-562: Target Stage (Slot) if (id >= 560 && id <= 562) { return { id, type: 'SELECT_STAGE', area_idx: id - 560, desc: `Select Slot ${id - 560}` }; } // 600-659: Select List Item if (id >= 600 && id <= 659) { return { id, type: 'SELECT', index: id - 600, desc: `Select Item ${id - 600}` }; } // 660-719: Select Discard if (id >= 660 && id <= 719) { return { id, type: 'SELECT_DISCARD', index: id - 660, desc: `Select Discard ${id - 660}` }; } // Fallback return { id, desc: `Action ${id}` }; } getCard(id) { if (!this.cardDb) return null; return this.cardDb.member_db[id] || (this.cardDb.live_db ? this.cardDb.live_db[id] : null) || this.cardDb.energy_db[id]; } } // Singleton instance export const wasmAdapter = new WasmAdapter();