MediaWiki:Pikcross.js

// Canvas constants. const CANVAS = { // Width. WIDTH: 800, // Height. HEIGHT: 800 };

// Length of a border gradient effect. const BORDER_GRADIENT_LENGTH = 20; // Width and height of each cell, in pixels. const CELL_SIZE = 32; // Padding between each cell. const CELL_PADDING = 1; // Maximum number of rows or columns a board can have. const MAX_COLS_OR_ROWS = 40; // Minimum number of rows or columns a board can have. const MIN_COLS_OR_ROWS = 2; // Padding between sections of the game screen. const SECTION_PADDING = 10; // Transition total duration. const TRANSITION_DURATION = 0.4;

// Color for the game's background. const COLOR_BG = 'rgb(17, 51, 17)'; // Color for the board's background. const COLOR_BOARD_BG = 'rgba(34, 68, 34, 0.6)'; // Color for the hints' background. const COLOR_HINTS_BG = 'rgb(51, 85, 51)'; // Color for the footer's background. const COLOR_FOOTER_BG = 'rgb(68, 102, 68)'; // Colors for the hint text. const COLOR_HINT = ['#622', '#338']; // Color for the hint text when defied. const COLOR_HINT_DEFIED = '#E11'; // Color to tint the hint button with when defied. const COLOR_HINT_DEFIED_TINT = 'rgba(255, 0, 0, 0.7)'; // Color for a button's background. const COLOR_BUTTON_BG = '#BCB'; // Colors for a button's shadow. const COLOR_BUTTON_SHADOW = ['rgba(64, 0, 0, 0.5)', 'rgba(0, 0, 64, 0.5)']; // Colors for buttons' text. const COLOR_BUTTON_TEXT = ['#622', '#338']; // Color for buttons' text while disabled. const COLOR_BUTTON_TEXT_DISABLED = '#888'; // Color for a blank cell. const COLOR_BLANK = '#EFE'; // Color for a filled cell. const COLOR_FILLED = '#696'; // Color for a marked cell. const COLOR_MARKED = '#EDD'; // Color for a maker tool that's green. const COLOR_MAKER_GREEN = '#262'; // Color for a maker tool that's red. const COLOR_MAKER_RED = '#622'; // Colors for generic text. const COLOR_TEXT_GENERIC = ['#ECC', '#BBE'];

// Value for a blank cell. const CELL_BLANK = 0; // Value for a filled cell. const CELL_FILLED = 1; // Value for a marked cell. const CELL_MARKED = 2;

// Main menu. const STATE_MAIN_MENU = 0; // Playing a puzzle. const STATE_PLAYING = 1; // Making a puzzle. const STATE_MAKING = 2;

// In-game pause menu. const PANEL_STATE_PAUSE = 0; // Congratulating the player on a finished puzzle. const PANEL_STATE_CONGRATS = 1; // Asking for the new puzzle's name. const PANEL_STATE_MAKING_NAME = 2; // Showing the new puzzle's code. const PANEL_STATE_MAKING_CODE = 3; // Asking for a custom puzzle code to play on. const PANEL_STATE_PLAYING_CODE = 4; // Warning the player of an error while loading the level. const PANEL_STATE_LOAD_ERROR = 5; // Warning the player of an error while saving the level. const PANEL_STATE_SAVE_ERROR = 6; // Warning the player they have a bookmark when they try to start a level. const PANEL_STATE_BOOKMARK_WARNING = 7; // Info. const PANEL_STATE_INFO = 8;

// When encoding or decoding data, do it for a level. const BOARD_DATA_CONTEXT_LEVEL = 0; // When encoding or decoding data, do it for a bookmark. const BOARD_DATA_CONTEXT_BOOKMARK = 1;

// Button. const GUI_ITEM_BUTTON = 0; // Text. const GUI_ITEM_TEXT = 1;

// Spritesheet, in the context of loading things. const LOAD_CONTENT_SPRITES = 0; // Game setup, in the context of loading things. const LOAD_CONTENT_SETUP = 1;

// All levels. const LEVELS = [ // P.   'BQV//zEAAVA=', // Leaf. 'CgrAw48///z5548fHwwABExlYWY=', // Bud. 'CgpYsOOee9///79//MAAA0J1ZA==', // Flower. 'Cgow4IHH/887x49//jEDBkZsb3dlcg==', // Red Pikmin. 'CgoAAuz/xh424AMDMggAClJlZCBQaWttaW4=', // Yellow Pikmin. 'Cgr4cIQgyOSr9lf/fA0DDVllbGxvdyBQaWttaW4=', // Blue Pikmin. 'Cgp4GHPaeGEBAxyw0IAEC0JsdWUgUGlrbWlu', // Bomb Rock. 'Cgp40Ofoz78/H7x3vMABCUJvbWIgUm9jaw==', // Swooping Snitchbug. 'Dw/HHxVVPpUgIDawO9g8CD/4L/6X/kv/JX8qnxZAARJTd29vcGluZyBTbml0Y2hidWc=', // Purple Pikmin. 'Dw8AZKAb50fcU2SPnuS/xA/8B/gD/AH+AD4AHgAJAA1QdXJwbGUgUGlrbWlu', // Rubber Ugly. 'Dw/gAfgB/gHXQG+R2YX/hP/8f/6//9//x//Bf4APAAtSdWJiZXIgVWdseQ==', // Breadbug. 'Dw/wDwQI/YsD737///++/+A/YA+wR5kHhP9j/yIAAAhCcmVhZGJ1Zw==', // White Pikmin. 'Dw8Hz3lA+AH4yf5n/wP/g/1j/8A/8F/4P/5P/3f/AQxXaGl0ZSBQaWttaW4=', // Burrowing Snagret. 'Dw8AD8AP/A//h99HzjPj/fE3/Al+gg/AB/AD+Pz/ARFCdXJyb3dpbmcgU25hZ3JldA==', // The Key. 'Dw8YAD6Af+D78PB54Png+fHwf/wf78fL8Q64A/gAAAdUaGUgS2V5', // Captain Olimar. 'FA/gAPwDAUIEEnwKf2W++l/9L/0bARb/Cf4E/gL/gTOAGcAP8AP4AQ5DYXB0YWluIE9saW1hcg==', // Data File. 'FA/+hwBE/6WAVdwtmdZYa6g11pJpcbQAWgCtqhZAS6UF0P5vAND/BwlEYXRhIEZpbGU=', // Sunseed Berry. 'Dw8AAAAAOALigoRggVEpdfv2O69eu2b38X/wBwAAAA1TdW5zZWVkIEJlcnJ5', // Citrus Lump. 'Dw8AAAAA/oGNIQ8LEyUCB4gh0IJgBFEQZIDBfwAAAAtDaXRydXMgTHVtcA==', // Juice. 'FA/wBwwG/gMBgf8gkUiQBED8H/2X/kv5pf9SfumplFXyJwGQAQz/AQVKdWljZQ==', // Rock Pikmin. 'FBQA/ADwH8CPA/4x8B+OGJus9cgYl/x/6f+v/v/9/x/9f5H/F/s/E/8r4B8AtgFADADPAwtSb2NrIFBpa21pbg==', // Pellet Posy. 'GQ8AAFABVAG9QT/R/PCedF8cF/oL+waTAbYASAAUAA4ABYACwAEQAagAz0CooKbgDAtQZWxsZXQgUG9zeQ==', // Spicy Spray. 'FBQABgBgAAA+AGAAAA8A/APgfwABCAgAgQAQBADCzCy8d8PuLbi7gf8f8P8A/gfAPwDwAAtTcGljeSBTcHJheQ==', // Winged Pikmin. 'FBQAOAN4fMD4CeO0GIPMMHgEPuBOfu9t7I6CBDDIAIz4gQ2YMEAtAlIioGYGREsADAcAAA1XaW5nZWQgUGlrbWlu', // Pyroclasmic Slooch. 'Hh4ACAAAjg8AgCYCACCHAACMYQAAYxgAADAAAAAMAAAABwAAQAEAAPgAAAAyAADAHwAAEM6DAx6//8H8k/NY4N8z53/uSYZ/Y3IHlrm0/2e+H/7I1z8A//T5PwO+A3AA/h8PANb/AQANAACAPQAAwAcAABJQeXJvY2xhc21pYyBTbG9vY2g=', // Sandbelching Meerslug. 'GRkA4AAA/A8A/v8A/+8DE/8PAvg/BOB52MLh4J/jQYv/gwD/BwP/D/j/H8D/PwD+fwDg/wCA/wEA/wMA/gcA/g/A/B+A/x/QfRR4AAAoAAAACE1lZXJzbHVn', // Waddlepuss. 'FB4A+AcAADEOAPiPDwDx/wRAjnMC8OP4AP5/fsD//z84H/8ft7//59rU97809f1PTnx+x4e/OQL4j1kA+HsYh/wCiGGeABw4MAD48wcKV2FkZGxlcHVzcw==', // Plasm Wraith. 'HhSAHwD+D/DPgf85/BjmB+N+cP4H/3/w/4/////3/wX/T+D8DODPAP4Y4I8D/zjwjwf/cPgPh/99+P/H/1/8/8X/T3j+BC8nwHIBP34MUGxhc20gV3JhaXRo', // Skutterchuck. 'Hh4AAAAAAMgA\AADNAACwSAAAchUAQCQKALBUBAAqFwJAJIIAUIQgAIhACAwSYQKMlPAMTNmwx1Qi9A1anO+Ay/8b4P3/DPj1nwG+/UHAjQs4uP8CBvOAgIAHEBAAAAQDAIAgAAAgBAAAAAEAAEAAAAAAAAxTa3V0dGVyY2h1Y2s=', // S.S. Drake. 'Hh4AAIAADwAg4AQACHgCAAK+8IeAPwYjwMtmCQCZuQPA9/4AGP/vAAMASGAAAiOEQAMSEfAQRYR4SBEQHkIMhAcQHgDBA/sRz4DBfxzgAeAByP9/gIs5IUCBZBBw8B8EEAYMAcQARgARABHAA4AHAAAAAApTLlMuIERyYWtl', // Fire-Breathing Feast. 'Hh4AADg4AIDJCWAwPwFM4lCAeh8SkP25DLKPp0Pe/Nnc8H+qS/yfrjz/o+nf/2z59/9K//2Bw38/4PE/c3h8QD9Wz/eflfz9Z4V//ynz33/P/vPPsfn+Exy8TwI5d/iA3wMe8AeGA/j19wN8/c0A1r8cABRGaXJlLUJyZWF0aGluZyBGZWFzdA==', // The 3 Koppaites. 'HigADwDgB+Z/BvgNGY8J/BsJAAn+GwEACD884TiIH3hSVYQP+BJHxD/O4jjEr8pCEMx3x0IQnAfhgg98h3DnOH4CIA8CvwUQfvAAmgj8bwAsBPAtx+kD+I0oYg/4j6piN8SPOOJjhh/HEcAFLkQIiAVgOATwDUAAHFgYgBB+PigAx/1/BM7//z8EMe/5H4IQ7tATghA4EBAPVGhlIDMgS29wcGFpdGVz', ];

// Sprite data in the spritesheet. const SPRITES = { // Blank cell. CELL_BLANK: { x: 0, y: 0, width: 32, height: 32 },   // Filled cell. CELL_FILLED: { x: 32, y: 0, width: 32, height: 32 },   // Marked cell. CELL_MARKED: { x: 64, y: 0, width: 32, height: 32 },   // Logo. LOGO: { x: 96, y: 0, width: 200, height: 47 },   // Hocotate background. HOCOTATE: { x: 0, y: 47, width: 800, height: 800 },   // Koppai background. KOPPAI: { x: 800, y: 47, width: 800, height: 800 } };

