@lichess-org/chessground 9.7.4 → 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,7 +59,7 @@ 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 = (
@@ -91,32 +76,41 @@ const canBeCapturedBySomeEnemyEnPassant = (
91
76
  friendly?.role === 'pawn' &&
92
77
  pos[1] === (friendly.color === 'white' ? 3 : 4) &&
93
78
  (!lastMove || util.diff(util.key2pos(lastMove[0])[1], pos[1]) === 2) &&
94
- [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
+ })
95
83
  );
96
84
  };
97
85
 
98
- const isPathClearEnoughOfFriendliesForPremove = (ctx: MobilityContext): boolean => {
86
+ const isPathClearEnoughOfFriendliesForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => {
99
87
  if (ctx.unrestrictedPremoves) return true;
100
- 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);
101
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
+ );
102
97
  return (
103
- !squaresOfFriendliesBetween.length ||
104
- (squaresOfFriendliesBetween.length === 1 &&
105
- canBeCapturedBySomeEnemyEnPassant(
106
- squaresOfFriendliesBetween[0],
107
- ctx.friendlies,
108
- ctx.enemies,
109
- ctx.lastMove,
110
- ) &&
111
- !squaresBetween.includes(
112
- util.squareShiftedVertically(squaresOfFriendliesBetween[0], ctx.color === 'white' ? -1 : 1),
113
- ))
98
+ squaresOfFriendliesBetween.length === 1 &&
99
+ canBeCapturedBySomeEnemyEnPassant(
100
+ firstSquareOfFriendliesBetween,
101
+ ctx.friendlies,
102
+ ctx.enemies,
103
+ ctx.lastMove,
104
+ ) &&
105
+ !!nextSquare &&
106
+ !squaresBetween.includes(nextSquare)
114
107
  );
115
108
  };
116
109
 
117
- const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext): boolean => {
110
+ const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => {
118
111
  if (ctx.unrestrictedPremoves) return true;
119
- 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);
120
114
  const squaresOfEnemiesBetween = squaresBetween.filter(s => ctx.enemies.has(s));
121
115
  if (squaresOfEnemiesBetween.length > 1) return false;
122
116
  if (!squaresOfEnemiesBetween.length) return true;
@@ -126,36 +120,38 @@ const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext): boolean =>
126
120
 
127
121
  const enemyStep = enemy.color === 'white' ? 1 : -1;
128
122
  const squareAbove = util.squareShiftedVertically(enemySquare, enemyStep);
129
- const enemyPawnDests: cg.Key[] = [
130
- ...util.adjacentSquares(squareAbove).filter(s => canEnemyPawnCaptureOnSquare(enemySquare, s, ctx)),
131
- ...[squareAbove, util.squareShiftedVertically(squareAbove, enemyStep)].filter(
132
- (s: cg.Key | undefined) => s && canEnemyPawnAdvanceToSquare(enemySquare, s, ctx),
133
- ),
134
- ];
135
- 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];
136
132
  return enemyPawnDests.some(square => !badSquares.includes(square));
137
133
  };
138
134
 
139
- const isPathClearEnoughForPremove = (ctx: MobilityContext): boolean =>
140
- isPathClearEnoughOfFriendliesForPremove(ctx) && isPathClearEnoughOfEnemiesForPremove(ctx);
135
+ const isPathClearEnoughForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean =>
136
+ isPathClearEnoughOfFriendliesForPremove(ctx, isPawnAdvance) &&
137
+ isPathClearEnoughOfEnemiesForPremove(ctx, isPawnAdvance);
141
138
 
142
139
  const pawn: Mobility = (ctx: MobilityContext) => {
143
140
  const step = ctx.color === 'white' ? 1 : -1;
144
- if (util.diff(ctx.pos1[0], ctx.pos2[0]) > 1) return false;
145
- 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]))
146
143
  return (
147
- util.pawnDirAdvance(...ctx.pos1, ...ctx.pos2, ctx.color === 'white') &&
148
- 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)
149
146
  );
150
- }
151
- if (ctx.pos2[1] !== ctx.pos1[1] + step) return false;
147
+ if (ctx.dest.pos[1] !== ctx.orig.pos[1] + step) return false;
152
148
  if (ctx.unrestrictedPremoves || isDestOccupiedByEnemy(ctx)) return true;
153
149
  if (isDestOccupiedByFriendly(ctx)) return isDestControlledByEnemy(ctx);
