Skip to content
Merged
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
20 changes: 20 additions & 0 deletions BrowserExtension/src/Infrastructure/Base/puzzle-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<any> {
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;
}
}
Loading