// Screen section coordinate information. const sectionCoords = { // Main menu header. mainMenuHeader: { x: 0, y: 0, width: CANVAS.WIDTH, height: CANVAS.HEIGHT * 0.2 },   // Main menu level selection. levelSelect: { x: 0, y: CANVAS.HEIGHT * 0.2, width: CANVAS.WIDTH, height: CANVAS.HEIGHT * 0.8 },   // Board coordinates. board: { x: 0, y: 0, width: 0, height: 0 },   // Miniature coordinates. miniature: { x: 0, y: 0, width: 0, height: 0 },   // Row hints coordinates. rowBanner: { x: 0, y: 0, width: 0, height: 0 },   // Column hints coordinates. colBanner: { x: 0, y: 0, width: 0, height: 0 },   // Footer coordinates. footer: { x: 0, y: 0, width: 0, height: 0 },   // Panel. panel: { x: CANVAS.WIDTH * 0.2, y: CANVAS.HEIGHT * 0.2, width: CANVAS.WIDTH * 0.6, height: CANVAS.HEIGHT * 0.6 } }; //Coordinates for GUI items. const guiItemCoords = { // Title, in the main menu. mainMenuTitle: { x: 0.3, y: 0.2, width: 0.4, height: 0.4 },   // Continue button in the main menu. mainMenuContinue: { x: 0.025, y: 0.12, width: 0.20, height: 0.30 },   // Info button in the main menu. mainMenuInfo: { x: 0.025, y: 0.58, width: 0.20, height: 0.30 },   // Make custom button in the main menu. mainMenuMake: { x: 0.775, y: 0.12, width: 0.20, height: 0.30 },   // Play custom button in the main menu. mainMenuCustom: { x: 0.775, y: 0.58, width: 0.20, height: 0.30 },   // Swap side button in the main menu. mainMenuSide: { x: 0.40, y: 0.65, width: 0.20, height: 0.22 },   // Pause button. pause: { x: SECTION_PADDING, y: SECTION_PADDING, width: 45, y2: -SECTION_PADDING },   // Title, in the playing state. playingTitle: { x: 0.425, y: 0.10, width: 0.15, height: 0.40 },   // Level number, in the playing state. playingLevel: { x: 0.00, y: 0.50, width: 1.00, height: 0.40 },   // Zoom in button. zoomIn: { x: -110, y: SECTION_PADDING, width: 45, y2: -SECTION_PADDING },   // Zoom out button. zoomOut: { x: -55, y: SECTION_PADDING, width: 45, y2: -SECTION_PADDING },   // Pause panel continue button. continue: { x: 0.2, y: 0.1, width: 0.6, height: 0.15 },   // Pause panel restart button. restart: { x: 0.2, y: 0.3, width: 0.6, height: 0.15 },   // Pause panel finish button. finish: { x: 0.2, y: 0.5, width: 0.6, height: 0.15 },   // Pause panel quit button. quit: { x: 0.2, y: 0.75, width: 0.6, height: 0.15 },   // General panel ok button. ok: { x: 0.2, y: 0.75, width: 0.6, height: 0.15 },   // Congrats panel header text. congratsHeader: { x: 0.2, y: 0.05, width: 0.6, height: 0.15 },   // Congrats panel puzzle miniature. congratsMiniature: { x: 0.2, y: 0.20, width: 0.6, height: 0.40 },   // Congrats panel level name text. congratsLevelName: { x: 0.2, y: 0.60, width: 0.6, height: 0.15 },   // New puzzle panel name prompt. newPuzzleNamePrompt: { x: 0.0, y: 0.2, width: 1.0, height: 0.1 },   // New puzzle panel done text. newPuzzleDoneText: { x: 0.0, y: 0.2, width: 1.0, height: 0.1 },   // Custom puzzle panel explanation text. customPuzzleText: { x: 0.0, y: 0.2, width: 1.0, height: 0.1 },   // Load error explanation 1 text. loadError1: { x: 0.0, y: 0.2, width: 1.0, height: 0.1 },   // Load error explanation 2 text. loadError2: { x: 0.0, y: 0.4, width: 1.0, height: 0.1 },   // Load error explanation 3 text. loadError3: { x: 0.0, y: 0.5, width: 1.0, height: 0.1 },   // Bookmark warning explanation 1 text. bookmarkWarning1: { x: 0.0, y: 0.15, width: 1.0, height: 0.1 },   // Bookmark warning explanation 2 text. bookmarkWarning2: { x: 0.0, y: 0.20, width: 1.0, height: 0.1 },   // Bookmark warning explanation 3 text. bookmarkWarning3: { x: 0.0, y: 0.25, width: 1.0, height: 0.1 },   // Bookmark warning explanation 4 text. bookmarkWarning4: { x: 0.0, y: 0.30, width: 1.0, height: 0.1 },   // Bookmark warning explanation 5 text. bookmarkWarning5: { x: 0.0, y: 0.35, width: 1.0, height: 0.1 },   // Bookmark warning go back button. bookmarkWarningBack: { x: 0.2, y: 0.55, width: 0.6, height: 0.15 },   // Bookmark warning play level button. bookmarkWarningStart: { x: 0.2, y: 0.75, width: 0.6, height: 0.15 },   // Info 1 text. info1: { x: 0.0, y: 0.15, width: 1.0, height: 0.1 },   // Info 2 text. info2: { x: 0.0, y: 0.30, width: 1.0, height: 0.1 },   // Info 3 text. info3: { x: 0.0, y: 0.35, width: 1.0, height: 0.1 },   // Info 4 text. info4: { x: 0.0, y: 0.40, width: 1.0, height: 0.1 },   // Info 5 text. info5: { x: 0.0, y: 0.45, width: 1.0, height: 0.1 }, };

// Camera information. let cam = { // Current coordinates. Used for panning. coords: {x: 0, y: 0}, // Current zoom level. zoom: 1.0 }; // Player input information. Is controlled by both the mouse and mobile touches. let input = { // Coordinates in the game world. worldCoords: {x: 0, y: 0}, // Coordinates on-screen, with 0,0 being the top-left of the canvas. screenCoords: {x: 0, y: 0}, // Is the player currently dragging? dragging: false, // X/Y coordinates of where the dragging started. dragStart: {x: 0, y: 0}, // Lock either coordinate when dragging. dragLockCoord: {x: false, y: false}, // What is the player currently doing to the cells? -1 means nothing. dragAction: -1, // Is the player dragging to pan the view? dragPanning: false }; // Board information. let board = { // Number of rows. Cache for convenience. nrRows: 0, // Number of columns. Cache for convenience. nrCols: 0, // State of every cell. cells: [], // Solution. solution: [], // Hints for each row. rowHints: [], // Hints for each column. colHints: [], // Whether a given row is auto-marked. autoMarkedRows: [], // Whether a given column is auto-marked. autoMarkedCols: [], // Whether a given row is defied. defiedRows: [], // Whether a given column is defied. defiedCols: [], // Puzzle name. name: '', // Level number. 0 means custom. levelNumber: 0, // Level code. levelCode: '', }; // Array of true/false, representing whether a given level's been cleared. let progression = []; // Array with data about all levels. let levelsData = []; // Canvas object. let canvas; // Canvas HTML element. let canvasEl; // Input box HTML element. let inputEl; // Spritesheet image. let sprites; // What things have been loaded. If any of these are false, the game's not ready. Use the LOAD_CONTENTS_ constants. let loaded = [false, false]; // Game state. let state = STATE_MAIN_MENU; // Are we currently showing the panel? let inPanel = false; // State of the panel. let panelState = PANEL_STATE_PAUSE; // Gradients to show on the borders of the board, when parts of the board are off-camera. let borderGradients = { left: null, right: null, up: null, down: null } // Have we warned the player of their pending bookmark yet? let gaveBookmarkWarning = false; // Current game side. 0 for Hocotate, 1 for Koppai. let gameSide = 0; // Scene transition time left. let transitionAnimTime = 0; // Transition phase. let transitionPhase = 0; // State to change to after the transition. let transitionNewState = 0; // Transition code to run after the state change. undefined for none. let transitionCodeAfter = undefined;

/** * Code to run when the player presses Ok when the panel shows the input box. */ function acceptInputBox { let input = inputEl.value; input = input.replace(/[^\x00-\x7F]/g, "");

switch(panelState) { case PANEL_STATE_MAKING_NAME:

if(inputEl.value.length == 0) return; if(input.length == 0) input = 'Puzzle'; board.name = input; var boardData = { nrRows: board.nrRows, nrCols: board.nrCols, cells: board.cells, name: board.name };

inputEl.value = encodeBoard(boardData, BOARD_DATA_CONTEXT_LEVEL); panelState = PANEL_STATE_MAKING_CODE; showInputBox(true);

break; case PANEL_STATE_MAKING_CODE:

hideInputBox; inPanel = false;

break;

case PANEL_STATE_PLAYING_CODE:

hideInputBox;

if(inputEl.value.length == 0) { inPanel = false; } else { if(loadLevel(0, input, STATE_PLAYING)) { changeState(STATE_PLAYING, function { inPanel = false; }); clearBookmark; } else { panelState = PANEL_STATE_LOAD_ERROR; }       }

break;

}

updateCanvas; }

/** * Checks if all hints in a row/column have been filled by the player. * @param {array} hints Array of hints to check. */ function allHintsAreFilled(hints) { for(var h = 0; h < hints.length; h++) { if(hints[h].state != CELL_FILLED) return false; }   return true; }

