Source: level.js

// Level representation and manipulation

// As in specification ~
MAX_LEVEL_WIDTH = 30
MAX_LEVEL_HEIGHT = 20

/**
 * Represents a single level
 */
class Level {
  constructor() {
    this.width = 0
    this.height = 0
    this.walls = []
    this.boxes = []
    this.targets = []
    this.player = undefined
    this.difficulty = undefined
    this.index = undefined
    this.name = ""
    this.completed = false
    this.moves = 0
  }
}

/** Creates an independent copy of the level
 * 
 * @param {Level} level 
 */
function clone_level(level) {
  let obj = new Level()
  obj.width = level.width;
  obj.height = level.height;
  obj.walls = deep_array_copy(level.walls)
  obj.boxes = deep_array_copy(level.boxes)
  obj.targets = deep_array_copy(level.targets)
  obj.player = deep_array_copy(level.player)
  obj.difficulty = level.difficulty
  obj.index = level.index
  obj.name = level.name
  obj.completed = level.completed
  obj.moves = level.moves
  return obj
}

/** Deeply copies an array
 * 
 * @param {any[]} array 
 * @returns {any[]} deep copy of the array
 */
function deep_array_copy(array) {
  if (array == undefined) return undefined
  return JSON.parse(JSON.stringify(array));
}

/** Checks if arrays contain elements equal with == operator
 * 
 * @param {any[]} first 
 * @param {any[]} second 
 * @returns {boolean}
 */
function arrays_equal(first, second) {
  if (first == undefined || second == undefined) return false
  if (first.length != second.length) return false;
  let r = first.every((el, index) => second[index] == el)
  return r
}

/** Checks if given level is completed (all box are satisfied)
 *  
 * @param {Level} level 
 * @returns {boolean}  true if level is completed, false otherwise 
 */
function is_level_completed(level) {
  return level.boxes.every(box => is_box_satisfied(level, box))
}

/** Returns index of box in the level
 * 
 * @param {Level} level 
 * @param {number[]} box Position of the box
 * @returns {number}
 */
function get_box_index(level, box) {
  for (let i = 0; i < level.boxes.length; i++)
    if (arrays_equal(level.boxes[i], box))
      return i
}

/** Checks if given box is satisfied (there's a target on its position)
 * 
 * @param {Level} level 
 * @param {number[]} box 
 * @returns {boolean}
 */
function is_box_satisfied(level, box) {
  return level.targets.some(target => arrays_equal(target, box))
}

/** Returns index of target in the level
 * 
 * @param {Level} level 
 * @param {number[]} target Position of the target
 * @returns {number}
 */
function get_target_index(level, target) {
  for (let i = 0; i < level.targets.length; i++)
    if (arrays_equal(level.targets[i], target))
      return i
}

/** Counts how many boxes are satisfied
 * 
 * @param {Level} level 
 * @returns {number}
 */
function satisfied_boxes_count(level) {
  let result = 0
  for (let box of level.boxes)
    if (is_box_satisfied(level, box)) result++
  return result
}

/** Removes a wall from given position if there's any
 * 
 * @param {Level} level 
 * @param {number} wall 
 */
function remove_wall(level, wall) {
  remove_level_object(level.walls, wall)
}

/** Removes a target from given position if there's any
 * 
 * @param {Level} level 
 * @param {number} target 
 */
function remove_target(level, target) {
  remove_level_object(level.targets, target)
}

/** Removes a box from given position if there's any
 * 
 * @param {Level} level 
 * @param {number} box
 */
function remove_box(level, box) {
  remove_level_object(level.boxes, box)
}

function remove_level_object(array, object) {
  let index = undefined
  for (let i = 0; i < array.length; i++)
    if (arrays_equal(array[i], object)) index = i

  if (index != undefined)
    array.splice(index, 1)
}

/** Checks if given position allows to be put on it.
 *  Position cannot be wall and cannot contain a box 
 *  
 *  Does not consider player because only player can move elements around
 *  and there's only one player
 *  
 * @param {Levels} level 
 * @param {number[]} position 
 * @returns {boolean}
 */
function can_walk_into_without_pushing(level, position) {
  let [x, y] = position

  // Forbid leaving the board
  if (x < 0 || x >= level.width || y < 0 || y >= level.height) return false

  // And walking into walls
  else if (level.walls.some(wall => wall[0] == x && wall[1] == y)) return false

  // or boxes
  else if (level.boxes.some(box => box[0] == x && box[1] == y)) return false

  else
    return true
}
/** Checks whether box can be pushed by offset
 * 
 * @param {LeveL} level 
 * @param {number[]} box_position 
 * @param {number[]} offset 
 * @returns {boolean}
 */
function can_push_box(level, box_position, offset) {
  let [x, y] = box_position
  let [dx, dy] = offset
  x += dx
  y += dy
  return can_walk_into_without_pushing(level, [x, y]);
}

/** Checks whether player can move by offset
 * 
 * @param {Level} level 
 * @param {number} offset 
 * @returns {boolean}
 */
function can_move_or_push(level, offset) {
  let [x, y] = level.player
  let [dx, dy] = offset

  if (dx * dy != 0 || Math.abs(dx) > 1 || Math.abs(dy) > 1)
    return false

  x += dx
  y += dy

  if (can_walk_into_without_pushing(level, [x, y])) return true
  // If there's a box we can move only when we push it
  else if (level.boxes.some(box => box[0] == x && box[1] == y)) { return can_push_box(level, [x, y], offset) }

  else
    return false;
}

function move(position, offset) {
  let [x, y] = position
  let [dx, dy] = offset
  return [x + dx, y + dy]
}

/** Resizes level and removes elements that are no longer inside
 * 
 * @param {Level} level 
 * @param {number} width 
 * @param {number} height 
 */
function resize_level(level, width, height) {
  level.width = width
  level.height = height
  level.walls = level.walls.filter(e => e[0] < width && e[1] < height)
  level.targets = level.targets.filter(e => e[0] < width && e[1] < height)
  level.boxes = level.boxes.filter(e => e[0] < width && e[1] < height)
  if (level.player != undefined && (level.player[0] >= width || level.player[1] >= height))
    level.player = undefined
}