text: /* @license
  * xiangqiboard.js v@VERSION
  * https://github.com/lengyanyu258/xiangqiboardjs/
  *
  * Copyright (c) 2017, Chris Oakman
  * Copyright (c) 2018, @lengyanyu258
  * Released under the MIT license
  * https://github.com/lengyanyu258/xiangqiboardjs/blob/master/LICENSE.md
  */
  
  // start anonymous scope
  ;(function () {
  'use strict'
  
  const $ = window['jQuery']
  
  // ---------------------------------------------------------------------------
  // Constants
  // ---------------------------------------------------------------------------
  
  const ROW_TOP = 9
  const ROW_LOW = 0
  const ROW_LENGTH = ROW_TOP - ROW_LOW + 1
  const COLUMNS = Object.freeze('abcdefghi'.split(''))
  const COLS_TOP = Object.freeze('123456789'.split(''))
  const COLS_LOW = Object.freeze('987654321'.split(''))
  const DEFAULT_DRAG_THROTTLE_RATE = 20
  const ELLIPSIS = '...'
  const MINIMUM_JQUERY_VERSION = '1.8.3'
  const RUN_ASSERTS = true
  const START_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR'
  const START_POSITION = fenToObj(START_FEN)
  
  // default animation speeds
  const DEFAULT_APPEAR_SPEED = 200
  const DEFAULT_MOVE_SPEED = 200
  const DEFAULT_SNAPBACK_SPEED = 60
  const DEFAULT_SNAP_SPEED = 30
  const DEFAULT_TRASH_SPEED = 100
  
  // use unique class names to prevent clashing with anything else on the page and simplify selectors
  // NOTE: these should never change
  const CSS = Object.freeze({
      clearfix: 'clearfix-5f3b5',
      board: 'board-1ef78',
      square: 'square-2b8ce',
      highlight1: 'highlight1-e13fc',
      highlight2: 'highlight2-e0a03',
      notation: 'notation-8c7a2',
      alpha: 'alpha-f4ef2',
      numeric: 'numeric-fe76e',
      row: 'row-cb702',
      piece: 'piece-1e8b9',
      sparePieces: 'spare-pieces-9e77b',
      xiangqiboard: 'xiangqiboard-8ddcb',
      sparePiecesTop: 'spare-pieces-top-e4b47',
      sparePiecesBottom: 'spare-pieces-bottom-29dac'
  })
  
  // ---------------------------------------------------------------------------
  // Misc Util Functions
  // ---------------------------------------------------------------------------
  
  function throttle (f, interval, scope) {
      let timeout = 0
      let shouldFire = false
      let args = []
  
      const handleTimeout = function () {
      timeout = 0
      if (shouldFire) {
          shouldFire = false
          fire()
      }
      }
  
      const fire = function () {
      timeout = window.setTimeout(handleTimeout, interval)
      f.apply(scope, args)
      }
  
      return function (_args) {
      args = arguments
      if (!timeout) {
          fire()
      } else {
          shouldFire = true
      }
      }
  }
  
  // function debounce (f, interval, scope) {
  //   var timeout = 0
  //   return function (_args) {
  //     window.clearTimeout(timeout)
  //     var args = arguments
  //     timeout = window.setTimeout(function () {
  //       f.apply(scope, args)
  //     }, interval)
  //   }
  // }
  
  function uuid () {
      return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function (c) {
      const r = (Math.random() * 16) | 0
      return r.toString(16)
      })
  }
  
  function deepCopy (thing) {
      return JSON.parse(JSON.stringify(thing))
  }
  
  function parseSemVer (version) {
      const tmp = version.split('.')
      return {
      major: parseInt(tmp[0], 10),
      minor: parseInt(tmp[1], 10),
      patch: parseInt(tmp[2], 10)
      }
  }
  
  // returns true if version is >= minimum
  function validSemanticVersion (version, minimum) {
      version = parseSemVer(version)
      minimum = parseSemVer(minimum)
  
      const versionNum = (version.major * 100000 * 100000) +
                          (version.minor * 100000) +
                          version.patch
      const minimumNum = (minimum.major * 100000 * 100000) +
                          (minimum.minor * 100000) +
                          minimum.patch
  
      return versionNum >= minimumNum
  }
  
  function interpolateTemplate (str, obj) {
      for (const key in obj) {
      if (!obj.hasOwnProperty(key)) continue
      const keyTemplateStr = '{' + key + '}'
      const value = obj[key]
      while (str.indexOf(keyTemplateStr) !== -1) {
          str = str.replace(keyTemplateStr, value)
      }
      }
      return str
  }
  
  if (RUN_ASSERTS) {
      console.assert(interpolateTemplate('abc', {a: 'x'}) === 'abc')
      console.assert(interpolateTemplate('{a}bc', {}) === '{a}bc')
      console.assert(interpolateTemplate('{a}bc', {p: 'q'}) === '{a}bc')
      console.assert(interpolateTemplate('{a}bc', {a: 'x'}) === 'xbc')
      console.assert(interpolateTemplate('{a}bc{a}bc', {a: 'x'}) === 'xbcxbc')
      console.assert(interpolateTemplate('{a}{a}{b}', {a: 'x', b: 'y'}) === 'xxy')
  }
  
  // ---------------------------------------------------------------------------
  // Predicates
  // ---------------------------------------------------------------------------
  
  function isString (s) {
      return typeof s === 'string'
  }
  
  function isFunction (f) {
      return typeof f === 'function'
  }
  
  function isInteger (n) {
      return typeof n === 'number' &&
              isFinite(n) &&
              Math.floor(n) === n
  }
  
  function validAnimationSpeed (speed) {
      if (speed === 'fast' || speed === 'slow') return true
      if (!isInteger(speed)) return false
      return speed >= 0
  }
  
  function validThrottleRate (rate) {
      return isInteger(rate) &&
              rate >= 1
  }
  
  function validMove (move) {
      // move should be a string
      if (!isString(move)) return false
  
      // move should be in the form of "e2-e4", "f6-d5"
      const squares = move.split('-')
      if (squares.length !== 2) return false
  
      return validSquare(squares[0]) && validSquare(squares[1])
  }
  
  function validSquare (square) {
      return isString(square) && square.search(/^[a-i][0-9]$/) !== -1
  }
  
  if (RUN_ASSERTS) {
      console.assert(validSquare('a1'))
      console.assert(validSquare('e2'))
      console.assert(validSquare('g9'))
      console.assert(!validSquare('D2'))
      console.assert(!validSquare('a'))
      console.assert(!validSquare(true))
      console.assert(!validSquare(null))
      console.assert(!validSquare({}))
  }
  
  function validPieceCode (code) {
      // TODO: Compatible with other representations:
      // r and w both represent the red side
      // Adviser, Bachelor and Guard are equal,
      // Bishop, Elephant and Minister are equal,
      // Horse and Knight represent the same
      return isString(code) && code.search(/^[br][KABNRCP]$/) !== -1
  }
  
  if (RUN_ASSERTS) {
      console.assert(validPieceCode('bP'))
      console.assert(validPieceCode('bK'))
      console.assert(validPieceCode('rK'))
      console.assert(validPieceCode('rR'))
      console.assert(!validPieceCode('RR'))
      console.assert(!validPieceCode('Rr'))
      console.assert(!validPieceCode('a'))
      console.assert(!validPieceCode(true))
      console.assert(!validPieceCode(null))
      console.assert(!validPieceCode({}))
  }
  
  function validFen (fen) {
      if (!isString(fen)) return false
  
      // cut off any move, castling, etc info from the end
      // we're only interested in position information
      fen = fen.replace(/ .+$/, '')
  
      // expand the empty square numbers to just 1s
      fen = expandFenEmptySquares(fen)
  
      // FEN should be ROW_LENGTH sections separated by slashes
      const chunks = fen.split('/')
      if (chunks.length !== ROW_LENGTH) return false
  
      // check each section
      for (let i = 0; i < ROW_LENGTH; i++) {
      if (chunks[i].length !== COLUMNS.length ||
          chunks[i].search(/[^kabnrcpKABNRCP1]/) !== -1) {
          return false
      }
      }
  
      return true
  }
  
  if (RUN_ASSERTS) {
      console.assert(validFen(START_FEN))
      console.assert(validFen('9/9/9/9/9/9/9/9/9/9'))
      console.assert(validFen('r1bakab1r/9/1cn2cn2/p1p1p1p1p/9/9/P1P1P1P1P/1C2C1N2/9/RNBAKABR1'))
      console.assert(validFen('rnbakabnr/9/1c2c4/p1p1C1p1p/9/9/P1P1P1P1P/1C7/9/RNBAKABNR b - - 0 2'))
      console.assert(!validFen('rnbakabnz/9/1c2c4/p1p1C1p1p/9/9/P1P1P1P1P/1C7/9/RNBAKABNR b - - 0 2'))
      console.assert(!validFen('anbrkqbnr/9/9/9/9/9/P1P1P1P1P/9/9/9'))
      console.assert(!validFen('rnbakabnr/p1p1p1p1p/9/9/9/9/P1P1P1P1P/'))
      console.assert(!validFen('rnbakabnr/p1p1p1p1p/9/9/9/9/P1P1P1P1P/RNBAKABN'))
      console.assert(!validFen('999999/p1p1p1p1p/9/9/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR'))
      console.assert(!validFen('rnbakabnr/p1p1p1p1p/74/9/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR'))
      console.assert(!validFen({}))
  }
  
  function validPositionObject (pos) {
      if (!$.isPlainObject(pos)) return false
  
      for (const i in pos) {
      if (!pos.hasOwnProperty(i)) continue
  
      if (!validSquare(i) || !validPieceCode(pos[i])) {
          return false
      }
      }
  
      return true
  }
  
  if (RUN_ASSERTS) {
      console.assert(validPositionObject(START_POSITION))
      console.assert(validPositionObject({}))
      console.assert(validPositionObject({e2: 'rP'}))
      console.assert(validPositionObject({e2: 'rP', d2: 'rP'}))
      console.assert(!validPositionObject({e2: 'BP'}))
      console.assert(!validPositionObject({y2: 'rP'}))
      console.assert(!validPositionObject(null))
      console.assert(!validPositionObject('start'))
      console.assert(!validPositionObject(START_FEN))
  }
  
  function isTouchDevice () {
      return 'ontouchstart' in document.documentElement
  }
  
  function validJQueryVersion () {
      return typeof window.$ &&
              $.fn &&
              $.fn.jquery &&
              validSemanticVersion($.fn.jquery, MINIMUM_JQUERY_VERSION)
  }
  
  // ---------------------------------------------------------------------------
  // Chess Util Functions
  // ---------------------------------------------------------------------------
  
  // convert FEN piece code to bP, rK, etc
  function fenToPieceCode (piece) {
      // black piece
      if (piece.toLowerCase() === piece) {
      return 'b' + piece.toUpperCase()
      }
  
      // red piece
      return 'r' + piece.toUpperCase()
  }
  
  // convert bP, rK, etc code to FEN structure
  function pieceCodeToFen (piece) {
      const pieceCodeLetters = piece.split('')
  
      // black piece
      if (pieceCodeLetters[0] === 'b') {
      return pieceCodeLetters[1].toLowerCase()
      }
  
      // red piece
      return pieceCodeLetters[1].toUpperCase()
  }
  
  // convert FEN string to position object
  // returns false if the FEN string is invalid
  function fenToObj (fen) {
      if (!validFen(fen)) return false
  
      // cut off any move, castling, etc info from the end
      // we're only interested in position information
      fen = fen.replace(/ .+$/, '')
  
      const rows = fen.split('/')
      const position = {}
  
      let currentRow = ROW_TOP
      for (let i = 0; i < ROW_LENGTH; i++) {
      const row = rows[i].split('')
      let colIdx = 0
  
      // loop through each character in the FEN section
      for (let j = 0; j < row.length; j++) {
          // number / empty squares
          if (row[j].search(/[1-9]/) !== -1) {
          const numEmptySquares = parseInt(row[j], 10)
          colIdx = colIdx + numEmptySquares
          } else {
          // piece
          const square = COLUMNS[colIdx] + currentRow
          position[square] = fenToPieceCode(row[j])
          colIdx = colIdx + 1
          }
      }
  
      currentRow = currentRow - 1
      }
  
      return position
  }
  
  // position object to FEN string
  // returns false if the obj is not a valid position object
  function objToFen (obj) {
      if (!validPositionObject(obj)) return false
  
      let fen = ''
  
      let currentRow = ROW_TOP
      for (let i = 0; i < ROW_LENGTH; i++) {
      for (let j = 0; j < COLUMNS.length; j++) {
          const square = COLUMNS[j] + currentRow
  
          // piece exists
          if (obj.hasOwnProperty(square)) {
          fen = fen + pieceCodeToFen(obj[square])
          } else {
          // empty space
          fen = fen + '1'
          }
      }
  
      if (i !== ROW_TOP) {
          fen = fen + '/'
      }
  
      currentRow = currentRow - 1
      }
  
      // squeeze the empty numbers together
      fen = squeezeFenEmptySquares(fen)
  
      return fen
  }
  
  if (RUN_ASSERTS) {
      console.assert(objToFen(START_POSITION) === START_FEN)
      console.assert(objToFen({}) === '9/9/9/9/9/9/9/9/9/9')
      console.assert(objToFen({a2: 'rP', 'b2': 'bP'}) === '9/9/9/9/9/9/9/Pp7/9/9')
  }
  
  function squeezeFenEmptySquares (fen) {
      return fen.replace(/111111111/g, '9')
              .replace(/11111111/g, '8')
              .replace(/1111111/g, '7')
              .replace(/111111/g, '6')
              .replace(/11111/g, '5')
              .replace(/1111/g, '4')
              .replace(/111/g, '3')
              .replace(/11/g, '2')
  }
  
  function expandFenEmptySquares (fen) {
      return fen.replace(/9/g, '111111111')
              .replace(/8/g, '11111111')
              .replace(/7/g, '1111111')
              .replace(/6/g, '111111')
              .replace(/5/g, '11111')
              .replace(/4/g, '1111')
              .replace(/3/g, '111')
              .replace(/2/g, '11')
  }
  
  // returns the distance between two squares
  function squareDistance (squareA, squareB) {
      const squareAArray = squareA.split('')
      const squareAx = COLUMNS.indexOf(squareAArray[0]) + 1
      const squareAy = parseInt(squareAArray[1], 10)
  
      const squareBArray = squareB.split('')
      const squareBx = COLUMNS.indexOf(squareBArray[0]) + 1
      const squareBy = parseInt(squareBArray[1], 10)
  
      const xDelta = Math.abs(squareAx - squareBx)
      const yDelta = Math.abs(squareAy - squareBy)
  
      if (xDelta >= yDelta) return xDelta
      return yDelta
  }
  
  // returns the square of the closest instance of piece
  // returns false if no instance of piece is found in position
  function findClosestPiece (position, piece, square) {
      // create array of closest squares from square
      const closestSquares = createRadius(square)
  
      // search through the position in order of distance for the piece
      for (let i = 0; i < closestSquares.length; i++) {
      const s = closestSquares[i]
  
      if (position.hasOwnProperty(s) && position[s] === piece) {
          return s
      }
      }
  
      return false
  }
  
  // returns an array of closest squares from square
  function createRadius (square) {
      const squares = []
  
      // calculate distance of all squares
      for (let i = 0; i < COLUMNS.length; i++) {
      for (let j = 0; j < ROW_LENGTH; j++) {
          const s = COLUMNS[i] + j
  
          // skip the square we're starting from
          if (square === s) continue
  
          squares.push({
          square: s,
          distance: squareDistance(square, s)
          })
      }
      }
  
      // sort by distance
      squares.sort(function (a, b) {
      return a.distance - b.distance
      })
  
      // just return the square code
      const surroundingSquares = []
      for (let i = 0; i < squares.length; i++) {
      surroundingSquares.push(squares[i].square)
      }
  
      return surroundingSquares
  }
  
  // given a position and a set of moves, return a new position
  // with the moves executed
  function calculatePositionFromMoves (position, moves) {
      const newPosition = deepCopy(position)
  
      for (const i in moves) {
      if (!moves.hasOwnProperty(i)) continue
  
      // skip the move if the position doesn't have a piece on the source square
      if (!newPosition.hasOwnProperty(i)) continue
  
      const piece = newPosition[i]
      delete newPosition[i]
      newPosition[moves[i]] = piece
      }
  
      return newPosition
  }
  
  // TODO: add some asserts here for calculatePositionFromMoves
  
  // ---------------------------------------------------------------------------
  // HTML
  // ---------------------------------------------------------------------------
  
  function buildContainerHTML (hasSparePieces) {
      let html = '<div class="{xiangqiboard}">'
  
      if (hasSparePieces) {
      html += '<div class="{sparePieces} {sparePiecesTop}"></div>'
      }
  
      html += '<div class="{board}"></div>'
  
      if (hasSparePieces) {
      html += '<div class="{sparePieces} {sparePiecesBottom}"></div>'
      }
  
      html += '</div>'
  
      return interpolateTemplate(html, CSS)
  }
  
  // ---------------------------------------------------------------------------
  // Config
  // ---------------------------------------------------------------------------
  
  function expandConfigArgumentShorthand (config) {
      if (config === 'start') {
      config = {position: deepCopy(START_POSITION)}
      } else if (validFen(config)) {
      config = {position: fenToObj(config)}
      } else if (validPositionObject(config)) {
      config = {position: deepCopy(config)}
      }
  
      // config must be an object
      if (!$.isPlainObject(config)) config = {}
  
      return config
  }
  
  // validate config / set default options
  function expandConfig (config) {
      // default for orientation is red
      if (config.orientation !== 'black') config.orientation = 'red'
  
      // default for showNotation is false
      if (config.showNotation !== true) config.showNotation = false
  
      // default for draggable is false
      if (config.draggable !== true) config.draggable = false
  
      // default for dropOffBoard is 'snapback'
      if (config.dropOffBoard !== 'trash') config.dropOffBoard = 'snapback'
  
      // default for sparePieces is false
      if (config.sparePieces !== true) config.sparePieces = false
  
      // draggable must be true if sparePieces is enabled
      if (config.sparePieces) config.draggable = true
  
      // default piece theme is wikimedia
      if (!config.hasOwnProperty('pieceTheme') ||
          (!isString(config.pieceTheme) && !isFunction(config.pieceTheme))) {
      config.pieceTheme = $('#piecesUrl').val() + '/img/xiangqipieces/graphic/{piece}.svg'
      }
  
      // default board theme is wikimedia
      if (!config.hasOwnProperty('boardTheme') || !isString(config.boardTheme)) {
      //config.boardTheme = '/img/xiangqiboards/wikimedia/xiangqiboard.svg'
      config.boardTheme = $('#piecesUrl').val() + '/img/xiangqiboards/ban-co.svg'
      }
  
      // animation speeds
      if (!validAnimationSpeed(config.appearSpeed)) config.appearSpeed = DEFAULT_APPEAR_SPEED
      if (!validAnimationSpeed(config.moveSpeed)) config.moveSpeed = DEFAULT_MOVE_SPEED
      if (!validAnimationSpeed(config.snapbackSpeed)) config.snapbackSpeed = DEFAULT_SNAPBACK_SPEED
      if (!validAnimationSpeed(config.snapSpeed)) config.snapSpeed = DEFAULT_SNAP_SPEED
      if (!validAnimationSpeed(config.trashSpeed)) config.trashSpeed = DEFAULT_TRASH_SPEED
  
      // throttle rate
      if (!validThrottleRate(config.dragThrottleRate)) config.dragThrottleRate = DEFAULT_DRAG_THROTTLE_RATE
  
      return config
  }
  
  // ---------------------------------------------------------------------------
  // Dependencies
  // ---------------------------------------------------------------------------
  
  // check for a compatible version of jQuery
  function checkJQuery () {
      if (!validJQueryVersion()) {
      const errorMsg = 'Xiangqiboard Error 1005: Unable to find a valid version of jQuery. ' +
          'Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or higher on the page' +
          '\n\n' +
          'Exiting' + ELLIPSIS
      window.alert(errorMsg)
      return false
      }
  
      return true
  }
  
  // return either boolean false or the $container element
  function checkContainerArg (containerElOrString) {
      if (containerElOrString === '') {
      const errorMsg1 = 'Xiangqiboard Error 1001: ' +
          'The first argument to Xiangqiboard() cannot be an empty string.' +
          '\n\n' +
          'Exiting' + ELLIPSIS
      window.alert(errorMsg1)
      return false
      }
  
      // convert containerEl to query selector if it is a string
      if (isString(containerElOrString) &&
          containerElOrString.charAt(0) !== '#') {
      containerElOrString = '#' + containerElOrString
      }
  
      // containerEl must be something that becomes a jQuery collection of size 1
      const $container = $(containerElOrString)
      if ($container.length !== 1) {
      const errorMsg2 = 'Xiangqiboard Error 1003: ' +
          'The first argument to Xiangqiboard() must be the ID of a DOM node, ' +
          'an ID query selector, or a single DOM node.' +
          '\n\n' +
          'Exiting' + ELLIPSIS
      window.alert(errorMsg2)
      return false
      }
  
      return $container
  }
  
  // ---------------------------------------------------------------------------
  // Constructor
  // ---------------------------------------------------------------------------
  
  /**
   * @return {null}
   */
  function constructor (containerElOrString, config) {
      // first things first: check basic dependencies
      if (!checkJQuery()) {
      return null
      }
      let $container = checkContainerArg(containerElOrString)
      if (!$container) {
      return null
      }
  
      // ensure the config object is what we expect
      config = expandConfigArgumentShorthand(config)
      config = expandConfig(config)
  
      // DOM elements
      let $board = null
      let $draggedPiece = null
      let $sparePiecesTop = null
      let $sparePiecesBottom = null
  
      // constructor return object
      const widget = {}
  
      // -------------------------------------------------------------------------
      // Stateful
      // -------------------------------------------------------------------------
  
      let boardBorderSize = 2
      let currentOrientation = 'red'
      let currentPosition = {}
      let draggedPiece = null
      let draggedPieceLocation = null
      let draggedPieceSource = null
      let isDragging = false
      const sparePiecesElsIds = {}
      const squareElsIds = {}
      let squareElsOffsets = {}
      let squareSize = 16
  
      // -------------------------------------------------------------------------
      // Validation / Errors
      // -------------------------------------------------------------------------
  
      function error (code, msg, obj) {
          // do nothing if showErrors is not set
      if (
          config.hasOwnProperty('showErrors') !== true ||
          config.showErrors === false
          ) {
          return
      }
  
      let errorText = 'Xiangqiboard Error ' + code + ': ' + msg
  
          // print to console
      if (
          config.showErrors === 'console' &&
          typeof console === 'object' &&
          typeof console.log === 'function'
          ) {
          console.log(errorText)
          if (arguments.length >= 2) {
          console.log(obj)
          }
          return
      }
  
          // alert errors
      if (config.showErrors === 'alert') {
          if (obj) {
          errorText += '\n\n' + JSON.stringify(obj)
          }
          window.alert(errorText)
          return
      }
  
      // custom function
      if (isFunction(config.showErrors)) {
          config.showErrors(code, msg, obj)
      }
      }
  
      function setInitialState () {
      currentOrientation = config.orientation
  
      // make sure position is valid
      if (config.hasOwnProperty('position')) {
          if (config.position === 'start') {
          currentPosition = deepCopy(START_POSITION)
          } else if (validFen(config.position)) {
          currentPosition = fenToObj(config.position)
          } else if (validPositionObject(config.position)) {
          currentPosition = deepCopy(config.position)
          } else {
          error(
              7263,
              'Invalid value passed to config.position.',
              config.position
              )
          }
      }
      }
  
      // -------------------------------------------------------------------------
      // DOM Misc
      // -------------------------------------------------------------------------
  
      // calculates square size based on the width of the container
      // got a little CSS black magic here, so let me explain:
      // get the width of the container element (could be anything), reduce by 1 for
      // fudge factor, and then keep reducing until we find an exact mod COLUMNS.length for
      // our square size
      function calculateSquareSize () {
      let containerWidth = parseInt($container.width(), 10)
  
      // defensive, prevent infinite loop
      if (!containerWidth || containerWidth <= 0) {
          return 0
      }
  
      // pad one pixel
      let boardWidth = containerWidth - 1
  
      while (boardWidth % COLUMNS.length !== 0 && boardWidth > 0) {
          boardWidth = boardWidth - 1
      }
  
      return boardWidth / COLUMNS.length
      }
  
      // create random IDs for elements
      function createElIds () {
      // squares on the board
      for (let i = 0; i < COLUMNS.length; i++) {
          for (let j = ROW_LOW; j <= ROW_TOP; j++) {
          const square = COLUMNS[i] + j
          squareElsIds[square] = square + '-' + uuid()
          }
      }
  
      // spare pieces
      const pieces = 'KABNRCP'.split('')
      for (let i = 0; i < pieces.length; i++) {
          const whitePiece = 'r' + pieces[i]
          const blackPiece = 'b' + pieces[i]
          sparePiecesElsIds[whitePiece] = whitePiece + '-' + uuid()
          sparePiecesElsIds[blackPiece] = blackPiece + '-' + uuid()
      }
      }
  
      // -------------------------------------------------------------------------
      // Markup Building
      // -------------------------------------------------------------------------
  
      function buildBoardHTML (orientation) {
      if (orientation !== 'black') {
          orientation = 'red'
      }
  
      let html = ''
  
      // algebraic notation / orientation
      const alpha = deepCopy(COLUMNS)
      const col_top = deepCopy(COLS_TOP)
      const col_low = deepCopy(COLS_LOW)
      let row = ROW_TOP
      if (orientation === 'black') {
          alpha.reverse()
          row = ROW_LOW
      }
  
      for (let i = 0; i < ROW_LENGTH; i++) {
          html += '<div class="{row}">'
          for (let j = 0; j < COLUMNS.length; j++) {
          const square = alpha[j] + row
  
          html += '<div class="{square} square-' + square + '" ' +
              'style="width:' + squareSize + 'px;height:' + squareSize + 'px;" ' +
              'id="' + squareElsIds[square] + '" ' +
              'data-square="' + square + '">'
  
          if (config.showNotation) {
              // alpha notation
              if (row === ROW_TOP) {
                  html += '<div class="{notation} {alpha}" style="top: 1px; bottom: auto; font-weight: bold;">' + col_top[j] + '</div>'
              }
              if (row === ROW_LOW) {
                  html += '<div class="{notation} {alpha}" style="top: auto; bottom: 1px; font-weight: bold;">' + col_low[j] + '</div>'
              }
          }
  
          html += '</div>' // end .square
          }
          html += '<div class="{clearfix}"></div></div>'
  
          if (orientation === 'red') {
          row = row - 1
          } else {
          row = row + 1
          }
      }
  
      return interpolateTemplate(html, CSS)
      }
  
      function buildBoardCSS (orientation) {
      if (orientation !== 'black') {
          orientation = 'red'
      }
  
      const css = {}
      css['background'] = 'url("' + config.boardTheme + '") no-repeat'
      css['background-size'] = '100%'
      if (orientation === 'black') {
          // css['transform'] = 'rotate(180deg)'
      }
  
      return css
      }
  
      function buildPieceImgSrc (piece) {
      if (isFunction(config.pieceTheme)) {
          return config.pieceTheme(piece)
      }
  
      // is string
      return interpolateTemplate(config.pieceTheme, {piece: piece})
      }
  
      function buildPieceHTML (piece, hidden, id) {
      let html = '<img src="' + buildPieceImgSrc(piece) + '" '
      if (isString(id) && id !== '') {
          html += 'id="' + id + '" '
      }
      html += 'alt="' + piece + '" ' +
          'class="{piece}" ' +
          'data-piece="' + piece + '" ' +
          'style="width:' + squareSize + 'px;' + 'height:' + squareSize + 'px;'
  
      if (hidden) {
          html += 'display:none;'
      }
  
      html += '" />'
  
      return interpolateTemplate(html, CSS)
      }
  
      function buildSparePiecesHTML (color) {
      let pieces = ['rK', 'rA', 'rB', 'rN', 'rR', 'rC', 'rP']
      if (color === 'black') {
          pieces = ['bK', 'bA', 'bB', 'bN', 'bR', 'bC', 'bP']
      }
  
      let html = ''
      for (let i = 0; i < pieces.length; i++) {
          html += buildPieceHTML(pieces[i], false, sparePiecesElsIds[pieces[i]])
      }
  
      return html
      }
  
      // -------------------------------------------------------------------------
      // Animations
      // -------------------------------------------------------------------------
  
      function animateSquareToSquare (src, dest, piece, completeFn) {
      // get information about the source and destination squares
      const $srcSquare = $('#' + squareElsIds[src])
      const srcSquarePosition = $srcSquare.offset()
      const $destSquare = $('#' + squareElsIds[dest])
      const destSquarePosition = $destSquare.offset()
  
      // create the animated piece and absolutely position it
      // over the source square
      const animatedPieceId = uuid()
      $('body').append(buildPieceHTML(piece, true, animatedPieceId))
      const $animatedPiece = $('#' + animatedPieceId)
      $animatedPiece.css({
          display: '',
          position: 'absolute',
          top: srcSquarePosition.top,
          left: srcSquarePosition.left
      })
  
      // remove original piece from source square
      $srcSquare.find('.' + CSS.piece).remove()
  
      function onFinishAnimation1 () {
          // add the "real" piece to the destination square
          $destSquare.append(buildPieceHTML(piece))
  
          // remove the animated piece
          $animatedPiece.remove()
  
          // run complete function
          if (isFunction(completeFn)) {
          completeFn()
          }
      }
  
      // animate the piece to the destination square
      const opts = {
          duration: config.moveSpeed,
          complete: onFinishAnimation1
      }
      $animatedPiece.animate(destSquarePosition, opts)
      }
  
      function animateSparePieceToSquare (piece, dest, completeFn) {
      const srcOffset = $('#' + sparePiecesElsIds[piece]).offset()
      const $destSquare = $('#' + squareElsIds[dest])
      const destOffset = $destSquare.offset()
  
      // create the animate piece
      const pieceId = uuid()
      $('body').append(buildPieceHTML(piece, true, pieceId))
      const $animatedPiece = $('#' + pieceId)
      $animatedPiece.css({
          display: '',
          position: 'absolute',
          left: srcOffset.left,
          top: srcOffset.top
      })
  
      // on complete
      function onFinishAnimation2 () {
          // add the "real" piece to the destination square
          $destSquare.find('.' + CSS.piece).remove()
          $destSquare.append(buildPieceHTML(piece))
  
          // remove the animated piece
          $animatedPiece.remove()
  
          // run complete function
          if (isFunction(completeFn)) {
          completeFn()
          }
      }
  
      // animate the piece to the destination square
      const opts = {
          duration: config.moveSpeed,
          complete: onFinishAnimation2
      }
      $animatedPiece.animate(destOffset, opts)
      }
  
      // execute an array of animations
      function doAnimations (animations, oldPos, newPos) {
      if (animations.length === 0) return
  
      let numFinished = 0
  
      function onFinishAnimation3 () {
          // exit if all the animations aren't finished
          numFinished = numFinished + 1
          if (numFinished !== animations.length) return
  
          drawPositionInstant()
  
          // run their onMoveEnd function
          if (isFunction(config.onMoveEnd)) {
          config.onMoveEnd(deepCopy(oldPos), deepCopy(newPos))
          }
      }
  
      for (let i = 0; i < animations.length; i++) {
          const animation = animations[i]
  
          // clear a piece
          if (animation.type === 'clear') {
          $('#' + squareElsIds[animation.square] + ' .' + CSS.piece)
              .fadeOut(config.trashSpeed, onFinishAnimation3)
  
          // add a piece with no spare pieces - fade the piece onto the square
          } else if (animation.type === 'add' && !config.sparePieces) {
          $('#' + squareElsIds[animation.square])
              .append(buildPieceHTML(animation.piece, true))
              .find('.' + CSS.piece)
              .fadeIn(config.appearSpeed, onFinishAnimation3)
  
          // add a piece with spare pieces - animate from the spares
          } else if (animation.type === 'add' && config.sparePieces) {
          animateSparePieceToSquare(animation.piece, animation.square, onFinishAnimation3)
  
          // move a piece from squareA to squareB
          } else if (animation.type === 'move') {
          animateSquareToSquare(animation.source, animation.destination, animation.piece, onFinishAnimation3)
          }
      }
      }
  
      // calculate an array of animations that need to happen in order to get
      // from pos1 to pos2
      function calculateAnimations (pos1, pos2) {
      // make copies of both
      pos1 = deepCopy(pos1)
      pos2 = deepCopy(pos2)
  
      const animations = []
      const squaresMovedTo = {}
  
      // remove pieces that are the same in both positions
      for (const i in pos2) {
          if (!pos2.hasOwnProperty(i)) continue
  
          if (pos1.hasOwnProperty(i) && pos1[i] === pos2[i]) {
          delete pos1[i]
          delete pos2[i]
          }
      }
  
      // find all the "move" animations
      for (const i in pos2) {
          if (!pos2.hasOwnProperty(i)) continue
  
          const closestPiece = findClosestPiece(pos1, pos2[i], i)
          if (closestPiece) {
          animations.push({
              type: 'move',
              source: closestPiece,
              destination: i,
              piece: pos2[i]
          })
  
          delete pos1[closestPiece]
          delete pos2[i]
          squaresMovedTo[i] = true
          }
      }
  
      // "add" animations
      for (const i in pos2) {
          if (!pos2.hasOwnProperty(i)) continue
  
          animations.push({
          type: 'add',
          square: i,
          piece: pos2[i]
          })
  
          delete pos2[i]
      }
  
      // "clear" animations
      for (const i in pos1) {
          if (!pos1.hasOwnProperty(i)) continue
  
          // do not clear a piece if it is on a square that is the result
          // of a "move", ie: a piece capture
          if (squaresMovedTo.hasOwnProperty(i)) continue
  
          animations.push({
          type: 'clear',
          square: i,
          piece: pos1[i]
          })
  
          delete pos1[i]
      }
  
      return animations
      }
  
      // -------------------------------------------------------------------------
      // Control Flow
      // -------------------------------------------------------------------------
  
      function drawPositionInstant () {
      // clear the board
      $board.find('.' + CSS.piece).remove()
  
      // add the pieces
      for (const i in currentPosition) {
          if (!currentPosition.hasOwnProperty(i)) continue
  
          $('#' + squareElsIds[i]).append(buildPieceHTML(currentPosition[i]))
      }
      }
  
      function drawBoard () {
      $board.html(buildBoardHTML(currentOrientation, squareSize, config.showNotation))
      $board.css(buildBoardCSS(currentOrientation))
      drawPositionInstant()
  
      if (config.sparePieces) {
          if (currentOrientation === 'black') {
          $sparePiecesTop.html(buildSparePiecesHTML('red'))
          $sparePiecesBottom.html(buildSparePiecesHTML('black'))
          } else {
          $sparePiecesTop.html(buildSparePiecesHTML('black'))
          $sparePiecesBottom.html(buildSparePiecesHTML('red'))
          }
      }
      }
  
      function setCurrentPosition (position) {
      const oldPos = deepCopy(currentPosition)
      const newPos = deepCopy(position)
      const oldFen = objToFen(oldPos)
      const newFen = objToFen(newPos)
  
      // do nothing if no change in position
      if (oldFen === newFen) return
  
      // run their onChange function
      if (isFunction(config.onChange)) {
          config.onChange(oldPos, newPos)
      }
  
      // update state
      currentPosition = position
      }
  
      function isXYOnSquare (x, y) {
      for (const i in squareElsOffsets) {
          if (!squareElsOffsets.hasOwnProperty(i)) continue
  
          const s = squareElsOffsets[i]
          if (x >= s.left &&
              x < s.left + squareSize &&
              y >= s.top &&
              y < s.top + squareSize) {
          return i
          }
      }
  
      return 'offboard'
      }
  
      // records the XY coordinates of every square into memory
      function captureSquareOffsets () {
      squareElsOffsets = {}
  
      for (const i in squareElsIds) {
          if (!squareElsIds.hasOwnProperty(i)) continue
  
          squareElsOffsets[i] = $('#' + squareElsIds[i]).offset()
      }
      }
  
      function removeSquareHighlights () {
      $board
          .find('.' + CSS.square)
          .removeClass(CSS.highlight1 + ' ' + CSS.highlight2)
      }
  
      function snapbackDraggedPiece () {
      // there is no "snapback" for spare pieces
      if (draggedPieceSource === 'spare') {
          trashDraggedPiece()
          return
      }
  
      removeSquareHighlights()
  
      // animation complete
      function complete () {
          drawPositionInstant()
          $draggedPiece.css('display', 'none')
  
          // run their onSnapbackEnd function
          if (isFunction(config.onSnapbackEnd)) {
          config.onSnapbackEnd(
              draggedPiece,
              draggedPieceSource,
              deepCopy(currentPosition),
              currentOrientation
              )
          }
      }
  
      // get source square position
      const sourceSquarePosition = $('#' + squareElsIds[draggedPieceSource]).offset()
  
      // animate the piece to the target square
      const opts = {
          duration: config.snapbackSpeed,
          complete: complete
      }
      $draggedPiece.animate(sourceSquarePosition, opts)
  
      // set state
      isDragging = false
      }
  
      function trashDraggedPiece () {
      removeSquareHighlights()
  
      // remove the source piece
      const newPosition = deepCopy(currentPosition)
      delete newPosition[draggedPieceSource]
      setCurrentPosition(newPosition)
  
      // redraw the position
      drawPositionInstant()
  
      // hide the dragged piece
      $draggedPiece.fadeOut(config.trashSpeed)
  
      // set state
      isDragging = false
      }
  
      function dropDraggedPieceOnSquare (square) {
      removeSquareHighlights()
  
      // update position
      const newPosition = deepCopy(currentPosition)
      delete newPosition[draggedPieceSource]
      newPosition[square] = draggedPiece
      setCurrentPosition(newPosition)
  
      // get target square information
      const targetSquarePosition = $('#' + squareElsIds[square]).offset()
  
      // animation complete
      function onAnimationComplete () {
          drawPositionInstant()
          $draggedPiece.css('display', 'none')
  
          // execute their onSnapEnd function
          if (isFunction(config.onSnapEnd)) {
          config.onSnapEnd(draggedPieceSource, square, draggedPiece)
          }
      }
  
      // snap the piece to the target square
      const opts = {
          duration: config.snapSpeed,
          complete: onAnimationComplete
      }
      $draggedPiece.animate(targetSquarePosition, opts)
  
      // set state
      isDragging = false
      }
  
      function beginDraggingPiece (source, piece, x, y) {
      // run their custom onDragStart function
      // their custom onDragStart function can cancel drag start
      if (isFunction(config.onDragStart) &&
          config.onDragStart(source, piece, deepCopy(currentPosition), currentOrientation) === false) {
          return
      }
  
      // set state
      isDragging = true
      draggedPiece = piece
      draggedPieceSource = source
  
      // if the piece came from spare pieces, location is offboard
      if (source === 'spare') {
          draggedPieceLocation = 'offboard'
      } else {
          draggedPieceLocation = source
      }
  
      // capture the x, y coordinates of all squares in memory
      captureSquareOffsets()
  
      // create the dragged piece
      $draggedPiece.attr('src', buildPieceImgSrc(piece)).css({
          display: '',
          position: 'absolute',
          left: x - squareSize / 2,
          top: y - squareSize / 2
      })
  
      if (source !== 'spare') {
          // highlight the source square and hide the piece
          $('#' + squareElsIds[source])
          .addClass(CSS.highlight1)
          .find('.' + CSS.piece)
          .css('display', 'none')
      }
      }
  
      function updateDraggedPiece (x, y) {
      // put the dragged piece over the mouse cursor
      $draggedPiece.css({
          left: x - squareSize / 2,
          top: y - squareSize / 2
      })
  
      // get location
      const location = isXYOnSquare(x, y)
  
      // do nothing if the location has not changed
      if (location === draggedPieceLocation) return
  
      // remove highlight from previous square
      if (validSquare(draggedPieceLocation)) {
          $('#' + squareElsIds[draggedPieceLocation]).removeClass(CSS.highlight2)
      }
  
      // add highlight to new square
      if (validSquare(location)) {
          $('#' + squareElsIds[location]).addClass(CSS.highlight2)
      }
  
      // run onDragMove
      if (isFunction(config.onDragMove)) {
          config.onDragMove(
              location,
              draggedPieceLocation,
              draggedPieceSource,
              draggedPiece,
              deepCopy(currentPosition),
              currentOrientation
          )
      }
  
      // update state
      draggedPieceLocation = location
      }
  
      function stopDraggedPiece (location) {
      // determine what the action should be
      let action = 'drop'
      if (location === 'offboard' && config.dropOffBoard === 'snapback') {
          action = 'snapback'
      }
      if (location === 'offboard' && config.dropOffBoard === 'trash') {
          action = 'trash'
      }
  
      // run their onDrop function, which can potentially change the drop action
      if (isFunction(config.onDrop)) {
          const newPosition = deepCopy(currentPosition)
  
          // source piece is a spare piece and position is off the board
          // if (draggedPieceSource === 'spare' && location === 'offboard') {...}
          // position has not changed; do nothing
  
          // source piece is a spare piece and position is on the board
          if (draggedPieceSource === 'spare' && validSquare(location)) {
          // add the piece to the board
          newPosition[location] = draggedPiece
          }
  
          // source piece was on the board and position is off the board
          if (validSquare(draggedPieceSource) && location === 'offboard') {
          // remove the piece from the board
          delete newPosition[draggedPieceSource]
          }
  
          // source piece was on the board and position is on the board
          if (validSquare(draggedPieceSource) && validSquare(location)) {
          // move the piece
          delete newPosition[draggedPieceSource]
          newPosition[location] = draggedPiece
          }
  
          const oldPosition = deepCopy(currentPosition)
  
          const result = config.onDrop(
              draggedPieceSource,
              location,
              draggedPiece,
              newPosition,
              oldPosition,
              currentOrientation
          )
          if (result === 'snapback' || result === 'trash') {
          action = result
          }
      }
  
      // do it!
      if (action === 'snapback') {
          snapbackDraggedPiece()
      } else if (action === 'trash') {
          trashDraggedPiece()
      } else if (action === 'drop') {
          dropDraggedPieceOnSquare(location)
      }
      }
  
      // -------------------------------------------------------------------------
      // Public Methods
      // -------------------------------------------------------------------------
  
      // clear the board
      widget.clear = function (useAnimation) {
      widget.position({}, useAnimation)
      }
  
      // remove the widget from the page
      widget.destroy = function () {
      // remove markup
      $container.html('')
      $draggedPiece.remove()
  
      // remove event handlers
      $container.unbind()
      }
  
      // shorthand method to get the current FEN
      widget.fen = function () {
      return widget.position('fen')
      }
  
      // flip orientation
      widget.flip = function () {
      return widget.orientation('flip')
      }
  
      // move pieces
      // TODO: this method should be variadic as well as accept an array of moves
      widget.move = function () {
      // no need to throw an error here; just do nothing or return the current position
      if (arguments.length === 0) return currentPosition
  
      let useAnimation = true
  
      // collect the moves into an object
      const moves = {}
      for (let i = 0; i < arguments.length; i++) {
          // any "false" to this function means no animations
          if (arguments[i] === false) {
          useAnimation = false
          continue
          }
  
          // skip invalid arguments
          if (!validMove(arguments[i])) {
          error(2826, 'Invalid move passed to the move method.', arguments[i])
          continue
          }
  
          const tmp = arguments[i].split('-')
          moves[tmp[0]] = tmp[1]
      }
  
      // calculate position from moves
      const newPos = calculatePositionFromMoves(currentPosition, moves)
  
      // update the board
      widget.position(newPos, useAnimation)
  
      // return the new position object
      return newPos
      }
  
      widget.orientation = function (arg) {
      // no arguments, return the current orientation
      if (arguments.length === 0) {
          return currentOrientation
      }
  
      if (arg === 'flip') {
          // flip orientation
          currentOrientation = currentOrientation === 'black' ? 'red' : 'black'
      } else {
          // set to red or black
          if (arg !== 'black') {
          currentOrientation = 'red'
          } else {
          currentOrientation = arg
          }
      }
  
      drawBoard()
      return currentOrientation
      }
  
      widget.position = function (position, useAnimation) {
      // no arguments, return the current position
      if (arguments.length === 0) {
          return deepCopy(currentPosition)
      }
  
      // get position as FEN
      if (isString(position) && position.toLowerCase() === 'fen') {
          return objToFen(currentPosition)
      }
  
      // start position
      if (isString(position) && position.toLowerCase() === 'start') {
          position = deepCopy(START_POSITION)
      }
  
      // convert FEN to position object
      if (validFen(position)) {
          position = fenToObj(position)
      }
  
      // validate position object
      if (!validPositionObject(position)) {
          error(6482, 'Invalid value passed to the position method.', position)
          return
      }
  
      // default for useAnimations is true
      if (useAnimation !== false) useAnimation = true
  
      if (useAnimation) {
          // start the animations
          const animations = calculateAnimations(currentPosition, position)
          doAnimations(animations, currentPosition, position)
  
          // set the new position
          setCurrentPosition(position)
      } else {
          // instant update
          setCurrentPosition(position)
          drawPositionInstant()
      }
      }
  
      widget.resize = function () {
      // calculate the new square size
      squareSize = calculateSquareSize()
  
      // set board width
      $board.css('width', squareSize * COLUMNS.length + 'px')
  
      // set drag piece size
      $draggedPiece.css({
          height: squareSize,
          width: squareSize
      })
  
      // spare pieces
      if (config.sparePieces) {
          $container
          .find('.' + CSS.sparePieces)
          .css('paddingLeft', squareSize + boardBorderSize + 'px')
      }
  
      // redraw the board
      drawBoard()
      }
  
      // set the starting position
      widget.start = function (useAnimation) {
      widget.position('start', useAnimation)
      }
  
      // -------------------------------------------------------------------------
      // Browser Events
      // -------------------------------------------------------------------------
  
      function stopDefault (evt) {
      evt.preventDefault()
      }
  
      function mousedownSquare (evt) {
          // do nothing if we're not draggable
      if (!config.draggable) return
  
      // do nothing if there is no piece on this square
      const square = $(this).attr('data-square')
      if (!validSquare(square)) return
      if (!currentPosition.hasOwnProperty(square)) return
  
      beginDraggingPiece(square, currentPosition[square], evt.pageX, evt.pageY)
      }
  
      function touchstartSquare (e) {
      // do nothing if we're not draggable
      if (!config.draggable) return
  
      // do nothing if there is no piece on this square
      const square = $(this).attr('data-square')
      if (!validSquare(square)) return
      if (!currentPosition.hasOwnProperty(square)) return
  
      e = e.originalEvent
      beginDraggingPiece(
          square,
          currentPosition[square],
          e.changedTouches[0].pageX,
          e.changedTouches[0].pageY
          )
      }
  
      function mousedownSparePiece (evt) {
      // do nothing if sparePieces is not enabled
      if (!config.sparePieces) return
  
      const piece = $(this).attr('data-piece')
  
      beginDraggingPiece('spare', piece, evt.pageX, evt.pageY)
      }
  
      function touchstartSparePiece (e) {
      // do nothing if sparePieces is not enabled
      if (!config.sparePieces) return
  
      const piece = $(this).attr('data-piece')
  
      e = e.originalEvent
      beginDraggingPiece(
          'spare',
          piece,
          e.changedTouches[0].pageX,
          e.changedTouches[0].pageY
          )
      }
  
      function mousemoveWindow (evt) {
      if (isDragging) {
          updateDraggedPiece(evt.pageX, evt.pageY)
      }
      }
  
      const throttledMousemoveWindow = throttle(mousemoveWindow, config.dragThrottleRate)
  
      function touchmoveWindow (evt) {
      // do nothing if we are not dragging a piece
      if (!isDragging) return
  
      // prevent screen from scrolling
      evt.preventDefault()
  
      updateDraggedPiece(evt.originalEvent.changedTouches[0].pageX,
                          evt.originalEvent.changedTouches[0].pageY)
      }
  
      const throttledTouchmoveWindow = throttle(touchmoveWindow, config.dragThrottleRate)
  
      function mouseupWindow (evt) {
      // do nothing if we are not dragging a piece
      if (!isDragging) return
  
      // get the location
      const location = isXYOnSquare(evt.pageX, evt.pageY)
  
      stopDraggedPiece(location)
      }
  
      function touchendWindow (evt) {
      // do nothing if we are not dragging a piece
      if (!isDragging) return
  
      // get the location
      const location = isXYOnSquare(evt.originalEvent.changedTouches[0].pageX,
                                      evt.originalEvent.changedTouches[0].pageY)
  
      stopDraggedPiece(location)
      }
  
      function mouseenterSquare (evt) {
      // do not fire this event if we are dragging a piece
      // NOTE: this should never happen, but it's a safeguard
      if (isDragging) return
  
      // exit if they did not provide a onMouseoverSquare function
      if (!isFunction(config.onMouseoverSquare)) return
  
      // get the square
      const square = $(evt.currentTarget).attr('data-square')
  
      // NOTE: this should never happen; defensive
      if (!validSquare(square)) return
  
      // get the piece on this square
      let piece = false
      if (currentPosition.hasOwnProperty(square)) {
          piece = currentPosition[square]
      }
  
      // execute their function
      config.onMouseoverSquare(square, piece, deepCopy(currentPosition), currentOrientation)
      }
  
      function mouseleaveSquare (evt) {
      // do not fire this event if we are dragging a piece
      // NOTE: this should never happen, but it's a safeguard
      if (isDragging) return
  
      // exit if they did not provide an onMouseoutSquare function
      if (!isFunction(config.onMouseoutSquare)) return
  
      // get the square
      const square = $(evt.currentTarget).attr('data-square')
  
      // NOTE: this should never happen; defensive
      if (!validSquare(square)) return
  
      // get the piece on this square
      let piece = false
      if (currentPosition.hasOwnProperty(square)) {
          piece = currentPosition[square]
      }
  
      // execute their function
      config.onMouseoutSquare(square, piece, deepCopy(currentPosition), currentOrientation)
      }
  
      // -------------------------------------------------------------------------
      // Initialization
      // -------------------------------------------------------------------------
  
      function addEvents () {
      // prevent "image drag"
      $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault)
  
      // mouse drag pieces
      $board.on('mousedown', '.' + CSS.square, mousedownSquare)
      $container.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, mousedownSparePiece)
  
      // mouse enter / leave square
      $board
          .on('mouseenter', '.' + CSS.square, mouseenterSquare)
          .on('mouseleave', '.' + CSS.square, mouseleaveSquare)
  
      // piece drag
      const $window = $(window)
      $window
          .on('mousemove', throttledMousemoveWindow)
          .on('mouseup', mouseupWindow)
  
      // touch drag pieces
      if (isTouchDevice()) {
          $board.on('touchstart', '.' + CSS.square, touchstartSquare)
          $container.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, touchstartSparePiece)
          $window
          .on('touchmove', throttledTouchmoveWindow)
          .on('touchend', touchendWindow)
      }
      }
  
      function initDOM () {
      // create unique IDs for all the elements we will create
      createElIds()
  
      // build board and save it in memory
      $container.html(buildContainerHTML(config.sparePieces))
      $board = $container.find('.' + CSS.board)
  
      if (config.sparePieces) {
          $sparePiecesTop = $container.find('.' + CSS.sparePiecesTop)
          $sparePiecesBottom = $container.find('.' + CSS.sparePiecesBottom)
      }
  
      // create the drag piece
      const draggedPieceId = uuid()
      $('body').append(buildPieceHTML('rP', true, draggedPieceId))
      $draggedPiece = $('#' + draggedPieceId)
  
      // TODO: need to remove this dragged piece element if the board is no
      // longer in the DOM
  
      // get the border size
      boardBorderSize = parseInt($board.css('borderLeftWidth'), 10)
  
      // set the size and draw the board
      widget.resize()
      }
  
      // -------------------------------------------------------------------------
      // Initialization
      // -------------------------------------------------------------------------
  
      setInitialState()
      initDOM()
      addEvents()
  
      // return the widget object
      return widget
  } // end constructor
  
  // TODO: do module exports here
  window['Xiangqiboard'] = constructor
  
  // expose util functions
  window['Xiangqiboard']['fenToObj'] = fenToObj
  window['Xiangqiboard']['objToFen'] = objToFen
  })() // end anonymous wrapper