lila/ui/chess/src/promotion.ts

175 lines
4.8 KiB
TypeScript

import { h } from 'snabbdom';
import { MaybeVNode, bind, onInsert } from 'common/snabbdom';
import { Api as CgApi } from 'chessground/api';
import { DrawShape } from 'chessground/draw';
import * as cgUtil from 'chessground/util';
import * as cg from 'chessground/types';
export type Callback = (orig: Key, dest: Key, role: cg.Role) => void;
interface Promoting {
orig: Key;
dest: Key;
pre: boolean;
callback: Callback;
}
const PROMOTABLE_ROLES: cg.Role[] = ['queen', 'knight', 'rook', 'bishop'];
export function promote(g: CgApi, key: Key, role: cg.Role): void {
const piece = g.state.pieces.get(key);
if (piece && piece.role == 'pawn') {
g.setPieces(
new Map([
[
key,
{
color: piece.color,
role,
promoted: true,
},
],
])
);
}
}
export class PromotionCtrl {
private promoting?: Promoting;
private prePromotionRole?: cg.Role;
constructor(
private withGround: <A>(f: (cg: CgApi) => A) => A | false | undefined,
private onCancel: () => void,
private redraw: () => void,
private autoQueenPref: Prefs.AutoQueen = Prefs.AutoQueen.Never
) {}
start = (orig: Key, dest: Key, callback: Callback, meta?: cg.MoveMetadata, forceAutoQueen = false): boolean =>
this.withGround(g => {
const premovePiece = g.state.pieces.get(orig);
const piece = premovePiece || g.state.pieces.get(dest);
if (
piece?.role == 'pawn' &&
((dest[1] == '8' && g.state.turnColor == 'black') || (dest[1] == '1' && g.state.turnColor == 'white'))
) {
if (this.prePromotionRole && meta?.premove) {
this.doPromote({ orig, dest, callback }, this.prePromotionRole);
return true;
}
if (
!meta?.ctrlKey &&
!this.promoting &&
(this.autoQueenPref === Prefs.AutoQueen.Always ||
(this.autoQueenPref === Prefs.AutoQueen.OnPremove && premovePiece) ||
forceAutoQueen)
) {
if (premovePiece) this.setPrePromotion(dest, 'queen');
else this.doPromote({ orig, dest, callback }, 'queen');
return true;
}
this.promoting = { orig, dest, pre: !!premovePiece, callback };
this.redraw();
return true;
}
return false;
}) || false;
cancel = (): void => {
this.cancelPrePromotion();
if (this.promoting) {
this.promoting = undefined;
this.onCancel();
this.redraw();
}
};
cancelPrePromotion = (): void => {
if (this.prePromotionRole) {
this.withGround(g => g.setAutoShapes([]));
this.prePromotionRole = undefined;
this.redraw();
}
};
view = (antichess?: boolean): MaybeVNode => {
const promoting = this.promoting;
if (!promoting) return;
return (
this.withGround(g =>
this.renderPromotion(
promoting.dest,
antichess ? PROMOTABLE_ROLES.concat('king') : PROMOTABLE_ROLES,
cgUtil.opposite(g.state.turnColor),
g.state.orientation
)
) || null
);
};
private finish(role: cg.Role): void {
const promoting = this.promoting;
if (promoting) {
this.promoting = undefined;
if (promoting.pre) this.setPrePromotion(promoting.dest, role);
else this.doPromote(promoting, role);
this.redraw();
}
}
private doPromote(promoting: Omit<Promoting, 'pre'>, role: cg.Role): void {
this.withGround(g => promote(g, promoting.dest, role));
promoting.callback(promoting.orig, promoting.dest, role);
}
private setPrePromotion(dest: cg.Key, role: cg.Role): void {
this.prePromotionRole = role;
this.withGround(g =>
g.setAutoShapes([
{
orig: dest,
piece: {
color: cgUtil.opposite(g.state.turnColor),
role,
opacity: 0.8,
},
brush: '',
} as DrawShape,
])
);
}
private renderPromotion(dest: Key, pieces: cg.Role[], color: Color, orientation: Color): MaybeVNode {
let left = (7 - cgUtil.key2pos(dest)[0]) * 12.5;
if (orientation === 'white') left = 87.5 - left;
const vertical = color === orientation ? 'top' : 'bottom';
return h(
'div#promotion-choice.' + vertical,
{
hook: onInsert(el => {
el.addEventListener('click', this.cancel);
el.oncontextmenu = () => false;
}),
},
pieces.map((serverRole, i) => {
const top = (color === orientation ? i : 7 - i) * 12.5;
return h(
'square',
{
attrs: {
style: 'top: ' + top + '%;left: ' + left + '%',
},
hook: bind('click', e => {
e.stopPropagation();
this.finish(serverRole);
}),
},
[h('piece.' + serverRole + '.' + color)]
);
})
);
}
}