Slide Puzzle in Google Sheets

Gists

Abstract

This report demonstrates the onSelectionChange simple trigger in Google Sheets by creating a slide puzzle. Selecting cells triggers script execution, enabling interactive gameplay.

Introduction

At Google Sheets, simple triggers can be used. When simple triggers are used, users can automatically run Google Apps Script based on actions in the spreadsheet. Among the simple triggers is onSelectionChange. This can automatically run a script when a cell is selected. In this report, as a simple sample script demonstrating the use of onSelectionChange, I created a slide puzzle in Google Sheets. The goal of the slide puzzle is to align the numbers by sliding them. Here, onSelectionChange is used to detect the selected number and slide it. The aim of this report is to learn about the simple trigger onSelectionChange.

Usage

Please do the following flow.

1. Prepare sample spreadsheet

Please create a standalone script in the Google Apps Script editor. Copy and paste the following script into the editor, and run the function createSampleSpreadsheet. This will create a sample spreadsheet for testing the slide puzzle in the root folder of your Google Drive.

Then, please open the created spreadsheet. You will see a spreadsheet identical to the above demonstration.

/**
 * ### Description
 * Create a sample Spreadsheet to the root folder.
 */
function createSampleSpreadsheet() {
  const spreadsheetName = "sample spreadsheet";
  const ss = SpreadsheetApp.create(spreadsheetName, 10, 8);
  const sheet = ss.getSheets()[0].setName("Sheet1");
  const values = [
    ["Slide puzzle", null],
    ["START GAME", null],
    ["Puzzle range", "C5:G9"],
    ["Status", null],
  ];
  sheet.getRange("A1:B4").setValues(values);
  sheet.getRange("B2").insertCheckboxes();
  sheet.getRange("A1").setFontSize(30).setFontColor("#ffffff");
  sheet.getRange("A2:B4").setBackground("#fce5cd");
  sheet.getRange("A1:H1").merge().setBackground("#1155cc");
  sheet
    .getRange("C5:G9")
    .setFontSize(30)
    .setHorizontalAlignment("center")
    .setVerticalAlignment("middle")
    .setBackground("#efefef")
    .setBorder(
      true,
      true,
      true,
      true,
      true,
      true,
      "#000000",
      SpreadsheetApp.BorderStyle.SOLID
    );
  sheet.setColumnWidths(3, 6, 55).setRowHeights(5, 6, 55);
}

2. Main script

Please open the script editor of the created spreadsheet, and copy and paste the following script to the script editor, and save the script.

/**
 * Slide Puzzle in Google Sheets
 * Author: Kanshi Tanaike
 */

/**
 * ### Description
 * Convert Class Range object to Gridrange as an array.
 *
 * @param {SpreadsheetApp.Range} r Range.
 * @return {Array} Gridrange.
 */
function getRowsCols_(r) {
  const startRow = r.getRow();
  const endRow = r.getNumRows() + startRow;
  const startCol = r.getColumn();
  const endCol = r.getNumColumns() + startCol;
  return [startRow, endRow, startCol, endCol];
}

/**
 * ### Description
 * Shuffle range by creating the number values.
 *
 * @param {SpreadsheetApp.Range} range Range.
 * @return {void}
 */
function shuffle_(range) {
  const rows = range.getNumRows();
  const cols = range.getNumColumns();
  const array = [...Array(rows * cols)].map((_, i, a) =>
    a.length - 1 == i ? "" : i + 1
  );
  array.sort(() => Math.random() - 0.5); // Ref: https://stackoverflow.com/q/2450954
  const values = [...Array(Math.ceil(array.length / cols))].map((_) =>
    array.splice(0, cols)
  ); // Ref: https://github.com/tanaikech/UtlApp?tab=readme-ov-file#splitarray
  range.setValues(values);
}

/**
 * ### Description
 * Simple trigger of OnEdit.
 * Please directly run this function by the script editor.
 *
 * @param {Object} e Event object of the simple trigger of OnEdit.
 * @return {void}
 */
