Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions ChromeExtensions/Masyu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Masyu Solver Extension

This Chrome Extension solves Masyu puzzles on `https://gridpuzzle.com/masyu`.

## Installation

1. Open Chrome and go to `chrome://extensions/`.
2. Enable "Developer mode" in the top right.
3. Click "Load unpacked".
4. Select the `ChromeExtensions/Masyu` folder.

## Usage

1. Navigate to a Masyu puzzle on `https://gridpuzzle.com/masyu`.
2. Open the extension popup (click the puzzle piece icon).
3. Click "Solve".
4. The extension will scrape the grid, solve it using Python (via local Pyodide), and interact with the page to fill the solution.

## Architecture

- **Manifest V3**
- **Popup**: Loads Pyodide (WASM) locally to run Python code without external CSP issues.
- **Content Script**: Scrapes grid data and clicks the canvas to apply moves.
- **Solver**: Pure Python logic (Constraint Propagation + Backtracking).
171 changes: 171 additions & 0 deletions ChromeExtensions/Masyu/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@

// Masyu Content Script
// Extracts grid data and interacts with the page

console.log("Masyu Solver Content Script Loaded");

// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "getGrid") {
const gridData = scrapeGrid();
sendResponse({ grid: gridData });
} else if (request.action === "applySolution") {
applySolution(request.solution);
sendResponse({ success: true });
}
return true;
});

function scrapeGrid() {
// GridPuzzle stores data in a script tag with variables like gpl.pq
// Since we can't access page variables directly from content script context easily without injection,
// we will parse the script tags in the DOM.

const scripts = document.getElementsByTagName('script');
let pq = null;
let size = null;

for (let script of scripts) {
const text = script.innerText;
if (text.includes('gpl.pq')) {
const pqMatch = text.match(/gpl\.pq{1,2}\s*=\s*"(.*?)";/);
const sizeMatch = text.match(/gpl\.[Ss]ize\s*=\s*(\d+);/);

if (pqMatch && sizeMatch) {
pq = pqMatch[1];
size = parseInt(sizeMatch[1]);
break;
}
}
}

if (!pq || !size) {
console.error("Could not find grid data");
return null;
}

// Decode if needed (basic check if it looks like base64, though Python provider handles it)
// For simplicity, let's assume it matches the Python logic.
// However, JS environment might need explicit decoding if it is base64.
// The Python provider checks 'gpl' prefix or valid base64.
// Let's just send the raw string and size to Python side?
// Wait, we are running Python in WASM here. We can replicate the logic in JS or Python.
// Let's replicate the basic parsing in JS to send a Matrix.

let decodedPq = pq;
if (pq.startsWith('gpl')) {
decodedPq = atob(pq.substring(3));
}

// Split into matrix
const matrix = [];
let idx = 0;
// The data string format: each char is a cell.
// | might be a separator

const cleanStr = decodedPq.split('|').join('');

for (let r = 0; r < size; r++) {
const row = [];
for (let c = 0; c < size; c++) {
const char = cleanStr[idx];
// Convert to domain format: 'w', 'b', ' '
let val = ' ';
if (char === 'W') val = 'w';
else if (char === 'B') val = 'b';
row.push(val);
idx++;
}
matrix.push(row);
}

return matrix;
}

function applySolution(solution) {
// solution contains h_edges and v_edges (1 or 0)
// h_edges[r][c] is edge between (r,c) and (r,c+1)
// v_edges[r][c] is edge between (r,c) and (r+1,c)

// GridPuzzle interaction: Click on edges or draw lines.
// Usually clicking the border between cells toggles line/x/empty.
// We need to find the clickable elements.

// Reverse engineering the DOM:
// Cells are likely divs. Edges might be handled by canvas or specific divs.
// GridPuzzle usually uses Canvas.

// If it uses Canvas, we can't click DOM elements easily.
// However, they often have a "keyboard" mode or "click" handler on the canvas.
// Simulating clicks on coordinates.

// Let's check if there are clickable elements for edges.
// Usually hidden divs or we have to calculate coordinates on the canvas.

const canvas = document.getElementById('canvas_1'); // Common ID
if (!canvas) {
console.error("Canvas not found");
return;
}

const rect = canvas.getBoundingClientRect();
const size = solution.h.length; // rows
// Approximate cell size
const cellWidth = rect.width / size;
const cellHeight = rect.height / size;

// We need to simulate clicks.
// Vertical edges: between (r,c) and (r,c+1) ?? No, that's horizontal edge.
// h_edges[r][c] -> right of (r,c). Center: (c+1)*width, (r+0.5)*height

const actions = [];

// Horizontal edges (Right of r,c)
for (let r = 0; r < size; r++) {
for (let c = 0; c < size - 1; c++) {
if (solution.h[r][c] === 1) {
// Click the boundary between (r,c) and (r,c+1)
const x = (c + 1) * cellWidth;
const y = (r + 0.5) * cellHeight;
actions.push({x, y});
}
}
}

// Vertical edges (Bottom of r,c)
for (let r = 0; r < size - 1; r++) {
for (let c = 0; c < size; c++) {
if (solution.v[r][c] === 1) {
// Click boundary between (r,c) and (r+1,c)
const x = (c + 0.5) * cellWidth;
const y = (r + 1) * cellHeight;
actions.push({x, y});
}
}
}

processActions(actions, canvas, rect);
}

