From f7d341181901107f73e3929f8a17d3abd839aadc Mon Sep 17 00:00:00 2001 From: D-TheProgrammer <151149998+D-TheProgrammer@users.noreply.github.com> Date: Thu, 13 Jun 2024 19:19:38 +0200 Subject: [PATCH] Last Ai autofiling + Tutorial Menu + DeleteAllPoly (#28) * Last AI using autofiling + Tutorial Menu + DeleteAllPolyo * Last Ai autofiling + Tutorial Menu + DeleteAllPoly * Add files via upload * update * Update polyomino.js --------- Co-authored-by: Viet Nguyen <77735678+Viet281101@users.noreply.github.com> --- 2D/js/ai.js | 81 ++++++++++++++++++++++++ 2D/js/main.js | 30 ++++++++- 2D/js/popup/solve.js | 9 +-- 2D/js/popup/tutorial.js | 133 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 7 deletions(-) diff --git a/2D/js/ai.js b/2D/js/ai.js index 2614100..a7e304e 100644 --- a/2D/js/ai.js +++ b/2D/js/ai.js @@ -239,3 +239,84 @@ export function randomBacktrackingTiling(polyominoes, gridBoard, placePolyomino, }; placeNextPolyomino(); }; + + +/** + * Automatic tiling the grid based on polyomino placed on the field by the user. + * + * @param {GridBoard} gridBoard - The grid board object representing the grid on which the polyominoes will be placed. + * @param {Array} polyominoes - The array of polyominoes to be placed on the grid board. + * @param {Function} placePolyomino - The function to place a polyomino on the grid board. + * @param {Function} removePolyomino - The function to remove a polyomino from the grid board. + * @param {Function} redraw - The function to redraw the grid board after placing or removing a polyomino. + * @param {Function} duplicatePolyomino - The function to duplicate a polyomino. + * @param {Function} message - An optional callback function to be called when the tiling is complete. + * @return {void} This function does not return anything. + */ + +export function fullAutoTiling(gridBoard, polyominoes, placePolyomino, removePolyomino, redraw, duplicatePolyomino, message) { + function canPlace(polyomino, x, y) { + const originalX = polyomino.x; + const originalY = polyomino.y; + polyomino.x = x; + polyomino.y = y; + if (gridBoard.isInBounds(polyomino) && !gridBoard.isOverlapping(polyomino)) { + polyomino.x = originalX; + polyomino.y = originalY; + return true; + } + polyomino.x = originalX; + polyomino.y = originalY; + return false; + }; + + function placeAllPolyominoes(index) { + if (index >= polyominoes.length) { return true; } + const polyomino = polyominoes[index]; + + for (let row = 0; row < gridBoard.rows; row++) { + for (let col = 0; col < gridBoard.cols; col++) { + for (let rotation = 0; rotation < 4; rotation++) { + const x = col * gridBoard.gridSize + gridBoard.gridOffsetX; + const y = row * gridBoard.gridSize + gridBoard.gridOffsetY; + + if (canPlace(polyomino, x, y)) { + const originalX = polyomino.x; + const originalY = polyomino.y; + + polyomino.x = x; + polyomino.y = y; + duplicatePolyomino(polyomino); + placePolyomino(polyomino); + polyomino.isPlaced = true; + + if (placeAllPolyominoes(index + 1)) { return true; } + + polyomino.x = originalX; + polyomino.y = originalY; + polyomino.isPlaced = false; + gridBoard.removePolyomino(polyomino); + } + polyomino.rotateRight(); + } + } + } + return placeAllPolyominoes(index + 1); + }; + + const originalStates = polyominoes.map(p => ({ x: p.x, y: p.y, isPlaced: p.isPlaced })); + if (placeAllPolyominoes(0)) { + console.log("placement ok"); + redraw(); + if (message) { message(); } + } else { + polyominoes.forEach((p, i) => { + p.x = originalStates[i].x; + p.y = originalStates[i].y; + p.isPlaced = originalStates[i].isPlaced; + }); + console.log("rien trouve"); + redraw(); + } +} + diff --git a/2D/js/main.js b/2D/js/main.js index 9493ade..0cb3faa 100644 --- a/2D/js/main.js +++ b/2D/js/main.js @@ -2,7 +2,7 @@ import { GridBoard } from './board.js'; import { Polyomino, getRandomColor } from './polyomino.js'; import { GUIController } from './gui.js'; import { Toolbar } from './toolbar.js'; -import { backtrackingAutoTiling, bruteForceTiling, randomTiling, randomBacktrackingTiling } from './ai.js'; +import { backtrackingAutoTiling, bruteForceTiling, randomTiling, randomBacktrackingTiling, fullAutoTiling } from './ai.js'; class MainApp { constructor() { @@ -135,7 +135,7 @@ class MainApp { }; handleMouseMove(mousePos) { - if (this.isBlackening) this.canvas.style.cursor = 'url("../assets/cursor_blacken.png"), auto'; + if (this.isBlackening) this.canvas.style.cursor = 'url("../assets/cursor_blackend.png"), auto'; this.polyominoes.forEach(polyomino => polyomino.onMouseMove(mousePos)); if (this.tooltipPolyomino && !this.selectedPolyomino?.isDragging) { let found = false; @@ -198,7 +198,8 @@ class MainApp { deleteAllPolyominos() { this.polyominoes = []; - this.selectedPolyomino = null; + this.selectedPoyomino = null; + this.autoWhitening() ; this.redraw(); }; @@ -378,6 +379,7 @@ class MainApp { this.placePolyomino.bind(this), this.gridBoard.removePolyomino.bind(this), this.redraw.bind(this), + () => { this.showMessageBox(messageBox); } ); }, 1000); @@ -417,6 +419,28 @@ class MainApp { ); }, 1000); }; + + fullAutoTiling() { + this.resetBoard(); + + const messageBox = this.createMessageBox(2); + + setTimeout(() => { + fullAutoTiling( + this.gridBoard, + this.polyominoes, + this.placePolyomino.bind(this), + this.gridBoard.removePolyomino.bind(this.gridBoard), + this.redraw.bind(this), + this.duplicatePolyomino.bind(this), + () => { + this.showMessageBox(messageBox); + } + ); + }, 1000); + + console.log("Auto Tiling process initiated."); + }; }; const main_app = new MainApp(); diff --git a/2D/js/popup/solve.js b/2D/js/popup/solve.js index 682efda..3dd6796 100644 --- a/2D/js/popup/solve.js +++ b/2D/js/popup/solve.js @@ -12,12 +12,10 @@ export function showSolvePopup(toolbar) { { label: '2) Brute force method :', underline: true, icon: '../assets/ic_solution.png', description: 'Brute force tries all possible combinations of polyominoes on the grid to find a solution. It is guaranteed to find a solution if one exists but is computationally expensive and slow for large grids.' }, { label: '3) Random method :', underline: true, icon: '../assets/ic_solution.png', description: 'The random method places polyominoes randomly on the grid. It is fast but does not guarantee a solution or full coverage. It is useful for generating quick and varied patterns.' }, { label: '4) Random backtracking :', underline: true, icon: '../assets/ic_solution.png', description: 'Random backtracking combines random placement with backtracking to find a solution. It is more efficient than brute force but less predictable than pure backtracking.' }, - { label: '5) Genetic Algorithm :', underline: true, icon: '../assets/ic_solution.png', description: 'Genetic Algorithm (GA) uses principles of natural selection and genetics to find the best arrangement of polyominoes. It starts with a population of random solutions and improves them over generations through crossover and mutation.' }, - { label: '6) Simulated Annealing :', underline: true, icon: '../assets/ic_solution.png', description: 'Simulated Annealing is an optimization algorithm inspired by the process of heating and cooling metal. It allows accepting worse solutions under certain conditions to escape local optima, aiming to find a better overall solution.' }, - { label: '7) Ant Colony Optimization :', underline: true, icon: '../assets/ic_solution.png', description: 'Ant Colony Optimization (ACO) is inspired by the foraging behavior of ants. It uses a group of artificial ants to explore solutions and leave pheromones, guiding other ants to follow the best paths found.' }, - { label: '8) Greedy Algorithm :', underline: true, icon: '../assets/ic_solution.png', description: 'The Greedy Algorithm makes the best choice at each step without considering the whole problem. It can quickly find a valid solution but does not guarantee a globally optimal solution.' } + { label: '5) Full Automatic Tiling :', underline: true, icon: '../assets/ic_solution.png', description: 'Automatically fills the grid using user-selected pieces from outside the grid, such as filling the grid entirely with monominoes if only one monomino is chosen.' } ]; + const startY = 60; const rowHeight = 60; const colX = 30; @@ -160,6 +158,9 @@ function attachSolveClickEvent(toolbar, popup, row, y) { case '4) Random backtracking :': toolbar.mainApp.randomBacktrackingTiling(); break; + case '5) Full Automatic Tiling :': + toolbar.mainApp.fullAutoTiling(); + break; } if (toolbar.isMobile) toolbar.closePopup('solve'); } diff --git a/2D/js/popup/tutorial.js b/2D/js/popup/tutorial.js index b41580a..ff9c9e2 100644 --- a/2D/js/popup/tutorial.js +++ b/2D/js/popup/tutorial.js @@ -5,4 +5,137 @@ export function showTutorialPopup(toolbar) { ctx.fillStyle = '#a0a0a0'; ctx.fillRect(0, 0, popup.width, popup.height); + + const rows = [ + { label: 'Auto Tiling the Polyominoes Blocks', title: true }, + { label: '1) Create Any Polyomino :', underline: true, icon: '../assets/ic_plus.png', description: 'Click this icon to open a menu. Select the polyomino you want to spawn it on the field. Drag and drop it onto the grid.' }, + { label: '2) Manipulate the Polyominoes :', underline: true, icon: '../assets/ic_solution.png', description: 'Each polyomino has an interactive menu. Click on the polyomino to : \n - rotate left or rotate right \n - flip \n - duplicate \n(the clone will appear below \n the piece selected in another color) \n - or delete.' }, + { label: '3) Grid Settings :', underline: true, icon: '../assets/ic_table.png', description: 'This menu lets you create a new board by setting the grid\'s length and height. Click to create the grid. \n Options include : \n - deleting the grid \n - blocking cells to prevent polyomino placement \n - creating automatically random \n black cells \n - Clear all black cells \n - and swapping black and empty cells. ' }, + { label: '4) Solving Grid :', underline: true, icon: '../assets/ic_solving.png', description: 'Click this menu to use various AI solvers. Descriptions are available for each solver; just click to read them.' }, + { label: '5) Use Settings :', underline: true, icon: '../assets/ic_solution.png', description: 'This section facilitates user testing. In this menu, you can: \n - Reset the position \n of the polyominoes to their original spawn points.\n - Shuffle the positions \n of the polyominoes on the field, useful if duplicated pieces overlap.\n - Delete all polyominoes \n from the field and the grid.' } + ]; + + + const subIcons = [ + { path: '../assets/ic_rotate_right.png', x: 245, y: 339 }, + { path: '../assets/ic_rotate_left.png', x: 285, y: 339}, + { path: '../assets/ic_flip.png', x: 95, y: 357}, + { path: '../assets/ic_duplicate.png', x: 135, y: 375}, + { path: '../assets/ic_trash.png', x: 135, y: 440}, + { path: '../assets/ic_draw.png', x: 80, y: 560}, + { path: '../assets/ic_trash.png', x: 182, y: 597}, + { path: '../assets/ic_blacken_cell.png', x: 310, y: 620}, + { path: '../assets/ic_random_blacken_cell.png', x: 275, y: 655}, + { path: '../assets/ic_whiten.png', x: 220, y: 697}, + { path: '../assets/ic_invert_blacken.png', x: 320, y: 717}, + { path: '../assets/ic_reset.png', x: 200, y: 975}, + { path: '../assets/ic_shuffle.png', x: 210, y: 1037}, + { path: '../assets/ic_trash.png', x: 250, y: 1107}, + ]; + + + const startY = 60; + const rowHeight = 60; + const colX = 25; + const maxWidth = 280; + + const dropdowns = {}; + let clickAreas = []; + + rows.forEach((row, index) => { + const y = startY + index * rowHeight; + ctx.font = '21px Pixellari'; + ctx.fillStyle = '#000'; + ctx.fillText(row.label, colX, y + 20); + + + if (row.description) { + dropdowns[index] = { description: row.description, expanded: true, y: y + 40 }; + } + }); + + + function wrapText(ctx, text, x, y, maxWidth, lineHeight) { + const paragraphs = text.split('\n'); + let totalLines = 0; + + paragraphs.forEach(paragraph => { + const words = paragraph.split(' '); + let line = ''; + const lines = []; + + for (let n = 0; n < words.length; n++) { + let testLine = line + words[n] + ' '; + let metrics = ctx.measureText(testLine); + let testWidth = metrics.width; + if (testWidth > maxWidth && n > 0) { + lines.push(line); + line = words[n] + ' '; + } else { + line = testLine; + } + } + lines.push(line); + lines.forEach((line, index) => { + ctx.fillText(line, x, y + totalLines * lineHeight + index * lineHeight); + }); + totalLines += lines.length; + }); + + return totalLines; + } + + + function redrawPopup() { + ctx.clearRect(0, 0, popup.width, popup.height); + ctx.fillStyle = '#a0a0a0'; + ctx.fillRect(0, 0, popup.width, popup.height); + + let yOffset = 0; + clickAreas = []; + rows.forEach((row, index) => { + const y = startY + index * rowHeight + yOffset; + ctx.font = '21px Pixellari'; + ctx.fillStyle = '#000'; + ctx.fillText(row.label, colX, y + 20); + if (row.underline) { + ctx.beginPath(); + ctx.moveTo(colX, y + 25); + ctx.lineTo(colX + ctx.measureText(row.label).width, y + 25); + ctx.stroke(); + } + + if (row.icon) { + const icon = new Image(); + icon.src = row.icon; + icon.onload = () => { + ctx.drawImage(icon, popup.width - 64, y - 14, 50, 50); + }; + clickAreas.push({ index, rect: { x: popup.width - 94, y: y - 14, width: 50, height: 50 }, type: 'icon' }); + } + + subIcons.forEach(subIcon => { + if (subIcon.path ) { + const subIconImage = new Image(); + subIconImage.src = subIcon.path; + subIconImage.onload = () => { + ctx.drawImage(subIconImage, subIcon.x, subIcon.y, 25, 25); + }; + } + }); + + + clickAreas.push({ index, rect: { x: colX, y, width: popup.width - colX - 100, height: rowHeight }, type: 'label' }); + + if (dropdowns[index] && dropdowns[index].expanded) { + ctx.font = '16px Pixellari'; + ctx.fillStyle = '#000'; + const linesCount = wrapText(ctx, dropdowns[index].description, colX + 15, y + 55, maxWidth, 20); + yOffset += linesCount * 20; + } + }); + }; + + redrawPopup(); }; +