function onEdit(e) {
  const { range } = e;
  const sheet = range.getSheet();
  const p = PropertiesService.getScriptProperties();
  const a1Notation = range.getA1Notation();
  if (a1Notation == "B2") {
    const puzzleRange = sheet.getRange("B3").getDisplayValue();
    let status = null;
    if (range.isChecked()) {
      p.setProperty("status", "start");
      status = "start";
      shuffle_(sheet.getRange(puzzleRange));
    } else {
      p.deleteProperty("status");
    }
    sheet.getRange("B4").setValue(status);
    p.setProperty("count", "0");
    p.setProperty("puzzleRange", puzzleRange);
  }
}

/**
 * ### Description
 * Simple trigger of OnSelectionChange.
 * Please directly run this function by the script editor.
 *
 * @param {Object} e Event object of the simple trigger of OnSelectionChange.
 * @return {void}
 */
function onSelectionChange(e) {
  const { source } = e;
  const p = PropertiesService.getScriptProperties();
  const { status, count, puzzleRange, previousRange } = p.getProperties();
  if (status != "start") return;
  if (!puzzleRange) {
    Browser.msgBox("Please set the puzzle range to the cell B4.");
    return;
  }
  const sheet = source.getActiveSheet();
  const activeRange = sheet.getActiveRange();
  const statusRange = sheet.getRange("B4");
  const puzzleRangeObj = sheet.getRange(puzzleRange);
  const [crStartRow, crEndRow, crStartCol, crEndCol] =
    getRowsCols_(activeRange);
  const [pzrStartRow, pzrEndRow, pzrStartCol, pzrEndCol] =
    getRowsCols_(puzzleRangeObj);
  statusRange.setValue("Running...");
  if (
    crStartRow < pzrStartRow ||
    crEndRow > pzrEndRow ||
    crStartCol < pzrStartCol ||
    crEndCol > pzrEndCol
  ) {
    p.deleteProperty("previousRange");
    statusRange.setValue("Out of range");
    SpreadsheetApp.flush();
    Utilities.sleep(2000);
  } else {
    if (previousRange && activeRange.isBlank()) {
      const previousRangeObj = sheet.getRange(previousRange);
      const [prStartRow, , prStartCol] = getRowsCols_(previousRangeObj);
      const check = [
        crStartRow - 1 == prStartRow && crStartCol == prStartCol,
        crStartRow + 1 == prStartRow && crStartCol == prStartCol,
        crStartRow == prStartRow && crStartCol - 1 == prStartCol,
        crStartRow == prStartRow && crStartCol + 1 == prStartCol,
      ].some((k) => k);
      if (check) {
        const c = Number(count) + 1;
        previousRangeObj.copyTo(activeRange);
        previousRangeObj.clearContent();
        const puzzleRangeValues = puzzleRangeObj.getValues().flat().join(",");
        const goal = [
          ...Array(
            puzzleRangeObj.getNumRows() * puzzleRangeObj.getNumColumns()
          ),
        ]
          .map((_, i, a) => (a.length - 1 == i ? "" : i + 1))
          .join(",");
        if (puzzleRangeValues == goal) {
          p.deleteProperty("status");
          p.setProperty("count", "0");
          sheet.getRange("B2").uncheck();
          statusRange.setValue(null);
          Browser.msgBox(`Finished. Total movements were ${c}.`);
          return;
        } else {
          p.setProperty("count", c);
        }
      }
    }
  }
  p.setProperty("previousRange", activeRange.getA1Notation());
  statusRange.setValue("Next");
}

3. Reopen spreadsheet

To correctly use the simple triggers, please close the spreadsheet and reopen it.

4. Testing

First, please see the demonstration video at the top of this post. When you check the checkbox in “B2”, numbers are randomly placed into the cells “C5:G9”. When you click each cell, you can arrange the numbers by sliding them. When the numbers are lined up in order, the puzzle is finished.

When you change the range in cell “B3” and check the checkbox “B2”, a new puzzle is created and you can play it.

 Share!