TETR.IO custom presets

Add presets to TETR.IO custom games

// ==UserScript==
// @name         TETR.IO custom presets
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Add presets to TETR.IO custom games
// @author       0tv0
// @license      MIT
// @match        https://tetr.io/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tetr.io
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    function sleep(ms) {
      return new Promise((res, rej) => setTimeout(res, ms));
    };

    function grabElements() {
      let boardWidth = document.querySelector("#custom_content_game > div:nth-child(2) > div:nth-child(9) > input");
      let boardHeight = document.querySelector("#custom_content_game > div:nth-child(2) > div:nth-child(10) > input");
      let metaConfigPanel = document.querySelector("#custom_content_meta > div:nth-child(2)");
      if(boardWidth && boardHeight && metaConfigPanel) return { boardWidth, boardHeight, metaConfigPanel };
      return null;
    };

    async function waitForElements() {
      let t;
      while(1) {
        t = grabElements();
        if(t) {
          console.log(t);
          break;
        };
        await sleep(100);
      };
      return t;
    };

    const TETRAMINOS = [ 's', 'z', 'j', 'l', 't', 'i', 'o', '_', '#' ];
    const NAMES = [ 'S piece', 'Z piece', 'J piece', 'L piece', 'T piece', 'I piece', 'O piece', 'Empty block', 'Garbage Mino' ];

    function createBoard(width, height) {
      return new Array(height).fill(null).map(x => new Array(width).fill(null).map(y => '_'));
    };

    function createMap(map = [], queue = [], held = null) {
      if(!held) held = null;
      if(!(typeof map === 'object' && Array.isArray(map))) throw new Error("Arg 1: Invalid type (May only be of type: Array)");
      if(map.filter(x => !(typeof x === 'object' && Array.isArray(x))).length > 0) throw new Error("Arg 1 (In content): Invalid type (May only be of type: Array)");
      if(!(typeof queue === 'object' && Array.isArray(queue))) throw new Error("Arg 2: Invalid type (May only be of type: Array)");
      if(queue.filter(x => !TETRAMINOS.includes(x)).length > 0) throw new Error("Arg 2 (In content): Invalid tetramino (Can only be 1 of " + TETRAMINOS.join(', ') + ") - make sure you are using lowercase letters");
      if(!TETRAMINOS.includes(held) && held !== null) throw new Error("Arg 3: Invalid tetramino (Can only be 1 of " + TETRAMINOS.join(', ') + ", OR falsy / false value) - make sure you are using lowercase letters");

      let width = map[0].length; // first row
      if(Math.max(...map.map(c => c.length)) !== width) throw new Error("Arg 1 (In content while parsing): a row is too large");
      if(Math.min(...map.map(c => c.length)) !== width) throw new Error("Arg 1 (In content while parsing): a row is too small");
      let height = map.length;

      let data = {
        width, height,
        sequence: ''
      };

      map.forEach(row => {
        row.forEach((piece, column) => {
          // because of the way this data is iterated through, we don't actually have to do anything to organize it, stuff will fall into place correctly
          data.sequence += piece;
        });
      });

      if(queue.length || held) data.sequence += '?';

      queue.forEach(tetramino => data.sequence += tetramino);

      if(held) data.sequence += '?' + held;

      return data;
    };

    var presets = [
      {
        name: "None",
        minWidth: null,
        minHeight: null,
        maxWidth: null,
        maxHeight: null,
        requiresEvenHeight: false,
        requiresEvenWidth: false,
        generator: (width, height) => {
          let board = createBoard(width, height);
          return createMap(board, [], null);
        }
      },
      {
        name: "4 Wide",
        minWidth: 4,
        // we technically still can setup a 4 wide, it just won't have any padding
        maxWidth: null,
        minHeight: 2,
        maxHeight: null,
        requiresEvenWidth: true,
        // while yes, it could be made with an odd width
        // i'm not going to code that in
        // plus anyone who plays with like board with 9 is just a psychopath
        requiresEvenHeight: false,
        generator: (width, height) => {
          let board = createBoard(width, height + 20);
          let padding = (width - 4) / 2;
          board.forEach(row => {
            // go from top to bottom row
            for(let i = 0; i < padding; i++) row[i] = row[(row.length - 1) - i] = i ? "#" : "z";
          });
          // construct the S piece
          // this should work even if width is like 4
          board[board.length - 1][padding - 1] = board[board.length - 1][padding] = 's';
          board[board.length - 2][padding] = board[board.length - 2][padding + 1] = 's';

          return createMap(board, [], null);
        }
      }
    ];

    function calculateAvailablePresets(width, height) {
      let data = presets;
      if(width % 2 === 1) data = data.filter(x => !x.requiresEvenWidth);
      if(height % 2 === 1) data = data.filter(x => !x.requiresEvenHeight);
      data = data.filter(x => typeof x.minWidth !== 'number' ? true : x.minWidth <= width);
      data = data.filter(x => typeof x.minHeight !== 'number' ? true : x.minHeight <= height);
      data = data.filter(x => typeof x.maxWidth !== 'number' ? true : x.maxWidth >= width);
      data = data.filter(x => typeof x.maxHeight !== 'number' ? true : x.maxHeight >= height);
      return data;
    };

    waitForElements().then(el => {
      let roomConfig = el.metaConfigPanel;

      /* THIS SECTION OF CODE BELOW IS FROM TETR.IO PLUS, IT WAS NOT MADE BY ME */

      let row = document.createElement('div');
      row.classList.add('room_config_row', 'flex-row', 'ns');
      roomConfig.insertBefore(row, roomConfig.firstChild);

      let label = document.createElement('div');
      label.classList.add('room_config_label', 'flex-item', 'ns');
      label.innerText = 'Use custom map';
      row.appendChild(label);

      let check = document.createElement('input');
      check.classList.add('room_config_item', 'flex-item');
      check.type = 'checkbox';
      row.appendChild(check);

      let row2 = document.createElement('div');
      row2.classList.add('room_config_row', 'flex-row', 'ns');
      roomConfig.insertBefore(row2, row.nextSibling);

      let label2 = document.createElement('div');
      label2.classList.add('room_config_label', 'flex-item', 'ns');
      label2.innerText = 'Custom map string';
      row2.appendChild(label2);

      let mapInput = document.createElement('input');
      mapInput.classList.add('room_config_item', 'flex-item');
      mapInput.style.fontFamily = 'monospace';

      check.addEventListener('change', evt => {
        mapInput.setAttribute("data-index", check.checked ? "map" : "");
      });

      row2.appendChild(mapInput);

      /* END COPIED SECTION */

      // hide custom map option
      row.style.display = 'none';
      row2.style.display = 'none';
      check.click();

      function setCustomMapToPreset(presetObject, width, height) {
        mapInput.value = presetObject.generator(width, height).sequence;
      };

      let presetElement = document.createElement('div');
      presetElement.classList.add('room_config_row', 'flex-row', 'imp');
      presetElement.title = "The preset to generate the map from, if any.";

      let presetLabel = document.createElement('div');
      presetLabel.classList.add('room_config_label', 'flex-item', 'ns');
      presetLabel.innerText = 'board preset';

      let presetOptions = document.createElement('div');
      presetOptions.classList.add('room_config_item', 'room_config_spinner', 'flex-item', 'ns');
      presetOptions.setAttribute('data-index', 'preset');
      presetOptions.innerText = "None [1/" + presets.length + "]";

      presetElement.appendChild(presetLabel);
      presetElement.appendChild(presetOptions);

      let mainConfig = el.boardHeight.parentElement.parentElement;
      let bagType = mainConfig.children[1];
      mainConfig.insertBefore(presetElement, bagType);

      // 5 minute crafts ahh fix for the option not being actually coded into the game and having function
      let gameButton = document.querySelector("#custom_opts_game");
      let optionsCycle = 0;
      let available = calculateAvailablePresets(parseInt(el.boardWidth.value), parseInt(el.boardHeight.value));
      let selected = "None";

      let recalculate = () => {
        console.log("Recalculation of presets in progress.");
        available = calculateAvailablePresets(parseInt(el.boardWidth.value), parseInt(el.boardHeight.value));

        if(available.map(x => x.name === selected).length === 0) {
          console.log("Board had illegal preset, removed");
          optionsCycle = 0;
        };

        setCustomMapToPreset(available[optionsCycle], parseInt(el.boardWidth.value), parseInt(el.boardHeight.value));
        presetOptions.innerText = available[optionsCycle].name + " [" + (optionsCycle + 1) + "/" + available.length + "]";

        console.log("All done");
      };

      el.boardWidth.onchange = recalculate;
      el.boardHeight.onchange = recalculate;

      presetOptions.onclick = () => {
        optionsCycle = ++optionsCycle % available.length;
        console.log({ optionsCycle, available });

        recalculate();
        gameButton.click(); // play the sound effect
      };
    });
})();