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:
25
src/pages/tarot/hooks/useTarotShuffle.js
Normal file
25
src/pages/tarot/hooks/useTarotShuffle.js
Normal 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 };
|
||||
}
|
||||
47
src/pages/tarot/hooks/useTarotShuffle.test.js
Normal file
47
src/pages/tarot/hooks/useTarotShuffle.test.js
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user