154
150
  else
155
151
  return (
156
152
  canSomeEnemyPawnAdvanceToDest(ctx) ||
157
153
  canBeCapturedBySomeEnemyEnPassant(
158
- util.pos2key([ctx.pos2[0], ctx.pos2[1] + step]),
154
+ util.pos2key([ctx.dest.pos[0], ctx.dest.pos[1] + step]),
159
155
  ctx.friendlies,
160
156
  ctx.enemies,
161
157
  ctx.lastMove,
@@ -165,37 +161,37 @@ const pawn: Mobility = (ctx: MobilityContext) => {
165
161
  };
166
162
 
167
163
  const knight: Mobility = (ctx: MobilityContext) =>
168
- util.knightDir(...ctx.pos1, ...ctx.pos2) &&
164
+ util.knightDir(...ctx.orig.pos, ...ctx.dest.pos) &&
169
165
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx));
170
166
 
171
167
  const bishop: Mobility = (ctx: MobilityContext) =>
172
- util.bishopDir(...ctx.pos1, ...ctx.pos2) &&
173
- isPathClearEnoughForPremove(ctx) &&
168
+ util.bishopDir(...ctx.orig.pos, ...ctx.dest.pos) &&
169
+ isPathClearEnoughForPremove(ctx, false) &&
174
170
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx));
175
171
 
176
172
  const rook: Mobility = (ctx: MobilityContext) =>
177
- util.rookDir(...ctx.pos1, ...ctx.pos2) &&
178
- isPathClearEnoughForPremove(ctx) &&
173
+ util.rookDir(...ctx.orig.pos, ...ctx.dest.pos) &&
174
+ isPathClearEnoughForPremove(ctx, false) &&
179
175
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx));
180
176
 
181
177
  const queen: Mobility = (ctx: MobilityContext) => bishop(ctx) || rook(ctx);
182
178
 
183
179
  const king: Mobility = (ctx: MobilityContext) =>
184
- (util.kingDirNonCastling(...ctx.pos1, ...ctx.pos2) &&
180
+ (util.kingDirNonCastling(...ctx.orig.pos, ...ctx.dest.pos) &&
185
181
  (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx))) ||
186
182
  (ctx.canCastle &&
187
- ctx.pos1[1] === ctx.pos2[1] &&
188
- ctx.pos1[1] === (ctx.color === 'white' ? 0 : 7) &&
189
- ((ctx.pos1[0] === 4 &&
190
- ((ctx.pos2[0] === 2 && ctx.rookFilesFriendlies.includes(0)) ||
191
- (ctx.pos2[0] === 6 && ctx.rookFilesFriendlies.includes(7)))) ||
192
- 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])) &&
193
189
  (ctx.unrestrictedPremoves ||
194
190
  /* The following checks if no non-rook friendly piece is in the way between the king and its castling destination.
195
191
  Note that for the Chess960 edge case of Kb1 "long castling", the check passes even if there is a piece in the way
196
192
  on c1. But this is fine, since premoving from b1 to a1 as a normal move would have already returned true. */
197
193
  util
198
- .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])
199
195
  .map(s => ctx.allPieces.get(s))
200
196
  .every(p => !p || util.samePiece(p, { role: 'rook', color: ctx.color }))));
201
197
 
@@ -210,10 +206,12 @@ export function premove(state: HeadlessState, key: cg.Key): cg.Key[] {
210
206
  const color = piece.color,
211
207
  friendlies = new Map([...pieces].filter(([_, p]) => p.color === color)),
212
208
  enemies = new Map([...pieces].filter(([_, p]) => p.color === util.opposite(color))),
213
- pos = util.key2pos(key),
214
- mobility: Mobility = mobilityByRole[piece.role],
215
- ctx = {
216
- 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,
217
215
  allPieces: pieces,
218
216
  friendlies: friendlies,
219
217
  enemies: enemies,
@@ -227,5 +225,5 @@ export function premove(state: HeadlessState, key: cg.Key): cg.Key[] {
227
225
  .map(([k]) => util.key2pos(k)[0]),
228
226
  lastMove: state.lastMove,
229
227
  };
230
- return util.allPos.filter(pos2 => mobility({ ...ctx, pos2 })).map(util.pos2key);
228
+ return util.allPosAndKey.filter(dest => mobility({ ...partialCtx, dest })).map(pk => pk.key);
231
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);