@lichess-org/chessground 9.7.2 → 9.8.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.
package/src/premove.ts CHANGED
@@ -1,29 +1,14 @@
1
1
  import * as util from './util.js';
2
2
  import * as cg from './types.js';
3
3
  import { HeadlessState } from './state.js';
4
+ import { Mobility, MobilityContext } from './types.js';
4
5
 
5
- type MobilityContext = {
6
- pos1: cg.Pos;
7
- pos2: cg.Pos;
8
- allPieces: cg.Pieces;
9
- friendlies: cg.Pieces;
10
- enemies: cg.Pieces;
11
- unrestrictedPremoves: boolean;
12
- color: cg.Color;
13
- canCastle: boolean;
14
- rookFilesFriendlies: number[];
15
- lastMove: cg.Key[] | undefined;
16
- };
17
-
18
- type Mobility = (ctx: MobilityContext) => boolean;
19
-
20
- const isDestOccupiedByFriendly = (ctx: MobilityContext): boolean =>
21
- ctx.friendlies.has(util.pos2key(ctx.pos2));
6
+ const isDestOccupiedByFriendly = (ctx: MobilityContext): boolean => ctx.friendlies.has(ctx.dest.key);
22
7
 
23
- const isDestOccupiedByEnemy = (ctx: MobilityContext): boolean => ctx.enemies.has(util.pos2key(ctx.pos2));
8
+ const isDestOccupiedByEnemy = (ctx: MobilityContext): boolean => ctx.enemies.has(ctx.dest.key);
24
9
 
25
- const anyPieceBetween = (pos1: cg.Pos, pos2: cg.Pos, pieces: cg.Pieces): boolean =>
26
- util.squaresBetween(...pos1, ...pos2).some(s => pieces.has(s));
10
+ const anyPieceBetween = (orig: cg.Pos, dest: cg.Pos, pieces: cg.Pieces): boolean =>
11
+ util.squaresBetween(...orig, ...dest).some(s => pieces.has(s));
27
12
 