/** * Given an X or Y coordinate of a GUI item inside of some parent coordinates, this returns * what the final on-screen coordinates are, depending on how the GUI item's coordinates are formatted. * This function can take either X coordinates and widths, or it can take Y coordinates and heights. * 1 to infinity means the item is offset these many pixels from the parent's start. * 0 to 1 means the item is within this ratio of parent size, starting from the parent's start. * -1 to 0 means the item is within this ratio of parent size, starting from the parent's end. * -Infinity to -1 means the item is offset these many pixels from the parent's end. * @param {number} coord Coordinate to calculate. * @param {number} parentStart Start coordinate of the parent. * @param {number} parentSize Size of the parent. * @returns The calculated final coordinate. */ function calculateChildCoord(coord, parentStart, parentSize) { let parentEnd = parentStart + parentSize; if(coord > 1.00) { // Pixel offset from start. return parentStart + coord; } else if(coord >= 0.00) { // Ratio of size from start. return parentStart + (parentSize * coord); } else if(coord > -1.00) { // Ratio of size from end. return parentEnd - (parentSize * -coord); } else { // Pixel offset from end. return parentEnd - -coord; } }

/** * Returns the final X and Y coordinates of a child object, making use of calculateChildCoord. * @param {object} coords Object with the child coordinates. * @param {object} parentCoords Object with the parent coordinates. */ function calculateChildCoords(coords, parentCoords) { return { x: calculateChildCoord(coords.x, parentCoords.x, parentCoords.width), y: calculateChildCoord(coords.y, parentCoords.y, parentCoords.height) }; }

/** * Given a width or height of a GUI item inside of some parent coordinates, this returns * what the final on-screen size is, depending on how the GUI item's width/height is formatted. * This function can take either X coordinates and widths, or it can take Y coordinates and heights. * 1 to infinity means the item's size is these many pixels. * 0 to 1 means the item is this ratio of parent size. * @param {number} coord Coordinate to calculate. * @param {number} parentSize Size of the parent. * @returns The calculated final size. */ function calculateChildDimension(coord, parentSize) { if(coord > 1.00) { // Pixel size. return coord; } else if(coord > 0.00) { // Ratio size. return parentSize * coord; } }

/** * Returns the final X and Y dimensions of a child object, making use of calculateChildDimension. * @param {object} coords Object with the child coordinates. * @param {object} finalXY The object's final X and Y coordinates, calculated elsewhere. * @param {object} parentCoords Object with the parent coordinates. */ function calculateChildDimensions(coords, finalXY, parentCoords) { let finalCoords = {}; if(coords.x2 == undefined) { finalCoords.width = calculateChildDimension(coords.width, parentCoords.width); } else { let x2 = calculateChildCoord(coords.x2, parentCoords.x, parentCoords.width); finalCoords.width = x2 - finalXY.x;   } if(coords.y2 == undefined) { finalCoords.height = calculateChildDimension(coords.height, parentCoords.height); } else { let y2 = calculateChildCoord(coords.y2, parentCoords.y, parentCoords.height); finalCoords.height = y2 - finalXY.y;   } return finalCoords; }

/** * Changes the state of a cell, and updates everything accordingly. * @param {number} row Cell row number. * @param {number} col Cell column number. * @param {number} newState The cell's new state. * @returns True if the cell changed, false otherwise. */ function changeCell(row, col, newState) { if(board.cells[row][col] == newState) { return false; }

board.cells[row][col] = newState;

if(state == STATE_PLAYING) { updateRow(row); updateColumn(col);

let solved = true; for(var r = 0; r < board.nrRows; r++) { for(var c = 0; c < board.nrCols; c++) { var cellState = board.cells[r][c]; if(cellState == CELL_MARKED) cellState = CELL_BLANK; if(cellState != board.solution[r][c]) { solved = false; break; }           }            if(!solved) break; }

if(solved) { if(board.levelNumber != 0) { progression[board.levelNumber - 1] = true; saveProgression; }           clearBookmark; inPanel = true; panelState = PANEL_STATE_CONGRATS; } else { saveBookmark; }   }

return true; }

/** * Changes the current game state with a transition. * @param {number} newState New state. * @param {function} codeAfter Code to run after the transition. */ function changeState(newState, codeAfter) { transitionNewState = newState; transitionAnimTime = TRANSITION_DURATION; transitionPhase = 0; transitionCodeAfter = codeAfter; }

/** * Clamps the camera coordinates to make sure they don't go too far to the left, right, up, or down. * Also snaps to 0,0 if it's close enough. */ function clampCamera { cam.coords.x = Math.min(cam.coords.x, (board.nrCols - 1) * (CELL_SIZE + CELL_PADDING)); cam.coords.x = Math.max(cam.coords.x, -(sectionCoords.board.width - CELL_SIZE)); cam.coords.y = Math.min(cam.coords.y, (board.nrRows - 1) * (CELL_SIZE + CELL_PADDING)); cam.coords.y = Math.max(cam.coords.y, -(sectionCoords.board.height - CELL_SIZE));

cam.zoom = Math.max(cam.zoom, 0.1); cam.zoom = Math.min(cam.zoom, 5); }

/** * Debug function that automatically marks all levels as cleared. * This does not save the player's progression. */ function clearAllLevels { for(var l = 0; l < progression.length; l++) { progression[l] = true; }   updateCanvas; }

/** * Clears the player's bookmark. */ function clearBookmark { localStorage.setItem('pikcrossBookmark', ''); }

/** * Decodes a base64-encoded string into a board. * @param {string} data Encoded board text. * @param {number} context BOARD_DATA_CONTEXT_LEVEL to decode level data. BOARD_DATA_CONTEXT_BOOKMARK to decode bookmark data. * @returns Board data. Returns null on error. */ function decodeBoard(data, context) { let result = { nrRows: 0, nrCols: 0, cells: [], name: '', nrRowHints: 0, rowHints: [], nrColHints: 0, colHints: [], levelNumber: 0, levelCode: '', }   let nrCellBits = context == BOARD_DATA_CONTEXT_LEVEL ? 1 : 2;

try {

let reader = new StrBitReader(atob(data));

result.nrRows = reader.readNumber; result.nrCols = reader.readNumber; for(var r = 0; r < result.nrRows; r++) { result.cells.push([]); for(var c = 0; c < result.nrCols; c++) { result.cells[r].push(reader.readNumber(nrCellBits)); }       }

if(context == BOARD_DATA_CONTEXT_LEVEL) {

let nrNameChars = reader.readNumber; for(var c = 0; c < nrNameChars; c++) { result.name += String.fromCharCode(reader.readNumber); }

} else {

result.nrRowHints = reader.readNumber; result.nrColHints = reader.readNumber; for(var r = 0; r < result.nrRowHints; r++) { result.rowHints.push(reader.readNumber(1)); }           for(var c = 0; c < result.nrColHints; c++) { result.colHints.push(reader.readNumber(1)); }

result.levelNumber = reader.readNumber;

let nrCodeChars = reader.readNumber; for(var c = 0; c < nrCodeChars; c++) { result.levelCode += String.fromCharCode(reader.readNumber); }

}

return result;

} catch { return null; } }

/** * Draws the board game screen. */ function drawBoard { const boardX2 = sectionCoords.board.x + sectionCoords.board.width; const boardY2 = sectionCoords.board.y + sectionCoords.board.height; // Board, one cell at a time. canvas.save; canvas.beginPath; canvas.rect(sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height); canvas.clip;

drawSprite(gameSide == 0 ? SPRITES.HOCOTATE : SPRITES.KOPPAI, sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height); canvas.fillStyle = COLOR_BOARD_BG; canvas.fillRect(sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height); for(var r = 0; r < board.nrRows; r++) { for(var c = 0; c < board.nrCols; c++) { let startX = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;           let startY = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;            let sprite = null; if(board.cells[r][c] == CELL_BLANK) { if(board.autoMarkedRows[r] || board.autoMarkedCols[c]) { sprite = SPRITES.CELL_MARKED; } else { sprite = SPRITES.CELL_BLANK; }           } else if(board.cells[r][c] == CELL_FILLED) { sprite = SPRITES.CELL_FILLED; } else { sprite = SPRITES.CELL_MARKED; }           drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE); }   }

// Board fives grid. canvas.lineWidth = 3; for(var r = 5; r < board.nrRows; r += 5) { let y = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;       let minX = sectionCoords.board.x - cam.coords.x;        let maxX = sectionCoords.board.x + board.nrCols * CELL_SIZE + board.nrCols * CELL_PADDING - cam.coords.x;        canvas.beginPath; canvas.moveTo(minX, y); canvas.lineTo(maxX, y); canvas.stroke; }   for(var c = 5; c < board.nrCols; c += 5) { let x = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;       let minY = sectionCoords.board.y - cam.coords.y        let maxY = sectionCoords.board.y + board.nrRows * CELL_SIZE + board.nrRows * CELL_PADDING - cam.coords.y;        canvas.beginPath; canvas.moveTo(x, minY); canvas.lineTo(x, maxY); canvas.stroke; }   canvas.font = 'bold 12px sans'; canvas.fillStyle = '#888'; canvas.textAlign = 'right'; canvas.textBaseline = 'bottom'; for(var r = 5; r < board.nrRows; r += 5) { let y = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;       let maxX = sectionCoords.board.x + board.nrCols * CELL_SIZE + board.nrCols * CELL_PADDING - cam.coords.x;        maxX = Math.min(maxX, boardX2); canvas.fillText(r, maxX - 1, y); }   canvas.textAlign = 'right'; canvas.textBaseline = 'bottom'; for(var c = 5; c < board.nrCols; c += 5) { let x = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;       let maxY = sectionCoords.board.y + board.nrRows * CELL_SIZE + board.nrRows * CELL_PADDING - cam.coords.y;        maxY = Math.min(maxY, boardY2); canvas.fillText(c, x - 1, maxY); }   // Board border gradients. if(cam.coords.x > 0) { canvas.fillStyle = borderGradients.left; canvas.fillRect(sectionCoords.board.x, sectionCoords.board.y - cam.coords.y, BORDER_GRADIENT_LENGTH, board.nrRows * (CELL_SIZE + CELL_PADDING)); }   if(cam.coords.x < -(sectionCoords.board.width - board.nrCols * (CELL_SIZE + CELL_PADDING))) { canvas.fillStyle = borderGradients.right; canvas.fillRect(boardX2 - BORDER_GRADIENT_LENGTH, sectionCoords.board.y - cam.coords.y, boardX2, board.nrRows * (CELL_SIZE + CELL_PADDING)); }   if(cam.coords.y > 0) { canvas.fillStyle = borderGradients.up; canvas.fillRect(sectionCoords.board.x - cam.coords.x, sectionCoords.board.y, board.nrCols * (CELL_SIZE + CELL_PADDING), BORDER_GRADIENT_LENGTH); }   if(cam.coords.y < -(sectionCoords.board.height - board.nrRows * (CELL_SIZE + CELL_PADDING))) { canvas.fillStyle = borderGradients.down; canvas.fillRect(sectionCoords.board.x - cam.coords.x, boardY2 - BORDER_GRADIENT_LENGTH, board.nrCols * (CELL_SIZE + CELL_PADDING), boardY2); }   canvas.restore;

// Miniature. canvas.fillStyle = COLOR_BOARD_BG; canvas.fillRect(sectionCoords.miniature.x, sectionCoords.miniature.y, sectionCoords.miniature.width, sectionCoords.miniature.height); drawMiniature(board.nrRows, board.nrCols, board.cells, {x: 0, y: 0, width: 1, height: 1}, sectionCoords.miniature, COLOR_TEXT_GENERIC[gameSide]);

if(state == STATE_PLAYING) { // Hints. canvas.font = 'bold 24px sans';

// Row hints. let cellWidth = CELL_SIZE; let cellHeight = CELL_SIZE;

canvas.save; canvas.beginPath; canvas.rect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height); canvas.clip;

canvas.fillStyle = COLOR_HINTS_BG; canvas.fillRect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);

for(r = 0; r < board.nrRows; r++) { for(h = 0; h < board.rowHints[r].length; h++) { let xIndexOffset = board.rowHints[r].length - h - 1; let startX = sectionCoords.rowBanner.x + sectionCoords.rowBanner.width - cellWidth; startX -= xIndexOffset * cellWidth + xIndexOffset * CELL_PADDING; let startY = sectionCoords.rowBanner.y + r * cellWidth + r * CELL_PADDING - cam.coords.y;               let sprite = null; if(board.rowHints[r][h].state == CELL_BLANK) { sprite = SPRITES.CELL_BLANK; } else { sprite = SPRITES.CELL_FILLED; }               drawSprite(sprite, startX, startY, cellWidth, cellHeight); if(board.defiedRows[r]) { canvas.lineWidth = 5; canvas.strokeStyle = COLOR_HINT_DEFIED_TINT; canvas.strokeRect(startX, startY, cellWidth, cellHeight); canvas.fillStyle = COLOR_HINT_DEFIED; } else { canvas.fillStyle = COLOR_HINT[gameSide]; }               canvas.fillText(board.rowHints[r][h].nr, startX + cellWidth / 2, startY + cellHeight / 2 + 2); }       }

canvas.restore;

// Column hints. cellWidth = CELL_SIZE; cellHeight = CELL_SIZE; canvas.save; canvas.beginPath; canvas.rect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height); canvas.clip; canvas.fillStyle = COLOR_HINTS_BG; canvas.fillRect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);

for(c = 0; c < board.nrCols; c++) { for(h = 0; h < board.colHints[c].length; h++) { let yIndexOffset = board.colHints[c].length - h - 1; let startX = sectionCoords.colBanner.x + c * cellWidth + c * CELL_PADDING - cam.coords.x;               let startY = sectionCoords.colBanner.y + sectionCoords.colBanner.height - cellHeight; startY -= yIndexOffset * cellHeight + yIndexOffset * CELL_PADDING; let sprite = null; if(board.colHints[c][h].state == CELL_BLANK) { sprite = SPRITES.CELL_BLANK; } else { sprite = SPRITES.CELL_FILLED; }               drawSprite(sprite, startX, startY, cellWidth, cellHeight); if(board.defiedCols[c]) { canvas.lineWidth = 5; canvas.strokeStyle = COLOR_HINT_DEFIED_TINT; canvas.strokeRect(startX, startY, cellWidth, cellHeight); canvas.fillStyle = COLOR_HINT_DEFIED; } else { canvas.fillStyle = COLOR_HINT[gameSide]; }               canvas.fillText(board.colHints[c][h].nr, startX + cellWidth / 2, startY + cellHeight / 2 + 2); }       }

canvas.restore;

} else if (state == STATE_MAKING) {

// Maker grid tools. canvas.font = 'bold ' + (CELL_SIZE - 2) + 'px sans'; canvas.textAlign = 'center'; canvas.textBaseline = 'middle';

// Row hints. canvas.save; canvas.beginPath; canvas.rect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height); canvas.clip;

canvas.fillStyle = COLOR_HINTS_BG; canvas.fillRect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);

for(r = 0; r < board.nrRows; r++) { for(b = 0; b < 3; b++) { let xIndexOffset = 3 - b - 1; let startX = sectionCoords.rowBanner.x + sectionCoords.rowBanner.width - CELL_SIZE; startX -= xIndexOffset * CELL_SIZE + xIndexOffset * CELL_PADDING; let startY = sectionCoords.rowBanner.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;               let sprite = SPRITES.CELL_BLANK; drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE); let text = ''; switch(b) { case 0: canvas.fillStyle = COLOR_MAKER_RED; text = 'x'                   break; case 1: canvas.fillStyle = COLOR_MAKER_GREEN; text = '↑' break; case 2: canvas.fillStyle = COLOR_MAKER_GREEN; text = '↓'; break; }               canvas.fillText(text, startX + CELL_SIZE / 2, startY + CELL_SIZE / 2 + 2); }       }

canvas.restore;

// Column hints. canvas.save; canvas.beginPath; canvas.rect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height); canvas.clip; canvas.fillStyle = COLOR_HINTS_BG; canvas.fillRect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);

for(c = 0; c < board.nrCols; c++) { for(b = 0; b < 3; b++) { let yIndexOffset = 3 - b - 1; let startX = sectionCoords.colBanner.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;               let startY = sectionCoords.colBanner.y + sectionCoords.colBanner.height - CELL_SIZE; startY -= yIndexOffset * CELL_SIZE + yIndexOffset * CELL_PADDING; let sprite = SPRITES.CELL_BLANK; drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE); let text = ''; switch(b) { case 0: canvas.fillStyle = COLOR_MAKER_RED; text = 'x'                   break; case 1: canvas.fillStyle = COLOR_MAKER_GREEN; text = '←' break; case 2: canvas.fillStyle = COLOR_MAKER_GREEN; text = '→'; break; }               canvas.fillText(text, startX + CELL_SIZE / 2, startY + CELL_SIZE / 2 + 2); }       }

canvas.restore;

}

