feat(phase2): 타로 셔플·reference 순수 유틸 + 테스트
Fisher-Yates 셔플, 카드 픽 생성, 참고 블록/메타데이터 빌더 구현. Task 4(interpret API)·Task 6(UI)에서 소비됨. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
28
lib/__tests__/tarot-reference.test.ts
Normal file
28
lib/__tests__/tarot-reference.test.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { buildReferenceBlock, buildContextMeta } from '../tarot/reference';
|
||||||
|
import { findCard } from '../tarot/cards';
|
||||||
|
|
||||||
|
const picks = [
|
||||||
|
{ card: findCard('the-fool')!, position: '과거', reversed: false },
|
||||||
|
{ card: findCard('the-magician')!, position: '현재', reversed: true },
|
||||||
|
{ card: findCard('the-high-priestess')!, position: '미래', reversed: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('buildReferenceBlock', () => {
|
||||||
|
it('각 카드의 위치·정역·키워드·의미를 텍스트 블록으로 만든다', () => {
|
||||||
|
const block = buildReferenceBlock(picks);
|
||||||
|
expect(block).toContain('과거');
|
||||||
|
expect(block).toContain('The Fool');
|
||||||
|
expect(block).toContain('정방향');
|
||||||
|
expect(block).toContain('역방향');
|
||||||
|
expect(block.length).toBeGreaterThan(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('buildContextMeta', () => {
|
||||||
|
it('메이저 비율·원소 분포·정역 흐름을 계산한다', () => {
|
||||||
|
const meta = buildContextMeta(picks);
|
||||||
|
expect(meta.major_minor_ratio).toBe('3:0');
|
||||||
|
expect(meta.orientation_flow).toBe('upright→reversed→upright');
|
||||||
|
expect(typeof meta.element_distribution).toBe('object');
|
||||||
|
});
|
||||||
|
});
|
||||||
20
lib/__tests__/tarot-shuffle.test.ts
Normal file
20
lib/__tests__/tarot-shuffle.test.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { fisherYates, buildShuffle } from '../tarot/shuffle';
|
||||||
|
import { TAROT_DECK } from '../tarot/cards';
|
||||||
|
|
||||||
|
describe('fisherYates', () => {
|
||||||
|
it('원본을 변형하지 않고 같은 원소 집합을 반환한다', () => {
|
||||||
|
const input = [1, 2, 3, 4, 5];
|
||||||
|
const out = fisherYates(input);
|
||||||
|
expect(input).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
expect([...out].sort()).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('buildShuffle', () => {
|
||||||
|
it('요청한 수만큼, 중복 없이, reversed 필드를 갖고 반환한다', () => {
|
||||||
|
const out = buildShuffle(TAROT_DECK, 20);
|
||||||
|
expect(out).toHaveLength(20);
|
||||||
|
expect(new Set(out.map((c) => c.slug)).size).toBe(20);
|
||||||
|
for (const c of out) expect(typeof c.reversed).toBe('boolean');
|
||||||
|
});
|
||||||
|
});
|
||||||
29
lib/tarot/reference.ts
Normal file
29
lib/tarot/reference.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Pick } from './shuffle';
|
||||||
|
|
||||||
|
export function buildReferenceBlock(picks: Pick[]): string {
|
||||||
|
return picks
|
||||||
|
.map((p, i) => {
|
||||||
|
const c = p.card;
|
||||||
|
const dir = p.reversed ? '역방향' : '정방향';
|
||||||
|
const kws = (p.reversed ? c.reversedKeywords : c.keywords).join(', ');
|
||||||
|
const meaning = p.reversed ? c.meaningReversed : c.meaningUpright;
|
||||||
|
const arcana = c.arcana === 'major' ? `Major (${c.id})` : `Minor (${c.suit})`;
|
||||||
|
return [
|
||||||
|
`## ${i + 1}. 위치: ${p.position} | 카드: ${c.nameEn} (${dir})`,
|
||||||
|
`- 아르카나: ${arcana}`,
|
||||||
|
`- 원소: ${c.element}`,
|
||||||
|
`- ${dir} 키워드: ${kws}`,
|
||||||
|
`- ${dir} 의미: ${meaning}`,
|
||||||
|
].join('\n');
|
||||||
|
})
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildContextMeta(picks: Pick[]) {
|
||||||
|
const major = picks.filter((p) => p.card.arcana === 'major').length;
|
||||||
|
const minor = picks.length - major;
|
||||||
|
const element_distribution: Record<string, number> = { air: 0, water: 0, fire: 0, earth: 0 };
|
||||||
|
for (const p of picks) element_distribution[p.card.element] += 1;
|
||||||
|
const orientation_flow = picks.map((p) => (p.reversed ? 'reversed' : 'upright')).join('→');
|
||||||
|
return { major_minor_ratio: `${major}:${minor}`, element_distribution, orientation_flow };
|
||||||
|
}
|
||||||
18
lib/tarot/shuffle.ts
Normal file
18
lib/tarot/shuffle.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { TarotCard } from './cards';
|
||||||
|
|
||||||
|
export type Pick = { card: TarotCard; position: string; reversed: boolean };
|
||||||
|
|
||||||
|
export function fisherYates<T>(input: T[]): T[] {
|
||||||
|
const a = [...input];
|
||||||
|
for (let i = a.length - 1; i > 0; i -= 1) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[a[i], a[j]] = [a[j], a[i]];
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildShuffle(deck: TarotCard[], size: number): (TarotCard & { reversed: boolean })[] {
|
||||||
|
return fisherYates(deck)
|
||||||
|
.slice(0, size)
|
||||||
|
.map((c) => ({ ...c, reversed: Math.random() < 0.5 }));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user