UNPKG

belote

Version:

A TypeScript library for playing Belote card game with bot support.

310 lines (309 loc) 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mulberry32 = mulberry32; exports.shuffleCards = shuffleCards; exports.getCardValue = getCardValue; exports.firstLetterUpperCase = firstLetterUpperCase; exports.getCardImageUrl = getCardImageUrl; exports.getCardColorImageUrl = getCardColorImageUrl; exports.createDeck = createDeck; exports.isConsecutiveSequence = isConsecutiveSequence; exports.validateCalling = validateCalling; exports.getHighestCall = getHighestCall; exports.compareCallStrength = compareCallStrength; exports.getHighestCardInSequence = getHighestCardInSequence; exports.canPlayCard = canPlayCard; exports.findLegalCard = findLegalCard; exports.canBeatCard = canBeatCard; exports.getHighestTrumpInTrick = getHighestTrumpInTrick; exports.getHighestNonTrumpInTrick = getHighestNonTrumpInTrick; exports.cardColorLocalized = cardColorLocalized; exports.cardTypeLocalized = cardTypeLocalized; exports.normalizeLocalColor = normalizeLocalColor; exports.normalizeLocalType = normalizeLocalType; const types_1 = require("./types"); function mulberry32(seed) { return function () { let t = seed += 0x6D2B79F5; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function shuffleCards(deck, times = 1, rng) { if (!Array.isArray(deck)) throw new Error('Deck must be an array of cards.'); else if (times < 1) throw new Error('Times must be greater than 0.'); const random = rng ?? mulberry32(Date.now()); for (let t = 0; t < times; t++) { for (let i = deck.length - 1; i > 0; i--) { const j = Math.floor(random() * (i + 1)); [deck[i], deck[j]] = [deck[j], deck[i]]; } } return deck; } function getCardValue(card, adut) { if (card.color === adut) return types_1.CardPointsAdut[firstLetterUpperCase(card.type)]; else return types_1.CardPoints[firstLetterUpperCase(card.type)]; } function firstLetterUpperCase(str) { if (str.length === 0) return str; return str.charAt(0).toUpperCase() + str.slice(1); } function getCardImageUrl(card, size = 'medium', imageProvider) { const localizedColor = cardColorLocalized(card.color); const localizedType = cardTypeLocalized(card.type); return `${imageProvider}/${size}/${localizedColor}/${localizedType}.webp`; } function getCardColorImageUrl(color, size = 'medium', imageProvider) { const localizedColor = cardColorLocalized(color); return `${imageProvider}/${size}/colors/${localizedColor}.webp`; } function createDeck(shuffle = 1) { const deck = []; for (const color of types_1.cardColors) { for (const type of types_1.cardTypes) { deck.push({ color, type }); } } return shuffleCards(deck, shuffle); } function isConsecutiveSequence(cards) { for (let i = 0; i < cards.length - 1; i++) { const currIndex = types_1.cardTypeOrder.indexOf(cards[i].type); const nextIndex = types_1.cardTypeOrder.indexOf(cards[i + 1].type); if (nextIndex !== currIndex + 1) return false; } return true; } function validateCalling(cards, adut) { let bestCall = null; const cardsByColor = { hearts: [], diamonds: [], clubs: [], spades: [] }; const cardsByType = { Ace: [], King: [], Queen: [], Jack: [], Ten: [], Nine: [], Eight: [], Seven: [], }; for (const card of cards) { cardsByColor[card.color].push(card); cardsByType[firstLetterUpperCase(card.type)].push(card); } const tryUpdateBest = (call) => { if (!bestCall || call.type > bestCall.type) bestCall = call; }; // 1. Check for Belot (8 cards of same color). for (const color of types_1.cardColors) { if (cards.length === 8 && cardsByColor[color].length === 8) { return { type: types_1.Callings.Belot }; } } // 2. Check Bela: exactly 2 cards - King and Queen of adut. if (cards.length === 2 && cardsByColor[adut].some((c) => c.type === 'king') && cardsByColor[adut].some((c) => c.type === 'queen')) { if (cards.every((c) => c.color === adut && (c.type === 'king' || c.type === 'queen'))) { tryUpdateBest({ type: types_1.Callings.Bela }); } } // 2. Check four of a kind. for (const type of ['Jack', 'Nine', 'Ace', 'King', 'Queen', 'Ten']) { if (cards.length === 4 && cardsByType[type].length === 4) { switch (type) { case 'Jack': tryUpdateBest({ type: types_1.Callings.Jack4 }); break; case 'Nine': tryUpdateBest({ type: types_1.Callings.Nine4 }); break; case 'Ace': tryUpdateBest({ type: types_1.Callings.Ace4 }); break; case 'King': tryUpdateBest({ type: types_1.Callings.King4 }); break; case 'Queen': tryUpdateBest({ type: types_1.Callings.Queen4 }); break; case 'Ten': tryUpdateBest({ type: types_1.Callings.Ten4 }); break; } } } // 3. Check for sequences. if (cards.length >= 3) { const firstColor = cards[0].color; if (cards.every((c) => c.color === firstColor)) { const sortedCards = cards.slice().sort((a, b) => types_1.cardTypeOrder.indexOf(a.type) - types_1.cardTypeOrder.indexOf(b.type)); if (isConsecutiveSequence(sortedCards)) { const callKey = `Sequence${cards.length}`; if (types_1.Callings[callKey]) tryUpdateBest({ type: types_1.Callings[callKey] }); else if (cards.length >= 5) tryUpdateBest({ type: types_1.Callings.Sequence5 }); } } } return bestCall; } function getHighestCall(calls) { if (calls.length === 0) return null; return calls.reduce((highest, current) => { if (!highest) return current; const comparison = compareCallStrength(current, highest); return comparison > 0 ? current : highest; }); } function compareCallStrength(call1, call2) { const getCallStrength = (call) => { switch (call) { case types_1.Callings.Sequence3: return 1; case types_1.Callings.Sequence4: return 2; case types_1.Callings.Sequence5: return 3; case types_1.Callings.Sequence6: return 4; case types_1.Callings.Sequence7: return 5; case types_1.Callings.Bela: return 6; case types_1.Callings.Ace4: return 7; case types_1.Callings.King4: return 8; case types_1.Callings.Queen4: return 9; case types_1.Callings.Jack4: return 10; case types_1.Callings.Nine4: return 11; case types_1.Callings.Ten4: return 12; case types_1.Callings.Belot: return 13; default: return 0; } }; const strength1 = getCallStrength(call1.call); const strength2 = getCallStrength(call2.call); if (strength1 !== strength2) { return strength1 - strength2; } if (call1.call >= types_1.Callings.Sequence3 && call1.call <= types_1.Callings.Sequence7) { const highestCard1 = getHighestCardInSequence(call1.cards); const highestCard2 = getHighestCardInSequence(call2.cards); return types_1.CardPoints[firstLetterUpperCase(highestCard1.type)] - types_1.CardPoints[firstLetterUpperCase(highestCard2.type)]; } return 0; } function getHighestCardInSequence(cards) { return cards.reduce((highest, current) => { return types_1.CardPoints[firstLetterUpperCase(current.type)] > types_1.CardPoints[firstLetterUpperCase(highest.type)] ? current : highest; }); } function canPlayCard(card, currentTrick, adut, playerCards) { if (currentTrick.length === 0) return true; const leadingSuit = currentTrick[0].color; const hasLeadingSuit = playerCards.some((c) => c.color === leadingSuit); // If player has leading suit, must follow suit. if (hasLeadingSuit) { if (card.color !== leadingSuit) return false; // If leading suit is trump, must play higher trump if possible. if (leadingSuit === adut) { const mustBeatCard = getHighestTrumpInTrick(currentTrick, adut); if (mustBeatCard && canBeatCard(card, mustBeatCard, adut)) { const hasHigherTrump = playerCards.some((c) => c.color === adut && canBeatCard(c, mustBeatCard, adut)); return !hasHigherTrump || canBeatCard(card, mustBeatCard, adut); } } else { // Non-trump suit - must play higher if possible (unless trump was played). const trumpPlayed = currentTrick.some((c) => c.color === adut); if (!trumpPlayed) { const highestCard = getHighestNonTrumpInTrick(currentTrick, adut); if (highestCard && canBeatCard(card, highestCard, adut)) { const hasHigherCard = playerCards.some((c) => c.color === leadingSuit && canBeatCard(c, highestCard, adut)); return !hasHigherCard || canBeatCard(card, highestCard, adut); } } } return true; } // No leading suit - must play trump if available. const hasTrump = playerCards.some((c) => c.color === adut); if (hasTrump) { if (card.color !== adut) return false; // Must beat highest trump if possible. const highestTrump = getHighestTrumpInTrick(currentTrick, adut); if (highestTrump) { const hasHigherTrump = playerCards.some((c) => c.color === adut && canBeatCard(c, highestTrump, adut)); return !hasHigherTrump || canBeatCard(card, highestTrump, adut); } } // No leading suit, no trump - can play any card. return true; } function findLegalCard(currentTrick, adut, playerCards) { return playerCards.find((card) => canPlayCard(card, currentTrick, adut, playerCards)) || null; } function canBeatCard(card, targetCard, adut) { if (card.color === adut && targetCard.color === adut) return types_1.CardPointsAdut[firstLetterUpperCase(card.type)] > types_1.CardPointsAdut[firstLetterUpperCase(targetCard.type)]; if (card.color === adut && targetCard.color !== adut) return true; if (card.color !== adut && targetCard.color === adut) return false; if (card.color === targetCard.color) { return types_1.CardPoints[firstLetterUpperCase(card.type)] > types_1.CardPoints[firstLetterUpperCase(targetCard.type)]; } return false; } function getHighestTrumpInTrick(trick, adut) { const trumpCards = trick.filter((c) => c.color === adut); if (trumpCards.length === 0) return null; return trumpCards.reduce((highest, current) => types_1.CardPointsAdut[firstLetterUpperCase(current.type)] > types_1.CardPointsAdut[firstLetterUpperCase(highest.type)] ? current : highest); } function getHighestNonTrumpInTrick(trick, adut) { const nonTrumpCards = trick.filter((c) => c.color !== adut); if (nonTrumpCards.length === 0) return null; return nonTrumpCards.reduce((highest, current) => types_1.CardPoints[firstLetterUpperCase(current.type)] > types_1.CardPoints[firstLetterUpperCase(highest.type)] ? current : highest); } function cardColorLocalized(color) { switch (color) { case 'hearts': return 'herc'; case 'diamonds': return 'karo'; case 'clubs': return 'tref'; case 'spades': return 'pik'; } } function cardTypeLocalized(type) { switch (type) { case 'ace': return 'as'; case 'king': return 'kralj'; case 'queen': return 'baba'; case 'jack': return 'decko'; case 'ten': return 'deset'; case 'nine': return 'devet'; case 'eight': return 'osam'; case 'seven': return 'sedam'; } } function normalizeLocalColor(color) { switch (color) { case 'herc': return 'hearts'; case 'karo': return 'diamonds'; case 'tref': return 'clubs'; case 'pik': return 'spades'; } } function normalizeLocalType(type) { switch (type) { case 'as': return 'ace'; case 'kralj': return 'king'; case 'baba': return 'queen'; case 'decko': return 'jack'; case 'deset': return 'ten'; case 'devet': return 'nine'; case 'osam': return 'eight'; case 'sedam': return 'seven'; } }