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