Документация/Обзор
Шахматы
Шахматная доска — допустимые ходы подсвечиваются, фигура в руке говорит, почему поле отклонено, пешка спрашивает, кем ей стать, а бот отвечает настоящими драгами.
Страница переведена ИИ и может содержать неточности. Читать английский оригинал
Загружаем демо…
<script setup lang="ts">
import { provide } from 'vue';
import { DnDProvider, type Announcements } from '@vue-dnd-kit/core';
import Board from './Board.vue';
import { NAMES, type Piece } from './chess';
import { GAME, createGame } from './game';
// Provided above the provider: the overlays it renders read the game too.
const game = createGame();
provide(GAME, game);
// Screen readers hear pieces and squares, not ids.
const name = (data: unknown) => {
const piece = data as Piece;
return `${piece.color === 'w' ? 'white' : 'black'} ${NAMES[piece.kind]}`;
};
const from = (data: unknown) => (data as Piece).square;
const stays = (data: unknown) => `The ${name(data)} stays on ${from(data)}.`;
const announcements: Partial<Announcements> = {
onDragStart: ({ active }) => {
const keys = active.sensor === 'keyboard' ? ' Arrow keys go through its moves.' : '';
return `Picked up the ${name(active.data)} on ${from(active.data)}.${keys}`;
},
// Over an illegal square, the reason the overlay shows: "f7, pinned".
onDragOver: ({ active, over }) =>
over ? `${over.id}${over.accepted ? '' : `, ${game.whyNot(active.id, String(over.id)).toLowerCase()}`}` : '',
onDragEnd: ({ active, over, dropped }) => {
if (!dropped || !over) return stays(active.data);
const turn = game.last.value;
const takes = turn?.captured ? `, takes the ${name(turn.captured)}` : '';
const becomes = turn?.promotion ? ` and becomes a ${NAMES[turn.promotion]}` : '';
return `The ${name(active.data)} moves to ${over.id}${takes}${becomes}.`;
},
onDragCancel: ({ active }) => stays(active.data),
onDragResume: ({ active }) => `Still holding the ${name(active.data)}.`,
};
</script>
<template>
<!-- Touch drags vibrate on a legal square, and when a drop is refused. -->
<DnDProvider :announcements="announcements" :haptics="{ enter: true, refuse: true }">
<Board />
</DnDProvider>
</template>На что обратить внимание
Square.vue: каждое из 64 полей — цель, чейaccept— функция. Её спрашивают, пока фигуру тащат, так что поле принимает только допустимые ходы этой фигуры; бросок куда-либо ещё отклоняется, и фигура летит обратно. Допустимые поля подсвечиваются поisAccepting: никакое собственное состояние игры не следит за драгом.Piece.vue: обычный перетаскиваемый элемент,disabled, когда ход не его стороны.keyboardMode: 'droppables'заставляет стрелки прыгать между полями, которые принимают фигуру: её допустимыми ходами, и ничем больше.snapCenterToCursorдержит фигуру по центру под указателем, а касание тащит сразу (activation).canDragне даёт вам трогать фигуры бота и доску, пока играет код, ничего не отключая: у драгов, начатых кодом, нет события ввода, поэтомуcanDragдля них не спрашивается.chess.tsиgame.ts: правила и партия, без всякого drag and drop: допустимые ходы с шахом, рокировка, взятие на проходе, превращение, ничья из-за недостатка материала, нотация, отмена. Ход вonDropсоздаёт новую позицию, и оверлей приземляется на сходившую фигуру.
Фигура в руке
PieceOverlay.vue — компонент overlay фигур. Он читает пропсы, которые получает любой
оверлей, и говорит, что сделал бы бросок на поле под ним: над отклонённым полем (over задан,
accepted равен false) фигура краснеет и называет причину, над взятием показывает мишень и то,
что она берёт. Когда бросок решён (status: 'dropping'), подсказка исчезает и фигура
опускается.
const props = defineProps<{
active: Active;
over: Over | null;
accepted: boolean;
status: DnDStatus;
}>();
const hint = computed(() => {
const square = props.over ? String(props.over.id) : null;
if (props.status !== 'dragging' || !square || square === piece.value.square) return null;
if (props.accepted) {
const taken = game.takes(piece.value.id, square);
return taken ? { type: 'capture', text: `Takes ${NAMES[taken.kind]}` } : null;
}
// "Pinned", "King in check", "Not a legal move"
return { type: 'refused', text: game.whyNot(piece.value.id, square) };
});
Board.vue рисует стрелку от поля, с которого пришла фигура, к полю под ней, по active и
over из useDnD(). haptics провайдера даёт вибрацию при драге касанием на допустимом поле и
когда бросок отклонён; announcements в index.vue произносят те же причины: «f7, pinned».
Превращение: бросок, который ждёт ответа
Над последней горизонталью пешка в руке показывает фигуру, в которую превращается: собственный
overOverlay поля (PromotionOverlay.vue) занимает место оверлея фигуры. При отпускании
onBeforeDrop поля возвращает промис, и оверлей ждёт на поле, пока PromotionPicker.vue,
обычный элемент страницы рядом с ним, спрашивает, какая фигура. Фигура отвечает 'accept',
Escape или Cancel — 'reject' (пешка летит обратно), а Keep dragging — 'continue': пешка
снова следует за указателем, а следующий клик бросает её и спрашивает снова.
makeDroppable(el, {
id: props.square,
accept: ({ active }) => game.canMove(active.id, props.square),
// Shown instead of the piece's overlay while a pawn is over the last rank.
overOverlay: computed(() =>
held.value?.kind === 'P' && isLastRank(props.square) ? PromotionOverlay : undefined,
),
// A promise: the drop waits on the square until the picker answers.
onBeforeDrop: ({ active }) =>
game.promotes(active.id, props.square) ? game.askPromotion(props.square) : 'accept',
onDrop: ({ active }) => game.move(String(active.id), props.square),
});
onBeforeDrop цели получает обычное событие драга и спрашивается, только когда её accept
принял драг; onBeforeDrop сортируемого списка получает SortableDropEvent списка с result
броска. overOverlay цели читается ещё раз после onDrop, для приземления: он проверяет пешку такой,
какой её взяли (active.data), а не партию, где к тому времени это уже ферзь.
Бот, повторы и рокировка: драги, которые проигрывает код
autoplay.ts играет ходы через @vue-dnd-kit/actions: ходы бота (bot.ts, жадного), ходы
Replay и шаг ладьи при каждой рокировке, включая вашу. Каждый — настоящий драг: фигуру берут,
ведут над доской кадр за кадром, бросают через accept и onDrop поля, он озвучивается и
анимируется, как ваши. Заскриптованное превращение приносит свою фигуру с собой, так что выбор не
открывается; Escape отменяет заскриптованный драг, как любой другой.
const { startDrag } = useDnDActions();
const drag = startDrag(id); // from the piece's center
for (let frame = 1; frame <= FRAMES; frame++) {
const t = easeOut(frame / FRAMES);
const point = { x: start.x + (end.x - start.x) * t, y: start.y + (end.y - start.y) * t };
if (!(await drag.moveTo(point))) return false; // cancelled meanwhile (Escape)
}
return drag.drop(); // true once it has landed, false if the square refused it
Рокировка играется двумя ходами, как руками: король идёт на два поля, затем его ладью перетаскивают через него, так что ладья никогда не прыгает.
Взятия, отмена и новая партия
Взятые фигуры уходят в Tray.vue своей стороны: сортируемый список, который используется
только ради animateChanges (disabled, там ничего не перетаскивается). Его enter — функция: новая
фигура прилетает с поля, на котором её взяли, а остальные освобождают место.
makeSortableList(tray, {
items: () => props.pieces,
getKey: (piece) => `taken-${piece.id}`,
disabled: true,
animateChanges: { enter: flyIn, leave: false },
});
Undo и New game меняют позицию без драга, поэтому glide.ts анимирует их вручную (FLIP):
каждая фигура летит оттуда, где была — с поля или из лотка, — туда, где она сейчас. Против бота
Undo отменяет ваш ход вместе с его ответом.
Подробнее: решение о броске ·
оверлей ·
программные драги ·
изменения данных: animateChanges ·
доступность