28
13
  const canEnemyPawnAdvanceToSquare = (pawnStart: cg.Key, dest: cg.Key, ctx: MobilityContext): boolean => {
29
14
  const piece = ctx.enemies.get(pawnStart);
@@ -53,10 +38,10 @@ const canEnemyPawnCaptureOnSquare = (pawnStart: cg.Key, dest: cg.Key, ctx: Mobil
53
38
  };
54
39
 
55
40
  const canSomeEnemyPawnAdvanceToDest = (ctx: MobilityContext): boolean =>
56
- [...ctx.enemies.keys()].some(key => canEnemyPawnAdvanceToSquare(key, util.pos2key(ctx.pos2), ctx));
41
+ [...ctx.enemies.keys()].some(key => canEnemyPawnAdvanceToSquare(key, ctx.dest.key, ctx));
57
42
 
58
43
  const isDestControlledByEnemy = (ctx: MobilityContext, pieceRolesExclude?: cg.Role[]): boolean => {
59
- const square: cg.Pos = ctx.pos2;
44
+ const square: cg.Pos = ctx.dest.pos;
60
45
  return [...ctx.enemies].some(([key, piece]) => {
61
46
  const piecePos = util.key2pos(key);
62
47
  return (
@@ -74,48 +59,58 @@ const isDestControlledByEnemy = (ctx: MobilityContext, pieceRolesExclude?: cg.Ro
74
59
 
75
60
  const isFriendlyOnDestAndAttacked = (ctx: MobilityContext): boolean =>
76
61
  isDestOccupiedByFriendly(ctx) &&
77
- (canBeCapturedBySomeEnemyEnPassant(util.pos2key(ctx.pos2), ctx.friendlies, ctx.enemies, ctx.lastMove) ||
62
+ (canBeCapturedBySomeEnemyEnPassant(ctx.dest.key, ctx.friendlies, ctx.enemies, ctx.lastMove) ||
78
63
  isDestControlledByEnemy(ctx));
79
64
 
80
65
  const canBeCapturedBySomeEnemyEnPassant = (
81
- potentialSquareOfFriendlyPawn: cg.Key,
66
+ potentialSquareOfFriendlyPawn: cg.Key | undefined,
82
67
  friendlies: cg.Pieces,
83
68
  enemies: cg.Pieces,
84
69
  lastMove?: cg.Key[],
85
70
  ): boolean => {
86
- if (lastMove && potentialSquareOfFriendlyPawn !== lastMove[1]) return false;
71
+ if (!potentialSquareOfFriendlyPawn || (lastMove && potentialSquareOfFriendlyPawn !== lastMove[1]))
72
+ return false;
87
73
  const pos = util.key2pos(potentialSquareOfFriendlyPawn);
88
74
  const friendly = friendlies.get(potentialSquareOfFriendlyPawn);
89
75
  return (
90
76
  friendly?.role === 'pawn' &&
91
77
  pos[1] === (friendly.color === 'white' ? 3 : 4) &&
92
78
  (!lastMove || util.diff(util.key2pos(lastMove[0])[1], pos[1]) === 2) &&
93
- [1, -1].some(delta => enemies.get(util.pos2key([pos[0] + delta, pos[1]]))?.role === 'pawn')
79
+ [1, -1].some(delta => {
80
+ const k = util.pos2key([pos[0] + delta, pos[1]]);
81
+ return !!k && enemies.get(k)?.role === 'pawn';
82
+ })
94
83
  );
95
84
  };
96
85
 
97
- const isPathClearEnoughOfFriendliesForPremove = (ctx: MobilityContext): boolean => {
86
+ const isPathClearEnoughOfFriendliesForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => {
98
87
  if (ctx.unrestrictedPremoves) return true;
99
- const squaresBetween = util.squaresBetween(...ctx.pos1, ...ctx.pos2);
88
+ const squaresBetween = util.squaresBetween(...ctx.orig.pos, ...ctx.dest.pos);
89
+ if (isPawnAdvance) squaresBetween.push(ctx.dest.key);
100
90
  const squaresOfFriendliesBetween = squaresBetween.filter(s => ctx.friendlies.has(s));
91
+ if (!squaresOfFriendliesBetween.length) return true;
92
+ const firstSquareOfFriendliesBetween = squaresOfFriendliesBetween[0];
93
+ const nextSquare = util.squareShiftedVertically(
94
+ firstSquareOfFriendliesBetween,
95
+ ctx.color === 'white' ? -1 : 1,
96
+ );
101
97
  return (
102
- !squaresOfFriendliesBetween.length ||
103
- (squaresOfFriendliesBetween.length === 1 &&
104
- canBeCapturedBySomeEnemyEnPassant(
105
- squaresOfFriendliesBetween[0],
106
- ctx.friendlies,
107
- ctx.enemies,
108
- ctx.lastMove,
109
- ) &&
110
- !squaresBetween.includes(
111
- util.squareShiftedVertically(squaresOfFriendliesBetween[0], ctx.color === 'white' ? -1 : 1),
112
- ))
98
+ squaresOfFriendliesBetween.length === 1 &&
99
+ canBeCapturedBySomeEnemyEnPassant(
100
+ firstSquareOfFriendliesBetween,
101
+ ctx.friendlies,
102
+ ctx.enemies,
103
+ ctx.lastMove,
104
+ ) &&
105
+ !!nextSquare &&
106
+ !squaresBetween.includes(nextSquare)
113
107
  );
114
108
  };
115
109
 
116
- const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext): boolean => {
110
+ const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => {
117
111
  if (ctx.unrestrictedPremoves) return true;
118
- const squaresBetween = util.squaresBetween(...ctx.pos1, ...ctx.pos2);
112
+ const squaresBetween = util.squaresBetween(...ctx.orig.pos, ...ctx.dest.pos);
113
+ if (isPawnAdvance) squaresBetween.push(ctx.dest.key);
119
114
  const squaresOfEnemiesBetween = squaresBetween.filter(s => ctx.enemies.has(s));
120
115
  if (squaresOfEnemiesBetween.length > 1) return false;
121
116
  if (!squaresOfEnemiesBetween.length) return true;
@@ -125,36 +120,38 @@ const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext): boolean =>
125
120
 
126
121
  const enemyStep = enemy.color === 'white' ? 1 : -1;
127
122
  const squareAbove = util.squareShiftedVertically(enemySquare, enemyStep);
128
- const enemyPawnDests: cg.Key[] = [
129
- ...util.adjacentSquares(squareAbove).filter(s => canEnemyPawnCaptureOnSquare(enemySquare, s, ctx)),
130
- ...[squareAbove, util.squareShiftedVertically(squareAbove, enemyStep)].filter(
131
- s => s && canEnemyPawnAdvanceToSquare(enemySquare, s, ctx),
132
- ),
133
- ];
134
- const badSquares = [...squaresBetween, util.pos2key(ctx.pos1)];
123
+ const enemyPawnDests: cg.Key[] = squareAbove
124
+ ? [
125
+ ...util.adjacentSquares(squareAbove).filter(s => canEnemyPawnCaptureOnSquare(enemySquare, s, ctx)),
126
+ ...[squareAbove, util.squareShiftedVertically(squareAbove, enemyStep)]
127
+ .filter(s => !!s)
128
+ .filter(s => canEnemyPawnAdvanceToSquare(enemySquare, s, ctx)),
129
+ ]
130
+ : [];
131
+ const badSquares = [...squaresBetween, ctx.orig.key];
135
132
  return enemyPawnDests.some(square => !badSquares.includes(square));
136
133
  };
137
134
 
138
- const isPathClearEnoughForPremove = (ctx: MobilityContext): boolean =>
139
- isPathClearEnoughOfFriendliesForPremove(ctx) && isPathClearEnoughOfEnemiesForPremove(ctx);
135
+ const isPathClearEnoughForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean =>
136
+ isPathClearEnoughOfFriendliesForPremove(ctx, isPawnAdvance) &&
137
+ isPathClearEnoughOfEnemiesForPremove(ctx, isPawnAdvance);
140
138
 
141
139
  const pawn: Mobility = (ctx: MobilityContext) => {
142
140
  const step = ctx.color === 'white' ? 1 : -1;
143
- if (util.diff(ctx.pos1[0], ctx.pos2[0]) > 1) return false;
144
- if (!util.diff(ctx.pos1[0], ctx.pos2[0])) {
141
+ if (util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) > 1) return false;
142
+ if (!util.diff(ctx.orig.pos[0], ctx.dest.pos[0]))
145
143
  return (
146
- util.pawnDirAdvance(...ctx.pos1, ...ctx.pos2, ctx.color === 'white') &&
147
- isPathClearEnoughForPremove({ ...ctx, pos2: [ctx.pos2[0], ctx.pos2[1] + step] })
144
+ util.pawnDirAdvance(...ctx.orig.pos, ...ctx.dest.pos, ctx.color === 'white') &&
145
+ isPathClearEnoughForPremove(ctx, true)
148
146
  );
149
- }
150
- if (ctx.pos2[1] !== ctx.pos1[1] + step) return false;
147
+ if (ctx.dest.pos[1] !== ctx.orig.pos[1] + step) return false;
151
148
  if (ctx.unrestrictedPremoves || isDestOccupiedByEnemy(ctx)) return true;
152
149
  if (isDestOccupiedByFriendly(ctx)) return isDestControlledByEnemy(ctx);
153
150
  else
154
151
  return (
155
152
  canSomeEnemyPawnAdvanceToDest(ctx) ||
156
153
  canBeCapturedBySomeEnemyEnPassant(
157
- util.pos2key([ctx.pos2[0], ctx.pos2[1] + step]),
154
+ util.pos2key([ctx.dest.pos[0], ctx.dest.pos[1] + step]),
158
155
  ctx.friendlies,
159
156
  ctx.enemies,
160
157
  ctx.lastMove,
@@ -164,37 +161,37 @@ const pawn: Mobility = (ctx: MobilityContext) => {
164
161
  };
165
162
 
166
163
  const knight: Mobility = (ctx: MobilityContext) =>
167
- util.knightDir(...ctx.pos1, ...ctx.pos2) &&
164
+ util.knightDir(...ctx.orig.pos, ...ctx.dest.pos) &&
168
165
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx));
169
166
 
170
167
  const bishop: Mobility = (ctx: MobilityContext) =>
171
- util.bishopDir(...ctx.pos1, ...ctx.pos2) &&
172
- isPathClearEnoughForPremove(ctx) &&
168
+ util.bishopDir(...ctx.orig.pos, ...ctx.dest.pos) &&
169
+ isPathClearEnoughForPremove(ctx, false) &&
173
170
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx));
174
171
 
175
172
  const rook: Mobility = (ctx: MobilityContext) =>
176
- util.rookDir(...ctx.pos1, ...ctx.pos2) &&
177
- isPathClearEnoughForPremove(ctx) &&
173
+ util.rookDir(...ctx.orig.pos, ...ctx.dest.pos) &&
174
+ isPathClearEnoughForPremove(ctx, false) &&
178
175
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx));
179
176
 
180
177
  const queen: Mobility = (ctx: MobilityContext) => bishop(ctx) || rook(ctx);
181
178
 
182
179
  const king: Mobility = (ctx: MobilityContext) =>
183
- (util.kingDirNonCastling(...ctx.pos1, ...ctx.pos2) &&
180
+ (util.kingDirNonCastling(...ctx.orig.pos, ...ctx.dest.pos) &&
184
181
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx))) ||
185
182
  (ctx.canCastle &&
186
- ctx.pos1[1] === ctx.pos2[1] &&
187
- ctx.pos1[1] === (ctx.color === 'white' ? 0 : 7) &&
188
- ((ctx.pos1[0] === 4 &&
189
- ((ctx.pos2[0] === 2 && ctx.rookFilesFriendlies.includes(0)) ||
190
- (ctx.pos2[0] === 6 && ctx.rookFilesFriendlies.includes(7)))) ||
191
- ctx.rookFilesFriendlies.includes(ctx.pos2[0])) &&
183
+ ctx.orig.pos[1] === ctx.dest.pos[1] &&
184
+ ctx.orig.pos[1] === (ctx.color === 'white' ? 0 : 7) &&
185
+ ((ctx.orig.pos[0] === 4 &&
186
+ ((ctx.dest.pos[0] === 2 && ctx.rookFilesFriendlies.includes(0)) ||
187
+ (ctx.dest.pos[0] === 6 && ctx.rookFilesFriendlies.includes(7)))) ||
188
+ ctx.rookFilesFriendlies.includes(ctx.dest.pos[0])) &&
192
189
  (ctx.unrestrictedPremoves ||
193
190
  /* The following checks if no non-rook friendly piece is in the way between the king and its castling destination.
194
191
  Note that for the Chess960 edge case of Kb1 "long castling", the check passes even if there is a piece in the way
195
192
  on c1. But this is fine, since premoving from b1 to a1 as a normal move would have already returned true. */
196
193
  util
197
- .squaresBetween(...ctx.pos1, ctx.pos2[0] > ctx.pos1[0] ? 7 : 1, ctx.pos2[1])
194
+ .squaresBetween(...ctx.orig.pos, ctx.dest.pos[0] > ctx.orig.pos[0] ? 7 : 1, ctx.dest.pos[1])
198
195
  .map(s => ctx.allPieces.get(s))
199
196
  .every(p => !p || util.samePiece(p, { role: 'rook', color: ctx.color }))));
200
197
 
@@ -209,10 +206,12 @@ export function premove(state: HeadlessState, key: cg.Key): cg.Key[] {
209
206
  const color = piece.color,
210
207
  friendlies = new Map([...pieces].filter(([_, p]) => p.color === color)),
211
208
  enemies = new Map([...pieces].filter(([_, p]) => p.color === util.opposite(color))),
212
- pos = util.key2pos(key),
213
- mobility: Mobility = mobilityByRole[piece.role],
214
- ctx = {
215
- pos1: pos,
209
+ orig = { key, pos: util.key2pos(key) },
210
+ mobility: Mobility = (ctx: MobilityContext) =>
211
+ mobilityByRole[piece.role](ctx) && state.premovable.additionalPremoveRequirements(ctx),
212
+ partialCtx = {
213
+ orig,
214
+ role: piece.role,
216
215
  allPieces: pieces,
217
216
  friendlies: friendlies,
218
217
  enemies: enemies,
@@ -226,5 +225,5 @@ export function premove(state: HeadlessState, key: cg.Key): cg.Key[] {
226
225
  .map(([k]) => util.key2pos(k)[0]),
227
226
  lastMove: state.lastMove,
228
227
  };
229
- return util.allPos.filter(pos2 => mobility({ ...ctx, pos2 })).map(util.pos2key);
228
+ return util.allPosAndKey.filter(dest => mobility({ ...partialCtx, dest })).map(pk => pk.key);
230
229
  }
package/src/state.ts CHANGED
@@ -23,6 +23,7 @@ export interface HeadlessState {
23
23
  blockTouchScroll: boolean; // block scrolling via touch dragging on the board
24
24
  touchIgnoreRadius: number; // ignore touches within a radius of an occupied square, in units of its circumradius
25
25
  pieceKey: boolean; // add a data-key attribute to piece elements
26
+ pixelCoordsOfMouchDownToMaybeClearShapes?: cg.NumberPair;
26
27
  trustAllEvents?: boolean; // disable checking for human only input (e.isTrusted)
27
28
  highlight: {
28
29
  lastMove: boolean; // add last-move class to squares
@@ -53,6 +54,7 @@ export interface HeadlessState {
53
54
  customDests?: cg.Dests; // use custom valid premoves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]}
54
55
  current?: cg.KeyPair; // keys of the current saved premove ["e2" "e4"]
55
56
  unrestrictedPremoves?: boolean; // if falsy, the positions of friendly pieces will be used to trim premove options
57
+ additionalPremoveRequirements: cg.Mobility;
56
58
  events: {
57
59
  set?: (orig: cg.Key, dest: cg.Key, metadata?: cg.SetPremoveMetadata) => void; // called after the premove has been set
58
60
  unset?: () => void; // called after the premove has been unset
@@ -145,6 +147,7 @@ export function defaults(): HeadlessState {
145
147
  enabled: true,
146
148
  showDests: true,
147
149
  castle: true,
150
+ additionalPremoveRequirements: _ => true,
148
151
  events: {},
149
152
  },
150
153
  predroppable: {
@@ -174,7 +177,7 @@ export function defaults(): HeadlessState {
174
177
  enabled: true, // can draw
175
178
  visible: true, // can view
176
179
  defaultSnapToValidMove: true,
177
- eraseOnClick: true,
180
+ eraseOnMovablePieceClick: true,
178
181
  shapes: [],
179
182
  autoShapes: [],
180
183
  brushes: {
package/src/types.ts CHANGED
@@ -5,6 +5,10 @@ export type Rank = (typeof ranks)[number];
5
5
  export type Key = 'a0' | `${File}${Rank}`;
6
6
  export type FEN = string;
7
7
  export type Pos = [number, number];
8
+ export interface PosAndKey {
9
+ pos: Pos;
10
+ key: Key;
11
+ }
8
12
  export interface Piece {
9
13
  role: Role;
10
14
  color: Color;
@@ -112,3 +116,19 @@ export type BrushColor = 'green' | 'red' | 'blue' | 'yellow';
112
116
  export type SquareClasses = Map<Key, string>;
113
117
 
114
118
  export type DirectionalCheck = (x1: number, y1: number, x2: number, y2: number) => boolean;
119
+
120
+ export type MobilityContext = {
121
+ orig: PosAndKey;
122
+ dest: PosAndKey;
123
+ role: Role;
124
+ allPieces: Pieces;
125
+ friendlies: Pieces;
126
+ enemies: Pieces;
127
+ unrestrictedPremoves: boolean;
128
+ color: Color;
129
+ canCastle: boolean;
130
+ rookFilesFriendlies: number[];
131
+ lastMove: Key[] | undefined;
132
+ };
133
+
134
+ export type Mobility = (ctx: MobilityContext) => boolean;
package/src/util.ts CHANGED
@@ -4,7 +4,10 @@ export const invRanks: readonly cg.Rank[] = [...cg.ranks].reverse();
4
4
 
5
5
  export const allKeys: readonly cg.Key[] = cg.files.flatMap(f => cg.ranks.map(r => (f + r) as cg.Key));
6
6
 
7
- export const pos2key = (pos: cg.Pos): cg.Key => allKeys[8 * pos[0] + pos[1]];
7
+ export const pos2key = (pos: cg.Pos): cg.Key | undefined =>
8
+ pos.every(x => x >= 0 && x <= 7) ? allKeys[8 * pos[0] + pos[1]] : undefined;
9
+
10
+ export const pos2keyUnsafe = (pos: cg.Pos): cg.Key => pos2key(pos)!;
8
11
 
9
12
  export const key2pos = (k: cg.Key): cg.Pos => [k.charCodeAt(0) - 97, k.charCodeAt(1) - 49];
10
13
 
@@ -16,6 +19,8 @@ export const uciToMove = (uci: string | undefined): cg.Key[] | undefined => {
16
19
 
17
20
  export const allPos: readonly cg.Pos[] = allKeys.map(key2pos);
18
21
 
22
+ export const allPosAndKey: readonly cg.PosAndKey[] = allKeys.map((key, i) => ({ key, pos: allPos[i] }));
23
+
19
24
  export function memo<A>(f: () => A): cg.Memo<A> {
20
25
  let v: A | undefined;
21
26
  const ret = (): A => {
@@ -148,7 +153,7 @@ export const squaresBetween = (x1: number, y1: number, x2: number, y2: number):
148
153
  x += stepX;
149
154
  y += stepY;
150
155
  }
151
- return squares.map(sq => pos2key(sq));
156
+ return squares.map(pos2key).filter(k => k !== undefined);
152
157
  };
153
158
 
154
159
  export const adjacentSquares = (square: cg.Key): cg.Key[] => {
@@ -156,10 +161,10 @@ export const adjacentSquares = (square: cg.Key): cg.Key[] => {
156
161
  const adjacentSquares: cg.Pos[] = [];
157
162
  if (pos[0] > 0) adjacentSquares.push([pos[0] - 1, pos[1]]);
158
163
  if (pos[0] < 7) adjacentSquares.push([pos[0] + 1, pos[1]]);
159
- return adjacentSquares.map(pos2key);
164
+ return adjacentSquares.map(pos2key).filter(k => k !== undefined);
160
165
  };
161
166
 
162
- export const squareShiftedVertically = (square: cg.Key, delta: number): cg.Key => {
167
+ export const squareShiftedVertically = (square: cg.Key, delta: number): cg.Key | undefined => {
163
168
  const pos = key2pos(square);
164
169
  pos[1] += delta;
165
170
  return pos2key(pos);