1import { Client } from "@modelcontextprotocol/sdk/client/index.js";2import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";34const url = new URL(process.env.MCPSWEEPER_URL ?? "http://127.0.0.1:8787/mcp");5const size = Number(process.env.MCPSWEEPER_SIZE ?? 64);6const seed = process.env.MCPSWEEPER_SEED ?? "agent-speed-run";7const delayMs = Number(process.env.MCPSWEEPER_DELAY_MS ?? 0);8const client = new Client({ name: "mcpsweeper-solver", version: "0.1.0" });910function deduce(position) {11 const decodeDeltas = (deltas) => {12 let cell = 0;13 return deltas.map((delta) => (cell += delta));14 };15 const frontier = decodeDeltas(position.frontier_deltas);16 const frontierSet = new Set(frontier);17 const flagged = new Set(decodeDeltas(position.flagged_deltas));18 const clues = [];19 let clueCell = 0;20 for (const code of position.clue_deltas) {21 clueCell += Math.floor(code / 9);22 clues.push({ cell: clueCell, clue: code % 9 });23 }24 const safe = new Set();25 const mines = new Set();26 for (const { cell, clue } of clues) {27 const row = Math.floor(cell / position.width);28 const col = cell % position.width;29 const cells = [];30 let adjacentFlags = 0;31 for (let rowOffset = -1; rowOffset <= 1; rowOffset += 1) {32 for (let colOffset = -1; colOffset <= 1; colOffset += 1) {33 if (rowOffset === 0 && colOffset === 0) continue;34 const nextRow = row + rowOffset;35 const nextCol = col + colOffset;36 if (nextRow < 0 || nextRow >= position.height || nextCol < 0 || nextCol >= position.width) continue;37 const neighbor = nextRow * position.width + nextCol;38 if (frontierSet.has(neighbor)) cells.push(neighbor);39 if (flagged.has(neighbor)) adjacentFlags += 1;40 }41 }42 const remaining = clue - adjacentFlags;43 if (remaining === 0) cells.forEach((cell) => safe.add(cell));44 if (cells.length > 0 && remaining === cells.length) cells.forEach((cell) => mines.add(cell));45 }46 for (const mine of mines) safe.delete(mine);47 return { reveal: [...safe], flag: [...mines] };48}4950function positionFrom(result) {51 const position = result.structuredContent?.position;52 if (!position || !Array.isArray(position.frontier_deltas) || !Array.isArray(position.clue_deltas)) {53 throw new Error("Tool result did not contain a compact position");54 }55 return position;56}5758await client.connect(new StreamableHTTPClientTransport(url));59const startedAt = performance.now();6061try {62 const started = await client.callTool({ name: "start_game", arguments: { size, density: 0.1, seed } });63 const gameId = started.structuredContent?.game_id;64 if (typeof gameId !== "string") throw new Error("start_game did not return a game_id");65 let position = started.structuredContent.position;66 let totalRevealed = 0;67 let totalFlagged = 0;68 let visualRevision = 0;69 let initialObservation = null;70 let positionChars = JSON.stringify(position).length;71 let visualChars = 0;7273 while (position.status === "playing") {74 const action = deduce(position);75 if (action.reveal.length === 0 && action.flag.length === 0) {76 throw new Error(`No deterministic move at revision ${position.revision}`);77 }78 const result = await client.callTool({79 name: "play_batch",80 arguments: {81 game_id: gameId,82 expected_revision: position.revision,83 reveal_cells: action.reveal.slice(0, 8192),84 flag_cells: action.flag.slice(0, 8192),85 intent: `Frontier deduction: ${Math.min(action.reveal.length, 8192)} safe, ${Math.min(action.flag.length, 8192)} mines`,86 },87 });88 if (result.isError) throw new Error(result.content?.[0]?.text ?? "play_batch failed");89 totalRevealed += Math.min(action.reveal.length, 8192);90 totalFlagged += Math.min(action.flag.length, 8192);91 position = positionFrom(result);92 positionChars += JSON.stringify(position).length;93 const watchedBatch = await client.callTool({94 name: "watch_game",95 arguments: { game_id: gameId, since_revision: visualRevision },96 });97 const visual = watchedBatch.structuredContent?.observation;98 if (!visual || !Array.isArray(visual.rows) || visual.revision !== position.revision) {99 throw new Error(`Visual watcher did not advance to revision ${position.revision}`);100 }101 if (visualRevision === 0) initialObservation = watchedBatch.structuredContent?.initial_observation;102 visualRevision = visual.revision;103 visualChars += JSON.stringify(visual).length;104 if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));105 }106107 const watched = await client.callTool({108 name: "watch_game",109 arguments: { game_id: gameId, since_revision: 0 },110 });111 const replayEvents = watched.structuredContent?.events;112 if (!Array.isArray(replayEvents) || !initialObservation) throw new Error("Replay history was not returned");113 if (replayEvents.length !== position.revision) throw new Error("Replay history is incomplete");114115 console.log(116 JSON.stringify(117 {118 game_id: gameId,119 status: position.status,120 size: `${position.width}x${position.height}`,121 mines: position.mine_count,122 batches: position.revision,123 submitted_safe_moves: totalRevealed,124 submitted_flags: totalFlagged,125 elapsed_ms: Math.round(performance.now() - startedAt),126 replay_events: replayEvents.length,127 initial_revision: initialObservation.revision,128 agent_position_chars: positionChars,129 visual_snapshot_chars: visualChars,130 agent_payload_reduction_percent: Math.round((1 - positionChars / visualChars) * 100),131 },132 null,133 2,134 ),135 );136} finally {137 await client.close();138}