// Footer. canvas.fillStyle = COLOR_FOOTER_BG; canvas.fillRect(sectionCoords.footer.x, sectionCoords.footer.y, sectionCoords.footer.width, sectionCoords.footer.height); let logoXY = calculateChildCoords(guiItemCoords.playingTitle, sectionCoords.footer); let logoWH = calculateChildDimensions(guiItemCoords.playingTitle, logoXY, sectionCoords.footer); let levelNumberStr = board.levelNumber == 0 ? 'Custom Level' : ('Level ' + board.levelNumber); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.pause, sectionCoords.footer, '...', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); drawSprite(SPRITES.LOGO, logoXY.x, logoXY.y, logoWH.width, logoWH.height); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.playingLevel, sectionCoords.footer, levelNumberStr, COLOR_TEXT_GENERIC[gameSide], 'bold 20px sans'); }

/** * Draws a generic GUI item in specific coordinates, like a button, some text, an image, etc. * @param {number} type Type of GUI item. * @param {object} coords Coordinates of the object, relative to the parent. * @param {object} parentCoords Coordinates of the parent section. * @param {any} fg Contents of the foreground, if any. * @param {any} fgStyle Foreground style, if any. * @param {any} fgFont Foreground content font, if any. * @param {any} bgStyle Background style. If undefined, the background won't be drawn. */ function drawGuiItem(type, coords, parentCoords, fg, fgStyle, fgFont, bgStyle) { if(parentCoords == undefined) { parentCoords = { x: 0, y: 0, width: CANVAS.WIDTH, height: CANVAS.HEIGHT };   }

let finalXY = calculateChildCoords(coords, parentCoords); let finalWH = calculateChildDimensions(coords, finalXY, parentCoords); let finalCoords = { x: finalXY.x,       y: finalXY.y,        width: finalWH.width, height: finalWH.height };

// Button shadow. if(type == GUI_ITEM_BUTTON && bgStyle != undefined) { canvas.fillStyle = COLOR_BUTTON_SHADOW[gameSide]; canvas.fillRect(finalCoords.x + 4, finalCoords.y + 4, finalCoords.width, finalCoords.height); }

// Background. if(bgStyle != undefined) { canvas.fillStyle = bgStyle; canvas.fillRect(finalCoords.x, finalCoords.y, finalCoords.width, finalCoords.height); }

// Foreground. if(fg != undefined) { if(fgStyle != undefined) { canvas.fillStyle = fgStyle; }       if(fgFont != undefined) { canvas.font = fgFont; }       canvas.fillText(fg, finalCoords.x + finalCoords.width / 2, finalCoords.y + finalCoords.height / 2 + 2); } }

/** * Draws the main menu. */ function drawMainMenu { // Header. canvas.fillStyle = COLOR_FOOTER_BG; canvas.fillRect(sectionCoords.mainMenuHeader.x + SECTION_PADDING, sectionCoords.mainMenuHeader.y + SECTION_PADDING, sectionCoords.mainMenuHeader.width - SECTION_PADDING * 2, sectionCoords.mainMenuHeader.height - SECTION_PADDING * 2);

let bookmarkData = getBookmarkData; let logoXY = calculateChildCoords(guiItemCoords.mainMenuTitle, sectionCoords.mainMenuHeader); let logoWH = calculateChildDimensions(guiItemCoords.mainMenuTitle, logoXY, sectionCoords.mainMenuHeader); drawSprite(SPRITES.LOGO, logoXY.x, logoXY.y, logoWH.width, logoWH.height); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader, 'Continue', bookmarkData != null ? COLOR_BUTTON_TEXT[gameSide] : COLOR_BUTTON_TEXT_DISABLED, '20px sans', COLOR_BUTTON_BG); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuInfo, sectionCoords.mainMenuHeader, 'Info', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuMake, sectionCoords.mainMenuHeader, 'Make custom', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuCustom, sectionCoords.mainMenuHeader, 'Play custom', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG); if(firstSideCleared) drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuSide, sectionCoords.mainMenuHeader, 'Swap sides', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);

// Level selection. drawSprite(gameSide == 0 ? SPRITES.HOCOTATE : SPRITES.KOPPAI, sectionCoords.levelSelect.x + SECTION_PADDING, sectionCoords.levelSelect.y + SECTION_PADDING, sectionCoords.levelSelect.width - SECTION_PADDING * 2, sectionCoords.levelSelect.height - SECTION_PADDING * 2); canvas.fillStyle = COLOR_BOARD_BG; canvas.fillRect(sectionCoords.levelSelect.x + SECTION_PADDING, sectionCoords.levelSelect.y + SECTION_PADDING, sectionCoords.levelSelect.width - SECTION_PADDING * 2, sectionCoords.levelSelect.height - SECTION_PADDING * 2);

let buttonPadding = SECTION_PADDING * 2; let buttonWidth = (sectionCoords.levelSelect.width - buttonPadding * 5) / 4; let levelHeight = (sectionCoords.levelSelect.height - buttonPadding * 5) / 4; let buttonHeight = levelHeight * 0.75; for(var c = 0; c < 4; c++) { for(var r = 0; r < 4; r++) { let levelNumber = r * 4 + c + 1; levelNumber += 16 * gameSide; let cleared = progression[levelNumber - 1]; drawGuiItem(               GUI_ITEM_BUTTON,                {                    x: buttonPadding + c * (buttonWidth + buttonPadding),                    y: buttonPadding + r * (levelHeight + buttonPadding),                    width: buttonWidth,                    height: buttonHeight                },                sectionCoords.levelSelect,                '',                COLOR_BUTTON_TEXT[gameSide],                'bold 48px sans',                COLOR_BUTTON_BG            ); if(cleared) { drawMiniature(                   levelsData[levelNumber - 1].nrRows,                    levelsData[levelNumber - 1].nrCols,                    levelsData[levelNumber - 1].cells,                    {                        x: buttonPadding + c * (buttonWidth + buttonPadding) + 8,                        y: buttonPadding + r * (levelHeight + buttonPadding) + 8,                        width: buttonWidth - 16,                        height: buttonHeight - 16                    },                    sectionCoords.levelSelect,                    COLOR_BUTTON_TEXT[gameSide]                ); canvas.textAlign = 'left'; canvas.textBaseline = 'top'; drawGuiItem(                   GUI_ITEM_TEXT,                    {                        x: buttonPadding + c * (buttonWidth + buttonPadding),                        y: buttonPadding + r * (levelHeight + buttonPadding),                        width: 10,                        height: 5                    },                    sectionCoords.levelSelect,                    levelsData[levelNumber - 1].nrRows + 'x' + levelsData[levelNumber - 1].nrCols,                    COLOR_BUTTON_SHADOW[gameSide],                    '9px sans'                ); }           let levelName = levelNumber + ': ' + (cleared ? levelsData[levelNumber - 1].name : '???'); canvas.textAlign = 'center'; canvas.textBaseline = 'middle'; drawGuiItem(               GUI_ITEM_TEXT,                {                    x: buttonPadding + c * (buttonWidth + buttonPadding),                    y: buttonPadding + r * (levelHeight + buttonPadding) + buttonHeight + 2,                    width: buttonWidth,                    height: (levelHeight - buttonHeight)                },                sectionCoords.levelSelect,                levelName,                COLOR_TEXT_GENERIC[gameSide],                'bold 14px sans'            ); }   } }

/** * Draws a miniature of the puzzle on-screen. * @param {number} nrRows Number of rows to draw. * @param {number} nrCols Number of columns to draw. * @param {array} cells Cells to draw. * @param {object} coords X, Y, width, and height of the miniature. * @param {object} parentCoords Object with the parent coordinates. * @param {color} color Color of each pixel. */ function drawMiniature(nrRows, nrCols, cells, coords, parentCoords, color) { if(parentCoords == undefined) { parentCoords = { x: 0, y: 0, width: CANVAS.WIDTH, height: CANVAS.HEIGHT };   }

let finalXY = calculateChildCoords(coords, parentCoords); let finalWH = calculateChildDimensions(coords, finalXY, parentCoords); let finalCoords = { x: finalXY.x,       y: finalXY.y,        width: finalWH.width, height: finalWH.height };

let miniatureCellNormalWidth = finalCoords.width / nrCols; let miniatureCellNormalHeight = finalCoords.height / nrRows; let miniatureCellSize = Math.min(miniatureCellNormalWidth, miniatureCellNormalHeight); let miniatureFullWidth = miniatureCellSize * nrCols; let miniatureFullHeight = miniatureCellSize * nrRows; let miniatureStartX = finalCoords.x + (finalCoords.width - miniatureFullWidth) / 2; let miniatureStartY = finalCoords.y + (finalCoords.height - miniatureFullHeight) / 2; for(var r = 0; r < nrRows; r++) { for(var c = 0; c < nrCols; c++) { if(cells[r][c] != CELL_FILLED) { continue; }           let startX = Math.round(miniatureStartX + c * miniatureCellSize); let startY = Math.round(miniatureStartY + r * miniatureCellSize);

canvas.fillStyle = color; canvas.fillRect(               startX,                startY,                Math.ceil(miniatureCellSize),                Math.ceil(miniatureCellSize)            ); }   } }

/** * Draws the panel. */ function drawPanel { // Background. canvas.fillStyle = 'rgba(0, 0, 0, 0.5)'; canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT);

canvas.fillStyle = 'rgba(0, 0, 0, 0.5)'; canvas.fillRect(sectionCoords.panel.x + 8, sectionCoords.panel.y + 8, sectionCoords.panel.width, sectionCoords.panel.height); canvas.fillStyle = COLOR_FOOTER_BG; canvas.fillRect(sectionCoords.panel.x, sectionCoords.panel.y, sectionCoords.panel.width, sectionCoords.panel.height);

switch(panelState) { case PANEL_STATE_PAUSE: // Menu buttons. drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.continue, sectionCoords.panel, 'Continue', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.restart, sectionCoords.panel, 'Restart', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); if(state == STATE_MAKING) { drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.finish, sectionCoords.panel, 'Finish', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); }       drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.quit, sectionCoords.panel, 'Quit', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break; case PANEL_STATE_CONGRATS: // Congrats screen. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsHeader, sectionCoords.panel, (board.levelNumber == 0 ? 'CUSTOM LEVEL' : ('LEVEL ' + board.levelNumber)) + ' CLEAR!', COLOR_BUTTON_TEXT[gameSide], '36px sans'); drawMiniature(board.nrRows, board.nrCols, board.cells, guiItemCoords.congratsMiniature, sectionCoords.panel, COLOR_BUTTON_TEXT[gameSide]); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsLevelName, sectionCoords.panel, board.name, COLOR_BUTTON_TEXT[gameSide], 'italic 24px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_MAKING_NAME: // Asking the new puzzle's name. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.newPuzzleNamePrompt, sectionCoords.panel, 'What\'s this puzzle\'s name?', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_MAKING_CODE: // Showing the new puzzle's code. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.newPuzzleDoneText, sectionCoords.panel, 'Done! Copy this code and share it around!', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_PLAYING_CODE: // Asking a puzzle's code to play on. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.customPuzzleText, sectionCoords.panel, 'Please paste the puzzle\'s code here.', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_LOAD_ERROR: // Warning the player there was an error while loading. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError1, sectionCoords.panel, 'Error loading level!', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError2, sectionCoords.panel, 'Please make sure everything', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError3, sectionCoords.panel, 'is correct and try again.', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_SAVE_ERROR: // Warning the player there was an error while saving. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError1, sectionCoords.panel, 'Error saving level!', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError2, sectionCoords.panel, 'You must have at least', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError3, sectionCoords.panel, 'one filled cell!', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_BOOKMARK_WARNING: // Warning the player they have a bookmark when starting a level. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning1, sectionCoords.panel, 'If you start a new puzzle, you\'ll', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning2, sectionCoords.panel, 'lose your old bookmark progress! In', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning3, sectionCoords.panel, 'the main menu, press "Continue" to', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning4, sectionCoords.panel, 'resume your bookmark, or press the', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning5, sectionCoords.panel, 'puzzle button again to start anyway.', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

case PANEL_STATE_INFO: // Game info. drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info1, sectionCoords.panel, '**TODO: INFO GOES HERE**', COLOR_BUTTON_BG, 'bold 20px sans'); drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info2, sectionCoords.panel, '( Note added by Pikipedia:', COLOR_BUTTON_BG, '20px sans');       drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info3, sectionCoords.panel, 'Information on Pikcross, including', COLOR_BUTTON_BG, '20px sans');        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info4, sectionCoords.panel, 'how to play, can be found here:', COLOR_BUTTON_BG, '20px sans');        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info5, sectionCoords.panel, 'https://www.pikminwiki.com/Pikcross )', COLOR_BUTTON_BG, '20px sans'); drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG); break;

} }

