219 lines
6.5 KiB
TypeScript
219 lines
6.5 KiB
TypeScript
import { Prop, prop } from 'common';
|
|
import { storedProp } from 'common/storage';
|
|
import debounce from 'common/debounce';
|
|
import { sync, Sync } from 'common/sync';
|
|
import { opposite } from 'chessground/util';
|
|
import * as xhr from './explorerXhr';
|
|
import { winnerOf, colorOf } from './explorerUtil';
|
|
import * as gameUtil from 'game';
|
|
import AnalyseCtrl from '../ctrl';
|
|
import { Hovering, ExplorerData, ExplorerDb, OpeningData, SimpleTablebaseHit, ExplorerOpts } from './interfaces';
|
|
import { CancellableStream } from 'common/ndjson';
|
|
import { ExplorerConfigCtrl } from './explorerConfig';
|
|
import { clearLastShow } from './explorerView';
|
|
|
|
function pieceCount(fen: Fen) {
|
|
const parts = fen.split(/\s/);
|
|
return parts[0].split(/[nbrqkp]/i).length - 1;
|
|
}
|
|
|
|
function tablebasePieces(variant: VariantKey) {
|
|
switch (variant) {
|
|
case 'standard':
|
|
case 'fromPosition':
|
|
case 'chess960':
|
|
return 7;
|
|
case 'atomic':
|
|
case 'antichess':
|
|
return 6;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
export const tablebaseGuaranteed = (variant: VariantKey, fen: Fen) => pieceCount(fen) <= tablebasePieces(variant);
|
|
|
|
export default class ExplorerCtrl {
|
|
allowed: Prop<boolean>;
|
|
enabled: Prop<boolean>;
|
|
withGames: boolean;
|
|
effectiveVariant: VariantKey;
|
|
config: ExplorerConfigCtrl;
|
|
|
|
loading = prop(true);
|
|
failing = prop<Error | null>(null);
|
|
hovering = prop<Hovering | null>(null);
|
|
movesAway = prop(0);
|
|
gameMenu = prop<string | null>(null);
|
|
lastStream = prop<Sync<CancellableStream> | null>(null);
|
|
cache: Dictionary<ExplorerData> = {};
|
|
|
|
constructor(readonly root: AnalyseCtrl, readonly opts: ExplorerOpts, allow: boolean) {
|
|
this.allowed = prop(allow);
|
|
this.enabled = root.embed ? prop(false) : storedProp('explorer.enabled', false);
|
|
this.withGames = root.synthetic || gameUtil.replayable(root.data) || !!root.data.opponent.ai;
|
|
this.effectiveVariant = root.data.game.variant.key === 'fromPosition' ? 'standard' : root.data.game.variant.key;
|
|
this.config = new ExplorerConfigCtrl(root, this.effectiveVariant, this.reload);
|
|
window.addEventListener('hashchange', this.checkHash, false);
|
|
this.checkHash();
|
|
}
|
|
|
|
checkHash = (e?: HashChangeEvent) => {
|
|
const m = location.hash.match(/#(?:explorer|opening)(?:\/([a-z0-9_-]{2,30}))?/i);
|
|
if (m && !this.root.embed) {
|
|
this.enabled(true);
|
|
if (m[1]) this.config.selectPlayer(m[1]);
|
|
if (e) this.root.redraw();
|
|
}
|
|
};
|
|
|
|
reload = () => {
|
|
this.cache = {};
|
|
this.setNode();
|
|
this.root.redraw();
|
|
};
|
|
|
|
destroy = clearLastShow;
|
|
|
|
private baseXhrOpening = () => ({
|
|
endpoint: this.opts.endpoint,
|
|
config: this.config.data,
|
|
});
|
|
|
|
fetch = debounce(
|
|
() => {
|
|
const fen = this.root.node.fen;
|
|
const processData = (res: ExplorerData) => {
|
|
this.cache[fen] = res;
|
|
this.movesAway(res.moves.length ? 0 : this.movesAway() + 1);
|
|
this.loading(false);
|
|
this.failing(null);
|
|
this.root.redraw();
|
|
};
|
|
const onError = (err: Error) => {
|
|
this.loading(false);
|
|
this.failing(err);
|
|
this.root.redraw();
|
|
};
|
|
const prev = this.lastStream();
|
|
if (prev) prev.promise.then(stream => stream.cancel());
|
|
if (this.withGames && this.tablebaseRelevant(this.effectiveVariant, fen))
|
|
xhr.tablebase(this.opts.tablebaseEndpoint, this.effectiveVariant, fen).then(processData, onError);
|
|
else
|
|
this.lastStream(
|
|
sync(
|
|
xhr
|
|
.opening(
|
|
{
|
|
...this.baseXhrOpening(),
|
|
db: this.db() as ExplorerDb,
|
|
variant: this.effectiveVariant,
|
|
rootFen: this.root.nodeList[0].fen,
|
|
play: this.root.nodeList.slice(1).map(s => s.uci!),
|
|
fen,
|
|
withGames: this.withGames,
|
|
},
|
|
processData
|
|
)
|
|
.then(stream => {
|
|
stream.end.promise.then(res => (res !== true ? onError(res) : this.root.redraw()));
|
|
return stream;
|
|
})
|
|
)
|
|
);
|
|
},
|
|
250,
|
|
true
|
|
);
|
|
|
|
empty: OpeningData = {
|
|
isOpening: true,
|
|
moves: [],
|
|
fen: '',
|
|
opening: this.root.data.game.opening,
|
|
};
|
|
|
|
tablebaseRelevant = (variant: VariantKey, fen: Fen) =>
|
|
pieceCount(fen) - 1 <= tablebasePieces(variant) && this.root.ceval.possible;
|
|
|
|
setNode = () => {
|
|
if (!this.enabled()) return;
|
|
this.gameMenu(null);
|
|
const node = this.root.node;
|
|
if (node.ply >= 50 && !this.tablebaseRelevant(this.effectiveVariant, node.fen)) {
|
|
this.cache[node.fen] = this.empty;
|
|
}
|
|
const cached = this.cache[node.fen];
|
|
if (cached) {
|
|
this.movesAway(cached.moves.length ? 0 : this.movesAway() + 1);
|
|
this.loading(false);
|
|
this.failing(null);
|
|
} else {
|
|
this.loading(true);
|
|
this.fetch();
|
|
}
|
|
};
|
|
|
|
db = () => this.config.data.db();
|
|
current = () => this.cache[this.root.node.fen];
|
|
toggle = () => {
|
|
this.movesAway(0);
|
|
this.enabled(!this.enabled());
|
|
this.setNode();
|
|
this.root.autoScroll();
|
|
};
|
|
disable = () => {
|
|
if (this.enabled()) {
|
|
this.enabled(false);
|
|
this.gameMenu(null);
|
|
this.root.autoScroll();
|
|
}
|
|
};
|
|
setHovering = (fen: Fen, uci: Uci | null) => {
|
|
this.hovering(uci ? { fen, uci } : null);
|
|
this.root.setAutoShapes();
|
|
};
|
|
onFlip = () => {
|
|
if (this.db() == 'player') {
|
|
this.cache = {};
|
|
this.setNode();
|
|
}
|
|
};
|
|
isIndexing = () => {
|
|
const stream = this.lastStream();
|
|
return !!stream && (!stream.sync || !stream.sync.end.sync);
|
|
};
|
|
fetchMasterOpening = (() => {
|
|
const masterCache: Dictionary<OpeningData> = {};
|
|
return (fen: Fen): Promise<OpeningData> => {
|
|
const val = masterCache[fen];
|
|
if (val) return Promise.resolve(val);
|
|
return new Promise(resolve =>
|
|
xhr.opening(
|
|
{
|
|
...this.baseXhrOpening(),
|
|
db: 'masters',
|
|
rootFen: fen,
|
|
play: [],
|
|
fen,
|
|
},
|
|
(res: OpeningData) => {
|
|
masterCache[fen] = res;
|
|
resolve(res);
|
|
}
|
|
)
|
|
);
|
|
};
|
|
})();
|
|
fetchTablebaseHit = async (fen: Fen): Promise<SimpleTablebaseHit> => {
|
|
const res = await xhr.tablebase(this.opts.tablebaseEndpoint, this.effectiveVariant, fen);
|
|
const move = res.moves[0];
|
|
if (move && move.dtz == null) throw 'unknown tablebase position';
|
|
return {
|
|
fen,
|
|
best: move && move.uci,
|
|
winner: res.checkmate ? opposite(colorOf(fen)) : res.stalemate ? undefined : winnerOf(fen, move!),
|
|
} as SimpleTablebaseHit;
|
|
};
|
|
}
|