@alepot55/chessboardjs 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1071,11 +1071,84 @@ var ChessboardLib = (function (exports) {
1071
1071
  * @constant
1072
1072
  * @type {Object}
1073
1073
  */
1074
+ /**
1075
+ * Flip mode options
1076
+ * @constant
1077
+ * @type {Object}
1078
+ */
1079
+ const FLIP_MODES = Object.freeze({
1080
+ visual: 'visual', // CSS flexbox visual flip (instant, no piece animation)
1081
+ animate: 'animate', // Animate pieces to mirrored positions (smooth animation)
1082
+ none: 'none' // No visual change, only internal orientation
1083
+ });
1084
+
1085
+ /**
1086
+ * Movement style options - how pieces travel from source to destination
1087
+ * @constant
1088
+ * @type {Object}
1089
+ */
1090
+ const MOVE_STYLES = Object.freeze({
1091
+ slide: 'slide', // Linear movement (default) - piece slides in straight line
1092
+ arc: 'arc', // Arc trajectory - piece lifts up then comes down
1093
+ hop: 'hop', // Parabolic jump - like a knight hopping
1094
+ teleport: 'teleport', // Instant - no animation, piece appears at destination
1095
+ fade: 'fade' // Crossfade - fades out at source, fades in at destination
1096
+ });
1097
+
1098
+ /**
1099
+ * Capture animation options - what happens to captured pieces
1100
+ * @constant
1101
+ * @type {Object}
1102
+ */
1103
+ const CAPTURE_STYLES = Object.freeze({
1104
+ fade: 'fade', // Fade out (default)
1105
+ shrink: 'shrink', // Shrink then fade
1106
+ instant: 'instant', // Disappears immediately
1107
+ explode: 'explode' // Scale up and fade (dramatic effect)
1108
+ });
1109
+
1110
+ /**
1111
+ * Appearance animation options - how new pieces appear on the board
1112
+ * @constant
1113
+ * @type {Object}
1114
+ */
1115
+ const APPEARANCE_STYLES = Object.freeze({
1116
+ fade: 'fade', // Opacity + subtle scale (default)
1117
+ pulse: 'pulse', // Double pulse scale bounce
1118
+ pop: 'pop', // Scale 0→1.15→1 with spring easing
1119
+ drop: 'drop', // Drops from above with bounce
1120
+ instant: 'instant' // No animation
1121
+ });
1122
+
1123
+ /**
1124
+ * Landing effect options - what happens when piece reaches destination
1125
+ * @constant
1126
+ * @type {Object}
1127
+ */
1128
+ const LANDING_EFFECTS = Object.freeze({
1129
+ none: 'none', // No effect (default)
1130
+ bounce: 'bounce', // Slight bounce on landing
1131
+ pulse: 'pulse', // Quick scale pulse
1132
+ settle: 'settle' // Subtle settling animation
1133
+ });
1134
+
1135
+ /**
1136
+ * Drag style options - how pieces behave during drag
1137
+ * @constant
1138
+ * @type {Object}
1139
+ */
1140
+ const DRAG_STYLES = Object.freeze({
1141
+ smooth: 'smooth', // Piece follows cursor smoothly (default)
1142
+ snap: 'snap', // Piece snaps to cursor position
1143
+ elastic: 'elastic' // Slight elastic lag behind cursor
1144
+ });
1145
+
1074
1146
  const DEFAULT_CONFIG$1 = Object.freeze({
1075
1147
  id: 'board',
1076
1148
  position: 'start',
1077
1149
  orientation: 'w',
1078
1150
  mode: 'normal',
1151
+ flipMode: 'visual', // 'visual', 'animate', or 'none'
1079
1152
  size: 'auto',
1080
1153
  draggable: true,
1081
1154
  hints: true,
@@ -1083,15 +1156,45 @@ var ChessboardLib = (function (exports) {
1083
1156
  movableColors: 'both',
1084
1157
  moveHighlight: true,
1085
1158
  overHighlight: true,
1086
- moveAnimation: 'ease',
1087
- moveTime: 'fast',
1088
- dropOffBoard: 'snapback',
1159
+
1160
+ // Movement configuration
1161
+ moveStyle: 'slide', // 'slide', 'arc', 'hop', 'teleport', 'fade'
1162
+ moveEasing: 'ease', // CSS easing function
1163
+ moveTime: 'fast', // Duration: 'instant', 'veryFast', 'fast', 'normal', 'slow', 'verySlow' or ms
1164
+ moveArcHeight: 0.3, // Arc height as ratio of distance (for 'arc' and 'hop' styles)
1165
+
1166
+ // Capture configuration
1167
+ captureStyle: 'fade', // 'fade', 'shrink', 'instant', 'explode'
1168
+ captureTime: 'fast', // Duration for capture animation
1169
+
1170
+ // Appearance configuration
1171
+ appearanceStyle: 'fade', // 'fade', 'pulse', 'pop', 'drop', 'instant'
1172
+ appearanceTime: 'fast', // Duration for appearance animation
1173
+
1174
+ // Landing effect configuration
1175
+ landingEffect: 'none', // 'none', 'bounce', 'pulse', 'settle'
1176
+ landingDuration: 150, // Duration for landing effect in ms
1177
+
1178
+ // Drag configuration
1179
+ dragStyle: 'smooth', // 'smooth', 'snap', 'elastic'
1180
+ dragScale: 1.05, // Scale factor while dragging (1.0 = no scale)
1181
+ dragOpacity: 0.9, // Opacity while dragging
1182
+
1183
+ // Snapback configuration
1089
1184
  snapbackTime: 'fast',
1090
- snapbackAnimation: 'ease',
1185
+ snapbackEasing: 'ease',
1186
+
1187
+ // Other timing
1188
+ dropOffBoard: 'snapback',
1091
1189
  dropCenterTime: 'veryFast',
1092
1190
  dropCenterAnimation: 'ease',
1093
1191
  fadeTime: 'fast',
1094
1192
  fadeAnimation: 'ease',
1193
+
1194
+ // Legacy compatibility
1195
+ moveAnimation: 'ease',
1196
+ snapbackAnimation: 'ease',
1197
+
1095
1198
  ratio: 0.9,
1096
1199
  piecesPath: '../assets/themes/default',
1097
1200
  animationStyle: 'simultaneous',
@@ -1186,6 +1289,7 @@ var ChessboardLib = (function (exports) {
1186
1289
  this.position = config.position;
1187
1290
  this.orientation = config.orientation;
1188
1291
  this.mode = config.mode;
1292
+ this.flipMode = this._validateFlipMode(config.flipMode);
1189
1293
  this.dropOffBoard = config.dropOffBoard;
1190
1294
  this.size = config.size;
1191
1295
  this.movableColors = config.movableColors;
@@ -1200,12 +1304,27 @@ var ChessboardLib = (function (exports) {
1200
1304
  this.onDrop = this._validateCallback(config.onDrop);
1201
1305
  this.onSnapbackEnd = this._validateCallback(config.onSnapbackEnd);
1202
1306
 
1203
- // Animation properties
1307
+ // Animation properties (legacy)
1204
1308
  this.moveAnimation = this._setTransitionFunction(config.moveAnimation);
1205
1309
  this.snapbackAnimation = this._setTransitionFunction(config.snapbackAnimation);
1206
1310
  this.dropCenterAnimation = this._setTransitionFunction(config.dropCenterAnimation);
1207
1311
  this.fadeAnimation = this._setTransitionFunction(config.fadeAnimation);
1208
1312
 
1313
+ // Movement configuration (new system)
1314
+ this.moveStyle = this._validateMoveStyle(config.moveStyle);
1315
+ this.moveEasing = this._setTransitionFunction(config.moveEasing);
1316
+ this.moveArcHeight = this._validateNumber(config.moveArcHeight, 0, 1, 'moveArcHeight');
1317
+ this.captureStyle = this._validateCaptureStyle(config.captureStyle);
1318
+ this.captureTime = this._setTime(config.captureTime);
1319
+ this.appearanceStyle = this._validateAppearanceStyle(config.appearanceStyle);
1320
+ this.appearanceTime = this._setTime(config.appearanceTime);
1321
+ this.landingEffect = this._validateLandingEffect(config.landingEffect);
1322
+ this.landingDuration = this._validateNumber(config.landingDuration, 0, 2000, 'landingDuration');
1323
+ this.dragStyle = this._validateDragStyle(config.dragStyle);
1324
+ this.dragScale = this._validateNumber(config.dragScale, 0.5, 2, 'dragScale');
1325
+ this.dragOpacity = this._validateNumber(config.dragOpacity, 0.1, 1, 'dragOpacity');
1326
+ this.snapbackEasing = this._setTransitionFunction(config.snapbackEasing);
1327
+
1209
1328
  // Boolean properties
1210
1329
  this.hints = this._setBoolean(config.hints);
1211
1330
  this.clickable = this._setBoolean(config.clickable);
@@ -1309,6 +1428,111 @@ var ChessboardLib = (function (exports) {
1309
1428
  return delay;
1310
1429
  }
1311
1430
 
1431
+ /**
1432
+ * Validates flip mode
1433
+ * @private
1434
+ * @param {string} mode - Flip mode
1435
+ * @returns {string} Validated mode
1436
+ * @throws {ConfigurationError} If mode is invalid
1437
+ */
1438
+ _validateFlipMode(mode) {
1439
+ if (!mode || !FLIP_MODES[mode]) {
1440
+ throw new ConfigurationError(
1441
+ `Invalid flip mode: ${mode}. Valid options: ${Object.keys(FLIP_MODES).join(', ')}`,
1442
+ 'flipMode',
1443
+ mode
1444
+ );
1445
+ }
1446
+ return mode;
1447
+ }
1448
+
1449
+ /**
1450
+ * Validates move style
1451
+ * @private
1452
+ * @param {string} style - Move style
1453
+ * @returns {string} Validated style
1454
+ */
1455
+ _validateMoveStyle(style) {
1456
+ if (!style || !MOVE_STYLES[style]) {
1457
+ console.warn(`Invalid move style: ${style}. Using 'slide'. Valid: ${Object.keys(MOVE_STYLES).join(', ')}`);
1458
+ return 'slide';
1459
+ }
1460
+ return style;
1461
+ }
1462
+
1463
+ /**
1464
+ * Validates capture style
1465
+ * @private
1466
+ * @param {string} style - Capture style
1467
+ * @returns {string} Validated style
1468
+ */
1469
+ _validateCaptureStyle(style) {
1470
+ if (!style || !CAPTURE_STYLES[style]) {
1471
+ console.warn(`Invalid capture style: ${style}. Using 'fade'. Valid: ${Object.keys(CAPTURE_STYLES).join(', ')}`);
1472
+ return 'fade';
1473
+ }
1474
+ return style;
1475
+ }
1476
+
1477
+ /**
1478
+ * Validates appearance style
1479
+ * @private
1480
+ * @param {string} style - Appearance style
1481
+ * @returns {string} Validated style
1482
+ */
1483
+ _validateAppearanceStyle(style) {
1484
+ if (!style || !APPEARANCE_STYLES[style]) {
1485
+ console.warn(`Invalid appearance style: ${style}. Using 'fade'. Valid: ${Object.keys(APPEARANCE_STYLES).join(', ')}`);
1486
+ return 'fade';
1487
+ }
1488
+ return style;
1489
+ }
1490
+
1491
+ /**
1492
+ * Validates landing effect
1493
+ * @private
1494
+ * @param {string} effect - Landing effect
1495
+ * @returns {string} Validated effect
1496
+ */
1497
+ _validateLandingEffect(effect) {
1498
+ if (!effect || !LANDING_EFFECTS[effect]) {
1499
+ console.warn(`Invalid landing effect: ${effect}. Using 'none'. Valid: ${Object.keys(LANDING_EFFECTS).join(', ')}`);
1500
+ return 'none';
1501
+ }
1502
+ return effect;
1503
+ }
1504
+
1505
+ /**
1506
+ * Validates drag style
1507
+ * @private
1508
+ * @param {string} style - Drag style
1509
+ * @returns {string} Validated style
1510
+ */
1511
+ _validateDragStyle(style) {
1512
+ if (!style || !DRAG_STYLES[style]) {
1513
+ console.warn(`Invalid drag style: ${style}. Using 'smooth'. Valid: ${Object.keys(DRAG_STYLES).join(', ')}`);
1514
+ return 'smooth';
1515
+ }
1516
+ return style;
1517
+ }
1518
+
1519
+ /**
1520
+ * Validates a number within a range
1521
+ * @private
1522
+ * @param {number} value - Value to validate
1523
+ * @param {number} min - Minimum value
1524
+ * @param {number} max - Maximum value
1525
+ * @param {string} name - Property name for error message
1526
+ * @returns {number} Validated value
1527
+ */
1528
+ _validateNumber(value, min, max, name) {
1529
+ if (typeof value !== 'number' || value < min || value > max) {
1530
+ console.warn(`Invalid ${name}: ${value}. Must be between ${min} and ${max}.`);
1531
+ return (min + max) / 2; // Return middle value as default
1532
+ }
1533
+ return value;
1534
+ }
1535
+
1312
1536
  /**
1313
1537
  * Sets a CSS custom property
1314
1538
  * @private
@@ -1414,6 +1638,7 @@ var ChessboardLib = (function (exports) {
1414
1638
  position: this.position,
1415
1639
  orientation: this.orientation,
1416
1640
  mode: this.mode,
1641
+ flipMode: this.flipMode,
1417
1642
  size: this.size,
1418
1643
  draggable: this.draggable,
1419
1644
  hints: this.hints,
@@ -1421,11 +1646,26 @@ var ChessboardLib = (function (exports) {
1421
1646
  movableColors: this.movableColors,
1422
1647
  moveHighlight: this.moveHighlight,
1423
1648
  overHighlight: this.overHighlight,
1424
- moveAnimation: this.moveAnimation,
1649
+ // Movement configuration
1650
+ moveStyle: this.moveStyle,
1651
+ moveEasing: this.moveEasing,
1425
1652
  moveTime: this.moveTime,
1426
- dropOffBoard: this.dropOffBoard,
1653
+ moveArcHeight: this.moveArcHeight,
1654
+ captureStyle: this.captureStyle,
1655
+ captureTime: this.captureTime,
1656
+ appearanceStyle: this.appearanceStyle,
1657
+ appearanceTime: this.appearanceTime,
1658
+ landingEffect: this.landingEffect,
1659
+ landingDuration: this.landingDuration,
1660
+ dragStyle: this.dragStyle,
1661
+ dragScale: this.dragScale,
1662
+ dragOpacity: this.dragOpacity,
1427
1663
  snapbackTime: this.snapbackTime,
1664
+ snapbackEasing: this.snapbackEasing,
1665
+ // Legacy
1666
+ moveAnimation: this.moveAnimation,
1428
1667
  snapbackAnimation: this.snapbackAnimation,
1668
+ dropOffBoard: this.dropOffBoard,
1429
1669
  dropCenterTime: this.dropCenterTime,
1430
1670
  dropCenterAnimation: this.dropCenterAnimation,
1431
1671
  fadeTime: this.fadeTime,
@@ -1630,6 +1870,121 @@ var ChessboardLib = (function (exports) {
1630
1870
  }
1631
1871
  }
1632
1872
 
1873
+ /**
1874
+ * Animate piece appearance with configurable style
1875
+ * @param {string} style - Appearance style: 'fade', 'pulse', 'pop', 'drop', 'instant'
1876
+ * @param {number} duration - Animation duration in ms
1877
+ * @param {Function} callback - Callback when complete
1878
+ */
1879
+ appearAnimate(style, duration, callback) {
1880
+ if (!this.element) {
1881
+ if (callback) callback();
1882
+ return;
1883
+ }
1884
+
1885
+ const element = this.element;
1886
+ const cleanup = () => {
1887
+ if (element) {
1888
+ element.style.opacity = '1';
1889
+ element.style.transform = '';
1890
+ }
1891
+ if (callback) callback();
1892
+ };
1893
+
1894
+ const smoothDecel = 'cubic-bezier(0.33, 1, 0.68, 1)';
1895
+ const springOvershoot = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
1896
+
1897
+ switch (style) {
1898
+ case 'instant':
1899
+ element.style.opacity = '1';
1900
+ cleanup();
1901
+ break;
1902
+
1903
+ case 'fade':
1904
+ if (element.animate) {
1905
+ element.style.opacity = '0';
1906
+ const anim = element.animate([
1907
+ { opacity: 0, transform: 'scale(0.95)' },
1908
+ { opacity: 1, transform: 'scale(1)' }
1909
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
1910
+ anim.onfinish = () => {
1911
+ anim.cancel();
1912
+ cleanup();
1913
+ };
1914
+ } else {
1915
+ element.style.opacity = '0';
1916
+ element.style.transform = 'scale(0.95)';
1917
+ setTimeout(cleanup, duration);
1918
+ }
1919
+ break;
1920
+
1921
+ case 'pulse':
1922
+ if (element.animate) {
1923
+ element.style.opacity = '0';
1924
+ const anim = element.animate([
1925
+ { opacity: 0, transform: 'scale(0.6)', offset: 0 },
1926
+ { opacity: 1, transform: 'scale(1.12)', offset: 0.3 },
1927
+ { opacity: 1, transform: 'scale(0.92)', offset: 0.55 },
1928
+ { opacity: 1, transform: 'scale(1.06)', offset: 0.8 },
1929
+ { opacity: 1, transform: 'scale(1)', offset: 1 }
1930
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
1931
+ anim.onfinish = () => {
1932
+ anim.cancel();
1933
+ cleanup();
1934
+ };
1935
+ } else {
1936
+ element.style.opacity = '0';
1937
+ element.style.transform = 'scale(0.6)';
1938
+ setTimeout(cleanup, duration);
1939
+ }
1940
+ break;
1941
+
1942
+ case 'pop':
1943
+ if (element.animate) {
1944
+ element.style.opacity = '0';
1945
+ const anim = element.animate([
1946
+ { opacity: 0, transform: 'scale(0)', offset: 0 },
1947
+ { opacity: 1, transform: 'scale(1.15)', offset: 0.6 },
1948
+ { opacity: 1, transform: 'scale(1)', offset: 1 }
1949
+ ], { duration, easing: springOvershoot, fill: 'forwards' });
1950
+ anim.onfinish = () => {
1951
+ anim.cancel();
1952
+ cleanup();
1953
+ };
1954
+ } else {
1955
+ element.style.opacity = '0';
1956
+ element.style.transform = 'scale(0)';
1957
+ setTimeout(cleanup, duration);
1958
+ }
1959
+ break;
1960
+
1961
+ case 'drop':
1962
+ if (element.animate) {
1963
+ element.style.opacity = '0';
1964
+ const anim = element.animate([
1965
+ { opacity: 0, transform: 'translateY(-15px) scale(0.95)', offset: 0 },
1966
+ { opacity: 1, transform: 'translateY(3px) scale(1.02)', offset: 0.5 },
1967
+ { opacity: 1, transform: 'translateY(-1px) scale(1)', offset: 0.75 },
1968
+ { opacity: 1, transform: 'translateY(0) scale(1)', offset: 1 }
1969
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
1970
+ anim.onfinish = () => {
1971
+ anim.cancel();
1972
+ cleanup();
1973
+ };
1974
+ } else {
1975
+ element.style.opacity = '0';
1976
+ element.style.transform = 'translateY(-15px) scale(0.95)';
1977
+ setTimeout(cleanup, duration);
1978
+ }
1979
+ break;
1980
+
1981
+ default:
1982
+ this.appearAnimate('fade', duration, callback);
1983
+ return;
1984
+ }
1985
+ }
1986
+
1987
+ /** @deprecated Use appearAnimate() instead */
1633
1988
  fadeIn(duration, speed, transition_f, callback) {
1634
1989
  let start = performance.now();
1635
1990
  let opacity = 0;
@@ -1663,15 +2018,105 @@ var ChessboardLib = (function (exports) {
1663
2018
  if (elapsed < duration) {
1664
2019
  requestAnimationFrame(fade);
1665
2020
  } else {
1666
- if (!piece.element) { console.debug(`[Piece] fadeOut: ${piece.id} - element is null (end)`); if (callback) callback(); return; }
2021
+ if (!piece.element) { if (callback) callback(); return; }
1667
2022
  piece.element.style.opacity = 0;
1668
- console.debug(`[Piece] fadeOut complete: ${piece.id}`);
2023
+ // Remove element from DOM after fade completes
2024
+ if (piece.element.parentNode) {
2025
+ piece.element.parentNode.removeChild(piece.element);
2026
+ }
1669
2027
  if (callback) callback();
1670
2028
  }
1671
2029
  };
1672
2030
  fade();
1673
2031
  }
1674
2032
 
2033
+ /**
2034
+ * Animate piece capture with configurable style
2035
+ * Uses fluid easing for smooth, connected animations
2036
+ * @param {string} style - Capture style: 'fade', 'shrink', 'instant', 'explode'
2037
+ * @param {number} duration - Animation duration in ms
2038
+ * @param {Function} callback - Callback when complete
2039
+ */
2040
+ captureAnimate(style, duration, callback) {
2041
+ if (!this.element) {
2042
+ if (callback) callback();
2043
+ return;
2044
+ }
2045
+
2046
+ const element = this.element;
2047
+ const cleanup = () => {
2048
+ if (element && element.parentNode) {
2049
+ element.parentNode.removeChild(element);
2050
+ }
2051
+ if (callback) callback();
2052
+ };
2053
+
2054
+ // Fluid easing functions
2055
+ const smoothDecel = 'cubic-bezier(0.33, 1, 0.68, 1)';
2056
+ const smoothAccel = 'cubic-bezier(0.32, 0, 0.67, 0)';
2057
+
2058
+ switch (style) {
2059
+ case 'instant':
2060
+ cleanup();
2061
+ break;
2062
+
2063
+ case 'fade':
2064
+ if (element.animate) {
2065
+ // Smooth fade with subtle scale down
2066
+ const anim = element.animate([
2067
+ { opacity: 1, transform: 'scale(1)' },
2068
+ { opacity: 0, transform: 'scale(0.9)' }
2069
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
2070
+ anim.onfinish = cleanup;
2071
+ } else {
2072
+ element.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
2073
+ element.style.opacity = '0';
2074
+ element.style.transform = 'scale(0.9)';
2075
+ setTimeout(cleanup, duration);
2076
+ }
2077
+ break;
2078
+
2079
+ case 'shrink':
2080
+ if (element.animate) {
2081
+ // Smooth shrink with accelerating easing for "sucked in" feel
2082
+ const anim = element.animate([
2083
+ { transform: 'scale(1)', opacity: 1, offset: 0 },
2084
+ { transform: 'scale(0.7)', opacity: 0.6, offset: 0.6 },
2085
+ { transform: 'scale(0)', opacity: 0, offset: 1 }
2086
+ ], { duration, easing: smoothAccel, fill: 'forwards' });
2087
+ anim.onfinish = cleanup;
2088
+ } else {
2089
+ element.style.transition = `transform ${duration}ms ease-in, opacity ${duration}ms ease-in`;
2090
+ element.style.transform = 'scale(0)';
2091
+ element.style.opacity = '0';
2092
+ setTimeout(cleanup, duration);
2093
+ }
2094
+ break;
2095
+
2096
+ case 'explode':
2097
+ if (element.animate) {
2098
+ // Subtle expand with smooth deceleration - less dramatic
2099
+ const anim = element.animate([
2100
+ { transform: 'scale(1)', opacity: 1, offset: 0 },
2101
+ { transform: 'scale(1.15)', opacity: 0.5, offset: 0.5 },
2102
+ { transform: 'scale(1.25)', opacity: 0, offset: 1 }
2103
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
2104
+ anim.onfinish = cleanup;
2105
+ } else {
2106
+ element.style.transition = `transform ${duration}ms ease-out, opacity ${duration}ms ease-out`;
2107
+ element.style.transform = 'scale(1.25)';
2108
+ element.style.opacity = '0';
2109
+ setTimeout(cleanup, duration);
2110
+ }
2111
+ break;
2112
+
2113
+ default:
2114
+ // Default to fade
2115
+ this.captureAnimate('fade', duration, callback);
2116
+ return;
2117
+ }
2118
+ }
2119
+
1675
2120
  setDrag(f) {
1676
2121
  if (!this.element) { console.debug(`[Piece] setDrag: ${this.id} - element is null`); return; }
1677
2122
 
@@ -1711,41 +2156,319 @@ var ChessboardLib = (function (exports) {
1711
2156
  }
1712
2157
  }
1713
2158
 
1714
- translate(to, duration, transition_f, speed, callback = null) {
1715
- if (!this.element) { console.debug(`[Piece] translate: ${this.id} - element is null`); if (callback) callback(); return; }
1716
- let sourceRect = this.element.getBoundingClientRect();
1717
- let targetRect = to.getBoundingClientRect();
1718
- let x_start = sourceRect.left + sourceRect.width / 2;
1719
- let y_start = sourceRect.top + sourceRect.height / 2;
1720
- let x_end = targetRect.left + targetRect.width / 2;
1721
- let y_end = targetRect.top + targetRect.height / 2;
1722
- let dx = x_end - x_start;
1723
- let dy = y_end - y_start;
2159
+ /**
2160
+ * Translate piece to target position with configurable movement style
2161
+ * Uses Chessground-style cubic easing for fluid, natural movement
2162
+ * @param {HTMLElement} to - Target element
2163
+ * @param {number} duration - Animation duration in ms
2164
+ * @param {Function} transition_f - Transition function (unused, for compatibility)
2165
+ * @param {number} speed - Speed factor (unused, for compatibility)
2166
+ * @param {Function} callback - Callback when complete
2167
+ * @param {Object} options - Movement options
2168
+ * @param {string} options.style - Movement style: 'slide', 'arc', 'hop', 'teleport', 'fade'
2169
+ * @param {string} options.easing - CSS easing function
2170
+ * @param {number} options.arcHeight - Arc height ratio (for arc/hop styles)
2171
+ * @param {string} options.landingEffect - Landing effect: 'none', 'bounce', 'pulse', 'settle'
2172
+ * @param {number} options.landingDuration - Landing effect duration in ms
2173
+ */
2174
+ translate(to, duration, transition_f, speed, callback = null, options = {}) {
2175
+ if (!this.element) {
2176
+ console.debug(`[Piece] translate: ${this.id} - element is null`);
2177
+ if (callback) callback();
2178
+ return;
2179
+ }
1724
2180
 
1725
- let keyframes = [
1726
- { transform: 'translate(0, 0)' },
1727
- { transform: `translate(${dx}px, ${dy}px)` }
1728
- ];
2181
+ const style = options.style || 'slide';
2182
+ const arcHeight = options.arcHeight || 0.3;
2183
+ const landingEffect = options.landingEffect || 'none';
2184
+ const landingDuration = options.landingDuration || 150;
2185
+
2186
+ // Map easing names to Chessground-style fluid cubic-bezier curves
2187
+ // Default: smooth deceleration like Lichess/Chessground
2188
+ const easingMap = {
2189
+ 'ease': 'cubic-bezier(0.33, 1, 0.68, 1)', // Chessground default - smooth decel
2190
+ 'linear': 'linear',
2191
+ 'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)', // Smooth acceleration
2192
+ 'ease-out': 'cubic-bezier(0.33, 1, 0.68, 1)', // Smooth deceleration (same as default)
2193
+ 'ease-in-out': 'cubic-bezier(0.65, 0, 0.35, 1)' // Smooth both ways
2194
+ };
2195
+ const easing = easingMap[options.easing] || easingMap['ease'];
2196
+
2197
+ // Handle teleport (instant)
2198
+ if (style === 'teleport' || duration === 0) {
2199
+ if (callback) callback();
2200
+ return;
2201
+ }
2202
+
2203
+ // Handle fade style
2204
+ if (style === 'fade') {
2205
+ this._translateFade(to, duration, easing, landingEffect, landingDuration, callback);
2206
+ return;
2207
+ }
2208
+
2209
+ // Calculate movement vectors
2210
+ const sourceRect = this.element.getBoundingClientRect();
2211
+ const targetRect = to.getBoundingClientRect();
2212
+ const dx = (targetRect.left + targetRect.width / 2) - (sourceRect.left + sourceRect.width / 2);
2213
+ const dy = (targetRect.top + targetRect.height / 2) - (sourceRect.top + sourceRect.height / 2);
2214
+ const distance = Math.sqrt(dx * dx + dy * dy);
2215
+
2216
+ // Build keyframes based on style
2217
+ let keyframes;
2218
+ let animationEasing = easing;
2219
+
2220
+ switch (style) {
2221
+ case 'arc':
2222
+ keyframes = this._buildArcKeyframes(dx, dy, distance, arcHeight);
2223
+ // Arc uses linear easing because the curve is in the keyframes
2224
+ animationEasing = 'linear';
2225
+ break;
2226
+ case 'hop':
2227
+ keyframes = this._buildHopKeyframes(dx, dy, distance, arcHeight);
2228
+ // Hop uses ease-out for natural landing feel
2229
+ animationEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
2230
+ break;
2231
+ case 'slide':
2232
+ default:
2233
+ keyframes = [
2234
+ { transform: 'translate(0, 0)' },
2235
+ { transform: `translate(${dx}px, ${dy}px)` }
2236
+ ];
2237
+ }
1729
2238
 
2239
+ // Animate
1730
2240
  if (this.element.animate) {
1731
- let animation = this.element.animate(keyframes, {
2241
+ const animation = this.element.animate(keyframes, {
1732
2242
  duration: duration,
1733
- easing: 'ease',
1734
- fill: 'none'
2243
+ easing: animationEasing,
2244
+ fill: 'forwards'
1735
2245
  });
1736
2246
 
1737
2247
  animation.onfinish = () => {
1738
- if (!this.element) { console.debug(`[Piece] translate.onfinish: ${this.id} - element is null`); if (callback) callback(); return; }
1739
- if (callback) callback();
2248
+ if (!this.element) {
2249
+ if (callback) callback();
2250
+ return;
2251
+ }
2252
+
2253
+ // Cancel animation and move piece in DOM first
2254
+ animation.cancel();
1740
2255
  if (this.element) this.element.style = '';
1741
- console.debug(`[Piece] translate complete: ${this.id}`);
2256
+
2257
+ // Callback moves piece to new square in DOM
2258
+ if (callback) callback();
2259
+
2260
+ // Apply landing effect AFTER piece is in new position
2261
+ if (landingEffect !== 'none') {
2262
+ // Small delay to ensure DOM is updated
2263
+ requestAnimationFrame(() => {
2264
+ this._applyLandingEffect(landingEffect, landingDuration);
2265
+ });
2266
+ }
1742
2267
  };
1743
2268
  } else {
1744
- this.element.style.transition = `transform ${duration}ms ease`;
2269
+ // Fallback for browsers without Web Animations API
2270
+ this.element.style.transition = `transform ${duration}ms ${animationEasing}`;
1745
2271
  this.element.style.transform = `translate(${dx}px, ${dy}px)`;
2272
+ setTimeout(() => {
2273
+ if (!this.element) {
2274
+ if (callback) callback();
2275
+ return;
2276
+ }
2277
+ if (this.element) this.element.style = '';
2278
+ if (callback) callback();
2279
+
2280
+ // Apply landing effect after DOM update
2281
+ if (landingEffect !== 'none') {
2282
+ requestAnimationFrame(() => {
2283
+ this._applyLandingEffect(landingEffect, landingDuration);
2284
+ });
2285
+ }
2286
+ }, duration);
2287
+ }
2288
+ }
2289
+
2290
+ /**
2291
+ * Build arc-shaped keyframes (smooth parabolic curve)
2292
+ * Uses many keyframes for fluid motion without relying on easing
2293
+ * @private
2294
+ */
2295
+ _buildArcKeyframes(dx, dy, distance, arcHeight) {
2296
+ const lift = distance * arcHeight;
2297
+ const steps = 10;
2298
+ const keyframes = [];
2299
+
2300
+ for (let i = 0; i <= steps; i++) {
2301
+ const t = i / steps;
2302
+ // Smooth easing for horizontal movement (Chessground-style cubic)
2303
+ const easedT = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
2304
+ // Parabolic arc for vertical lift: peaks at t=0.5
2305
+ const arcT = 4 * t * (1 - t); // Parabola: 0 at t=0, 1 at t=0.5, 0 at t=1
2306
+
2307
+ const x = dx * easedT;
2308
+ const y = dy * easedT - lift * arcT;
2309
+
2310
+ keyframes.push({
2311
+ transform: `translate(${x}px, ${y}px)`,
2312
+ offset: t
2313
+ });
2314
+ }
2315
+ return keyframes;
2316
+ }
2317
+
2318
+ /**
2319
+ * Build hop-shaped keyframes (knight-like jump with subtle scale)
2320
+ * More aggressive vertical movement, subtle scale for emphasis
2321
+ * @private
2322
+ */
2323
+ _buildHopKeyframes(dx, dy, distance, arcHeight) {
2324
+ const lift = distance * arcHeight * 1.2;
2325
+ const steps = 12;
2326
+ const keyframes = [];
2327
+
2328
+ for (let i = 0; i <= steps; i++) {
2329
+ const t = i / steps;
2330
+ // Chessground-style cubic easing for smooth acceleration/deceleration
2331
+ const easedT = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
2332
+ // Sharp parabolic hop - peaks earlier for snappier feel
2333
+ const hopT = Math.sin(t * Math.PI); // Sine for smooth hop curve
2334
+ // Subtle scale: peaks at 1.03 at the top of the hop
2335
+ const scale = 1 + 0.03 * hopT;
2336
+
2337
+ const x = dx * easedT;
2338
+ const y = dy * easedT - lift * hopT;
2339
+
2340
+ keyframes.push({
2341
+ transform: `translate(${x}px, ${y}px) scale(${scale})`,
2342
+ offset: t
2343
+ });
2344
+ }
2345
+ return keyframes;
2346
+ }
2347
+
2348
+ /**
2349
+ * Translate using fade effect (smooth crossfade with scale)
2350
+ * @private
2351
+ */
2352
+ _translateFade(to, duration, easing, landingEffect, landingDuration, callback) {
2353
+ const halfDuration = duration / 2;
2354
+ // Use smooth deceleration for fade
2355
+ const fadeEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
2356
+
2357
+ // Fade out with subtle scale down
2358
+ if (this.element.animate) {
2359
+ const fadeOut = this.element.animate([
2360
+ { opacity: 1, transform: 'scale(1)' },
2361
+ { opacity: 0, transform: 'scale(0.95)' }
2362
+ ], { duration: halfDuration, easing: fadeEasing, fill: 'forwards' });
2363
+
2364
+ fadeOut.onfinish = () => {
2365
+ if (!this.element) {
2366
+ if (callback) callback();
2367
+ return;
2368
+ }
2369
+ // Move instantly (hidden)
2370
+ fadeOut.cancel();
2371
+ this.element.style.opacity = '0';
2372
+ this.element.style.transform = 'scale(0.95)';
2373
+
2374
+ // Let parent move the piece in DOM, then fade in
2375
+ if (callback) callback();
2376
+
2377
+ // Fade in at new position with subtle scale up
2378
+ requestAnimationFrame(() => {
2379
+ if (!this.element) return;
2380
+ const fadeIn = this.element.animate([
2381
+ { opacity: 0, transform: 'scale(0.95)' },
2382
+ { opacity: 1, transform: 'scale(1)' }
2383
+ ], { duration: halfDuration, easing: fadeEasing, fill: 'forwards' });
2384
+
2385
+ fadeIn.onfinish = () => {
2386
+ if (this.element) {
2387
+ fadeIn.cancel();
2388
+ this.element.style.opacity = '';
2389
+ this.element.style.transform = '';
2390
+ this._applyLandingEffect(landingEffect, landingDuration);
2391
+ }
2392
+ };
2393
+ });
2394
+ };
2395
+ } else {
2396
+ // Fallback
2397
+ this.element.style.transition = `opacity ${halfDuration}ms ease, transform ${halfDuration}ms ease`;
2398
+ this.element.style.opacity = '0';
2399
+ this.element.style.transform = 'scale(0.95)';
2400
+ setTimeout(() => {
2401
+ if (callback) callback();
2402
+ if (this.element) {
2403
+ this.element.style.opacity = '1';
2404
+ this.element.style.transform = 'scale(1)';
2405
+ setTimeout(() => {
2406
+ if (this.element) this.element.style = '';
2407
+ }, halfDuration);
2408
+ }
2409
+ }, halfDuration);
2410
+ }
2411
+ }
2412
+
2413
+ /**
2414
+ * Apply landing effect after movement completes
2415
+ * Uses spring-like overshoot easing for natural, connected feel
2416
+ * @private
2417
+ */
2418
+ _applyLandingEffect(effect, duration, callback) {
2419
+ if (!this.element || effect === 'none') {
2420
+ if (callback) callback();
2421
+ return;
2422
+ }
2423
+
2424
+ let keyframes;
2425
+ // Overshoot easing for spring-like natural feel
2426
+ let effectEasing = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
2427
+
2428
+ switch (effect) {
2429
+ case 'bounce':
2430
+ // Subtle bounce using spring easing - single smooth bounce
2431
+ keyframes = [
2432
+ { transform: 'translateY(0)', offset: 0 },
2433
+ { transform: 'translateY(-5px)', offset: 0.4 },
2434
+ { transform: 'translateY(0)', offset: 1 }
2435
+ ];
2436
+ // Use ease-out for natural deceleration
2437
+ effectEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
2438
+ break;
2439
+ case 'pulse':
2440
+ // Subtle scale pulse - less aggressive, more refined
2441
+ keyframes = [
2442
+ { transform: 'scale(1)', offset: 0 },
2443
+ { transform: 'scale(1.08)', offset: 0.5 },
2444
+ { transform: 'scale(1)', offset: 1 }
2445
+ ];
2446
+ break;
2447
+ case 'settle':
2448
+ // Minimal settle - piece "clicks" into place
2449
+ keyframes = [
2450
+ { transform: 'scale(1.02)', offset: 0 },
2451
+ { transform: 'scale(1)', offset: 1 }
2452
+ ];
2453
+ effectEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
2454
+ break;
2455
+ default:
2456
+ if (callback) callback();
2457
+ return;
2458
+ }
2459
+
2460
+ if (this.element.animate) {
2461
+ const animation = this.element.animate(keyframes, {
2462
+ duration: duration,
2463
+ easing: effectEasing,
2464
+ fill: 'forwards'
2465
+ });
2466
+ animation.onfinish = () => {
2467
+ if (this.element) animation.cancel();
2468
+ if (callback) callback();
2469
+ };
2470
+ } else {
1746
2471
  if (callback) callback();
1747
- if (this.element) this.element.style = '';
1748
- console.debug(`[Piece] translate complete (no animate): ${this.id}`);
1749
2472
  }
1750
2473
  }
1751
2474
 
@@ -1866,9 +2589,9 @@ var ChessboardLib = (function (exports) {
1866
2589
  }
1867
2590
 
1868
2591
  putPiece(piece) {
1869
- // If there's already a piece, remove it first, but preserve if moving
2592
+ // If there's already a piece, destroy it to avoid orphaned DOM elements
1870
2593
  if (this.piece) {
1871
- this.removePiece(true);
2594
+ this.removePiece(false);
1872
2595
  }
1873
2596
  this.piece = piece;
1874
2597
  if (piece && piece.element) {
@@ -1980,7 +2703,7 @@ var ChessboardLib = (function (exports) {
1980
2703
  }
1981
2704
 
1982
2705
  getColor() {
1983
- return this.piece.getColor();
2706
+ return this.piece ? this.piece.getColor() : null;
1984
2707
  }
1985
2708
 
1986
2709
  check() {
@@ -4638,25 +5361,11 @@ var ChessboardLib = (function (exports) {
4638
5361
  to: move.to.id
4639
5362
  };
4640
5363
 
4641
- console.log('executeMove - move.promotion:', move.promotion);
4642
- console.log('executeMove - move.hasPromotion():', move.hasPromotion());
4643
-
4644
5364
  if (move.hasPromotion()) {
4645
5365
  moveOptions.promotion = move.promotion;
4646
5366
  }
4647
-
4648
- console.log('executeMove - moveOptions:', moveOptions);
4649
-
4650
- const result = game.move(moveOptions);
4651
- console.log('executeMove - result:', result);
4652
-
4653
- // Check what's actually on the board after the move
4654
- if (result) {
4655
- const pieceOnDestination = game.get(move.to.id);
4656
- console.log('executeMove - piece on destination after move:', pieceOnDestination);
4657
- }
4658
-
4659
- return result;
5367
+
5368
+ return game.move(moveOptions);
4660
5369
  }
4661
5370
 
4662
5371
  /**
@@ -4666,27 +5375,19 @@ var ChessboardLib = (function (exports) {
4666
5375
  */
4667
5376
  requiresPromotion(move) {
4668
5377
  const moveKey = `${move.from.id}->${move.to.id}`;
4669
- console.log('Checking if move requires promotion:', moveKey);
4670
-
4671
- // Get detailed stack trace
4672
- const stack = new Error().stack.split('\n');
4673
- console.log('Call stack:', stack[1]);
4674
- console.log('Caller:', stack[2]);
4675
- console.log('Caller 2:', stack[3]);
4676
- console.log('Caller 3:', stack[4]);
4677
-
4678
- // Track recently processed moves to prevent processing the same move multiple times
5378
+
5379
+ // Track recently processed moves - return cached result for duplicates
4679
5380
  const now = Date.now();
4680
- if (this._recentMoves.has(moveKey)) {
4681
- console.log('Move recently processed, skipping duplicate check');
4682
- return false; // Conservative: assume no promotion needed for duplicates
5381
+ if (this._recentMoves.has(moveKey) &&
5382
+ this._lastPromotionCheck === moveKey &&
5383
+ this._lastPromotionResult !== null) {
5384
+ return this._lastPromotionResult;
4683
5385
  }
4684
-
5386
+
4685
5387
  // Debounce identical promotion checks within 100ms
4686
- if (this._lastPromotionCheck === moveKey &&
5388
+ if (this._lastPromotionCheck === moveKey &&
4687
5389
  this._lastPromotionResult !== null &&
4688
5390
  now - this._lastPromotionTime < 100) {
4689
- console.log('Using cached promotion result:', this._lastPromotionResult);
4690
5391
  return this._lastPromotionResult;
4691
5392
  }
4692
5393
 
@@ -4727,53 +5428,41 @@ var ChessboardLib = (function (exports) {
4727
5428
  */
4728
5429
  _doRequiresPromotion(move) {
4729
5430
  if (!this.config.onlyLegalMoves) {
4730
- console.log('Not in legal moves mode, no promotion required');
4731
5431
  return false;
4732
5432
  }
4733
-
5433
+
4734
5434
  const game = this.positionService.getGame();
4735
5435
  if (!game) {
4736
- console.log('No game instance available');
4737
5436
  return false;
4738
5437
  }
4739
-
5438
+
4740
5439
  const piece = game.get(move.from.id);
4741
5440
  if (!piece || piece.type !== 'p') {
4742
- console.log('Not a pawn move, no promotion required');
4743
5441
  return false;
4744
5442
  }
4745
-
5443
+
4746
5444
  const targetRank = move.to.row;
4747
5445
  if (targetRank !== 1 && targetRank !== 8) {
4748
- console.log('Not reaching promotion rank, no promotion required');
4749
5446
  return false;
4750
5447
  }
4751
-
4752
- console.log('Pawn reaching promotion rank - promotion required');
4753
-
4754
- // Additional validation: check if the pawn can actually reach this square
5448
+
5449
+ // Validate pawn move
4755
5450
  if (!this._isPawnMoveValid(move.from, move.to, piece.color)) {
4756
- console.log('Pawn move not valid, no promotion required');
4757
5451
  return false;
4758
5452
  }
4759
-
4760
- // If we get here, it's a pawn move to the promotion rank
4761
- // Check if it's a legal move by using the game's moves() method
5453
+
5454
+ // Check if it's a legal move
4762
5455
  const legalMoves = game.moves({ square: move.from.id, verbose: true });
4763
5456
  const matchingMove = legalMoves.find(m => m.to === move.to.id);
4764
-
5457
+
4765
5458
  if (!matchingMove) {
4766
- console.log('Move not in legal moves list, no promotion required');
4767
5459
  return false;
4768
5460
  }
4769
-
4770
- // If the legal move has a promotion flag or is to a promotion rank, promotion is required
4771
- const requiresPromotion = matchingMove.flags.includes('p') ||
4772
- (piece.color === 'w' && targetRank === 8) ||
4773
- (piece.color === 'b' && targetRank === 1);
4774
-
4775
- console.log('Promotion required:', requiresPromotion);
4776
- return requiresPromotion;
5461
+
5462
+ // Promotion is required if move has promotion flag or reaches promotion rank
5463
+ return matchingMove.flags.includes('p') ||
5464
+ (piece.color === 'w' && targetRank === 8) ||
5465
+ (piece.color === 'b' && targetRank === 1);
4777
5466
  }
4778
5467
 
4779
5468
  /**
@@ -4789,43 +5478,34 @@ var ChessboardLib = (function (exports) {
4789
5478
  const toRank = to.row;
4790
5479
  const fromFile = from.col;
4791
5480
  const toFile = to.col;
4792
-
4793
- console.log(`Validating pawn move: ${from.id} -> ${to.id} (${color})`);
4794
- console.log(`Ranks: ${fromRank} -> ${toRank}, Files: ${fromFile} -> ${toFile}`);
4795
-
4796
- // Direction of pawn movement
5481
+
4797
5482
  const direction = color === 'w' ? 1 : -1;
4798
5483
  const rankDiff = toRank - fromRank;
4799
5484
  const fileDiff = Math.abs(toFile - fromFile);
4800
-
5485
+
4801
5486
  // Pawn can only move forward
4802
5487
  if (rankDiff * direction <= 0) {
4803
- console.log('Invalid: Pawn cannot move backward or stay in place');
4804
5488
  return false;
4805
5489
  }
4806
-
4807
- // Pawn can only move 1 rank at a time (except for double move from starting position)
5490
+
5491
+ // Pawn can only move 1-2 ranks
4808
5492
  if (Math.abs(rankDiff) > 2) {
4809
- console.log('Invalid: Pawn cannot move more than 2 ranks');
4810
5493
  return false;
4811
5494
  }
4812
-
5495
+
4813
5496
  // If moving 2 ranks, must be from starting position
4814
5497
  if (Math.abs(rankDiff) === 2) {
4815
5498
  const startingRank = color === 'w' ? 2 : 7;
4816
5499
  if (fromRank !== startingRank) {
4817
- console.log(`Invalid: Pawn cannot move 2 ranks from rank ${fromRank}`);
4818
5500
  return false;
4819
5501
  }
4820
5502
  }
4821
-
4822
- // Pawn can only move to adjacent files (diagonal capture) or same file (forward move)
5503
+
5504
+ // Pawn can only move to adjacent files or same file
4823
5505
  if (fileDiff > 1) {
4824
- console.log('Invalid: Pawn cannot move more than 1 file');
4825
5506
  return false;
4826
5507
  }
4827
-
4828
- console.log('Pawn move validation passed');
5508
+
4829
5509
  return true;
4830
5510
  }
4831
5511
 
@@ -4866,24 +5546,17 @@ var ChessboardLib = (function (exports) {
4866
5546
  * @private
4867
5547
  */
4868
5548
  _showPromotionInColumn(targetSquare, piece, squares, onPromotionSelect, onPromotionCancel) {
4869
- console.log('Setting up promotion for', targetSquare.id, 'piece color:', piece.color);
4870
-
4871
5549
  // Set up promotion choices starting from border row
4872
5550
  PROMOTION_PIECES.forEach((pieceType, index) => {
4873
5551
  const choiceSquare = this._findPromotionSquare(targetSquare, index, squares);
4874
-
5552
+
4875
5553
  if (choiceSquare) {
4876
5554
  const pieceId = pieceType + piece.color;
4877
5555
  const piecePath = this._getPiecePathForPromotion(pieceId);
4878
-
4879
- console.log('Setting up promotion choice:', pieceType, 'on square:', choiceSquare.id);
4880
-
5556
+
4881
5557
  choiceSquare.putPromotion(piecePath, () => {
4882
- console.log('Promotion choice selected:', pieceType);
4883
5558
  onPromotionSelect(pieceType);
4884
5559
  });
4885
- } else {
4886
- console.log('Could not find square for promotion choice:', pieceType, 'index:', index);
4887
5560
  }
4888
5561
  });
4889
5562
 
@@ -4910,40 +5583,28 @@ var ChessboardLib = (function (exports) {
4910
5583
  _findPromotionSquare(targetSquare, index, squares) {
4911
5584
  const col = targetSquare.col;
4912
5585
  const baseRow = targetSquare.row;
4913
-
4914
- console.log('Looking for promotion square - target:', targetSquare.id, 'index:', index, 'col:', col, 'baseRow:', baseRow);
4915
-
5586
+
4916
5587
  // Calculate row based on index and promotion direction
4917
- // Start from the border row (1 or 8) and go inward
4918
5588
  let row;
4919
5589
  if (baseRow === 8) {
4920
- // White promotion: start from row 8 and go down
4921
5590
  row = 8 - index;
4922
5591
  } else if (baseRow === 1) {
4923
- // Black promotion: start from row 1 and go up
4924
5592
  row = 1 + index;
4925
5593
  } else {
4926
- console.log('Invalid promotion row:', baseRow);
4927
5594
  return null;
4928
5595
  }
4929
-
4930
- console.log('Calculated row:', row);
4931
-
4932
- // Ensure row is within bounds
5596
+
4933
5597
  if (row < 1 || row > 8) {
4934
- console.log('Row out of bounds:', row);
4935
5598
  return null;
4936
5599
  }
4937
-
5600
+
4938
5601
  // Find square by row/col
4939
5602
  for (const square of Object.values(squares)) {
4940
5603
  if (square.col === col && square.row === row) {
4941
- console.log('Found promotion square:', square.id);
4942
5604
  return square;
4943
5605
  }
4944
5606
  }
4945
-
4946
- console.log('No square found for col:', col, 'row:', row);
5607
+
4947
5608
  return null;
4948
5609
  }
4949
5610
 
@@ -5179,50 +5840,61 @@ var ChessboardLib = (function (exports) {
5179
5840
  piece.setDrag(dragFunction(square, piece));
5180
5841
  }
5181
5842
 
5182
- if (fade && this.config.fadeTime > 0) {
5183
- piece.fadeIn(
5184
- this.config.fadeTime,
5185
- this.config.fadeAnimation,
5186
- this._getTransitionTimingFunction(),
5187
- callback
5188
- );
5843
+ if (fade) {
5844
+ const appearanceStyle = this.config.appearanceStyle || 'fade';
5845
+ const duration = this.config.appearanceTime || this.config.fadeTime || 200;
5846
+
5847
+ if (duration > 0 && appearanceStyle !== 'instant') {
5848
+ piece.appearAnimate(appearanceStyle, duration, () => {
5849
+ piece.visible();
5850
+ if (callback) callback();
5851
+ });
5852
+ } else {
5853
+ piece.visible();
5854
+ if (callback) callback();
5855
+ }
5189
5856
  } else {
5857
+ piece.visible();
5190
5858
  if (callback) callback();
5191
5859
  }
5192
-
5193
- piece.visible();
5194
5860
  }
5195
5861
 
5196
5862
  /**
5197
- * Removes a piece from a square with optional fade-out animation
5863
+ * Removes a piece from a square with configurable capture animation
5198
5864
  * @param {Square} square - Source square
5199
- * @param {boolean} [fade=true] - Whether to fade out the piece
5865
+ * @param {boolean} [animate=true] - Whether to animate the removal
5200
5866
  * @param {Function} [callback] - Callback when animation completes
5201
5867
  * @returns {Piece} The removed piece
5202
5868
  * @throws {PieceError} When square has no piece to remove
5203
5869
  */
5204
- removePieceFromSquare(square, fade = true, callback) {
5870
+ removePieceFromSquare(square, animate = true, callback) {
5205
5871
  console.debug(`[PieceService] removePieceFromSquare: ${square.id}`);
5206
5872
  square.check();
5207
5873
 
5208
5874
  const piece = square.piece;
5209
5875
  if (!piece) {
5210
5876
  if (callback) callback();
5211
- throw new PieceError(ERROR_MESSAGES.square_no_piece, null, square.getId());
5877
+ return null;
5212
5878
  }
5213
5879
 
5214
- if (fade && this.config.fadeTime > 0) {
5215
- piece.fadeOut(
5216
- this.config.fadeTime,
5217
- this.config.fadeAnimation,
5218
- this._getTransitionTimingFunction(),
5219
- callback
5220
- );
5880
+ // Always remove piece reference synchronously to prevent stale state
5881
+ // when a new update starts before the animation completes
5882
+ square.piece = null;
5883
+
5884
+ const captureStyle = this.config.captureStyle || 'fade';
5885
+ const duration = this.config.captureTime || this.config.fadeTime || 200;
5886
+
5887
+ if (animate && duration > 0) {
5888
+ // Animate visual element, then destroy
5889
+ piece.captureAnimate(captureStyle, duration, () => {
5890
+ piece.destroy();
5891
+ if (callback) callback();
5892
+ });
5221
5893
  } else {
5894
+ piece.destroy();
5222
5895
  if (callback) callback();
5223
5896
  }
5224
5897
 
5225
- square.removePiece();
5226
5898
  return piece;
5227
5899
  }
5228
5900
 
@@ -5241,12 +5913,22 @@ var ChessboardLib = (function (exports) {
5241
5913
  return;
5242
5914
  }
5243
5915
 
5916
+ // Build movement options from config
5917
+ const moveOptions = {
5918
+ style: this.config.moveStyle || 'slide',
5919
+ easing: this.config.moveEasing || this.config.moveAnimation || 'ease',
5920
+ arcHeight: this.config.moveArcHeight || 0.3,
5921
+ landingEffect: this.config.landingEffect || 'none',
5922
+ landingDuration: this.config.landingDuration || 150
5923
+ };
5924
+
5244
5925
  piece.translate(
5245
5926
  targetSquare,
5246
5927
  duration,
5247
5928
  this._getTransitionTimingFunction(),
5248
5929
  this.config.moveAnimation,
5249
- callback
5930
+ callback,
5931
+ moveOptions
5250
5932
  );
5251
5933
  }
5252
5934
 
@@ -5260,8 +5942,8 @@ var ChessboardLib = (function (exports) {
5260
5942
  */
5261
5943
  translatePiece(move, removeTarget, animate, dragFunction = null, callback = null) {
5262
5944
  console.debug(`[PieceService] translatePiece: ${move.piece.id} from ${move.from.id} to ${move.to.id}`);
5263
- if (!move.piece) {
5264
- console.warn('PieceService.translatePiece: move.piece is null, skipping translation');
5945
+ if (!move.piece || !move.piece.element) {
5946
+ console.warn('PieceService.translatePiece: move.piece or element is null, skipping translation');
5265
5947
  if (callback) callback();
5266
5948
  return;
5267
5949
  }
@@ -5273,6 +5955,12 @@ var ChessboardLib = (function (exports) {
5273
5955
  }
5274
5956
 
5275
5957
  const changeSquareCallback = () => {
5958
+ // If piece element was destroyed (e.g., by a newer update), skip
5959
+ if (!move.piece.element || !move.piece.element.parentNode) {
5960
+ if (callback) callback();
5961
+ return;
5962
+ }
5963
+
5276
5964
  // Check if piece still exists and is on the source square
5277
5965
  if (move.from.piece === move.piece) {
5278
5966
  move.from.removePiece(true); // Preserve the piece when moving
@@ -7651,7 +8339,8 @@ var ChessboardLib = (function (exports) {
7651
8339
  this._handleConstructorError(error);
7652
8340
  }
7653
8341
  this._undoneMoves = [];
7654
- this._updateBoardPieces(true, true); // Forza popolamento DOM subito
8342
+ this._destroyed = false;
8343
+ this._animationTimeouts = [];
7655
8344
  }
7656
8345
 
7657
8346
  /**
@@ -7763,6 +8452,11 @@ var ChessboardLib = (function (exports) {
7763
8452
  this._buildSquares();
7764
8453
  this._addListeners();
7765
8454
  this._updateBoardPieces(true, true); // Initial position load
8455
+
8456
+ // Apply flipped class if initial orientation is black
8457
+ if (this.coordinateService.getOrientation() === 'b' && this.boardService.element) {
8458
+ this.boardService.element.classList.add('flipped');
8459
+ }
7766
8460
  }
7767
8461
 
7768
8462
  /**
@@ -7791,9 +8485,7 @@ var ChessboardLib = (function (exports) {
7791
8485
  * Best practice: always remove squares (destroy JS/DOM) before clearing the board container.
7792
8486
  */
7793
8487
  _buildBoard() {
7794
- console.log('CHIAMATO: _buildBoard');
7795
8488
  if (this._isUndoRedo) {
7796
- console.log('SKIP _buildBoard per undo/redo');
7797
8489
  return;
7798
8490
  }
7799
8491
  // Forza la pulizia completa del contenitore board (DOM)
@@ -7813,9 +8505,7 @@ var ChessboardLib = (function (exports) {
7813
8505
  * @private
7814
8506
  */
7815
8507
  _buildSquares() {
7816
- console.log('CHIAMATO: _buildSquares');
7817
8508
  if (this._isUndoRedo) {
7818
- console.log('SKIP _buildSquares per undo/redo');
7819
8509
  return;
7820
8510
  }
7821
8511
  if (this.boardService && this.boardService.removeSquares) {
@@ -8028,6 +8718,14 @@ var ChessboardLib = (function (exports) {
8028
8718
  const isEnPassant = this.moveService.isEnPassant(gameMove);
8029
8719
 
8030
8720
  if (animate && move.from.piece) {
8721
+ // For simultaneous castle, start rook animation alongside the king
8722
+ const isSimultaneousCastle = isCastle && this.config.animationStyle === 'simultaneous';
8723
+ if (isSimultaneousCastle) {
8724
+ setTimeout(() => {
8725
+ this._handleCastleMove(gameMove, true);
8726
+ }, this.config.simultaneousAnimationDelay);
8727
+ }
8728
+
8031
8729
  this.pieceService.translatePiece(
8032
8730
  move,
8033
8731
  !!move.to.piece, // was there a capture?
@@ -8035,24 +8733,22 @@ var ChessboardLib = (function (exports) {
8035
8733
  this._createDragFunction.bind(this),
8036
8734
  () => {
8037
8735
  // After the main piece animation completes...
8038
- if (isCastle) {
8736
+ // For sequential castle, animate rook AFTER king finishes
8737
+ // For simultaneous, rook was already animated above - don't animate again
8738
+ if (isCastle && !isSimultaneousCastle) {
8039
8739
  this._handleSpecialMoveAnimation(gameMove);
8040
8740
  } else if (isEnPassant) {
8041
8741
  this._handleSpecialMoveAnimation(gameMove);
8042
8742
  }
8043
8743
  // Notify user that the move is fully complete
8044
8744
  this.config.onMoveEnd(gameMove);
8045
- // A final sync to ensure board is perfect
8046
- this._updateBoardPieces(false);
8745
+ // For simultaneous castle, the rook callback will handle the final sync
8746
+ // to avoid interfering with the ongoing rook animation
8747
+ if (!isSimultaneousCastle) {
8748
+ this._updateBoardPieces(false);
8749
+ }
8047
8750
  }
8048
8751
  );
8049
-
8050
- // For simultaneous castle, animate the rook alongside the king
8051
- if (isCastle && this.config.animationStyle === 'simultaneous') {
8052
- setTimeout(() => {
8053
- this._handleCastleMove(gameMove, true);
8054
- }, this.config.simultaneousAnimationDelay);
8055
- }
8056
8752
  } else {
8057
8753
  // If not animating, handle special moves immediately and update the board
8058
8754
  if (isCastle) {
@@ -8105,12 +8801,9 @@ var ChessboardLib = (function (exports) {
8105
8801
  const rookToSquare = this.boardService.getSquare(rookMove.to);
8106
8802
 
8107
8803
  if (!rookFromSquare || !rookToSquare || !rookFromSquare.piece) {
8108
- console.warn('Castle rook move failed - squares or piece not found');
8109
8804
  return;
8110
8805
  }
8111
8806
 
8112
- console.log(`Castle: moving rook from ${rookMove.from} to ${rookMove.to}`);
8113
-
8114
8807
  if (animate) {
8115
8808
  // Always use translatePiece for smooth sliding animation
8116
8809
  const rookPiece = rookFromSquare.piece;
@@ -8142,12 +8835,9 @@ var ChessboardLib = (function (exports) {
8142
8835
 
8143
8836
  const capturedSquareObj = this.boardService.getSquare(capturedSquare);
8144
8837
  if (!capturedSquareObj || !capturedSquareObj.piece) {
8145
- console.warn('En passant captured square not found or empty');
8146
8838
  return;
8147
8839
  }
8148
8840
 
8149
- console.log(`En passant: removing captured pawn from ${capturedSquare}`);
8150
-
8151
8841
  if (animate) {
8152
8842
  // Animate the captured pawn removal
8153
8843
  this.pieceService.removePieceFromSquare(capturedSquareObj, true);
@@ -8168,10 +8858,9 @@ var ChessboardLib = (function (exports) {
8168
8858
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load
8169
8859
  */
8170
8860
  _updateBoardPieces(animation = false, isPositionLoad = false) {
8171
- console.log('CHIAMATO: _updateBoardPieces', { animation, isPositionLoad, isUndoRedo: this._isUndoRedo });
8861
+ if (this._destroyed) return;
8172
8862
  // Check if services are available
8173
8863
  if (!this.positionService || !this.moveService || !this.eventService) {
8174
- console.log('Cannot update board pieces - services not available');
8175
8864
  return;
8176
8865
  }
8177
8866
 
@@ -8234,33 +8923,24 @@ var ChessboardLib = (function (exports) {
8234
8923
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load (affects delay)
8235
8924
  */
8236
8925
  _doUpdateBoardPieces(animation = false, isPositionLoad = false) {
8926
+ if (this._destroyed) return;
8237
8927
  // Skip update if we're in the middle of a promotion
8238
8928
  if (this._isPromoting) {
8239
- console.log('Skipping board update during promotion');
8240
8929
  return;
8241
8930
  }
8242
8931
 
8243
8932
  // Check if services are available
8244
8933
  if (!this.positionService || !this.positionService.getGame()) {
8245
- console.log('Cannot update board pieces - position service not available');
8246
8934
  return;
8247
8935
  }
8248
8936
 
8249
8937
  const squares = this.boardService.getAllSquares();
8250
8938
  const gameStateBefore = this.positionService.getGame().fen();
8251
-
8252
- console.log('_doUpdateBoardPieces - current FEN:', gameStateBefore);
8253
- console.log('_doUpdateBoardPieces - animation:', animation, 'style:', this.config.animationStyle, 'isPositionLoad:', isPositionLoad);
8254
-
8255
- // Determine which animation style to use
8256
8939
  const useSimultaneous = this.config.animationStyle === 'simultaneous';
8257
- console.log('_doUpdateBoardPieces - useSimultaneous:', useSimultaneous);
8258
8940
 
8259
8941
  if (useSimultaneous) {
8260
- console.log('Using simultaneous animation');
8261
- this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad);
8942
+ this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad, animation);
8262
8943
  } else {
8263
- console.log('Using sequential animation');
8264
8944
  this._doSequentialUpdate(squares, gameStateBefore, animation);
8265
8945
  }
8266
8946
  }
@@ -8273,7 +8953,23 @@ var ChessboardLib = (function (exports) {
8273
8953
  * @param {boolean} animation - Whether to animate
8274
8954
  */
8275
8955
  _doSequentialUpdate(squares, gameStateBefore, animation) {
8276
- // Mappa: squareId -> expectedPieceId
8956
+ // Cancel running animations and clean orphaned elements
8957
+ Object.values(squares).forEach(square => {
8958
+ const imgs = square.element.querySelectorAll('img.piece');
8959
+ imgs.forEach(img => {
8960
+ if (img.getAnimations) {
8961
+ img.getAnimations().forEach(anim => anim.cancel());
8962
+ }
8963
+ if (!square.piece || img !== square.piece.element) {
8964
+ img.remove();
8965
+ }
8966
+ });
8967
+ if (square.piece && square.piece.element) {
8968
+ square.piece.element.style = '';
8969
+ square.piece.element.style.opacity = '1';
8970
+ }
8971
+ });
8972
+
8277
8973
  const expectedMap = {};
8278
8974
  Object.values(squares).forEach(square => {
8279
8975
  expectedMap[square.id] = this.positionService.getGamePieceId(square.id);
@@ -8289,12 +8985,13 @@ var ChessboardLib = (function (exports) {
8289
8985
  return;
8290
8986
  }
8291
8987
 
8292
- // Se c'è un pezzo attuale ma non è quello atteso, rimuovilo
8988
+ // Remove current piece if it doesn't match expected
8293
8989
  if (currentPiece && currentPieceId !== expectedPieceId) {
8294
- this.pieceService.removePieceFromSquare(square, animation);
8990
+ // Always remove synchronously to avoid race condition with addition
8991
+ this.pieceService.removePieceFromSquare(square, false);
8295
8992
  }
8296
8993
 
8297
- // Se c'è un pezzo atteso ma non è quello attuale, aggiungilo
8994
+ // Add expected piece if it doesn't match current
8298
8995
  if (expectedPieceId && currentPieceId !== expectedPieceId) {
8299
8996
  const newPiece = this.pieceService.convertPiece(expectedPieceId);
8300
8997
  this.pieceService.addPieceOnSquare(
@@ -8319,9 +9016,43 @@ var ChessboardLib = (function (exports) {
8319
9016
  * @param {Object} squares - All squares
8320
9017
  * @param {string} gameStateBefore - Game state before update
8321
9018
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load
9019
+ * @param {boolean} [animation=true] - Whether to animate
8322
9020
  */
8323
- _doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false) {
8324
- // Matching greedy per distanza minima, robusto
9021
+ _doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false, animation = true) {
9022
+ // Increment generation to invalidate stale animation callbacks
9023
+ this._updateGeneration = (this._updateGeneration || 0) + 1;
9024
+ const generation = this._updateGeneration;
9025
+
9026
+ // Cancel pending animation timeouts from previous update
9027
+ if (this._animationTimeouts) {
9028
+ this._animationTimeouts.forEach(tid => clearTimeout(tid));
9029
+ this._animationTimeouts = [];
9030
+ }
9031
+
9032
+ // Cancel all running animations and force-sync DOM state
9033
+ Object.values(squares).forEach(square => {
9034
+ const imgs = square.element.querySelectorAll('img.piece');
9035
+ imgs.forEach(img => {
9036
+ // Cancel all Web Animations on this element so onfinish callbacks don't fire
9037
+ if (img.getAnimations) {
9038
+ img.getAnimations().forEach(anim => anim.cancel());
9039
+ }
9040
+ // Remove orphaned images not matching current piece
9041
+ if (!square.piece || img !== square.piece.element) {
9042
+ img.remove();
9043
+ }
9044
+ });
9045
+ // Reset current piece element to clean state (remove animation artifacts)
9046
+ if (square.piece && square.piece.element) {
9047
+ square.piece.element.style = '';
9048
+ square.piece.element.style.opacity = '1';
9049
+ // Ensure element is attached to correct square
9050
+ if (!square.element.contains(square.piece.element)) {
9051
+ square.element.appendChild(square.piece.element);
9052
+ }
9053
+ }
9054
+ });
9055
+
8325
9056
  const currentMap = {};
8326
9057
  const expectedMap = {};
8327
9058
 
@@ -8347,35 +9078,16 @@ var ChessboardLib = (function (exports) {
8347
9078
  const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
8348
9079
  let animationIndex = 0;
8349
9080
 
8350
- Object.keys(expectedMap).forEach(key => {
8351
- totalAnimations += Math.max((currentMap[key] || []).length, expectedMap[key].length);
8352
- });
8353
-
8354
- if (totalAnimations === 0) {
8355
- this._addListeners();
8356
- const gameStateAfter = this.positionService.getGame().fen();
8357
- if (gameStateBefore !== gameStateAfter) {
8358
- this.config.onChange(gameStateAfter);
8359
- }
8360
- return;
8361
- }
8362
-
8363
- const onAnimationComplete = () => {
8364
- animationsCompleted++;
8365
- if (animationsCompleted === totalAnimations) {
8366
- this._addListeners();
8367
- const gameStateAfter = this.positionService.getGame().fen();
8368
- if (gameStateBefore !== gameStateAfter) {
8369
- this.config.onChange(gameStateAfter);
8370
- }
8371
- }
8372
- };
9081
+ // First pass: compute matching for all piece types
9082
+ const allRemovals = [];
9083
+ const allAdditions = [];
9084
+ const allMoves = [];
8373
9085
 
8374
9086
  Object.keys(expectedMap).forEach(key => {
8375
9087
  const fromList = (currentMap[key] || []).slice();
8376
9088
  const toList = expectedMap[key].slice();
8377
9089
 
8378
- // 1. Costruisci matrice delle distanze
9090
+ // Build distance matrix
8379
9091
  const distances = [];
8380
9092
  for (let i = 0; i < fromList.length; i++) {
8381
9093
  distances[i] = [];
@@ -8385,10 +9097,9 @@ var ChessboardLib = (function (exports) {
8385
9097
  }
8386
9098
  }
8387
9099
 
8388
- // 2. Matching greedy: abbina i più vicini
9100
+ // Greedy matching: pair closest pieces
8389
9101
  const fromMatched = new Array(fromList.length).fill(false);
8390
9102
  const toMatched = new Array(toList.length).fill(false);
8391
- const moves = [];
8392
9103
 
8393
9104
  while (true) {
8394
9105
  let minDist = Infinity, minI = -1, minJ = -1;
@@ -8404,58 +9115,140 @@ var ChessboardLib = (function (exports) {
8404
9115
  }
8405
9116
  }
8406
9117
  if (minI === -1 || minJ === -1) break;
8407
- // Se la posizione è la stessa, non fare nulla (pezzo unchanged)
9118
+ fromMatched[minI] = true;
9119
+ toMatched[minJ] = true;
9120
+ // Skip unchanged pieces (same square)
8408
9121
  if (fromList[minI].square === toList[minJ].square) {
8409
- fromMatched[minI] = true;
8410
- toMatched[minJ] = true;
8411
9122
  continue;
8412
9123
  }
8413
- // Altrimenti, sposta il pezzo
8414
- moves.push({ from: fromList[minI].square, to: toList[minJ].square, piece: fromList[minI].square.piece });
8415
- fromMatched[minI] = true;
8416
- toMatched[minJ] = true;
9124
+ allMoves.push({ from: fromList[minI].square, to: toList[minJ].square, piece: fromList[minI].square.piece });
9125
+ }
9126
+
9127
+ // Collect unmatched current pieces (to remove)
9128
+ for (let i = 0; i < fromList.length; i++) {
9129
+ if (!fromMatched[i]) {
9130
+ allRemovals.push(fromList[i].square);
9131
+ }
9132
+ }
9133
+
9134
+ // Collect unmatched expected pieces (to add)
9135
+ for (let j = 0; j < toList.length; j++) {
9136
+ if (!toMatched[j]) {
9137
+ allAdditions.push({ square: toList[j].square, key });
9138
+ }
9139
+ }
9140
+ });
9141
+
9142
+ // Also count removals for pieces whose type doesn't exist in expectedMap
9143
+ Object.keys(currentMap).forEach(key => {
9144
+ if (!expectedMap[key]) {
9145
+ currentMap[key].forEach(entry => {
9146
+ allRemovals.push(entry.square);
9147
+ });
9148
+ }
9149
+ });
9150
+
9151
+ // Count only actual animations
9152
+ totalAnimations = allRemovals.length + allAdditions.length + allMoves.length;
9153
+
9154
+ if (totalAnimations === 0) {
9155
+ this._addListeners();
9156
+ const gameStateAfter = this.positionService.getGame().fen();
9157
+ if (gameStateBefore !== gameStateAfter) {
9158
+ this.config.onChange(gameStateAfter);
8417
9159
  }
9160
+ return;
9161
+ }
8418
9162
 
8419
- // 3. Rimuovi i pezzi non abbinati (presenti solo in fromList)
8420
- for (let i = 0; i < fromList.length; i++) {
8421
- if (!fromMatched[i]) {
8422
- setTimeout(() => {
8423
- this.pieceService.removePieceFromSquare(fromList[i].square, true, onAnimationComplete);
8424
- }, animationIndex * animationDelay);
8425
- animationIndex++;
8426
- }
9163
+ // Detach moving pieces from source squares BEFORE any removals/additions
9164
+ // This prevents additions to a move's source square from destroying the piece
9165
+ allMoves.forEach(move => {
9166
+ if (move.from.piece === move.piece) {
9167
+ move.from.removePiece(true); // preserve element, just detach reference
8427
9168
  }
9169
+ });
8428
9170
 
8429
- // 4. Aggiungi i pezzi non abbinati (presenti solo in toList)
8430
- for (let j = 0; j < toList.length; j++) {
8431
- if (!toMatched[j]) {
8432
- setTimeout(() => {
8433
- const newPiece = this.pieceService.convertPiece(key);
8434
- this.pieceService.addPieceOnSquare(
8435
- toList[j].square,
8436
- newPiece,
8437
- true,
8438
- this._createDragFunction.bind(this),
8439
- onAnimationComplete
8440
- );
8441
- }, animationIndex * animationDelay);
8442
- animationIndex++;
9171
+ // No animation: apply all changes synchronously
9172
+ if (!animation) {
9173
+ allRemovals.forEach(square => {
9174
+ this.pieceService.removePieceFromSquare(square, false);
9175
+ });
9176
+ allMoves.forEach(move => {
9177
+ this.pieceService.translatePiece(
9178
+ move, false, false, this._createDragFunction.bind(this)
9179
+ );
9180
+ });
9181
+ allAdditions.forEach(({ square, key }) => {
9182
+ const newPiece = this.pieceService.convertPiece(key);
9183
+ this.pieceService.addPieceOnSquare(
9184
+ square, newPiece, false, this._createDragFunction.bind(this)
9185
+ );
9186
+ });
9187
+ this._addListeners();
9188
+ const gameStateAfter = this.positionService.getGame().fen();
9189
+ if (gameStateBefore !== gameStateAfter) {
9190
+ this.config.onChange(gameStateAfter);
9191
+ }
9192
+ return;
9193
+ }
9194
+
9195
+ // Animated path
9196
+ if (!this._animationTimeouts) this._animationTimeouts = [];
9197
+
9198
+ const onAnimationComplete = () => {
9199
+ // Ignore callbacks from stale/destroyed boards
9200
+ if (this._destroyed || this._updateGeneration !== generation) return;
9201
+ animationsCompleted++;
9202
+ if (animationsCompleted === totalAnimations) {
9203
+ this._addListeners();
9204
+ const gameStateAfter = this.positionService.getGame().fen();
9205
+ if (gameStateBefore !== gameStateAfter) {
9206
+ this.config.onChange(gameStateAfter);
8443
9207
  }
8444
9208
  }
9209
+ };
8445
9210
 
8446
- // 5. Anima i movimenti
8447
- moves.forEach(move => {
8448
- setTimeout(() => {
8449
- this.pieceService.translatePiece(
8450
- move,
8451
- false,
8452
- true,
8453
- this._createDragFunction.bind(this),
8454
- onAnimationComplete
8455
- );
8456
- }, animationIndex * animationDelay);
8457
- animationIndex++;
8458
- });
9211
+ // Dispatch moves first (pieces already detached from source)
9212
+ allMoves.forEach(move => {
9213
+ const tid = setTimeout(() => {
9214
+ if (this._destroyed || this._updateGeneration !== generation) return;
9215
+ this.pieceService.translatePiece(
9216
+ move,
9217
+ false,
9218
+ true,
9219
+ this._createDragFunction.bind(this),
9220
+ onAnimationComplete
9221
+ );
9222
+ }, animationIndex * animationDelay);
9223
+ this._animationTimeouts.push(tid);
9224
+ animationIndex++;
9225
+ });
9226
+
9227
+ // Dispatch removals
9228
+ allRemovals.forEach(square => {
9229
+ const tid = setTimeout(() => {
9230
+ if (this._destroyed || this._updateGeneration !== generation) return;
9231
+ this.pieceService.removePieceFromSquare(square, true, onAnimationComplete);
9232
+ }, animationIndex * animationDelay);
9233
+ this._animationTimeouts.push(tid);
9234
+ animationIndex++;
9235
+ });
9236
+
9237
+ // Dispatch additions
9238
+ allAdditions.forEach(({ square, key }) => {
9239
+ const tid = setTimeout(() => {
9240
+ if (this._destroyed || this._updateGeneration !== generation) return;
9241
+ const newPiece = this.pieceService.convertPiece(key);
9242
+ this.pieceService.addPieceOnSquare(
9243
+ square,
9244
+ newPiece,
9245
+ true,
9246
+ this._createDragFunction.bind(this),
9247
+ onAnimationComplete
9248
+ );
9249
+ }, animationIndex * animationDelay);
9250
+ this._animationTimeouts.push(tid);
9251
+ animationIndex++;
8459
9252
  });
8460
9253
  }
8461
9254
 
@@ -8483,10 +9276,6 @@ var ChessboardLib = (function (exports) {
8483
9276
  }
8484
9277
  });
8485
9278
 
8486
- console.log('Position Analysis:');
8487
- console.log('Current pieces:', Array.from(currentPieces.entries()));
8488
- console.log('Expected pieces:', Array.from(expectedPieces.entries()));
8489
-
8490
9279
  // Identify different types of changes
8491
9280
  const moves = []; // Pieces that can slide to new positions
8492
9281
  const removes = []; // Pieces that need to be removed
@@ -8500,8 +9289,6 @@ var ChessboardLib = (function (exports) {
8500
9289
  const expectedPieceId = expectedPieces.get(square);
8501
9290
 
8502
9291
  if (currentPieceId === expectedPieceId) {
8503
- // Same piece type on same square - no movement needed
8504
- console.log(`UNCHANGED: ${currentPieceId} stays on ${square}`);
8505
9292
  unchanged.push({
8506
9293
  piece: currentPieceId,
8507
9294
  square: square
@@ -8523,7 +9310,6 @@ var ChessboardLib = (function (exports) {
8523
9310
 
8524
9311
  if (availableDestination) {
8525
9312
  const [toSquare, expectedId] = availableDestination;
8526
- console.log(`MOVE: ${currentPieceId} from ${fromSquare} to ${toSquare}`);
8527
9313
  moves.push({
8528
9314
  piece: currentPieceId,
8529
9315
  from: fromSquare,
@@ -8533,8 +9319,6 @@ var ChessboardLib = (function (exports) {
8533
9319
  });
8534
9320
  processedSquares.add(toSquare);
8535
9321
  } else {
8536
- // This piece needs to be removed
8537
- console.log(`REMOVE: ${currentPieceId} from ${fromSquare}`);
8538
9322
  removes.push({
8539
9323
  piece: currentPieceId,
8540
9324
  square: fromSquare,
@@ -8546,7 +9330,6 @@ var ChessboardLib = (function (exports) {
8546
9330
  // Third pass: handle pieces that need to be added
8547
9331
  expectedPieces.forEach((expectedPieceId, toSquare) => {
8548
9332
  if (!processedSquares.has(toSquare)) {
8549
- console.log(`ADD: ${expectedPieceId} to ${toSquare}`);
8550
9333
  adds.push({
8551
9334
  piece: expectedPieceId,
8552
9335
  square: toSquare,
@@ -8572,26 +9355,13 @@ var ChessboardLib = (function (exports) {
8572
9355
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load
8573
9356
  */
8574
9357
  _executeSimultaneousChanges(changeAnalysis, gameStateBefore, isPositionLoad = false) {
8575
- const { moves, removes, adds, unchanged } = changeAnalysis;
8576
-
8577
- console.log(`Position changes analysis:`, {
8578
- moves: moves.length,
8579
- removes: removes.length,
8580
- adds: adds.length,
8581
- unchanged: unchanged.length
8582
- });
8583
-
8584
- // Log unchanged pieces for debugging
8585
- if (unchanged.length > 0) {
8586
- console.log('Pieces staying in place:', unchanged.map(u => `${u.piece} on ${u.square}`));
8587
- }
9358
+ const { moves, removes, adds } = changeAnalysis;
8588
9359
 
8589
9360
  let animationsCompleted = 0;
8590
9361
  const totalAnimations = moves.length + removes.length + adds.length;
8591
9362
 
8592
9363
  // If no animations are needed, complete immediately
8593
9364
  if (totalAnimations === 0) {
8594
- console.log('No animations needed, completing immediately');
8595
9365
  this._addListeners();
8596
9366
 
8597
9367
  // Trigger change event if position changed
@@ -8604,9 +9374,7 @@ var ChessboardLib = (function (exports) {
8604
9374
 
8605
9375
  const onAnimationComplete = () => {
8606
9376
  animationsCompleted++;
8607
- console.log(`Animation completed: ${animationsCompleted}/${totalAnimations}`);
8608
9377
  if (animationsCompleted === totalAnimations) {
8609
- console.log('All simultaneous animations completed');
8610
9378
  this._addListeners();
8611
9379
 
8612
9380
  // Trigger change event if position changed
@@ -8617,45 +9385,33 @@ var ChessboardLib = (function (exports) {
8617
9385
  }
8618
9386
  };
8619
9387
 
8620
- // Determine delay: 0 for position loads, configured delay for normal moves
8621
9388
  const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
8622
- console.log(`Using animation delay: ${animationDelay}ms (position load: ${isPositionLoad})`);
8623
-
8624
9389
  let animationIndex = 0;
8625
9390
 
8626
- // Process moves (pieces sliding to new positions)
9391
+ // Process moves
8627
9392
  moves.forEach(move => {
8628
9393
  const delay = animationIndex * animationDelay;
8629
- console.log(`Scheduling move ${move.piece} from ${move.from} to ${move.to} with delay ${delay}ms`);
8630
-
8631
9394
  setTimeout(() => {
8632
9395
  this._animatePieceMove(move, onAnimationComplete);
8633
9396
  }, delay);
8634
-
8635
9397
  animationIndex++;
8636
9398
  });
8637
9399
 
8638
- // Process removes (pieces disappearing)
9400
+ // Process removes
8639
9401
  removes.forEach(remove => {
8640
9402
  const delay = animationIndex * animationDelay;
8641
- console.log(`Scheduling removal of ${remove.piece} from ${remove.square} with delay ${delay}ms`);
8642
-
8643
9403
  setTimeout(() => {
8644
9404
  this._animatePieceRemoval(remove, onAnimationComplete);
8645
9405
  }, delay);
8646
-
8647
9406
  animationIndex++;
8648
9407
  });
8649
9408
 
8650
- // Process adds (pieces appearing)
9409
+ // Process adds
8651
9410
  adds.forEach(add => {
8652
9411
  const delay = animationIndex * animationDelay;
8653
- console.log(`Scheduling addition of ${add.piece} to ${add.square} with delay ${delay}ms`);
8654
-
8655
9412
  setTimeout(() => {
8656
9413
  this._animatePieceAddition(add, onAnimationComplete);
8657
9414
  }, delay);
8658
-
8659
9415
  animationIndex++;
8660
9416
  });
8661
9417
  }
@@ -8671,23 +9427,16 @@ var ChessboardLib = (function (exports) {
8671
9427
  const piece = fromSquare.piece;
8672
9428
 
8673
9429
  if (!piece) {
8674
- console.warn(`No piece found on ${move.from} for move animation`);
8675
9430
  onComplete();
8676
9431
  return;
8677
9432
  }
8678
9433
 
8679
- console.log(`Animating piece move: ${move.piece} from ${move.from} to ${move.to}`);
8680
-
8681
- // Use translatePiece for smooth sliding animation
8682
9434
  this.pieceService.translatePiece(
8683
9435
  { from: fromSquare, to: toSquare, piece: piece },
8684
- false, // Assume no capture for now
8685
- true, // Always animate
9436
+ false,
9437
+ true,
8686
9438
  this._createDragFunction.bind(this),
8687
- () => {
8688
- console.log(`Piece move animation completed: ${move.piece} to ${move.to}`);
8689
- onComplete();
8690
- }
9439
+ onComplete
8691
9440
  );
8692
9441
  }
8693
9442
 
@@ -8698,12 +9447,7 @@ var ChessboardLib = (function (exports) {
8698
9447
  * @param {Function} onComplete - Callback when animation completes
8699
9448
  */
8700
9449
  _animatePieceRemoval(remove, onComplete) {
8701
- console.log(`Animating piece removal: ${remove.piece} from ${remove.square}`);
8702
-
8703
- this.pieceService.removePieceFromSquare(remove.squareObj, true, () => {
8704
- console.log(`Piece removal animation completed: ${remove.piece} from ${remove.square}`);
8705
- onComplete();
8706
- });
9450
+ this.pieceService.removePieceFromSquare(remove.squareObj, true, onComplete);
8707
9451
  }
8708
9452
 
8709
9453
  /**
@@ -8713,18 +9457,13 @@ var ChessboardLib = (function (exports) {
8713
9457
  * @param {Function} onComplete - Callback when animation completes
8714
9458
  */
8715
9459
  _animatePieceAddition(add, onComplete) {
8716
- console.log(`Animating piece addition: ${add.piece} to ${add.square}`);
8717
-
8718
9460
  const newPiece = this.pieceService.convertPiece(add.piece);
8719
9461
  this.pieceService.addPieceOnSquare(
8720
9462
  add.squareObj,
8721
9463
  newPiece,
8722
9464
  true,
8723
9465
  this._createDragFunction.bind(this),
8724
- () => {
8725
- console.log(`Piece addition animation completed: ${add.piece} to ${add.square}`);
8726
- onComplete();
8727
- }
9466
+ onComplete
8728
9467
  );
8729
9468
  }
8730
9469
 
@@ -8824,7 +9563,7 @@ var ChessboardLib = (function (exports) {
8824
9563
  const animate = opts.animate !== undefined ? opts.animate : true;
8825
9564
  // Use the default starting position from config or fallback
8826
9565
  const startPosition = this.config && this.config.position ? this.config.position : 'start';
8827
- this._updateBoardPieces(animate);
9566
+ // setPosition already calls _updateBoardPieces, don't call it twice
8828
9567
  return this.setPosition(startPosition, { animate });
8829
9568
  }
8830
9569
  /**
@@ -8839,10 +9578,13 @@ var ChessboardLib = (function (exports) {
8839
9578
  return false;
8840
9579
  }
8841
9580
  if (this._clearVisualState) this._clearVisualState();
9581
+
9582
+ // Clear the game state
8842
9583
  this.positionService.getGame().clear();
8843
- if (this._updateBoardPieces) {
8844
- this._updateBoardPieces(animate, true);
8845
- }
9584
+
9585
+ // Let _updateBoardPieces handle removal (no manual loop to avoid race conditions)
9586
+ this._updateBoardPieces(animate, true);
9587
+
8846
9588
  return true;
8847
9589
  }
8848
9590
 
@@ -8879,6 +9621,32 @@ var ChessboardLib = (function (exports) {
8879
9621
  }
8880
9622
  return false;
8881
9623
  }
9624
+ /**
9625
+ * Move a piece from one square to another
9626
+ * @param {string} moveStr - Move in format 'e2e4' or 'e7e8q' (with promotion)
9627
+ * @param {Object} [opts]
9628
+ * @param {boolean} [opts.animate=true]
9629
+ * @returns {Object|boolean} Move result or false if invalid
9630
+ */
9631
+ movePiece(moveStr, opts = {}) {
9632
+ const animate = opts.animate !== false;
9633
+ if (typeof moveStr !== 'string' || moveStr.length < 4) {
9634
+ return false;
9635
+ }
9636
+ const from = moveStr.slice(0, 2);
9637
+ const to = moveStr.slice(2, 4);
9638
+ const promotion = moveStr.length > 4 ? moveStr[4].toLowerCase() : undefined;
9639
+
9640
+ const moveObj = { from, to };
9641
+ if (promotion) moveObj.promotion = promotion;
9642
+
9643
+ const result = this.positionService.getGame().move(moveObj);
9644
+ if (result) {
9645
+ this._updateBoardPieces(animate);
9646
+ }
9647
+ return result || false;
9648
+ }
9649
+
8882
9650
  /**
8883
9651
  * Get legal moves for a square
8884
9652
  * @param {string} square
@@ -8893,10 +9661,10 @@ var ChessboardLib = (function (exports) {
8893
9661
  * @returns {string|null}
8894
9662
  */
8895
9663
  getPiece(square) {
8896
- // Restituisce sempre 'wq' (colore prima, tipo dopo, lowercase) o null
8897
- const sq = this.boardService.getSquare(square);
8898
- if (!sq) return null;
8899
- const piece = sq.piece;
9664
+ // Use game state as source of truth
9665
+ // Returns piece in format 'wq' (color + type)
9666
+ if (!this.positionService || !this.positionService.getGame()) return null;
9667
+ const piece = this.positionService.getGame().get(square);
8900
9668
  if (!piece) return null;
8901
9669
  return (piece.color + piece.type).toLowerCase();
8902
9670
  }
@@ -8957,45 +9725,355 @@ var ChessboardLib = (function (exports) {
8957
9725
  if (!this.validationService.isValidSquare(square)) {
8958
9726
  throw new Error(`[removePiece] Invalid square: ${square}`);
8959
9727
  }
8960
- const squareObj = this.boardService.getSquare(square);
8961
- if (!squareObj) return true;
8962
- if (!squareObj.piece) return true;
8963
- squareObj.piece = null;
9728
+ if (!this.positionService || !this.positionService.getGame()) {
9729
+ return false;
9730
+ }
8964
9731
  const game = this.positionService.getGame();
8965
- game.remove(square);
9732
+ // Remove from game state first (source of truth)
9733
+ const removed = game.remove(square);
9734
+ // Then update the board visually
9735
+ const squareObj = this.boardService.getSquare(square);
9736
+ if (squareObj) {
9737
+ squareObj.piece = null;
9738
+ }
8966
9739
  this._updateBoardPieces(animate);
8967
- return true;
9740
+ return removed !== null;
8968
9741
  }
8969
9742
 
8970
9743
  // --- BOARD CONTROL ---
8971
9744
  /**
8972
9745
  * Flip the board orientation
8973
9746
  * @param {Object} [opts]
8974
- * @param {boolean} [opts.animate=true]
9747
+ * @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
9748
+ * @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
8975
9749
  */
8976
9750
  flipBoard(opts = {}) {
9751
+ const flipMode = opts.mode || this.config.flipMode || 'visual';
9752
+
9753
+ // Update internal orientation state
8977
9754
  if (this.coordinateService && this.coordinateService.flipOrientation) {
8978
9755
  this.coordinateService.flipOrientation();
8979
9756
  }
8980
- if (this._buildBoard) this._buildBoard();
8981
- if (this._buildSquares) this._buildSquares();
8982
- if (this._addListeners) this._addListeners();
8983
- if (this._updateBoardPieces) this._updateBoardPieces(opts.animate !== false);
8984
- console.log('FEN dopo flip:', this.fen(), 'Orientamento:', this.coordinateService.getOrientation());
9757
+
9758
+ const boardElement = this.boardService.element;
9759
+ const isFlipped = this.coordinateService.getOrientation() === 'b';
9760
+
9761
+ switch (flipMode) {
9762
+ case 'visual':
9763
+ // CSS flexbox flip - instant, no piece animation needed
9764
+ this._flipVisual(boardElement, isFlipped);
9765
+ break;
9766
+
9767
+ case 'animate':
9768
+ // Animate pieces to mirrored positions
9769
+ this._flipAnimate(opts.animate !== false);
9770
+ break;
9771
+
9772
+ case 'none':
9773
+ // No visual change - only internal orientation updated
9774
+ // Useful for programmatic orientation without visual feedback
9775
+ break;
9776
+
9777
+ default:
9778
+ this._flipVisual(boardElement, isFlipped);
9779
+ }
9780
+ }
9781
+
9782
+ /**
9783
+ * Visual flip using CSS flexbox (instant)
9784
+ * @private
9785
+ * @param {HTMLElement} boardElement - Board DOM element
9786
+ * @param {boolean} isFlipped - Whether board should be flipped
9787
+ */
9788
+ _flipVisual(boardElement, isFlipped) {
9789
+ if (!boardElement) return;
9790
+
9791
+ if (isFlipped) {
9792
+ boardElement.classList.add('flipped');
9793
+ } else {
9794
+ boardElement.classList.remove('flipped');
9795
+ }
9796
+ }
9797
+
9798
+ /**
9799
+ * Animate flip using FLIP technique (First-Last-Invert-Play)
9800
+ * Same end state as visual mode (CSS flip), but pieces animate smoothly.
9801
+ * @private
9802
+ * @param {boolean} animate - Whether to animate the movement
9803
+ */
9804
+ _flipAnimate(animate) {
9805
+ const boardElement = this.boardService.element;
9806
+ if (!boardElement) return;
9807
+
9808
+ const squares = this.boardService.getAllSquares();
9809
+
9810
+ // FIRST: Record current visual position of every piece
9811
+ const pieceRects = {};
9812
+ for (const [id, square] of Object.entries(squares)) {
9813
+ if (square.piece && square.piece.element) {
9814
+ pieceRects[id] = square.piece.element.getBoundingClientRect();
9815
+ }
9816
+ }
9817
+
9818
+ // LAST: Apply CSS flip (instant) - same as visual mode
9819
+ const isFlipped = this.coordinateService.getOrientation() === 'b';
9820
+ this._flipVisual(boardElement, isFlipped);
9821
+
9822
+ if (!animate || Object.keys(pieceRects).length === 0) return;
9823
+
9824
+ // INVERT + PLAY: Animate each piece from old position to new
9825
+ const duration = this.config.moveTime || 200;
9826
+ const easing = 'cubic-bezier(0.33, 1, 0.68, 1)';
9827
+
9828
+ for (const [id, oldRect] of Object.entries(pieceRects)) {
9829
+ const square = squares[id];
9830
+ if (!square || !square.piece || !square.piece.element) continue;
9831
+
9832
+ const piece = square.piece;
9833
+ const newRect = piece.element.getBoundingClientRect();
9834
+ const dx = oldRect.left - newRect.left;
9835
+ const dy = oldRect.top - newRect.top;
9836
+
9837
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
9838
+
9839
+ if (piece.element.animate) {
9840
+ const anim = piece.element.animate([
9841
+ { transform: `translate(${dx}px, ${dy}px)` },
9842
+ { transform: 'translate(0, 0)' }
9843
+ ], { duration, easing, fill: 'forwards' });
9844
+ anim.onfinish = () => {
9845
+ anim.cancel();
9846
+ if (piece.element) piece.element.style.transform = '';
9847
+ };
9848
+ } else {
9849
+ // setTimeout fallback for jsdom / older browsers
9850
+ piece.element.style.transform = `translate(${dx}px, ${dy}px)`;
9851
+ setTimeout(() => {
9852
+ if (!piece.element) return;
9853
+ piece.element.style.transition = `transform ${duration}ms`;
9854
+ piece.element.style.transform = 'translate(0, 0)';
9855
+ setTimeout(() => {
9856
+ if (!piece.element) return;
9857
+ piece.element.style.transition = '';
9858
+ piece.element.style.transform = '';
9859
+ }, duration);
9860
+ }, 0);
9861
+ }
9862
+ }
9863
+ }
9864
+
9865
+ /**
9866
+ * Set the flip mode at runtime
9867
+ * @param {'visual'|'animate'|'none'} mode - The flip mode to use
9868
+ */
9869
+ setFlipMode(mode) {
9870
+ const validModes = ['visual', 'animate', 'none'];
9871
+ if (!validModes.includes(mode)) {
9872
+ console.warn(`Invalid flip mode: ${mode}. Valid options: ${validModes.join(', ')}`);
9873
+ return;
9874
+ }
9875
+ this.config.flipMode = mode;
9876
+ }
9877
+
9878
+ /**
9879
+ * Get the current flip mode
9880
+ * @returns {string} Current flip mode
9881
+ */
9882
+ getFlipMode() {
9883
+ return this.config.flipMode || 'visual';
9884
+ }
9885
+
9886
+ // --- MOVEMENT CONFIGURATION ---
9887
+
9888
+ /**
9889
+ * Set the movement style
9890
+ * @param {'slide'|'arc'|'hop'|'teleport'|'fade'} style - Movement style
9891
+ */
9892
+ setMoveStyle(style) {
9893
+ const validStyles = ['slide', 'arc', 'hop', 'teleport', 'fade'];
9894
+ if (!validStyles.includes(style)) {
9895
+ console.warn(`Invalid move style: ${style}. Valid: ${validStyles.join(', ')}`);
9896
+ return;
9897
+ }
9898
+ this.config.moveStyle = style;
9899
+ }
9900
+
9901
+ /**
9902
+ * Get the current movement style
9903
+ * @returns {string} Current movement style
9904
+ */
9905
+ getMoveStyle() {
9906
+ return this.config.moveStyle || 'slide';
9907
+ }
9908
+
9909
+ /**
9910
+ * Set the capture animation style
9911
+ * @param {'fade'|'shrink'|'instant'|'explode'} style - Capture style
9912
+ */
9913
+ setCaptureStyle(style) {
9914
+ const validStyles = ['fade', 'shrink', 'instant', 'explode'];
9915
+ if (!validStyles.includes(style)) {
9916
+ console.warn(`Invalid capture style: ${style}. Valid: ${validStyles.join(', ')}`);
9917
+ return;
9918
+ }
9919
+ this.config.captureStyle = style;
9920
+ }
9921
+
9922
+ /**
9923
+ * Get the current capture style
9924
+ * @returns {string} Current capture style
9925
+ */
9926
+ getCaptureStyle() {
9927
+ return this.config.captureStyle || 'fade';
9928
+ }
9929
+
9930
+ /**
9931
+ * Set the appearance animation style
9932
+ * @param {'fade'|'pulse'|'pop'|'drop'|'instant'} style - Appearance style
9933
+ */
9934
+ setAppearanceStyle(style) {
9935
+ const validStyles = ['fade', 'pulse', 'pop', 'drop', 'instant'];
9936
+ if (!validStyles.includes(style)) {
9937
+ console.warn(`Invalid appearance style: ${style}. Valid: ${validStyles.join(', ')}`);
9938
+ return;
9939
+ }
9940
+ this.config.appearanceStyle = style;
9941
+ }
9942
+
9943
+ /**
9944
+ * Get the current appearance style
9945
+ * @returns {string} Current appearance style
9946
+ */
9947
+ getAppearanceStyle() {
9948
+ return this.config.appearanceStyle || 'fade';
9949
+ }
9950
+
9951
+ /**
9952
+ * Set the landing effect
9953
+ * @param {'none'|'bounce'|'pulse'|'settle'} effect - Landing effect
9954
+ */
9955
+ setLandingEffect(effect) {
9956
+ const validEffects = ['none', 'bounce', 'pulse', 'settle'];
9957
+ if (!validEffects.includes(effect)) {
9958
+ console.warn(`Invalid landing effect: ${effect}. Valid: ${validEffects.join(', ')}`);
9959
+ return;
9960
+ }
9961
+ this.config.landingEffect = effect;
9962
+ }
9963
+
9964
+ /**
9965
+ * Get the current landing effect
9966
+ * @returns {string} Current landing effect
9967
+ */
9968
+ getLandingEffect() {
9969
+ return this.config.landingEffect || 'none';
8985
9970
  }
9971
+
9972
+ /**
9973
+ * Set the movement duration
9974
+ * @param {number|string} duration - Duration in ms or preset name ('instant', 'veryFast', 'fast', 'normal', 'slow', 'verySlow')
9975
+ */
9976
+ setMoveTime(duration) {
9977
+ const presets = { instant: 0, veryFast: 100, fast: 200, normal: 400, slow: 600, verySlow: 1000 };
9978
+ if (typeof duration === 'string' && presets[duration] !== undefined) {
9979
+ this.config.moveTime = presets[duration];
9980
+ } else if (typeof duration === 'number' && duration >= 0) {
9981
+ this.config.moveTime = duration;
9982
+ } else {
9983
+ console.warn(`Invalid move time: ${duration}`);
9984
+ }
9985
+ }
9986
+
9987
+ /**
9988
+ * Get the current movement duration
9989
+ * @returns {number} Duration in ms
9990
+ */
9991
+ getMoveTime() {
9992
+ return this.config.moveTime;
9993
+ }
9994
+
9995
+ /**
9996
+ * Set the easing function for movements
9997
+ * @param {string} easing - CSS easing function
9998
+ */
9999
+ setMoveEasing(easing) {
10000
+ const validEasings = ['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out'];
10001
+ if (!validEasings.includes(easing)) {
10002
+ console.warn(`Invalid easing: ${easing}. Valid: ${validEasings.join(', ')}`);
10003
+ return;
10004
+ }
10005
+ this.config.moveEasing = easing;
10006
+ }
10007
+
10008
+ /**
10009
+ * Configure multiple movement settings at once
10010
+ * @param {Object} options - Movement configuration
10011
+ * @param {string} [options.style] - Movement style
10012
+ * @param {string} [options.captureStyle] - Capture animation style
10013
+ * @param {string} [options.landingEffect] - Landing effect
10014
+ * @param {number|string} [options.duration] - Movement duration
10015
+ * @param {string} [options.easing] - Easing function
10016
+ * @param {number} [options.arcHeight] - Arc height for arc/hop styles (0-1)
10017
+ */
10018
+ configureMovement(options) {
10019
+ if (options.style) this.setMoveStyle(options.style);
10020
+ if (options.captureStyle) this.setCaptureStyle(options.captureStyle);
10021
+ if (options.appearanceStyle) this.setAppearanceStyle(options.appearanceStyle);
10022
+ if (options.landingEffect) this.setLandingEffect(options.landingEffect);
10023
+ if (options.duration !== undefined) this.setMoveTime(options.duration);
10024
+ if (options.easing) this.setMoveEasing(options.easing);
10025
+ if (options.arcHeight !== undefined) {
10026
+ this.config.moveArcHeight = Math.max(0, Math.min(1, options.arcHeight));
10027
+ }
10028
+ }
10029
+
10030
+ /**
10031
+ * Get all movement configuration
10032
+ * @returns {Object} Current movement configuration
10033
+ */
10034
+ getMovementConfig() {
10035
+ return {
10036
+ style: this.config.moveStyle || 'slide',
10037
+ captureStyle: this.config.captureStyle || 'fade',
10038
+ appearanceStyle: this.config.appearanceStyle || 'fade',
10039
+ landingEffect: this.config.landingEffect || 'none',
10040
+ duration: this.config.moveTime,
10041
+ easing: this.config.moveEasing || 'ease',
10042
+ arcHeight: this.config.moveArcHeight || 0.3
10043
+ };
10044
+ }
10045
+
8986
10046
  /**
8987
10047
  * Set the board orientation
8988
10048
  * @param {'w'|'b'} color
8989
10049
  * @param {Object} [opts]
8990
- * @param {boolean} [opts.animate=true]
10050
+ * @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
10051
+ * @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
8991
10052
  */
8992
10053
  setOrientation(color, opts = {}) {
8993
10054
  if (this.validationService.isValidOrientation(color)) {
8994
- this.coordinateService.setOrientation(color);
8995
- if (this._buildBoard) this._buildBoard();
8996
- if (this._buildSquares) this._buildSquares();
8997
- if (this._addListeners) this._addListeners();
8998
- if (this._updateBoardPieces) this._updateBoardPieces(opts.animate !== false);
10055
+ const currentOrientation = this.coordinateService.getOrientation();
10056
+ if (currentOrientation !== color) {
10057
+ this.coordinateService.setOrientation(color);
10058
+
10059
+ const flipMode = opts.mode || this.config.flipMode || 'visual';
10060
+ const boardElement = this.boardService.element;
10061
+ const isFlipped = color === 'b';
10062
+
10063
+ switch (flipMode) {
10064
+ case 'visual':
10065
+ this._flipVisual(boardElement, isFlipped);
10066
+ break;
10067
+ case 'animate':
10068
+ this._flipAnimate(opts.animate !== false);
10069
+ break;
10070
+ case 'none':
10071
+ // No visual change
10072
+ break;
10073
+ default:
10074
+ this._flipVisual(boardElement, isFlipped);
10075
+ }
10076
+ }
8999
10077
  }
9000
10078
  return this.coordinateService.getOrientation();
9001
10079
  }
@@ -9113,7 +10191,40 @@ var ChessboardLib = (function (exports) {
9113
10191
  /**
9114
10192
  * Destroy the board and cleanup
9115
10193
  */
9116
- destroy() { /* TODO: robust destroy logic */ }
10194
+ destroy() {
10195
+ this._destroyed = true;
10196
+
10197
+ // Remove all event listeners
10198
+ if (this.eventService) {
10199
+ this.eventService.removeAllListeners();
10200
+ this.eventService.destroy();
10201
+ }
10202
+
10203
+ // Clear all timeouts
10204
+ if (this._updateTimeout) {
10205
+ clearTimeout(this._updateTimeout);
10206
+ this._updateTimeout = null;
10207
+ }
10208
+
10209
+ // Clear all animation timeouts
10210
+ if (this._animationTimeouts) {
10211
+ this._animationTimeouts.forEach(tid => clearTimeout(tid));
10212
+ this._animationTimeouts = [];
10213
+ }
10214
+
10215
+ // Destroy services
10216
+ if (this.moveService) this.moveService.destroy();
10217
+ if (this.animationService && this.animationService.destroy) this.animationService.destroy();
10218
+ if (this.pieceService && this.pieceService.destroy) this.pieceService.destroy();
10219
+ if (this.boardService && this.boardService.destroy) this.boardService.destroy();
10220
+ if (this.positionService && this.positionService.destroy) this.positionService.destroy();
10221
+ if (this.coordinateService && this.coordinateService.destroy) this.coordinateService.destroy();
10222
+ if (this.validationService) this.validationService.destroy();
10223
+ if (this.config && this.config.destroy) this.config.destroy();
10224
+
10225
+ // Clear references
10226
+ this._cleanup();
10227
+ }
9117
10228
  /**
9118
10229
  * Rebuild the board
9119
10230
  */
@@ -9129,7 +10240,11 @@ var ChessboardLib = (function (exports) {
9129
10240
  * Set new config
9130
10241
  * @param {Object} newConfig
9131
10242
  */
9132
- setConfig(newConfig) { this.setConfig(newConfig); }
10243
+ setConfig(newConfig) {
10244
+ if (this.config && typeof this.config.update === 'function') {
10245
+ this.config.update(newConfig);
10246
+ }
10247
+ }
9133
10248
 
9134
10249
  // --- ALIASES/DEPRECATED ---
9135
10250
  /**
@@ -9164,17 +10279,10 @@ var ChessboardLib = (function (exports) {
9164
10279
  }
9165
10280
 
9166
10281
  /**
9167
- * Gets the current position as an object
9168
- * @returns {Object} Position object
9169
- */
9170
- position() {
9171
- return this.positionService.getPosition();
9172
- }
9173
-
9174
- /**
9175
- * Sets a new position
9176
- * @param {string|Object} position - New position
9177
- * @param {boolean} [animate=true] - Whether to animate
10282
+ * Gets or sets the current position
10283
+ * @param {string|Object} [position] - Position to set (FEN or object). If omitted, returns current position.
10284
+ * @param {boolean} [animate=true] - Whether to animate when setting
10285
+ * @returns {Object} Current position object (when getting)
9178
10286
  */
9179
10287
  position(position, animate = true) {
9180
10288
  if (position === undefined) {
@@ -9437,34 +10545,21 @@ var ChessboardLib = (function (exports) {
9437
10545
  // Ensure all public API methods from README are present and routed
9438
10546
  insert(square, piece) { return this.putPiece(piece, square); }
9439
10547
  get(square) { return this.getPiece(square); }
9440
- position(position, color) {
9441
- if (color) this.setOrientation(color);
9442
- return this.setPosition(position);
9443
- }
9444
- flip(animation = true) { return this.flipBoard({ animate: animation }); }
10548
+ // Note: position() is defined above at line ~1684 with getter/setter functionality
9445
10549
  build() { return this._initialize(); }
9446
10550
  resize(value) { return this.resizeBoard(value); }
9447
- destroy() { return this._cleanup(); }
9448
10551
  piece(square) { return this.getPiece(square); }
9449
- highlight(square) { return true; }
9450
- dehighlight(square) { return true; }
9451
- turn() { return this.positionService.getGame().turn(); }
9452
10552
  ascii() { return this.positionService.getGame().ascii(); }
9453
10553
  board() { return this.positionService.getGame().board(); }
9454
10554
  getCastlingRights(color) { return this.positionService.getGame().getCastlingRights(color); }
9455
10555
  getComment() { return this.positionService.getGame().getComment(); }
9456
10556
  getComments() { return this.positionService.getGame().getComments(); }
9457
- history(options = {}) { return this.positionService.getGame().history(options); }
9458
10557
  lastMove() { return this.positionService.getGame().lastMove(); }
9459
10558
  moveNumber() { return this.positionService.getGame().moveNumber(); }
9460
10559
  moves(options = {}) { return this.positionService.getGame().moves(options); }
9461
- pgn(options = {}) { return this.positionService.getGame().pgn(options); }
9462
10560
  squareColor(squareId) { return this.boardService.getSquare(squareId).isWhite() ? 'light' : 'dark'; }
9463
- isCheckmate() { return this.positionService.getGame().isCheckmate(); }
9464
- isDraw() { return this.positionService.getGame().isDraw(); }
9465
10561
  isDrawByFiftyMoves() { return this.positionService.getGame().isDrawByFiftyMoves(); }
9466
10562
  isInsufficientMaterial() { return this.positionService.getGame().isInsufficientMaterial(); }
9467
- isGameOver() { return this.positionService.getGame().isGameOver(); }
9468
10563
  isStalemate() { return this.positionService.getGame().isStalemate(); }
9469
10564
  isThreefoldRepetition() { return this.positionService.getGame().isThreefoldRepetition(); }
9470
10565
  load(fen, options = {}, animation = true) { return this.setPosition(fen, { ...options, animate: animation }); }