feat: Implement Tarot session management and reading spreads
- Added TarotSessionManager class to manage tarot reading sessions, including session creation, retrieval, reading addition, and cleanup of old sessions. - Defined various tarot spreads in a new spreads module, including single card, three card, Celtic cross, and more, with detailed descriptions and meanings for each position. - Created core types for tarot cards, readings, and sessions in a new types module to structure data effectively. - Configured TypeScript settings in tsconfig.json for improved development experience and compatibility.
This commit is contained in:
253
src/tarot/card-search.ts
Normal file
253
src/tarot/card-search.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { TarotCard } from './types.js';
|
||||
|
||||
export interface SearchOptions {
|
||||
keyword?: string;
|
||||
suit?: string;
|
||||
arcana?: 'major' | 'minor';
|
||||
element?: 'fire' | 'water' | 'air' | 'earth';
|
||||
number?: number;
|
||||
orientation?: 'upright' | 'reversed';
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
card: TarotCard;
|
||||
relevanceScore: number;
|
||||
matchedFields: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced search functionality for the tarot card database
|
||||
*/
|
||||
export class TarotCardSearch {
|
||||
private cards: readonly TarotCard[];
|
||||
|
||||
constructor(cards: readonly TarotCard[]) {
|
||||
this.cards = cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search cards by various criteria
|
||||
*/
|
||||
search(options: SearchOptions): SearchResult[] {
|
||||
let results: SearchResult[] = [];
|
||||
|
||||
for (const card of this.cards) {
|
||||
const result = this.evaluateCard(card, options);
|
||||
if (result.relevanceScore > 0) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by relevance score (highest first)
|
||||
return results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search by keyword in card meanings, keywords, and symbolism
|
||||
*/
|
||||
searchByKeyword(keyword: string): SearchResult[] {
|
||||
return this.search({ keyword });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cards by suit
|
||||
*/
|
||||
findBySuit(suit: string): TarotCard[] {
|
||||
return this.cards.filter(card => card.suit === suit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cards by arcana type
|
||||
*/
|
||||
findByArcana(arcana: 'major' | 'minor'): TarotCard[] {
|
||||
return this.cards.filter(card => card.arcana === arcana);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cards by element
|
||||
*/
|
||||
findByElement(element: 'fire' | 'water' | 'air' | 'earth'): TarotCard[] {
|
||||
return this.cards.filter(card => card.element === element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cards with similar meanings
|
||||
*/
|
||||
findSimilarCards(cardId: string, limit: number = 5): TarotCard[] {
|
||||
const targetCard = this.cards.find(card => card.id === cardId);
|
||||
if (!targetCard) return [];
|
||||
|
||||
const similarities: { card: TarotCard; score: number }[] = [];
|
||||
|
||||
for (const card of this.cards) {
|
||||
if (card.id === cardId) continue;
|
||||
|
||||
const score = this.calculateSimilarity(targetCard, card);
|
||||
similarities.push({ card, score });
|
||||
}
|
||||
|
||||
return similarities
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map(item => item.card);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random cards with optional filters
|
||||
*/
|
||||
getRandomCards(count: number = 1, options?: Partial<SearchOptions>): TarotCard[] {
|
||||
let filteredCards = this.cards;
|
||||
|
||||
if (options) {
|
||||
const searchResults = this.search(options as SearchOptions);
|
||||
filteredCards = searchResults.map(result => result.card);
|
||||
}
|
||||
|
||||
const shuffled = [...filteredCards].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, Math.min(count, shuffled.length));
|
||||
}
|
||||
|
||||
private evaluateCard(card: TarotCard, options: SearchOptions): SearchResult {
|
||||
let score = 0;
|
||||
const matchedFields: string[] = [];
|
||||
|
||||
// Exact matches get higher scores
|
||||
if (options.suit && card.suit === options.suit) {
|
||||
score += 10;
|
||||
matchedFields.push('suit');
|
||||
}
|
||||
|
||||
if (options.arcana && card.arcana === options.arcana) {
|
||||
score += 8;
|
||||
matchedFields.push('arcana');
|
||||
}
|
||||
|
||||
if (options.element && card.element === options.element) {
|
||||
score += 8;
|
||||
matchedFields.push('element');
|
||||
}
|
||||
|
||||
if (options.number !== undefined && card.number === options.number) {
|
||||
score += 10;
|
||||
matchedFields.push('number');
|
||||
}
|
||||
|
||||
// Keyword search in various fields
|
||||
if (options.keyword) {
|
||||
const keyword = options.keyword.toLowerCase();
|
||||
|
||||
// Search in card name
|
||||
if (card.name.toLowerCase().includes(keyword)) {
|
||||
score += 15;
|
||||
matchedFields.push('name');
|
||||
}
|
||||
|
||||
// Search in keywords
|
||||
const orientation = options.orientation || 'upright';
|
||||
const keywords = card.keywords[orientation];
|
||||
if (keywords.some(kw => kw.toLowerCase().includes(keyword))) {
|
||||
score += 12;
|
||||
matchedFields.push('keywords');
|
||||
}
|
||||
|
||||
// Search in meanings
|
||||
const meanings = card.meanings[orientation];
|
||||
for (const [field, meaning] of Object.entries(meanings)) {
|
||||
if (meaning.toLowerCase().includes(keyword)) {
|
||||
score += 8;
|
||||
matchedFields.push(`meaning_${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Search in symbolism
|
||||
if (card.symbolism.some(symbol => symbol.toLowerCase().includes(keyword))) {
|
||||
score += 6;
|
||||
matchedFields.push('symbolism');
|
||||
}
|
||||
|
||||
// Search in description
|
||||
if (card.description.toLowerCase().includes(keyword)) {
|
||||
score += 4;
|
||||
matchedFields.push('description');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
card,
|
||||
relevanceScore: score,
|
||||
matchedFields
|
||||
};
|
||||
}
|
||||
|
||||
private calculateSimilarity(card1: TarotCard, card2: TarotCard): number {
|
||||
let score = 0;
|
||||
|
||||
// Same suit/arcana
|
||||
if (card1.suit === card2.suit) score += 3;
|
||||
if (card1.arcana === card2.arcana) score += 2;
|
||||
if (card1.element === card2.element) score += 3;
|
||||
|
||||
// Similar numbers
|
||||
if (card1.number !== undefined && card2.number !== undefined) {
|
||||
const numDiff = Math.abs(card1.number - card2.number);
|
||||
if (numDiff <= 1) score += 2;
|
||||
else if (numDiff <= 2) score += 1;
|
||||
}
|
||||
|
||||
// Keyword overlap
|
||||
const keywords1 = [...card1.keywords.upright, ...card1.keywords.reversed];
|
||||
const keywords2 = [...card2.keywords.upright, ...card2.keywords.reversed];
|
||||
|
||||
for (const kw1 of keywords1) {
|
||||
for (const kw2 of keywords2) {
|
||||
if (kw1.toLowerCase() === kw2.toLowerCase()) {
|
||||
score += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about the card database
|
||||
*/
|
||||
getStatistics() {
|
||||
const stats = {
|
||||
totalCards: this.cards.length,
|
||||
majorArcana: this.cards.filter(c => c.arcana === 'major').length,
|
||||
minorArcana: this.cards.filter(c => c.arcana === 'minor').length,
|
||||
suits: {} as Record<string, number>,
|
||||
elements: {} as Record<string, number>,
|
||||
mostCommonKeywords: this.getMostCommonKeywords(10)
|
||||
};
|
||||
|
||||
// Count by suits
|
||||
for (const card of this.cards) {
|
||||
if (card.suit) {
|
||||
stats.suits[card.suit] = (stats.suits[card.suit] || 0) + 1;
|
||||
}
|
||||
if (card.element) {
|
||||
stats.elements[card.element] = (stats.elements[card.element] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private getMostCommonKeywords(limit: number): Array<{ keyword: string; count: number }> {
|
||||
const keywordCounts: Record<string, number> = {};
|
||||
|
||||
for (const card of this.cards) {
|
||||
const allKeywords = [...card.keywords.upright, ...card.keywords.reversed];
|
||||
for (const keyword of allKeywords) {
|
||||
keywordCounts[keyword] = (keywordCounts[keyword] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(keywordCounts)
|
||||
.map(([keyword, count]) => ({ keyword, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user