Analyze Ink Curve
analyze_ink_curveAnalyze a Disney Lorcana deck list for ink cost distribution, inkable ratio, and color balance. Paste a deck list to get curve analysis.
Instructions
Analyze a Disney Lorcana deck list for ink cost distribution, inkable ratio, and color balance. Paste a deck list to get curve analysis.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| deck_list | Yes | Deck list text, one card per line (e.g. "2 Elsa - Snow Queen") |
Implementation Reference
- src/tools/analyze-ink-curve.ts:6-128 (handler)Main handler function for the analyze_ink_curve tool. Registers the tool with server, parses a deck list, resolves cards against DB, computes cost histogram, inkable ratio, color distribution, and warnings, then returns formatted analysis text.
export function registerAnalyzeInkCurve(server: McpServer, db: Database.Database): void { server.registerTool( 'analyze_ink_curve', { title: 'Analyze Ink Curve', description: 'Analyze a Disney Lorcana deck list for ink cost distribution, inkable ratio, and color balance. Paste a deck list to get curve analysis.', inputSchema: { deck_list: z.string().describe('Deck list text, one card per line (e.g. "2 Elsa - Snow Queen")'), }, }, async (args) => { const entries = parseDeckList(args.deck_list); if (entries.length === 0) { return { content: [{ type: 'text' as const, text: 'Error: Could not parse any cards from the deck list. Use format: "2 Elsa - Snow Queen" (one card per line).' }], isError: true, }; } const result = resolveDeck(db, entries); if (result.entries.length === 0) { return { content: [{ type: 'text' as const, text: 'Error: No recognized cards found in the deck list.' }], isError: true, }; } // Compute statistics const costHistogram = new Map<number, number>(); let inkableCount = 0; let nonInkableCount = 0; const colorCounts = new Map<string, number>(); let totalCost = 0; let totalCards = 0; let cardsWithCost = 0; for (const entry of result.entries) { const { card, quantity } = entry; totalCards += quantity; if (card.inkwell) { inkableCount += quantity; } else { nonInkableCount += quantity; } // Color distribution const colorCount = colorCounts.get(card.color) ?? 0; colorCounts.set(card.color, colorCount + quantity); // Cost histogram if (card.cost !== null) { const current = costHistogram.get(card.cost) ?? 0; costHistogram.set(card.cost, current + quantity); totalCost += card.cost * quantity; cardsWithCost += quantity; } } const averageCost = cardsWithCost > 0 ? (totalCost / cardsWithCost).toFixed(2) : '—'; const inkablePercent = totalCards > 0 ? ((inkableCount / totalCards) * 100).toFixed(1) : '0'; // Build output const lines: string[] = []; lines.push('## Ink Curve Analysis\n'); lines.push(`**Total Cards:** ${totalCards}`); lines.push(`**Average Cost:** ${averageCost}\n`); // Cost histogram lines.push('### Cost Distribution'); const sortedCosts = [...costHistogram.entries()].sort((a, b) => a[0] - b[0]); for (const [cost, count] of sortedCosts) { const bar = '█'.repeat(count); lines.push(` ${cost}-cost: ${bar} (${count})`); } // Inkable ratio lines.push('\n### Inkable Ratio'); lines.push(` Inkable: ${inkableCount} (${inkablePercent}%)`); lines.push(` Non-inkable: ${nonInkableCount} (${(100 - parseFloat(inkablePercent)).toFixed(1)}%)`); // Color distribution lines.push('\n### Ink Colors'); const sortedColors = [...colorCounts.entries()].sort((a, b) => b[1] - a[1]); for (const [color, count] of sortedColors) { lines.push(` ${color}: ${count} cards`); } // Warnings const warnings: string[] = []; const inkableRatio = parseFloat(inkablePercent); if (inkableRatio < 40) { warnings.push(`⚠ Low inkable ratio (${inkablePercent}%) — consider adding more inkable cards. Below 40% may cause ink problems.`); } if (inkableRatio > 70) { warnings.push(`⚠ High inkable ratio (${inkablePercent}%) — above 70% means many key cards can be inked accidentally.`); } if (colorCounts.size > 2) { warnings.push(`⚠ Running ${colorCounts.size} ink colors — more than 2 colors can cause consistency issues.`); } if (warnings.length > 0) { lines.push('\n### Warnings'); for (const w of warnings) { lines.push(w); } } // Unrecognized cards if (result.unrecognized.length > 0) { lines.push('\n### Unrecognized Cards'); for (const name of result.unrecognized) { lines.push(` - ${name}`); } } return { content: [{ type: 'text' as const, text: lines.join('\n') }], }; }, ); } - src/tools/analyze-ink-curve.ts:13-15 (schema)Input schema for the tool: expects a 'deck_list' string describing one card per line.
inputSchema: { deck_list: z.string().describe('Deck list text, one card per line (e.g. "2 Elsa - Snow Queen")'), }, - src/server.ts:10-47 (registration)Import of registerAnalyzeInkCurve from the tools module, imported in server.ts.
import { registerAnalyzeInkCurve } from './tools/analyze-ink-curve.js'; import { registerAnalyzeLore } from './tools/analyze-lore.js'; import { registerFindSongSynergies } from './tools/find-song-synergies.js'; import { readFileSync, realpathSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import type Database from 'better-sqlite3'; export interface ServerOptions { db?: Database.Database; dataDir?: string; } export function createServer(options?: ServerOptions): McpServer { const __dirname = path.dirname(fileURLToPath(import.meta.url)); let version = '0.0.0'; try { const pkg = JSON.parse( readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'), ); version = pkg.version; } catch { // Fallback version if package.json not found (e.g., in tests) } const server = new McpServer({ name: 'lorcana-oracle', version, }); const db = options?.db ?? getDatabase(options?.dataDir); // Register tools registerSearchCards(server, db); registerBrowseSets(server, db); registerCharacterVersions(server, db); registerBrowseFranchise(server, db); registerAnalyzeInkCurve(server, db); - src/server.ts:47-47 (registration)Registration call: registerAnalyzeInkCurve(server, db) invoked in createServer() to wire the tool.
registerAnalyzeInkCurve(server, db); - src/lib/deck-parser.ts:73-94 (helper)resolveDeck helper: resolves parsed deck entries against the database using getCardByName, returning resolved entries and unrecognized card names.
export function resolveDeck( db: Database.Database, entries: DeckEntry[], ): DeckParseResult { const resolved: ResolvedDeckEntry[] = []; const unrecognized: string[] = []; for (const entry of entries) { const searchName = entry.version ? `${entry.cardName} - ${entry.version}` : entry.cardName; const card = getCardByName(db, searchName); if (card) { resolved.push({ quantity: entry.quantity, card }); } else { unrecognized.push(searchName); } } return { entries: resolved, unrecognized }; }