lila/ui/editor/src/ctrl.ts

223 lines
6.5 KiB
TypeScript

import { EditorState, Selected, Redraw, CastlingToggle, CastlingToggles, CASTLING_TOGGLES } from './interfaces';
import { Api as CgApi } from 'chessground/api';
import { Rules, Square } from 'chessops/types';
import { SquareSet } from 'chessops/squareSet';
import { Board } from 'chessops/board';
import { Setup, Material, RemainingChecks } from 'chessops/setup';
import { Castles, setupPosition } from 'chessops/variant';
import { makeFen, parseFen, parseCastlingFen, INITIAL_FEN, EMPTY_FEN, INITIAL_EPD } from 'chessops/fen';
import { defined, prop, Prop } from 'common';
import { rulesToVariant, variantToRules } from './chessops';
export default class EditorCtrl {
cfg: Editor.Config;
options: Editor.Options;
trans: Trans;
extraPositions: Editor.OpeningPosition[];
chessground: CgApi | undefined;
redraw: Redraw;
selected: Prop<Selected>;
initialFen: string;
pockets: Material | undefined;
turn: Color;
unmovedRooks: SquareSet | undefined;
castlingToggles: CastlingToggles<boolean>;
epSquare: Square | undefined;
remainingChecks: RemainingChecks | undefined;
rules: Rules;
halfmoves: number;
fullmoves: number;
constructor(cfg: Editor.Config, redraw: Redraw) {
this.cfg = cfg;
this.options = cfg.options || {};
this.trans = lichess.trans(this.cfg.i18n);
this.selected = prop('pointer');
this.extraPositions = [
{
fen: INITIAL_FEN,
epd: INITIAL_EPD,
name: this.trans('startPosition'),
},
{
fen: 'prompt',
name: this.trans('loadPosition'),
},
];
if (cfg.positions) {
cfg.positions.forEach(p => (p.epd = p.fen.split(' ').splice(0, 4).join(' ')));
}
if (cfg.endgamePositions) {
cfg.endgamePositions.forEach(p => (p.epd = p.fen.split(' ').splice(0, 4).join(' ')));
}
window.Mousetrap.bind('f', () => {
if (this.chessground) this.chessground.toggleOrientation();
redraw();
});
this.castlingToggles = { K: false, Q: false, k: false, q: false };
const params = new URLSearchParams(location.search);
this.rules = this.cfg.embed ? 'chess' : variantToRules(params.get('variant'));
this.initialFen = (cfg.fen || params.get('fen') || INITIAL_FEN).replace(/_/g, ' ');
this.redraw = () => {};
this.setFen(this.initialFen);
this.redraw = redraw;
}
onChange(): void {
const fen = this.getFen();
if (!this.cfg.embed) {
const params = new URLSearchParams();
if (fen !== INITIAL_FEN || this.rules !== 'chess') params.set('fen', fen);
if (this.rules !== 'chess') params.set('variant', rulesToVariant(this.rules));
const paramsString = params.toString();
window.history.replaceState(null, '', '/editor' + (paramsString ? '?' + paramsString : ''));
}
this.options.onChange?.(fen);
this.redraw();
}
private castlingToggleFen(): string {
let fen = '';
for (const toggle of CASTLING_TOGGLES) {
if (this.castlingToggles[toggle]) fen += toggle;
}
return fen;
}
private getSetup(): Setup {
const boardFen = this.chessground?.getFen() || this.initialFen;
const board = parseFen(boardFen).unwrap(
setup => setup.board,
_ => Board.empty()
);
return {
board,
pockets: this.pockets,
turn: this.turn,
unmovedRooks: this.unmovedRooks || parseCastlingFen(board, this.castlingToggleFen()).unwrap(),
epSquare: this.epSquare,
remainingChecks: this.remainingChecks,
halfmoves: this.halfmoves,
fullmoves: this.fullmoves,
};
}
getFen(): string {
return makeFen(this.getSetup(), { promoted: this.rules == 'crazyhouse' });
}
private getLegalFen(): string | undefined {
return setupPosition(this.rules, this.getSetup()).unwrap(
pos => {
return makeFen(pos.toSetup(), { promoted: pos.rules == 'crazyhouse' });
},
_ => undefined
);
}
private isPlayable(): boolean {
return setupPosition(this.rules, this.getSetup()).unwrap(
pos => !pos.isEnd(),
_ => false
);
}
getState(): EditorState {
return {
fen: this.getFen(),
legalFen: this.getLegalFen(),
playable: this.rules == 'chess' && this.isPlayable(),
};
}
makeAnalysisUrl(legalFen: string): string {
const variant = this.rules === 'chess' ? '' : rulesToVariant(this.rules) + '/';
return this.makeUrl(`/analysis/${variant}`, legalFen);
}
makeUrl(baseUrl: string, fen: string): string {
return baseUrl + encodeURIComponent(fen).replace(/%20/g, '_').replace(/%2F/g, '/');
}
bottomColor(): Color {
return this.chessground ? this.chessground.state.orientation : this.options.orientation || 'white';
}
setCastlingToggle(id: CastlingToggle, value: boolean): void {
if (this.castlingToggles[id] != value) this.unmovedRooks = undefined;
this.castlingToggles[id] = value;
this.onChange();
}
setTurn(turn: Color): void {
this.turn = turn;
this.onChange();
}
startPosition(): void {
this.setFen(INITIAL_FEN);
}
clearBoard(): void {
this.setFen(EMPTY_FEN);
}
loadNewFen(fen: string | 'prompt'): void {
if (fen === 'prompt') {
fen = (prompt('Paste FEN position') || '').trim();
if (!fen) return;
}
this.setFen(fen);
}
setFen(fen: string): boolean {
return parseFen(fen).unwrap(
setup => {
if (this.chessground) this.chessground.set({ fen });
this.pockets = setup.pockets;
this.turn = setup.turn;
this.unmovedRooks = setup.unmovedRooks;
this.epSquare = setup.epSquare;
this.remainingChecks = setup.remainingChecks;
this.halfmoves = setup.halfmoves;
this.fullmoves = setup.fullmoves;
const castles = Castles.fromSetup(setup);
this.castlingToggles['K'] = defined(castles.rook.white.h);
this.castlingToggles['Q'] = defined(castles.rook.white.a);
this.castlingToggles['k'] = defined(castles.rook.black.h);
this.castlingToggles['q'] = defined(castles.rook.black.a);
this.onChange();
return true;
},
_ => false
);
}
setRules(rules: Rules): void {
this.rules = rules;
if (rules != 'crazyhouse') this.pockets = undefined;
else if (!this.pockets) this.pockets = Material.empty();
if (rules != '3check') this.remainingChecks = undefined;
else if (!this.remainingChecks) this.remainingChecks = RemainingChecks.default();
this.onChange();
}
setOrientation(o: Color): void {
this.options.orientation = o;
if (this.chessground!.state.orientation !== o) this.chessground!.toggleOrientation();
this.redraw();
}
}