Documentação/Visão geral

Xadrez

Um tabuleiro de xadrez — os lances legais se acendem, a peça na mão diz por que uma casa é recusada, um peão pergunta em que vai se transformar, e um bot responde com arrastes de verdade.

Editar esta página

Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês

Live

Carregando a demo…

<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>

O que observar

  • Square.vue: cada uma das 64 casas é um alvo de drop cujo accept é uma função. Ela é consultada enquanto uma peça é arrastada, então a casa aceita só os lances legais daquela peça; um drop em qualquer outro lugar é recusado e a peça volta ao lugar. As casas legais se acendem a partir de isAccepting: nenhum estado do próprio jogo acompanha o arraste.
  • Piece.vue: um draggable simples, disabled quando não é a vez do seu lado. keyboardMode: 'droppables' faz as setas pularem entre as casas que aceitam a peça: os lances legais dela, nada mais. snapCenterToCursor mantém a peça centralizada sob o ponteiro, e um toque arrasta na hora (activation). canDrag impede que você mexa nas peças do bot e no tabuleiro enquanto o código joga, sem desativar nada: arrastes iniciados por código não têm evento de entrada, então canDrag não é consultado para eles.
  • chess.ts e game.ts: as regras e o jogo, sem nenhum arrastar e soltar neles: lances legais com xeque, roque, en passant, promoção, empates por falta de material, a notação, desfazer. Um lance no onDrop cria uma nova posição, e o overlay pousa na peça movida.

A peça na mão

PieceOverlay.vue é o componente de overlay das peças. Ele lê as props que todo overlay recebe e diz o que um drop na casa sob ele faria: sobre uma casa recusada (over definido, accepted falso) a peça fica vermelha e dá o motivo; sobre uma captura, ela mostra um alvo e o que captura. Quando o drop está decidido (status: 'dropping'), a dica some e a peça se assenta.

PieceOverlay.vue
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 desenha uma seta da casa de onde a peça veio até a casa sob ela, a partir do active e do over de useDnD(). Os haptics do provider vibram um arraste por toque sobre uma casa legal e quando um drop é recusado; os announcements em index.vue falam os mesmos motivos: "f7, pinned".

Promoção: um drop que espera uma resposta

Sobre a última fileira, um peão na mão mostra a peça em que vai se transformar: o overOverlay da própria casa (PromotionOverlay.vue) toma o lugar do da peça. Ao soltar, o onBeforeDrop da casa retorna uma promise, e o overlay espera sobre a casa enquanto PromotionPicker.vue, um elemento simples da página ao lado dela, pergunta qual peça. Uma peça responde 'accept', Escape ou Cancel 'reject' (o peão volta ao lugar), e Keep dragging 'continue': o peão volta a seguir o ponteiro, e o próximo clique o solta e pergunta de novo.

Square.vue
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),
});

O onBeforeDrop de um alvo de drop recebe o evento de arraste simples e só é consultado quando o accept dele aceitou o arraste; o de uma lista ordenável recebe o SortableDropEvent da lista, com o result do drop. O overOverlay do alvo é lido mais uma vez depois do onDrop, para o pouso: ele verifica o peão como ele foi pego (active.data), não o jogo, onde a essa altura ele já é uma dama.

O bot, os replays e o roque: arrastes executados por código

autoplay.ts joga lances com @vue-dnd-kit/actions: os do bot (bot.ts, um guloso), os do Replay, e o passo da torre em todo roque, incluindo os seus. Cada um é um arraste de verdade: pego, deslizado sobre o tabuleiro um quadro por vez, solto através do accept e do onDrop da casa, anunciado e animado como os seus. Uma promoção roteirizada já traz a sua peça, então o seletor não abre; Escape cancela um arraste roteirizado como qualquer outro.

autoplay.ts
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

O roque é jogado em dois movimentos, como à mão: o rei anda duas colunas, depois a torre dele é arrastada por cima dele, então a torre nunca pula.

Capturas, desfazer e um novo jogo

As peças capturadas vão para o Tray.vue do seu lado: uma lista ordenável usada só pelo animateChanges (disabled, nada é arrastado ali). O enter dela é uma função: uma nova peça voa para dentro a partir da casa em que foi capturada, e as outras abrem espaço.

Tray.vue
makeSortableList(tray, {
  items: () => props.pieces,
  getKey: (piece) => `taken-${piece.id}`,
  disabled: true,
  animateChanges: { enter: flyIn, leave: false },
});

Undo e New game mudam a posição sem arraste, então glide.ts os anima à mão (FLIP): cada peça voa de onde estava, em uma casa ou em uma bandeja, para onde está agora. Contra o bot, Undo desfaz o seu lance junto com a resposta dele.

Leia mais: Decidindo um drop · O overlay · Arrastes programáticos · Mudanças nos dados: animateChanges · Acessibilidade