/** * Draws a sprite. * @param object data Data about the sprite in the spritesheet, in the form {x, y, width, height}. * @param number x Top-left X coordinate to draw on. * @param number y Top-left Y coordinate to draw on. * @param number w Width to draw at. * @param number h Height to draw at. */ function drawSprite(data, x, y, w, h) { canvas.drawImage(sprites, data.x, data.y, data.width, data.height, x, y, w, h); }

/** * Encodes a board and some more data into a base64-encoded string. * @param {object} data Board data to encode. * @param {number} context BOARD_DATA_CONTEXT_LEVEL to encode level data. BOARD_DATA_CONTEXT_BOOKMARK to encode bookmark data. * @returns Encoded board text. Returns null on error. */ function encodeBoard(data, context) { try { let writer = new StrBitWriter; let nrCellBits = context == BOARD_DATA_CONTEXT_LEVEL ? 1 : 2;       writer.writeNumber(data.nrRows); writer.writeNumber(data.nrCols); for(var r = 0; r < data.nrRows; r++) { for(var c = 0; c < data.nrCols; c++) { writer.writeNumber(data.cells[r][c], nrCellBits); }       }

if(context == BOARD_DATA_CONTEXT_LEVEL) {

writer.writeNumber(data.name.length); for(var c = 0; c < data.name.length; c++) { writer.writeNumber(data.name.charCodeAt(c)); }

} else {

writer.writeNumber(data.nrRowHints); writer.writeNumber(data.nrColHints); for(var r = 0; r < data.nrRowHints; r++) { writer.writeNumber(data.rowHints[r], 1); }           for(var c = 0; c < data.nrColHints; c++) { writer.writeNumber(data.colHints[c], 1); }

writer.writeNumber(data.levelNumber);

writer.writeNumber(data.levelCode.length); for(var c = 0; c < data.levelCode.length; c++) { writer.writeNumber(data.levelCode.charCodeAt(c)); }

}

return btoa(writer.getStr);

} catch { return null; } }

/** * Given a list of cells, finds the first cell that is marked. It only checks inside of a given range. * @param {array} cells List of cells to check in. * @param {number} startIdx Index to start the check in, inclusive. * @param {number} length Number of cells to check. * @returns The index of the first marked cell, or -1 if none are marked. */ function findMarkedCellInList(cells, startIdx, length) { for(var c = startIdx; c < startIdx + length; c++) { if(c >= cells.length) return c;       if(cells[c] == CELL_MARKED) return c;    } return -1; }

/** * Returns whether the first side (i.e. the first 16 puzzles) is cleared or not. * @returns True if cleared, false otherwise. */ function firstSideCleared { for(var l = 0; l < 16; l++) { if(!progression[l]) return false; }   return true; }

/** * Returns which cell of the board is under the given screen coordinates. * @param {object} coords Object with the coordinates. * @returns The row and column, in an array. */ function getCellInCoords(coords) { let x = coords.x - sectionCoords.board.x + cam.coords.x;   let y = coords.y - sectionCoords.board.y + cam.coords.y;    let col = Math.floor(x / (CELL_SIZE + CELL_PADDING)); let row = Math.floor(y / (CELL_SIZE + CELL_PADDING)); return [row, col]; }

/** * Returns which column hint/maker button is under the given screen coordinates. * @param {object} coords Object with the coordinates. * @returns The column and button index numbers, in an array. Returns null if none. */ function getColumnBannerButtonInCoords(coords) { let hintsClickCoords = { x: coords.x - sectionCoords.colBanner.x + cam.coords.x,       y: coords.y - sectionCoords.colBanner.y    }; let col = Math.floor(hintsClickCoords.x / (CELL_SIZE + CELL_PADDING)); if(col < 0 || col >= board.nrCols) return null;

let button = Math.floor(hintsClickCoords.y / (CELL_SIZE + CELL_PADDING)); let maxButtons = Math.floor(sectionCoords.colBanner.height / (CELL_SIZE + CELL_PADDING)); let nrButtonsInCol = 0; if(state == STATE_PLAYING) { nrButtonsInCol = board.colHints[col].length; } else if(state == STATE_MAKING) { nrButtonsInCol = 3; }   button -= maxButtons - nrButtonsInCol;

if(button < 0) return null; if(state == STATE_PLAYING) { if(button >= board.colHints[col].length) return null; } else if(state == STATE_MAKING) { if(button >= 3) return null; }

return [col, button]; }

/** * Returns an array with the combos the player has for the given line. * @param {number} row Row number, if we're checking a row. null otherwise. * @param {number} col Column number, if we're checking a column. null otherwise. * @returns An array where each object is a combo. */ function getPlayerLineCombos(row, col) { let playerCombos = []; let curCombo = 0; let curComboStart = 0;

if(row != null) { for(var c = 0; c < board.nrCols; c++) { if(board.cells[row][c] == CELL_FILLED) { if(curCombo == 0) { curComboStart = c;               } curCombo++; } else { if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo}); curCombo = 0; }       }        } else { for(var r = 0; r < board.nrRows; r++) { if(board.cells[r][col] == CELL_FILLED) { if(curCombo == 0) { curComboStart = r;               } curCombo++; } else { if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo}); curCombo = 0; }       }     }

if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});

return playerCombos; }

/** * Returns which row hint/maker button is under the given screen coordinates. * @param {object} coords Object with the coordinates. * @returns The row and button index numbers, in an array. Returns null if none. */ function getRowBannerButtonInCoords(coords) { let hintsClickCoords = { x: coords.x - sectionCoords.rowBanner.x,       y: coords.y - sectionCoords.rowBanner.y + cam.coords.y    }; let row = Math.floor(hintsClickCoords.y / (CELL_SIZE + CELL_PADDING));

if(row < 0 || row >= board.nrRows) return null;

let button = Math.floor(hintsClickCoords.x / (CELL_SIZE + CELL_PADDING)); let maxButtons = Math.floor(sectionCoords.rowBanner.width / (CELL_SIZE + CELL_PADDING)); let nrButtonsInRow = 0; if(state == STATE_PLAYING) { nrButtonsInRow = board.rowHints[row].length; } else if(state == STATE_MAKING) { nrButtonsInRow = 3; }   button -= maxButtons - nrButtonsInRow;

if(button < 0) return null; if(state == STATE_PLAYING) { if(button >= board.rowHints[row].length) return null; } else if(state == STATE_MAKING) { if(button >= 3) return null; }

return [row, button]; }

/** * Hides the input box HTML element in the middle of the canvas. */ function hideInputBox { inputEl.style.display = 'none'; }

/** * Checks if a given point is inside a set of coordinates. * @param point Point to check, in the format {x, y}. * @param coords Coordinates to check, in the format {x, y, width, height}. * @param parentCoords If not undefined, then the previous coordinates are relative to these ones. */ function isPointInCoords(point, coords, parentCoords) { let finalCoords = { x: coords.x,       y: coords.y,        width: coords.width, height: coords.height };

if(parentCoords != undefined) { let finalXY = calculateChildCoords(coords, parentCoords); let finalWH = calculateChildDimensions(coords, finalXY, parentCoords); finalCoords = { x: finalXY.x,           y: finalXY.y,            width: finalWH.width, height: finalWH.height };   }

return (       point.x >= finalCoords.x &&        point.y >= finalCoords.y &&        point.x <= finalCoords.x + finalCoords.width &&        point.y <= finalCoords.y + finalCoords.height    ); }

/** * Loads the player's bookmark, if any, and returns its info. * @returns null if no data exists, or an object with the data otherwise. */ function getBookmarkData { var bookmarkString = localStorage.getItem('pikcrossBookmark'); if(bookmarkString == null || bookmarkString == '') return null;

return decodeBoard(bookmarkString, BOARD_DATA_CONTEXT_BOOKMARK); }

/** * Loads a level and initializes the board game screen. * @param {number} levelNumber Level number, or 0 for custom. * @param {string} levelCode Level code. * @param {number} stateContext What game state is this being loaded for? * @returns True on success, false on failure. */ function loadLevel(levelNumber, levelCode, stateContext) { if(stateContext == STATE_PLAYING && levelCode.length < 3) { return false; }

let nrRowBannerCells = 0; let nrColBannerCells = 0;

// Cleanup. cam.coords.x = 0; cam.coords.y = 0;

board.name = 'Puzzle'; board.nrRows = 0; board.nrCols = 0; board.cells = []; board.solution = []; board.rowHints = []; board.colHints = []; board.autoMarkedRows = []; board.autoMarkedCols = []; board.defiedRows = []; board.defiedCols = [];

board.levelNumber = levelNumber; board.levelCode = levelCode;

if(stateContext == STATE_PLAYING) {

// Load board. let levelData = decodeBoard(levelCode, BOARD_DATA_CONTEXT_LEVEL); // Sanity check. if(levelData == null) return false; if(levelData.nrRows < MIN_COLS_OR_ROWS) return false; if(levelData.nrRows > MAX_COLS_OR_ROWS) return false; if(levelData.nrCols < MIN_COLS_OR_ROWS) return false; if(levelData.nrCols > MAX_COLS_OR_ROWS) return false; let hasFilledCells = false; for(var r = 0; r < levelData.nrRows; r++) { for(var c = 0; c < levelData.nrCols; c++) { if(levelData.cells[r][c] == CELL_FILLED) { hasFilledCells = true; break; }           }            if(hasFilledCells) break; }       if(!hasFilledCells) return false; if(levelData.name.length == 0) levelData.name = 'Puzzle'; board.nrRows = levelData.nrRows; board.nrCols = levelData.nrCols; board.solution = levelData.cells; board.name = levelData.name;

// Hints. for(var r = 0; r < board.nrRows; r++) { board.rowHints.push([]); let hintNr = 0; for(var c = 0; c < board.nrCols; c++) { if(board.solution[r][c] == CELL_FILLED) { hintNr++; } else { if(hintNr > 0) { board.rowHints[r].push({ state: CELL_BLANK, nr: hintNr }); }                   hintNr = 0; }           }            if(hintNr > 0) { board.rowHints[r].push({ state: CELL_BLANK, nr: hintNr }); }           nrRowBannerCells = Math.max(nrRowBannerCells, board.rowHints[r].length); }       for(var c = 0; c < board.nrCols; c++) { board.colHints.push([]); let hintNr = 0; for(var r = 0; r < board.nrRows; r++) { if(board.solution[r][c] == CELL_FILLED) { hintNr++; } else { if(hintNr > 0) { board.colHints[c].push({ state: CELL_BLANK, nr: hintNr }); }                   hintNr = 0; }           }            if(hintNr > 0) { board.colHints[c].push({ state: CELL_BLANK, nr: hintNr }); }           nrColBannerCells = Math.max(nrColBannerCells, board.colHints[c].length); }

} else if(stateContext == STATE_MAKING) {

// Create an empty board. board.nrRows = 10; board.nrCols = 10; for(var r = 0; r < board.nrRows; r++) { board.cells.push([]); for(var c = 0; c < board.nrCols; c++) { board.cells[r].push(CELL_BLANK); }       }

// Maker grid tools. nrRowBannerCells = 3; nrColBannerCells = 3;

}   // Current cells state. board.cells = []; for(var r = 0; r < board.nrRows; r++) { board.cells.push([]); for(var c = 0; c < board.nrCols; c++) { board.cells[r].push(CELL_BLANK); }   }

for(var r = 0; r < board.nrRows; r++) { board.autoMarkedRows.push(false); board.defiedRows.push(false); }   for(var c = 0; c < board.nrCols; c++) { board.autoMarkedCols.push(false); board.defiedCols.push(false); }

// Coordinates. sectionCoords.miniature.x = SECTION_PADDING; sectionCoords.miniature.y = SECTION_PADDING; sectionCoords.miniature.width = nrRowBannerCells * (CELL_SIZE + CELL_PADDING); sectionCoords.miniature.height = nrColBannerCells * (CELL_SIZE + CELL_PADDING); sectionCoords.footer.x = sectionCoords.miniature.x;   sectionCoords.footer.y = CANVAS.HEIGHT - SECTION_PADDING - 64; sectionCoords.footer.width = CANVAS.WIDTH - SECTION_PADDING * 2; sectionCoords.footer.height = 64;

sectionCoords.rowBanner.x = sectionCoords.miniature.x;   sectionCoords.rowBanner.y = sectionCoords.miniature.y + sectionCoords.miniature.height + SECTION_PADDING; sectionCoords.rowBanner.width = sectionCoords.miniature.width; sectionCoords.rowBanner.height = sectionCoords.footer.y - SECTION_PADDING - sectionCoords.rowBanner.y;

sectionCoords.colBanner.x = sectionCoords.miniature.x + sectionCoords.miniature.width + SECTION_PADDING; sectionCoords.colBanner.y = sectionCoords.miniature.y;   sectionCoords.colBanner.width = CANVAS.WIDTH - SECTION_PADDING - sectionCoords.colBanner.x;    sectionCoords.colBanner.height = sectionCoords.miniature.height;

sectionCoords.board.x = sectionCoords.colBanner.x;   sectionCoords.board.y = sectionCoords.rowBanner.y;    sectionCoords.board.width = sectionCoords.colBanner.width; sectionCoords.board.height = sectionCoords.rowBanner.height;

// Border gradients. const boardX2 = sectionCoords.board.x + sectionCoords.board.width; const boardY2 = sectionCoords.board.y + sectionCoords.board.height; borderGradients.left = canvas.createLinearGradient(sectionCoords.board.x, 0, sectionCoords.board.x + BORDER_GRADIENT_LENGTH, 0), borderGradients.left.addColorStop(1, 'rgba(0, 128, 0, 0.0)'); borderGradients.left.addColorStop(0, 'rgba(0, 128, 0, 0.5)'); borderGradients.right = canvas.createLinearGradient(boardX2, 0, boardX2 - BORDER_GRADIENT_LENGTH, 0), borderGradients.right.addColorStop(1, 'rgba(0, 128, 0, 0.0)'); borderGradients.right.addColorStop(0, 'rgba(0, 128, 0, 0.5)'); borderGradients.up = canvas.createLinearGradient(0, sectionCoords.board.y, 0, sectionCoords.board.y + BORDER_GRADIENT_LENGTH), borderGradients.up.addColorStop(1, 'rgba(0, 128, 0, 0.0)'); borderGradients.up.addColorStop(0, 'rgba(0, 128, 0, 0.5)'); borderGradients.down = canvas.createLinearGradient(0, boardY2, 0, boardY2 - BORDER_GRADIENT_LENGTH), borderGradients.down.addColorStop(1, 'rgba(0, 128, 0, 0.0)'); borderGradients.down.addColorStop(0, 'rgba(0, 128, 0, 0.5)'); if(stateContext == STATE_PLAYING) { for(var r = 0; r < board.nrRows; r++) { updateRow(r); }       for(var c = 0; c < board.nrCols; c++) { updateColumn(c); }   }

return true; }

