diff --git a/BrowserExtension/src/Infrastructure/Base/puzzle-registry.ts b/BrowserExtension/src/Infrastructure/Base/puzzle-registry.ts index 98ec2437..5a15d70c 100644 --- a/BrowserExtension/src/Infrastructure/Base/puzzle-registry.ts +++ b/BrowserExtension/src/Infrastructure/Base/puzzle-registry.ts @@ -11,6 +11,8 @@ import { MidloopHandler } from '../Midloop/midloop-handler.js'; import { AkariHandler } from '../Akari/akari-handler.js'; import { PythonProviderHandler } from './python-provider-handler.js'; import { YajikabeHandler } from '../Yajikabe/yajikabe-handler.js'; +import { AquariumGridProvider } from '../PuzzlesMobile/Aquarium/aquarium-grid-provider.js'; +import { PuzzlesMobileGridProvider } from '../PuzzlesMobile/Base/puzzles-mobile-grid-provider.js'; export class PuzzleRegistry { private handlers: PuzzleHandler[] = []; @@ -34,6 +36,24 @@ export class PuzzleRegistry { static createDefault(): PuzzleRegistry { const registry = new PuzzleRegistry(); + + // Puzzles Mobile Implementations + registry.register(new BasePuzzleHandler('aquarium', 'puzzle-aquarium.com', new AquariumGridProvider())); + registry.register(new BasePuzzleHandler('tapa', 'puzzle-tapa.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('nurikabe', 'puzzle-nurikabe.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('hitori', 'puzzle-hitori.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('heyawake', 'puzzle-heyawake.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('minesweeper', 'puzzle-minesweeper.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('binairo', 'puzzle-binairo.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('fillomino', 'puzzle-fillomino.com', new PuzzlesMobileGridProvider())); + registry.register(new BasePuzzleHandler('shakashaka', 'puzzle-shakashaka.com', new PuzzlesMobileGridProvider())); + // registry.register(new BasePuzzleHandler('tents', 'puzzle-tents.com', new PuzzlesMobileGridProvider())); // Tents usually has outside clues but TapaProvider might extract grid if clues are in grid? No, Tents has outside clues. + // Tents needs Aquarium-like provider (Outside clues + Grid). + registry.register(new BasePuzzleHandler('norinori', 'puzzle-norinori.com', new AquariumGridProvider())); // Regions + registry.register(new BasePuzzleHandler('starbattle', 'puzzle-star-battle.com', new AquariumGridProvider())); // Regions + registry.register(new BasePuzzleHandler('renkatsu', 'puzzle-renkatsu.com', new AquariumGridProvider())); // Regions + + // Existing handlers registry.register(new KoburinHandler()); registry.register(new DetourHandler()); registry.register(new LinesweeperHandler()); diff --git a/BrowserExtension/src/Infrastructure/PuzzlesMobile/Aquarium/aquarium-grid-provider.ts b/BrowserExtension/src/Infrastructure/PuzzlesMobile/Aquarium/aquarium-grid-provider.ts new file mode 100644 index 00000000..37002b5f --- /dev/null +++ b/BrowserExtension/src/Infrastructure/PuzzlesMobile/Aquarium/aquarium-grid-provider.ts @@ -0,0 +1,119 @@ +import { PuzzlesMobileGridProvider } from "../Base/puzzles-mobile-grid-provider.js"; + +export class AquariumGridProvider extends PuzzlesMobileGridProvider { + protected parseDocument(doc: Document): { grid: any[][], regions?: any[][], extra?: any } { + const baseResult = super.parseDocument(doc); + const grid = baseResult.grid; // In base this might contain 0s or text + const size = grid.length; + + // Aquarium structure: + // Regions are defined by borders. + // Clues are in .taskTop and .taskLeft + + // Extract Clues + const topClues = Array.from(doc.querySelectorAll('.taskTop .taskCell')).map(el => parseInt(el.textContent || "0")); + const leftClues = Array.from(doc.querySelectorAll('.taskLeft .taskCell')).map(el => parseInt(el.textContent || "0")); + + // Extract Regions + // We need to build a region map based on 'b-r', 'b-b' classes on .cell.selectable + // We can do a BFS/DFS on the grid. + + const regions = Array.from({ length: size }, () => Array(size).fill(-1)); + const cells = Array.from(doc.querySelectorAll('.cell.selectable')); + + let currentRegionId = 1; + + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + if (regions[r][c] === -1) { + this.floodFillRegions(r, c, currentRegionId, regions, cells, size); + currentRegionId++; + } + } + } + + // Prepare result + // The API likely expects: + // grid: regions (ID matrix) + // extra_data: clues (concatenated top then left, or specifically formatted) + + // The .bru file shows "grid" as region IDs. + // "extra_data" as array of numbers. + + // Let's replace 'grid' content with 'regions'. + const regionGrid = regions; + + // Clues: The API .bru for Aquarium has "extra_data": [[5, 2, 3...]] + // It seems to be Top Clues then Left Clues flattened? + // Or Left then Top? + // Standard PuzzlesMobile order for extra_data usually matches the Python implementation. + // Python typically sends [col_clues, row_clues] flat? + // Or specific structure. + + // Let's assume standard concatenated: [...col_clues, ...row_clues] + const extra_data = [...topClues, ...leftClues]; + + return { + grid: regionGrid, + extra: [extra_data] // The API expects list of lists usually for extra_data + }; + } + + private floodFillRegions(startR: number, startC: number, id: number, regions: number[][], cells: Element[], size: number) { + const stack = [[startR, startC]]; + regions[startR][startC] = id; + + while (stack.length > 0) { + const [r, c] = stack.pop()!; + const index = r * size + c; + const cell = cells[index]; + const classes = cell.classList; + + // Check neighbors + + // Right + if (c < size - 1 && !classes.contains('b-r')) { + if (regions[r][c + 1] === -1) { + regions[r][c + 1] = id; + stack.push([r, c + 1]); + } + } + + // Down + if (r < size - 1 && !classes.contains('b-b')) { + if (regions[r + 1][c] === -1) { + regions[r + 1][c] = id; + stack.push([r + 1, c]); + } + } + + // Left (check neighbor's right border) + if (c > 0) { + const leftIndex = r * size + (c - 1); + const leftCell = cells[leftIndex]; + if (!leftCell.classList.contains('b-r')) { + if (regions[r][c - 1] === -1) { + regions[r][c - 1] = id; + stack.push([r, c - 1]); + } + } + } + + // Up (check neighbor's bottom border) + if (r > 0) { + const upIndex = (r - 1) * size + c; + const upCell = cells[upIndex]; + if (!upCell.classList.contains('b-b')) { + if (regions[r - 1][c] === -1) { + regions[r - 1][c] = id; + stack.push([r - 1, c]); + } + } + } + } + } + + protected parseCell(cell: Element, r: number, c: number, doc: Document): any { + return 0; // We compute regions separately + } +} diff --git a/BrowserExtension/src/Infrastructure/PuzzlesMobile/Base/puzzles-mobile-grid-provider.ts b/BrowserExtension/src/Infrastructure/PuzzlesMobile/Base/puzzles-mobile-grid-provider.ts new file mode 100644 index 00000000..8a4b116d --- /dev/null +++ b/BrowserExtension/src/Infrastructure/PuzzlesMobile/Base/puzzles-mobile-grid-provider.ts @@ -0,0 +1,53 @@ +import { GridProvider } from "../../Base/grid-provider.js"; +import { Grid } from "../../../Domain/Base/grid.js"; + +export class PuzzlesMobileGridProvider implements GridProvider { + public getGrid(): Grid { + const html = document.documentElement.outerHTML; + const data = this.extract(html); + const gridData = data.grid || []; + return new Grid(gridData); + } + + public extract(html: string): { grid: any[][], regions?: any[][], extra?: any } { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + return this.parseDocument(doc); + } + + protected parseDocument(doc: Document): { grid: any[][], regions?: any[][], extra?: any } { + const cells = Array.from(doc.querySelectorAll('.cell')); + const matrixCells = cells.filter(cell => cell.classList.contains('selectable')); + + if (matrixCells.length === 0) { + return { grid: [] }; + } + + const size = Math.sqrt(matrixCells.length); + const grid: any[][] = []; + + for (let r = 0; r < size; r++) { + grid[r] = []; + for (let c = 0; c < size; c++) { + const index = r * size + c; + if (index < matrixCells.length) { + const cell = matrixCells[index]; + const value = this.parseCell(cell, r, c, doc); + grid[r][c] = value; + } else { + grid[r][c] = null; + } + } + } + + return { grid }; + } + + protected parseCell(cell: Element, r: number, c: number, doc: Document): any { + const text = cell.textContent?.trim(); + if (text && !isNaN(parseInt(text))) { + return parseInt(text); + } + return 0; + } +}