feat(tarot): useTarotShuffle hook (Fisher-Yates + reversed 플래그) (T9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 00:33:03 +09:00
parent cdf8759aef
commit 1a7dfe73e4
2 changed files with 72 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
import { useCallback, useState } from 'react';
export function fisherYates(input) {
const a = [...input];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function buildShuffle(deck, size) {
return fisherYates(deck).slice(0, size).map((c) => ({
...c,
reversed: Math.random() < 0.5,
}));
}
export function useTarotShuffle(deck, size = 16) {
const [slice, setSlice] = useState(() => buildShuffle(deck, size));
const reshuffle = useCallback(() => {
setSlice(buildShuffle(deck, size));
}, [deck, size]);
return { slice, reshuffle };
}

View File

@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useTarotShuffle, fisherYates } from './useTarotShuffle';
describe('fisherYates', () => {
it('원본 길이를 유지하고 모든 원소를 포함한다', () => {
const src = [1, 2, 3, 4, 5, 6, 7, 8];
const out = fisherYates(src);
expect(out).toHaveLength(8);
expect([...out].sort()).toEqual(src);
});
it('새 배열을 반환한다 (mutation 없음)', () => {
const src = [1, 2, 3];
const out = fisherYates(src);
expect(src).toEqual([1, 2, 3]);
expect(out).not.toBe(src);
});
});
describe('useTarotShuffle', () => {
it('16장 슬라이스를 반환한다', () => {
const deck = Array.from({ length: 78 }, (_, i) => ({ id: i, slug: `c${i}` }));
const { result } = renderHook(() => useTarotShuffle(deck, 16));
expect(result.current.slice).toHaveLength(16);
});
it('reshuffle 시 새 슬라이스를 만든다 (대부분 다름)', () => {
const deck = Array.from({ length: 78 }, (_, i) => ({ id: i, slug: `c${i}` }));
const { result } = renderHook(() => useTarotShuffle(deck, 16));
const first = result.current.slice.map((c) => c.id);
act(() => result.current.reshuffle());
const second = result.current.slice.map((c) => c.id);
// 무작위지만 16장 모두 같을 확률은 사실상 0
const overlap = first.filter((id) => second.includes(id)).length;
expect(overlap).toBeLessThan(16);
});
it('reversed 플래그를 카드에 부여한다', () => {
const deck = Array.from({ length: 78 }, (_, i) => ({ id: i, slug: `c${i}` }));
const { result } = renderHook(() => useTarotShuffle(deck, 16));
for (const card of result.current.slice) {
expect(card).toHaveProperty('reversed');
expect(typeof card.reversed).toBe('boolean');
}
});
});