function processActions(actions, canvas, rect) {
if (actions.length === 0) return;

const action = actions.shift();
const clientX = rect.left + action.x;
const clientY = rect.top + action.y;

const eventOpts = {
bubbles: true,
cancelable: true,
view: window,
clientX: clientX,
clientY: clientY
};

// MouseDown -> MouseUp usually triggers it
canvas.dispatchEvent(new MouseEvent('mousedown', eventOpts));
canvas.dispatchEvent(new MouseEvent('mouseup', eventOpts));
canvas.dispatchEvent(new MouseEvent('click', eventOpts));

setTimeout(() => processActions(actions, canvas, rect), 10);
}
Binary file added ChromeExtensions/Masyu/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions ChromeExtensions/Masyu/lib/pyodide.asm.js

Large diffs are not rendered by default.

Binary file added ChromeExtensions/Masyu/lib/pyodide.asm.wasm
Binary file not shown.
12 changes: 12 additions & 0 deletions ChromeExtensions/Masyu/lib/pyodide.js

Large diffs are not rendered by default.

Binary file added ChromeExtensions/Masyu/lib/python_stdlib.zip
Binary file not shown.
36 changes: 36 additions & 0 deletions ChromeExtensions/Masyu/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"manifest_version": 3,
"name": "Masyu Solver WASM",
"version": "1.0",
"description": "Solves Masyu puzzles on gridpuzzle.com using Python and WASM.",
"permissions": [
"activeTab",
"scripting"
],
"host_permissions": [
"https://gridpuzzle.com/*"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
}
},
"content_scripts": [
{
"matches": ["https://gridpuzzle.com/*"],
"js": ["content.js"]
}
],
"web_accessible_resources": [
{
"resources": ["solver.py", "solver_logic.py"],
"matches": ["<all_urls>"]
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
}
19 changes: 19 additions & 0 deletions ChromeExtensions/Masyu/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Masyu Solver</title>
<style>
body { width: 200px; padding: 10px; font-family: sans-serif; }
button { width: 100%; padding: 10px; cursor: pointer; }
#status { margin-top: 10px; font-size: 12px; color: #555; }
</style>
<!-- Load Pyodide from local bundle -->
<script src="lib/pyodide.js"></script>
<script src="popup.js" type="module"></script>
</head>
<body>
<h3>Masyu Solver</h3>
<button id="solveBtn">Solve</button>
<div id="status">Ready</div>
</body>
</html>
80 changes: 80 additions & 0 deletions ChromeExtensions/Masyu/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@

// Popup script
const statusDiv = document.getElementById('status');
const solveBtn = document.getElementById('solveBtn');

let pyodide = null;

async function initPyodide() {
if (pyodide) return;
statusDiv.textContent = "Loading Python...";
// Point to local resources
pyodide = await loadPyodide({ indexURL: "./lib/" });
// Load local python files
const files = [
'solver_logic.py',
'solver.py'
];

for (const file of files) {
const response = await fetch(file);
const text = await response.text();
const filename = file.split('/').pop();
pyodide.FS.writeFile(filename, text);
}
statusDiv.textContent = "Python Ready.";
}

solveBtn.addEventListener('click', async () => {
try {
statusDiv.textContent = "Initializing...";
await initPyodide();

statusDiv.textContent = "Scraping Grid...";
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

chrome.tabs.sendMessage(tab.id, { action: "getGrid" }, async (response) => {
if (!response || !response.grid) {
statusDiv.textContent = "Error: No grid found.";
return;
}

statusDiv.textContent = "Solving...";
const grid = response.grid;

// Run Python Solver
// Pass grid as list of lists
pyodide.globals.set("grid_data", grid);

const pythonCode = `
import solver
import js

solution = solver.solve_masyu(grid_data.to_py())
solution
`;

const solution = await pyodide.runPythonAsync(pythonCode);

if (!solution) {
statusDiv.textContent = "No solution found.";
return;
}

const solObj = solution.toJs();
// Map comes as Map, convert to obj
const result = {
h: solObj.get('h'),
v: solObj.get('v')
};

statusDiv.textContent = "Applying...";
chrome.tabs.sendMessage(tab.id, { action: "applySolution", solution: result });
statusDiv.textContent = "Done!";
});

} catch (err) {
console.error(err);
statusDiv.textContent = "Error: " + err.message;
}
});
7 changes: 7 additions & 0 deletions ChromeExtensions/Masyu/solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

import sys
sys.path.append('.')
from solver_logic import solve_masyu_deduction

def solve_masyu(grid_data):
return solve_masyu_deduction(grid_data)
Loading
Loading