/** * Loads the player's global progression. */ function loadProgression { let progressionStr = localStorage.getItem('pikcrossProgression'); if(progressionStr == null || progressionStr.length == 0) { return; }   let reader = new StrBitReader(progressionStr); for(var l = 0; l < LEVELS.length; l++) { progression[l] = (reader.readNumber(1) == 1); } }

/** * Handler for when the player does an input down on the canvas. * This happens regardless of it being a mouse button down press, or a mobile touch start. * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons. * @param {number} mobile True if it was a mobile touch, false otherwise. */ function onCanvasInputDown(button, mobile) { for(var l = 0; l < loaded.length; l++) { if(!loaded[l]) return; }   if(transitionAnimTime > 0) return;

if(inPanel) { onCanvasInputDownInPanel(button, mobile); return; }

switch(state) { case STATE_MAIN_MENU: onCanvasInputDownInMainMenu(button, mobile); break; case STATE_PLAYING: case STATE_MAKING: let changesMade = onCanvasInputDownInGameplay(button, mobile); if(changesMade) { input.dragging = true; input.dragStart.x = input.screenCoords.x;           input.dragStart.y = input.screenCoords.y;        } break; } }

/** * Handler for when the player does an input down on the canvas, in the board game screen. * This happens regardless of it being a mouse button down press, or a mobile touch start. * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons. * @param {number} mobile True if it was a mobile touch, false otherwise. * @returns True if something happened, false otherwise. */ function onCanvasInputDownInGameplay(button, mobile) { let changesMade = false;

// Figure out where the player clicked. if(isPointInCoords(input.screenCoords, sectionCoords.board)) { // Clicked on the board.

let idxs = getCellInCoords(input.screenCoords); if(mobile) { if(toggleCellFillAndMark(idxs[0], idxs[1])) { changesMade = true; }       } else if(button == 0) { if(toggleCellFill(idxs[0], idxs[1])) { changesMade = true; }       } else if(button == 2 && state == STATE_PLAYING) { if(toggleCellMark(idxs[0], idxs[1])) { changesMade = true; }       }

} else if(       state == STATE_PLAYING &&        isPointInCoords(input.screenCoords, sectionCoords.rowBanner)    ) { // Clicked on the row hints.

let idxs = getRowBannerButtonInCoords(input.screenCoords); if(idxs != null && button == 0) { if(toggleRowHint(idxs[0], idxs[1])) { updateRow(idxs[0]); saveBookmark; changesMade = true; }       }

} else if(       state == STATE_PLAYING &&        isPointInCoords(input.screenCoords, sectionCoords.colBanner)    ) { // Clicked on the column hints.

let idxs = getColumnBannerButtonInCoords(input.screenCoords); if(idxs != null && button == 0) { if(toggleColumnHint(idxs[0], idxs[1])) { updateColumn(idxs[0]); saveBookmark; changesMade = true; }       }    } else if(        state == STATE_MAKING &&        isPointInCoords(input.screenCoords, sectionCoords.rowBanner)    ) { // Clicked on the maker mode row buttons.

let idxs = getRowBannerButtonInCoords(input.screenCoords); if(idxs != null && button == 0) { if(idxs[1] == 0) { // Delete row. if(board.nrRows > MIN_COLS_OR_ROWS) { board.cells.splice(idxs[0], 1); board.nrRows--; clampCamera; changesMade = true; }           } else if(idxs[1] == 1) { // New row above. if(board.nrRows < MAX_COLS_OR_ROWS) { let newCol = []; for(var c = 0; c < board.nrCols; c++) { newCol.push(CELL_BLANK); }                   board.cells.splice(idxs[0], 0, newCol); board.nrRows++; changesMade = true; }           } else if(idxs[1] == 2) { // New row below. if(board.nrRows < MAX_COLS_OR_ROWS) { let newCol = []; for(var c = 0; c < board.nrCols; c++) { newCol.push(CELL_BLANK); }                   board.cells.splice(idxs[0] + 1, 0, newCol); board.nrRows++; changesMade = true; }           }        }

} else if(       state == STATE_MAKING &&        isPointInCoords(input.screenCoords, sectionCoords.colBanner)    ) { // Clicked on the maker mode column buttons.

let idxs = getColumnBannerButtonInCoords(input.screenCoords); if(idxs != null && button == 0) { if(idxs[1] == 0) { // Delete column. if(board.nrCols > MIN_COLS_OR_ROWS) { for(var r = 0; r < board.nrRows; r++) { board.cells[r].splice(idxs[0], 1); }                   board.nrCols--; clampCamera; changesMade = true; }           } else if(idxs[1] == 1) { // New column above. if(board.nrCols < MAX_COLS_OR_ROWS) { for(var r = 0; r < board.nrRows; r++) { board.cells[r].splice(idxs[0], 0, CELL_BLANK); }                   board.nrCols++; changesMade = true; }           } else if(idxs[1] == 2) { // New column below. if(board.nrCols < MAX_COLS_OR_ROWS) { for(var r = 0; r < board.nrRows; r++) { board.cells[r].splice(idxs[0] + 1, 0, CELL_BLANK); }                   board.nrCols++; changesMade = true; }           }        }    } else if(isPointInCoords(input.screenCoords, guiItemCoords.pause, sectionCoords.footer)) { // Clicked on the pause button.

inPanel = true; panelState = PANEL_STATE_PAUSE; changesMade = true;

} else if(isPointInCoords(input.screenCoords, guiItemCoords.zoomIn, sectionCoords.footer)) { // Clicked on the zoom in button.

cam.zoom += 0.2;

} else if(isPointInCoords(input.screenCoords, guiItemCoords.zoomOut, sectionCoords.footer)) { // Clicked on the zoom out button.

cam.zoom -= 0.2;

}

if(!changesMade) { input.dragPanning = true; changesMade = true; }

if(changesMade) { updateCanvas; }

return changesMade; }

/** * Handler for when the player does an input down on the canvas, in the main menu. * This happens regardless of it being a mouse button down press, or a mobile touch start. * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons. * @param {number} mobile True if it was a mobile touch, false otherwise. */ function onCanvasInputDownInMainMenu(button, mobile) { if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader)) { // Clicked on the continue button. var bookmarkData = getBookmarkData; if(bookmarkData == null) return;

if(loadLevel(bookmarkData.levelNumber, bookmarkData.levelCode, STATE_PLAYING)) { board.cells = bookmarkData.cells; let hintIdx = 0; for(var r = 0; r < board.nrRows; r++) { for(var h = 0; h < board.rowHints[r].length; h++) { board.rowHints[r][h].state = bookmarkData.rowHints[hintIdx]; hintIdx++; }           }            hintIdx = 0; for(var c = 0; c < board.nrCols; c++) { for(var h = 0; h < board.colHints[c].length; h++) { board.colHints[c][h].state = bookmarkData.colHints[hintIdx]; hintIdx++; }           }

for(c = 0; c < board.nrCols; c++) { updateColumn(c); }           for(r = 0; r < board.nrRows; r++) { updateRow(r); }           changeState(STATE_PLAYING);

} else {

panelState = PANEL_STATE_LOAD_ERROR;

}

} else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuInfo, sectionCoords.mainMenuHeader)) { // Clicked on the info button. inPanel = true; panelState = PANEL_STATE_INFO;

} else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuMake, sectionCoords.mainMenuHeader)) { // Clicked on the make custom button. loadLevel(0, '', STATE_MAKING); changeState(STATE_MAKING);

} else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuCustom, sectionCoords.mainMenuHeader)) { // Clicked on the play custom button. let bookmarkData = getBookmarkData; if(bookmarkData != null && !gaveBookmarkWarning) { inPanel = true; panelState = PANEL_STATE_BOOKMARK_WARNING; gaveBookmarkWarning = true; } else { inPanel = true; panelState = PANEL_STATE_PLAYING_CODE; inputEl.value = ''; showInputBox(false); }

} else if(firstSideCleared && isPointInCoords(input.screenCoords, guiItemCoords.mainMenuSide, sectionCoords.mainMenuHeader)) { // Clicked on the side swapping button. gameSide = (gameSide == 0 ? 1 : 0); updateCanvas;

} else { // Check if the player clicked on one of the level buttons. let buttonPadding = SECTION_PADDING * 2; let buttonWidth = (sectionCoords.levelSelect.width - buttonPadding * 5) / 4; let levelHeight = (sectionCoords.levelSelect.height - buttonPadding * 5) / 4; let buttonHeight = levelHeight * 0.75; var clickedLevelIdx = -1; for(var c = 0; c < 4; c++) { for(var r = 0; r < 4; r++) { if(                   isPointInCoords( input.screenCoords, {                           x: buttonPadding + c * (buttonWidth + buttonPadding), y: buttonPadding + r * (levelHeight + buttonPadding), width: buttonWidth, height: buttonHeight },                       sectionCoords.levelSelect )               ) {                    clickedLevelIdx = r * 4 + c;                    clickedLevelIdx += 16 * gameSide; break; }           }            if(clickedLevelIdx != -1) { break; }       }

if(clickedLevelIdx != -1) { let bookmarkData = getBookmarkData; if(bookmarkData != null && !gaveBookmarkWarning) { inPanel = true; panelState = PANEL_STATE_BOOKMARK_WARNING; gaveBookmarkWarning = true; } else { if(loadLevel(clickedLevelIdx + 1, LEVELS[clickedLevelIdx], STATE_PLAYING)) { changeState(STATE_PLAYING); clearBookmark; } else { panelState = PANEL_STATE_LOAD_ERROR; }           }        }    } }

/** * Handler for when the player does an input down on the canvas, in the panel. * This happens regardless of it being a mouse button down press, or a mobile touch start. * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons. * @param {number} mobile True if it was a mobile touch, false otherwise. */ function onCanvasInputDownInPanel(button, mobile) { // Figure out where the player clicked. switch(panelState) { case PANEL_STATE_PAUSE:

if(isPointInCoords(input.screenCoords, guiItemCoords.continue, sectionCoords.panel)) { // Clicked on the continue button. inPanel = false; } else if(isPointInCoords(input.screenCoords, guiItemCoords.restart, sectionCoords.panel)) { // Clicked on the restart button. inPanel = false; loadLevel(board.levelNumber, board.levelCode, state); clearBookmark; } else if(isPointInCoords(input.screenCoords, guiItemCoords.finish, sectionCoords.panel)) { // Clicked on the finish button. if(state == STATE_MAKING) { let hasFilledCells = false; for(var r = 0; r < board.nrRows; r++) { for(var c = 0; c < board.nrCols; c++) { if(board.cells[r][c] == CELL_FILLED) { hasFilledCells = true; break; }                   }                    if(hasFilledCells) break; }

if(hasFilledCells) { panelState = PANEL_STATE_MAKING_NAME; inputEl.value = board.name; showInputBox(false, 20); } else { panelState = PANEL_STATE_SAVE_ERROR; }           }        } else if(isPointInCoords(input.screenCoords, guiItemCoords.quit, sectionCoords.panel)) { // Clicked on the quit button. changeState(STATE_MAIN_MENU, function { inPanel = false; }); } else { return; }       break;

case PANEL_STATE_CONGRATS:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

changeState(STATE_MAIN_MENU, function { inPanel = false; });

}       break;

