デモを読み込み中…
<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:ルールとゲームで、ドラッグ&ドロップは含みません:チェックを考慮した合法手、キャスリング、アンパッサン、プロモーション、駒不足による引き分け、棋譜表記、元に戻す。onDropでの着手が新しい局面を作り、オーバーレイは動いた駒に着地します。
手に持った駒
PieceOverlay.vue は駒の overlay コンポーネントです。すべてのオーバーレイが受け取る props を読み、その下のマスにドロップしたら何が起きるかを伝えます:拒否されたマスの上(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 は、駒が来たマスからその下のマスへの矢印を、useDnD() の active と over から描きます。プロバイダーの haptics は、タッチドラッグが合法なマスに乗ったときとドロップが拒否されたときに振動し、index.vue の announcements は同じ理由を読み上げます:「f7, pinned」。
プロモーション:答えを待つドロップ
最終段の上では、手に持ったポーンが成る駒を表示します:マス自身の overOverlay(PromotionOverlay.vue)が駒のオーバーレイの代わりになります。離すと、マスの onBeforeDrop が Promise を返し、その隣にあるページの通常の要素 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 は、ドロップの result を含むリストの SortableDropEvent を受け取ります。ターゲットの overOverlay は、着地のために onDrop の後にもう一度読み取られます:そのため、ゲーム(その時点でポーンはクイーンになっています)ではなく、持ち上げた時点のポーン(active.data)を確認しています。
ボット、リプレイ、キャスリング:コードが再生するドラッグ
autoplay.ts は @vue-dnd-kit/actions で手を指します:ボットの手(bot.ts、貪欲法のもの)、Replay の手、そしてあなたのものも含むすべてのキャスリングでのルークの移動です。どれも本物のドラッグです:持ち上げられ、1 フレームずつ盤の上を滑り、マスの 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
キャスリングは、手で指すときと同じく 2 手で再生されます:キングが 2 筋動き、次にそのルークがキングを越えてドラッグされるので、ルークが瞬間移動することはありません。
駒取り、元に戻す、新しいゲーム
取られた駒はその側の 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 · アクセシビリティ