case PANEL_STATE_MAKING_NAME:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

acceptInputBox; }       break;

case PANEL_STATE_MAKING_CODE:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

acceptInputBox;

}       break;

case PANEL_STATE_PLAYING_CODE:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

acceptInputBox;

}       break;

case PANEL_STATE_LOAD_ERROR:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

inPanel = false;

}       break;

case PANEL_STATE_SAVE_ERROR:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

inPanel = false;

}       break;

case PANEL_STATE_BOOKMARK_WARNING:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

inPanel = false;

}       break;

case PANEL_STATE_INFO:

if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) { // Clicked on the ok button.

inPanel = false;

}       break; }

updateCanvas; }

/** * Handler for when the player does an input move on the canvas. * This happens regardless of it being a mouse button down press, or a mobile touch start. */ function onCanvasInputMove { if(input.dragging && !input.dragPanning) {

let snappedMouse = { x: input.screenCoords.x, y: input.screenCoords.y }; let somethingChanged = false; if(input.dragLockCoord.x) snappedMouse.x = input.dragStart.x;       if(input.dragLockCoord.y) snappedMouse.y = input.dragStart.y;

if(           isPointInCoords(input.dragStart, sectionCoords.board) &&            isPointInCoords(input.screenCoords, sectionCoords.board)        ) { let idxs = getCellInCoords(snappedMouse); if(idxs != null) { if(toggleCellFill(idxs[0], idxs[1])) { somethingChanged = true; }           }

} else if(           state == STATE_PLAYING &&            isPointInCoords(input.dragStart, sectionCoords.rowBanner) &&            isPointInCoords(input.screenCoords, sectionCoords.rowBanner)        ) { let idxs = getRowBannerButtonInCoords(snappedMouse); if(idxs != null) { if(toggleRowHint(idxs[0], idxs[1])) { updateRow(idxs[0]); somethingChanged = true; }           }

} else if(           state == STATE_PLAYING &&            isPointInCoords(input.dragStart, sectionCoords.colBanner) &&            isPointInCoords(input.screenCoords, sectionCoords.colBanner)        ) { let idxs = getColumnBannerButtonInCoords(snappedMouse); if(idxs != null) { if(toggleColumnHint(idxs[0], idxs[1])) { updateColumn(idxs[0]); somethingChanged = true; }           }        }

if(somethingChanged) { if(!input.dragLockCoord.x && !input.dragLockCoord.y) { // This means we changed something that wasn't the thing the drag start changed. // We should lock the coordinates. let axisSnappedMouse = snapCoordsToAxis(input.screenCoords, input.dragStart); if(input.dragStart.x == axisSnappedMouse.x) { input.dragLockCoord.x = true; } else { input.dragLockCoord.y = true; }           }            updateCanvas; }

} }

/** * Handler for when the player presses the mouse button down on the canvas. * @param {event} event Event that triggered this callback. */ function onCanvasMouseDown(event) { if(event.button == 1) { event.preventDefault; }   updateInputFromMouse(event); onCanvasInputDown(event.button, false); }

/** * Handler for when the player moves the mouse cursor on the canvas. * @param {event} event Event that triggered this callback. */ function onCanvasMouseMove(event) { updateInputFromMouse(event); onCanvasInputMove; }

/** * Handler for when the player scrolls the mouse wheel on the canvas. * @param {event} event Event that triggered this callback. */ function onCanvasMouseWheel(event) { for(var l = 0; l < loaded.length; l++) { if(!loaded[l]) return; }   if(transitionAnimTime > 0) return;

updateCanvas; }

/** * Handler for when the player moves a mobile touch on the canvas. * @param {event} event Event that triggered this callback. */ function onCanvasTouchMove(event) { event.preventDefault; updateInputFromTouch(event); onCanvasInputMove; }

/** * Handler for when the player starts a mobile touch on the canvas. * @param {event} event Event that triggered this callback. */ function onCanvasTouchStart(event) { event.preventDefault; updateInputFromTouch(event); onCanvasInputDown(0, true); }

/** * Handler for when the player does an input move on the page. * This happens regardless of it being a mouse cursor move, or a mobile touch move. */ function onPageInputMove { if(input.dragging && input.dragPanning) { cam.coords.x -= (input.screenCoords.x - input.dragStart.x); cam.coords.y -= (input.screenCoords.y - input.dragStart.y); input.dragStart.x = input.screenCoords.x;       input.dragStart.y = input.screenCoords.y;        clampCamera; updateCanvas; } }

/** * Handler for when the player does an input up on the page. * This happens regardless of it being a mouse button up press, or a mobile touch end. */ function onPageInputUp { input.dragging = false; input.dragAction = -1; input.dragLockCoord.x = false; input.dragLockCoord.y = false; if(input.dragPanning) { input.dragPanning = false; if(Math.abs(cam.coords.x) < 10) { cam.coords.x = 0; }       if(Math.abs(cam.coords.y) < 10) { cam.coords.y = 0; }   }    updateCanvas; }

/** * Handler for when the player moves their mouse on the page. * @param {event} event Event that triggered this callback. */ function onPageMouseMove(event) { updateInputFromMouse(event); onPageInputMove; }

/** * Handler for when the player releases their mouse button on the page. * @param {event} event Event that triggered this callback. */ function onPageMouseUp(event) { onPageInputUp; }

/** * Handler for when the player moves their mobile touch on the page. * @param {event} event Event that triggered this callback. */ function onPageTouchMove(event) { if(!input.dragging) event.preventDefault; updateInputFromTouch(event); onPageInputMove; }

/** * Handler for when the player releases their mobile touch on the page. * @param {event} event Event that triggered this callback. */ function onPageTouchEnd(event) { onPageInputUp; }

/** * Handler for when the global game timer ticks. */ function onTimer { if(transitionAnimTime > 0) {

transitionAnimTime -= 1 / 60; if(transitionAnimTime <= 0) { if(transitionPhase == 0) { transitionPhase = 1; transitionAnimTime = TRANSITION_DURATION; state = transitionNewState; if(transitionCodeAfter !== undefined) transitionCodeAfter; } else { transitionAnimTime = 0; }       }        updateCanvas;

} }

/** * Checks whether a player's combo can ever be exactly the given size, * provided that the player can still fill more cells next to it. * Of course, if there are filled cells farther ahead that make the combo * too big, this will return false. * @param {object} combo Object with data about this combo. * @param {number} goalSize Size to check for. * @param {array} cells Array of cells in this line. * @returns True if it can, false if not. */ function playerComboCanBeSize(combo, goalSize, cells) { if(combo.nr > goalSize) return false;

let curSize = combo.nr; for(var c = combo.start + size; c <= cells.length; c++) { if(cells[c] == CELL_FILLED) { // It must use this filled cell as part of its combo. curSize++; } else if(cells[c] == CELL_BLANK) { // The player can stop here or keep going. if(curSize == goalSize) return true; curSize++; } else if(c == cells.length || cells[c] == CELL_MARKED) { // End of the line for this combo. return curSize == goalSize; }   }    return false; }

/** * Checks whether one of the player board's rows or columns defies the corresponding solution hint. * @param {number} row Row number, if we're checking a row. null otherwise. * @param {number} col Column number, if we're checking a column. null otherwise. * @param {array} playerCombos Array with the player's combos for this column/row. * @param {array} hints Array of hints for this column/row. * @returns True if it defies, false otherwise. */ function playerLineDefiesHints(row, col, playerCombos, hints) { // Setup. let playerCells = []; if(row != null) { for(var c = 0; c < board.nrCols; c++) { let cell = board.cells[row][c]; if(cell == CELL_BLANK && board.autoMarkedCols[c]) cell = CELL_MARKED; playerCells.push(cell); }       } else { for(var r = 0; r < board.nrRows; r++) { let cell = board.cells[r][col]; if(cell == CELL_BLANK && board.autoMarkedRows[r]) cell = CELL_MARKED; playerCells.push(cell); }    }

// Start by checking if any marked cell is in the way of the hints. let curCellIdx = 0; for(var h = 0; h < hints.length;) { if(curCellIdx >= playerCells.length) { // We've reached the end of the cells, and we still have hints to check. Too bad. return true; }       let markedCellIdx = findMarkedCellInList(playerCells, curCellIdx, hints[h].nr); if(markedCellIdx == -1) { // No marked cell. Move on to the next hint. curCellIdx += hints[h].nr + 1; h++; } else { // We've hit a marked cell. Let's move to after that cell and retry. curCellIdx = markedCellIdx + 1; }   }    if(playerCombos.length > hints.length) { //return true;

} else if(playerCombos.length == hints.length) { for(var c = 0; c < playerCombos.length; c++) { if(playerCombos[c] > hints[c].nr) return true; }

} else { let highestPlayerCombo = 0; let highestHintCombo = 0;

for(var c = 0; c < playerCombos.length; c++) { highestPlayerCombo = Math.max(highestPlayerCombo, playerCombos[c]); }       for(var h = 0; h < hints.length; h++) { highestHintCombo = Math.max(highestHintCombo, hints[h].nr); }       if(highestPlayerCombo > highestHintCombo) return true;

}

// All good! return false;

/*   // Possible combos for each hint. let possibleCombosPerHint = [];

for(var h = 0; h < hints.length; h++) {

let possibleCombos = []; for(var c = 0; c < playerCombos.length; c++) { if(playerComboCanBeSize(playerCombos[c], hints[h].nr, playerCells)) { possibleCombos.push(playerCombos[c]); }       }

possibleCombosPerHint[h] = possibleCombos; }

// I don't know what to do with this :(   */ }

/** * Checks whether one of the player board's rows or columns matches the corresponding solution hint. * This can be true even if the player marked the wrong cells. * @param {array} playerCombos Array with the player's combos for this column/row. * @param {array} hints Array of hints for this column/row. * @returns True if it matches, false otherwise. */ function playerLineMatchesHints(playerCombos, hints) { if(playerCombos.length != hints.length) return false; for(var h = 0; h < hints.length; h++) { if(hints[h].nr != playerCombos[h].nr) return false; }

return true; }

/** * Populates the board with cells from the given code, in the puzzle making state. * Debug function. * @param {string} code Code. * @returns True on success, false on failure. */ function populateFromCode(levelCode) { if(state != STATE_MAKING) return false; let levelData = decodeBoard(levelCode, BOARD_DATA_CONTEXT_LEVEL); if(levelData == null) return false;

board.nrRows = levelData.nrRows; board.nrCols = levelData.nrCols; board.cells = levelData.cells; board.name = levelData.name; updateCanvas; return true; }

/** * Saves the player's bookmark. */ function saveBookmark { if(state != STATE_PLAYING) return;

var boardData = { nrRows: board.nrRows, nrCols: board.nrCols, cells: board.cells, nrRowHints: 0, nrColHints: 0, rowHints: [], colHints: [], name: board.name, levelNumber: board.levelNumber, levelCode: board.levelCode };   for(var r = 0; r < board.nrRows; r++) { for(var h = 0; h < board.rowHints[r].length; h++) { boardData.rowHints.push(board.rowHints[r][h].state); }   }    boardData.nrRowHints = boardData.rowHints.length; for(var c = 0; c < board.nrCols; c++) { for(var h = 0; h < board.colHints[c].length; h++) { boardData.colHints.push(board.colHints[c][h].state); }   }    boardData.nrColHints = boardData.colHints.length; let boardStr = encodeBoard(boardData, BOARD_DATA_CONTEXT_BOOKMARK); localStorage.setItem('pikcrossBookmark', boardStr);

gaveBookmarkWarning = false; }

/** * Saves the player's global progression. */ function saveProgression { let writer = new StrBitWriter; for(var l = 0; l < LEVELS.length; l++) { writer.writeNumber(progression[l] ? 1 : 0, 1); }   localStorage.setItem('pikcrossProgression', writer.getStr); }

/** * Sets up the whole game. */ function pikcrossSetup { // HTML setup. let pikcrossDiv = document.getElementById('pikcross'); if(pikcrossDiv == null) return; pikcrossDiv.style.position = 'relative'; pikcrossDiv.style.width = '800px'; pikcrossDiv.style.height = '800px';

canvasEl = document.createElement('canvas'); canvasEl.width = CANVAS.WIDTH; canvasEl.height = CANVAS.HEIGHT; pikcrossDiv.appendChild(canvasEl);

inputEl = document.createElement('input'); inputEl.type = 'text'; inputEl.style.left = '30%'; inputEl.style.width = '40%'; inputEl.style.top = '48%'; inputEl.style.height = '4%'; inputEl.style.position = 'absolute'; inputEl.style.backgroundColor = '#BDB'; inputEl.style.fontFamily = 'monospace'; inputEl.addEventListener('focus', function(e) { e.target.select; }); inputEl.addEventListener('keyup', function(e) { if(e.key === 'Enter') acceptInputBox; }); hideInputBox; pikcrossDiv.appendChild(inputEl);

canvas = canvasEl.getContext('2d'); canvas.width = CANVAS.WIDTH; canvas.height = CANVAS.HEIGHT; canvas.textAlign = 'center'; canvas.textBaseline = 'middle'; // Listeners. canvasEl.addEventListener('wheel', onCanvasMouseWheel); canvasEl.addEventListener('mousedown', onCanvasMouseDown); canvasEl.addEventListener('touchstart', onCanvasTouchStart); canvasEl.addEventListener('mousemove', onCanvasMouseMove); canvasEl.addEventListener('touchmove', onCanvasTouchMove); canvasEl.addEventListener('contextmenu', function(e) { e.preventDefault; }, false); document.addEventListener('mousemove', onPageMouseMove); document.addEventListener('touchmove', onPageTouchMove); document.addEventListener('mouseup', onPageMouseUp); document.addEventListener('touchend', onPageTouchEnd);

// Spritesheet. sprites = new Image; sprites.onload = function { loaded[LOAD_CONTENT_SPRITES] = true; updateCanvas; };   sprites.src = 'https://pikmin.wiki.gallery/images/c/c7/Pikcross_spritesheet.png';

// Player progression. for(var l = 0; l < LEVELS.length; l++) { progression.push(false); }   loadProgression;

// Levels data. for(var l = 0; l < LEVELS.length; l++) { levelsData.push(decodeBoard(LEVELS[l], BOARD_DATA_CONTEXT_LEVEL)); }

// Global timer. setInterval(onTimer, 1000 / 60);

loaded[LOAD_CONTENT_SETUP] = true; }

/** * Shows the input box HTML element in the middle of the canvas. * It also selects the text. * @param {boolean} readOnly Whether the box should be read only or not. * @param {number} maxLength Maximum length for the input box. undefined for default. */ function showInputBox(readOnly, maxLength) { inputEl.style.display = 'block';

inputEl.removeAttribute('maxlength'); inputEl.removeAttribute('readonly'); if(maxLength > 0) inputEl.maxLength = maxLength; if(readOnly) inputEl.readOnly = true;

// This weird hack is necessary for some reason. window.setTimeout(       function {            inputEl.focus({ focusVisible: true });            inputEl.select;        },        0    ); }

/** * Given a set of coordinates, it snaps them so they are in the same axis as the anchor coordinates. * @param {object} coords Coordinates to snap. * @param {object} anchor Coordinates to compare against. * @returns An object with the snapped coordinates. */ function snapCoordsToAxis(coords, anchor) { let h_diff = Math.abs(coords.x - anchor.x); let v_diff = Math.abs(coords.y - anchor.y); if(h_diff > v_diff) { return {x: coords.x, y: anchor.y}; } else { return {x: anchor.x, y: coords.y}; } }

/** * Solves the puzzle. * Debug function. */ function solve { board.cells = board.solution; for(c = 0; c < board.nrCols; c++) { updateColumn(c); }   for(r = 0; r < board.nrRows; r++) { updateRow(r); }   updateCanvas; }

/** * Toggles whether a cell is filled or blank, if possible. * @param {number} row Row index number. * @param {number} col Column index number. * @returns True if the cell changed, false otherwise. */ function toggleCellFill(row, col) { if(row < 0 || row >= board.nrRows) return false; if(col < 0 || col >= board.nrCols) return false;

if(input.dragAction == -1) { // Figure out what the player wants to do in this move. if(board.cells[row][col] == CELL_FILLED) { input.dragAction = CELL_BLANK; } else { input.dragAction = CELL_FILLED; }   }

return changeCell(row, col, input.dragAction); }

/** * Toggles whether a cell is filled, marked, or blank, if possible. * @param {number} row Row index number. * @param {number} col Column index number. * @returns True if the cell changed, false otherwise. */ function toggleCellFillAndMark(row, col) { if(row < 0 || row >= board.nrRows) return false; if(col < 0 || col >= board.nrCols) return false;

if(input.dragAction == -1) { // Figure out what the player wants to do in this move. if(board.cells[row][col] == CELL_FILLED) { input.dragAction = CELL_MARKED; } else if(board.cells[row][col] == CELL_MARKED) { input.dragAction = CELL_BLANK; } else { input.dragAction = CELL_FILLED; }   }

return changeCell(row, col, input.dragAction); }

/** * Toggles whether a cell is marked or blank, if possible. * @param {number} row Row index number. * @param {number} col Column index number. * @returns True if the cell changed, false otherwise. */ function toggleCellMark(row, col) { if(row < 0 || row >= board.nrRows) return false; if(col < 0 || col >= board.nrCols) return false;

if(input.dragAction == -1) { // Figure out what the player wants to do in this move. if(board.cells[row][col] == CELL_MARKED) { input.dragAction = CELL_BLANK; } else { input.dragAction = CELL_MARKED; }   }

return changeCell(row, col, input.dragAction); }

/** * Toggles whether a column hint is marked or blank, if possible. * @param {number} col Column index number. * @param {number} hint Hint index number. * @returns True if the hint changed, false otherwise. */ function toggleColumnHint(col, hint) { if(input.dragAction == -1) { // Figure out what the player wants to do in this move. if(board.colHints[col][hint].state == CELL_FILLED) { input.dragAction = CELL_BLANK; } else { input.dragAction = CELL_FILLED; }   }

if(board.colHints[col][hint].state != input.dragAction) { board.colHints[col][hint].state = input.dragAction; return true; }   return false; }

/** * Toggles whether a row hint is marked or blank, if possible. * @param {number} row Row index number. * @param {number} hint Hint index number. * @returns True if the hint changed, false otherwise. */ function toggleRowHint(row, hint) { if(input.dragAction == -1) { // Figure out what the player wants to do in this move. if(board.rowHints[row][hint].state == CELL_FILLED) { input.dragAction = CELL_BLANK; } else { input.dragAction = CELL_FILLED; }   }

if(board.rowHints[row][hint].state != input.dragAction) { board.rowHints[row][hint].state = input.dragAction; return true; }   return false; }

/** * Updates the contents of the canvas. */ function updateCanvas { for(var l = 0; l < loaded.length; l++) { if(!loaded[l]) return; }

// Basic setup. canvas.setTransform(1, 0, 0, 1, 0, 0); canvas.fillStyle = COLOR_BG; canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT);

switch(state) { case STATE_MAIN_MENU: drawMainMenu; break; case STATE_PLAYING: drawBoard; break; case STATE_MAKING: drawBoard; break; }   if(inPanel) { drawPanel; }   if(transitionAnimTime > 0) { canvas.fillStyle = 'black'; let fillRatio = 1 - (transitionAnimTime / TRANSITION_DURATION); if(transitionPhase == 1) fillRatio = 1 - fillRatio; fillRatio *= 0.5; canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT * fillRatio); canvas.fillRect(0, 0, CANVAS.WIDTH * fillRatio, CANVAS.HEIGHT); canvas.fillRect(CANVAS.WIDTH * (1 - fillRatio), 0, CANVAS.WIDTH, CANVAS.HEIGHT); canvas.fillRect(0, CANVAS.HEIGHT * (1 - fillRatio), CANVAS.WIDTH, CANVAS.HEIGHT); } }

/** * Updates the state of a given column. * @param {number} col Column number to update. */ function updateColumn(col) { let playerCombos = getPlayerLineCombos(null, col); if(allHintsAreFilled(board.colHints[col]) && playerLineMatchesHints(playerCombos, board.colHints[col])) { board.autoMarkedCols[col] = true; } else { board.autoMarkedCols[col] = false; }   if(playerLineDefiesHints(null, col, playerCombos, board.colHints[col])) { board.defiedCols[col] = true; } else { board.defiedCols[col] = false; } }

/** * Updates the variables that hold the mouse state. * @param {event} event The event that triggered this. */ function updateInputFromMouse(event) { let br = canvasEl.getBoundingClientRect; input.screenCoords.x = event.clientX - br.left; input.screenCoords.y = event.clientY - br.top; input.worldCoords.x = input.screenCoords.x;   input.worldCoords.y = input.screenCoords.y;    input.worldCoords.x -= CANVAS.WIDTH / 2; input.worldCoords.y -= CANVAS.HEIGHT / 2; //input.worldCoords.x /= cam.zoom; //input.worldCoords.y /= cam.zoom; input.worldCoords.x += cam.coords.x;   input.worldCoords.y += cam.coords.y; }

/** * Updates the variables that hold the mouse state, using data from a mobile touch event. * @param {event} event The event that triggered this. */ function updateInputFromTouch(event) { let br = canvasEl.getBoundingClientRect; input.screenCoords.x = event.touches[0].clientX - br.left; input.screenCoords.y = event.touches[0].clientY - br.top; input.worldCoords.x = input.screenCoords.x;   input.worldCoords.y = input.screenCoords.y;    input.worldCoords.x -= CANVAS.WIDTH / 2; input.worldCoords.y -= CANVAS.HEIGHT / 2; //input.worldCoords.x /= cam.zoom; //input.worldCoords.y /= cam.zoom; input.worldCoords.x += cam.coords.x;   input.worldCoords.y += cam.coords.y; }

/** * Updates the state of a given row. * @param {number} row Row number to update. */ function updateRow(row) { let playerCombos = getPlayerLineCombos(row, null); if(allHintsAreFilled(board.rowHints[row]) && playerLineMatchesHints(playerCombos, board.rowHints[row])) { board.autoMarkedRows[row] = true; } else { board.autoMarkedRows[row] = false; }   if(playerLineDefiesHints(row, null, playerCombos, board.rowHints[row])) { board.defiedRows[row] = true; } else { board.defiedRows[row] = false; } }

/** * Makes writing bits into a string (8-bit int array) easy. */ class StrBitWriter { // Final string. #str = ''; // Current working byte value. #byteValue = 0; // Current bit's index. #bitIdx = 0; /**    * Encodes a number into the string. * @param {number} number Number to encode. 0 to 8 bits long. * @param {number} nrBits Amount of bits the number takes. 8 by default. */   writeNumber(number, nrBits) { if(nrBits === undefined) nrBits = 8; if(this.#bitIdx + nrBits > 8) { this.nextByte; }       for(var b = 0; b < nrBits; b++) { let bitValue = (number & (1 << b)) > 0; this.#byteValue |= (bitValue << this.#bitIdx); this.#bitIdx++; }       if(this.#bitIdx >= 8) { this.nextByte; }   }    /**     * Finishes writing to the current byte and goes to the next one. */   nextByte { this.#str += String.fromCharCode(this.#byteValue); this.#byteValue = 0; this.#bitIdx = 0; }

/**    * Obtains the finalized string (8-bit int array). * @returns The finalized string. */   getStr { let str = this.#str; if(this.#bitIdx > 0) { str += String.fromCharCode(this.#byteValue); }       return str; }

/**    * Resets the state of the writer. */   reset { this.#str = ''; this.#byteValue = 0; this.#bitIdx = 0; } }

/** * Makes reading bits from a string (8-bit int array) easy. */ class StrBitReader { // Full string. #str = ''; // Current byte's index. #byteIdx = 0; // Current bit's index. #bitIdx = 0;

/**    * Construcs a new instance. * @param {string} str String (8-bit int array) with the bits to read from. */   constructor(str) { this.#str = str; }

/**    * Reads a number from the next bits in the string. * @param {number} nrBits Amount of bits the number takes. 8 by default. * @returns The read number. */   readNumber(nrBits) { if(nrBits === undefined) nrBits = 8; if(this.#bitIdx + nrBits > 8) { this.nextByte; }       let number = 0; let byteValue = this.#str.charCodeAt(this.#byteIdx); for(var b = 0; b < nrBits; b++) { let bitMask = (1 << this.#bitIdx); let bitValue = (byteValue & bitMask) > 0; number |= (bitValue << b); this.#bitIdx++; }       if(this.#bitIdx >= 8) { this.nextByte; }       return number; }

/**    * Finishes reading the current byte and goes to the next one. */   nextByte { this.#byteIdx++; this.#bitIdx = 0; }

/**    * Resets the state of the reader. */   reset { this.#str = ''; this.#byteIdx = 0; this.#bitIdx = 0; } }

/** * Runs the game. */ pikcrossSetup;