# timOhjeet
/**
 * Global settings for cards and decks
 */
const cardSettings = {
    // Where to find card images. Image names should be like s01, h13, d07, c11
    imagePath: 'https://tim.jyu.fi/files/common/images/cards',
    // see https://tekeye.uk/playing_cards/svg-playing-cards?

    // Names of pointer images
    ptrImages: ['dlred', 'lblack', 'lyellow', 'blue', 'orange', 'green', 'black', 'red', 'lyellow', 'bblack', 'bred'],

    // Names for background images
    bgImages: ["back1", "back2", "back3", "back4", "back5", "back6", "back7", "back8", "back9", "back10", "back11", "back12", "back13"],
    // What background image to use from bgImages
    bgIndex:  11,

    deckWidth: "73px",  // width of the deck
    deckHeight: "98px", // height of the deck
    suits: "csdh",      // suit order in comaprision
}

const ruleAny = -20;

/**
 * Set object value from another object
 * @param objTo to what object to set
 * @param key string fo key to set
 * @param objFrom from what object to take the value
 */
function setObjectValue(objFrom, key, objTo) {
    if (objFrom == null || typeof objFrom !== 'object' || Array.isArray(objFrom)) return;
    if (objTo == null || typeof objTo !== 'object' || Array.isArray(objTo)) return;
    if (!key in objFrom) return;
    objTo[key] = objFrom[key];
}

/**
 * Copy object values from one object to another
 * If the value is on cardSettings, it is copied to that object
 * @param params where to copy from
 * @param objTo where to copy to if not in cardSettings, if null just use cardSettings
 */
function copyParamsValues(params, objTo=null) {
    if (params == null || typeof params !== 'object' || Array.isArray(params)) return;
    if (objTo == null || typeof objTo !== 'object' || Array.isArray(objTo)) objTo = null;
    for (let key in params) {
        let to = cardSettings;
        if (!(key in to)) to = objTo;
        if (to != null) to[key] = params[key];
    }
}

/**
 * Set component (assume has comp.element) absolute position
 * @param comp what component position to set
 * @param x left postion for the comp.element
 * @param y top position for the comp.element, if < 0 use bottom
 * @returns comp for chaining
 */
function setPosition(comp, x, y) {
    const elem = comp.element;
    if (elem == null) return comp;
    setElemPosition(elem, x, y);
    return comp;
}


/**
 * Set element absolute position
 * @param elem what element position to set
 * @param x left postion for the element
 * @param y top position for the element, if < 0 use bottom
 * @returns elem for chaining
 */
function setElemPosition(elem, x, y) {
    if (elem == null) return elem;
    elem.style.position = "absolute";
    elem.style.left = `${x}px`;
    if (y>=0) {
        elem.style.top = `${y}px`;
        elem.style.bottom = `unset`;
    }
    else {
        elem.style.top = `unset`;
        elem.style.bottom = `${-y}px`;
    }
    return elem;
}


/**
 * Set so that element cannot be selected
 * @param elem what element to set
 * @returns elem for chaining
 */
function setNoSelect(elem) {
    elem.style.userSelect = "none";
    elem.style.webkitUserSelect = "none";
    elem.style.MozUserSelect = "none";
    elem.style.msUserSelect = "none";
    return elem;
}


/**
 * Set element dimensions
 * @param elem what element dimensions to set
 * @param x left postion for the element
 * @param y top position for the element if < 0 use bottom
 * @param w width for the element
 * @param h height for the element
 * @returns elem for chaining
 */
function setElemDimensions(elem, x, y, w, h) {
    if (elem == null) return elem;
    elem.style.width = `${w}px`;
    elem.style.height = `${h}px`;
    return setElemPosition(elem, x, y);
}


/**
 * Check if device is iPad
 * @returns true if ipad
 */
function isIpad() {
    return /iPad|Macintosh/.test(navigator.userAgent) && 'ontouchend' in document;
}


/**
 * Parse integer from string, if not a number return default value
 * @param s string to parse
 * @param defaultValue value to return if not a number
 * @returns parse value or default value
 */
function safeParseInt(s, defaultValue = -1) {
    const result = parseInt(s);
    return isNaN(result) ? defaultValue : result;
}


function debugClear() {
    const debugElement = document.getElementById('debug');
    debugElement.innerHTML = '';
}


function debugLog(message) {
    const debugElement = document.getElementById('debug');
    // const logMessage = debugElement; // document.createElement('div');
    // logMessage.textContent = message;
    // debugElement.appendChild(logMessage);
    debugElement.style.position = 'absolute';
    debugElement.style.top = '100px';
    debugElement.innerHTML += "<br>" + message;
}


/**
 * Class for general card. Has drag and drop functionality.
 * Should have element for html element.
 */
class Card {
    constructor(value, id, img1, img2, visible) {
        this.img = [img2, img1];
        this.value = value;
        this.id = id;
        this.visible = visible ? 1 : 0;
        this.element = this.createCardElement();
        this.element.cardInstance = this;
        this.deck = null;
        this.tapHandler = null;  // mimic click event
        this.touchMode = 0; // 1 = start, 2 = move, 0 = none, mimic click event
    }

    createCardElement() {
        const cardElem = document.createElement('div');
        cardElem.classList.add('card');
        cardElem.draggable = true;
        cardElem.id = this.id;

        const img = document.createElement('img');
        cardElem.img = img;
        cardElem.img.src = this.img[this.visible];
        cardElem.img.draggable = false;

        cardElem.appendChild(img);

        cardElem.addEventListener('dragstart', (event) => this.handleDragStart(event));
        cardElem.addEventListener('dragend', (event) => this.handleDragEnd(event));
        // document.addEventListener('drag', (event) => this.handleDragMove(event));
        cardElem.addEventListener('touchstart', (event) => this.handleTouchStart(event));
        cardElem.addEventListener('touchmove', (event) => this.handleTouchMove(event));
        cardElem.addEventListener('touchend', (event) => this.handleTouchEnd(event));
        cardElem.addEventListener('click', (_event) => this.tap());

        // Firefox ei anna oiketa koordinaatteja drag-tapahtumasta
        document.addEventListener('dragover', (event) => this.handleDragMove(event));

        return cardElem;
    }


    removeAllEventListeners() {
        /*
        const cardElem = this.element.cloneNode(true);
        this.element.replaceWith(cardElem);
        this.element = cardElem;
         */

        this.element.draggable = false;
    }

    setBackGroundImage(img) {
        this.img[0] = img;
        this.element.img.src = this.img[this.visible];
    }

    setBackGroundImageByIndex(imgIndex) {
        this.setBackGroundImage(`${cardSettings.imagePath}/${cardSettings.bgImages[imgIndex]}.png`);
    }

    debugE(event) {
        debugLog(``+
            `e: ${event.clientX},${event.clientY} <br>`+
            `p: ${event.pageX},${event.pageY}<br>` +
            `s: ${event.screenX},${event.screenY}<br>` +
            `x: ${event.x},${event.y}<br>`
        )
    }

    setVisible(visible) {
        this.visible = visible ? 1 : 0;
        this.element.img.src = this.img[this.visible];
        this.element.img.style.display = "unset";
    }

    handleDragStart(event) {
        if (!this.element.draggable) return;
        // event.preventDefault();   // jos päällä Safari maalaa
        // this.startDrag(event, event.offsetX, event.offsetY);
        // this.debugE(event);
        this.startDrag(event, event.clientX, event.clientY, event.offsetX, event.offsetY);
    }

    handleDragEnd(event) {
        if (!this.element.draggable) return;
        event.preventDefault();
        // this.debugE(event);
        // this.endDrag(event, event.clientX, event.clientY);
        this.endDrag(event, this.element.lastClientX, this.element.lastClientY); // Mac Safari tarvitsee tämän
    }

    handleTouchStart(event) {
        if (!this.element.draggable) return;
        event.preventDefault();
        this.touchMode = 1; // 1 = start, 2 = move, 0 = none, mimic ckick event

        const touch = event.touches[0];
        const rect = touch.target.getBoundingClientRect();
        this.offsetX = touch.clientX - rect.left;
        this.offsetY = touch.clientY - rect.top;
        // do not start drag yet, if it is just click, first move starts
    }


    handleDragMove(event) {
        if (!this.element.draggable) return;
        // this.debugE(event);
        this.moveDrag(event.clientX, event.clientY);
    }

    handleTouchMove(event) {
        if (!this.element.draggable) return;
        // debugLog(`${event.clientX},${event.clientY} - ${event.pageX},${event.pageY}`)
        event.preventDefault();
        const touch = event.touches[0];
        this.touchMode++;
        if (this.touchMode === 3) { // if first move, start drag
            this.startDrag(event, touch.clientX, touch.clientY, this.offsetX, this.offsetY);
            this.touchMode++;
        } else
        this.moveDrag(touch.clientX, touch.clientY);
    }

    handleTouchEnd(event) {
        if (!this.element.draggable) return;
        const touch = event.changedTouches[0];
        if (this.touchMode && this.touchMode <= 2) { // Consider it a tap if no touch move
            this.tap();
            return;
        }

        this.endDrag(event, touch.clientX, touch.clientY);
    }

    tap() {
        if (this.tapHandler) this.tapHandler(this);
        if (this.deck) this.deck.tap(this);
    }

    startDrag(event, clientX, clientY, offsetX, offsetY) {
        const cardElem = this.element;
        cardElem.offsetX = offsetX;
        cardElem.offsetY = offsetY;
        const dragImage = cardElem.img.cloneNode(true);
        document.body.appendChild(dragImage);
        dragImage.style.position = 'absolute';
        dragImage.style.left = `${clientX - offsetX}px`;
        dragImage.style.top = `${clientY - offsetY}px`;
        dragImage.style.pointerEvents = 'none';
        dragImage.style.opacity = '1';
        dragImage.id = cardElem.id;
        cardElem.touchFeedback = dragImage;

        // Create an invisible element to use as the drag image
        // Does not work with Mac Safari
        const invisibleImage = document.createElement('div');
        invisibleImage.style.width = '1px';
        invisibleImage.style.height = '1px';
        invisibleImage.style.opacity = '0';
        document.body.appendChild(invisibleImage);

        if (event.dataTransfer) {
            // event.dataTransfer.setDragImage(dragImage, offsetX, offsetY);
            event.dataTransfer.setDragImage(invisibleImage, 0, 0);
        }

        setTimeout(() => {
            document.body.appendChild(cardElem);
            // cardElem.img.classList.add("hidden");
            cardElem.img.style.display = "none";
            invisibleImage.remove();
        }, 0);
    }

    moveDrag(clientX, clientY) {
        // debugClear();
        // debugLog(`${clientX},${clientY}`)
        if (clientX != 0 && clientY != 0) {
            this.element.lastClientX = clientX; // Maciä varten kun Safarissa endDrag cliect? toimii väärinpäin
            this.element.lastClientY = clientY;
        }
        const cardElem = this.element;
        if (cardElem.touchFeedback) {
            cardElem.touchFeedback.style.left = `${clientX - cardElem.offsetX}px`;
            cardElem.touchFeedback.style.top = `${clientY - cardElem.offsetY}px`;
        }
    }

    endDrag(event, clientX, clientY) {
        const card = this;
        const cardElem = card.element;
        // cardElem.img.classList.remove('hidden');
        cardElem.img.style.display = "unset";
        if (cardElem.touchFeedback) {
            document.body.removeChild(cardElem.touchFeedback);
            cardElem.touchFeedback = null;
        }

        // debugLog(`${event.pageX},${event.pageY}, ${event.screenX},${event.screenY}`);
        // const elementsUnderCursor = document.elementsFromPoint(clientX, clientY);
        const elementsUnderCursor = document.elementsFromPoint(clientX, clientY);
        const targetDeckElem = elementsUnderCursor.find(el => el.classList.contains('deck'));

        let deck = card.deck;
        if (targetDeckElem) {
            const targetDeck = targetDeckElem.deckInstance;
            if (targetDeck.isAllowedToDrop(card))
                deck = targetDeck;
        }

        let np = deck.nextCardPosition();
        let deckRect = deck.element.getBoundingClientRect();
        const dx = clientX - cardElem.offsetX - (deckRect.left + np.x);
        const dy = clientY - cardElem.offsetY - (deckRect.top + np.y);
        // const dropX = clientX - deckRect.left;
        // const dropY = clientY - deckRect.top;
        // debugLog(`${clientX},${clientY} -> ${dropX},${dropY} / ${cardElem.offsetX},${cardElem.offsetY}`)

        let s = Math.sqrt(dx * dx + dy * dy);
        let t = Math.min(s / deck.speed,2);

        cardElem.style.position = 'absolute';
        cardElem.style.left = `${clientX - cardElem.offsetX}px`;
        cardElem.style.top = `${clientY - cardElem.offsetY}px`;
        cardElem.style.transition = `transform ${t}s ease`;
        cardElem.style.transform = `translate(${-dx}px, ${-dy}px)`;
        setTimeout(() => {
            cardElem.style.transition = '';
            cardElem.style.transform = '';
            deck.addCard(card);
        }, Math.round(t * 1000));
    }

    cropToImg() {
        const img = this.element.img;
        img.onload = () => {
            this.updateElementSize(img);
        };
        if (img.complete) {
            img.onload(img);
        }
    }

    updateElementSize(img) {
        const rect = img.getBoundingClientRect();
        this.element.style.width = `${rect.width}px`;
        this.element.style.height = `${rect.height}px`;
    }

    static compareCards(a, b, useSuit = false) {
        if (a == null && b == null) return 0;
        if (a == null) return -1;
        if (b == null) return 1;
        if (a.value < b.value) return -1;
        if (a.value > b.value) return 1;
        if (!useSuit  || !a.suit || !b.suit) return 0;
        const ai = cardSettings.suits.indexOf(a.suit);
        const bi = cardSettings.suits.indexOf(b.suit);
        if (ai < bi) return -1;
        if (ai > bi) return 1;
        return 0;
    }
}


class PlayingCard extends Card {
    constructor(suit, value, visible) {
        const formattedValue = value < 10 ? `0${value}` : value;
        const id = `${suit}${formattedValue}`;
        const img1 = `${cardSettings.imagePath}/${id}.png`;
        const img2 = `${cardSettings.imagePath}/${cardSettings.bgImages[cardSettings.bgIndex]}.png`;
        super(value, id, img1, img2, visible);
        this.value = value;
        this.suit = suit;
        this.color = suit === 'c' || suit === 's' ? 'black' : 'red';
        this.element = this.createCardElement();
        this.element.cardInstance = this;
    }
}


class Deck {
    constructor(elementId, text) {
        this.maxCards = 10000;
        this.visible = false;
        this.element = this.createDeckElement(elementId);
        this.element.classList.add('deck');
        this.element.deckInstance = this;
        this.cards = [];
        this.addEventListeners();
        this.speed = 500;
        this.textElement = null;
        this.setCenterText(text);
        this.onInsert = null;
        this.onFull = null;
        this.allowedFunction = null;
        this.dx = 0;
        this.dy = 0;
        this.id = elementId;
        this.tapHandler = null;
    }

    createDeckElement(elementId) {
        const deckElement = document.createElement('div');
        deckElement.id = elementId;
        deckElement.style.position = "absolute";
        deckElement.style.top = "0";
        deckElement.style.left = "0";
        deckElement.style.width = cardSettings.deckWidth;
        deckElement.style.height = cardSettings.deckHeight;
        deckElement.style.border = "1px solid black";
        deckElement.draggable = false;
        return deckElement;
    }

    tap(card) {
        if (this.tapHandler) this.tapHandler(this, card);
    }

    addEventListeners() {
        this.element.addEventListener('dragover', (event) => {
            event.preventDefault();
        });
    }


    isAllowedToDrop(card) {
        if (this.cards.length >= this.maxCards) return false;
        if (!this.allowedFunction) return true;
        return this.allowedFunction(this, card);
    }

    nextCardPosition(delta = 0) {
        let n = this.cards.length + delta;
        return {x: n * this.dx, y: n * this.dy};
    }


    addCard(card) {
        if (!card) return;
        if (card.deck) card.deck.removeCard(card);
        this.cards.push(card);
        this.element.appendChild(card.element);
        card.element.style.position = 'absolute';
        let np = this.nextCardPosition(-1);
        card.element.style.left = np.x + 'px';
        card.element.style.top = np.y + 'px';
        card.deck = this;
        card.setVisible(this.visible);
        // seuraava on siksi että saadaan talteen kortin alkuperäinen sijainti
        // se katoaa muuten kun kortti siirretään pakasta toiseen
        setTimeout(() => {
            const rect = card.element.getBoundingClientRect();
            card.oldRect = {
                top: rect.top,
                right: rect.right,
                bottom: rect.bottom,
                left: rect.left,
                width: rect.width,
                height: rect.height,
                x: rect.x,
                y: rect.y
            };
            if (this.onInsert) this.onInsert(this, card);
            if (this.cards.length >= this.maxCards && this.onFull) this.onFull(this);
        }, 0);
    }

    animateAddCard(card, onArrival = null) {
        if (!card) {
            if (onArrival) onArrival(this, card);
            return;
        }
        if (card.deck) card.deck.removeCard(card);

        const startX = card.oldRect.left;
        const startY = card.oldRect.top;
        const endX = this.element.getBoundingClientRect().left;
        const endY = this.element.getBoundingClientRect().top;

        card.element.style.position = 'absolute';
        card.element.style.left = `${startX}px`;
        card.element.style.top = `${startY}px`;
        document.body.appendChild(card.element);

        const dx = startX - endX;
        const dy = startY - endY;

        let s = Math.sqrt(dx * dx + dy * dy);
        let t = Math.min((s / this.speed), 2).toFixed(1);

        let transitionHandled = false;
        const onTransitionEnd = () => {
            if (transitionHandled) return;
            transitionHandled = true;
            card.element.style.transition = '';
            card.element.style.transform = '';
            this.addCard(card);
            if (onArrival) onArrival(this, card);
            card.element.removeEventListener('transitionend', onTransitionEnd);
        };
        card.element.addEventListener('transitionend', onTransitionEnd, {once: true});

        setTimeout(() => { // if transition does not alert on end
            onTransitionEnd();
        }, t * 1000 + 100); // Add a small buffer to the timeout

        card.element.offsetHeight; // Force reflow to ensure transition is applied

        setTimeout(() => { // ilman tätä viivettä ei toinen siirto lähtenyt
            requestAnimationFrame(() => {
                card.element.style.transition = `transform ${t}s ease`;
                card.element.style.transform = `translate(${(-dx).toFixed(0)}px, ${(-dy).toFixed(0)}px)`;
            });
        }, 0);
    }


    removeCard(card, updateDisplay = true) {
        const index = this.cards.indexOf(card);
        if (index > -1) {
            this.cards.splice(index, 1);
        }
        if (updateDisplay) this.updateDeckDisplay();
    }

    removeAllCards() {
        for (let card of this.cards) {
            card.deck = null;
            this.element.removeChild(card.element);
        }
        this.cards = [];
        this.updateDeckDisplay();
    }

    moveCardsToTop(pickCardsString) {
        if (!pickCardsString) return;
        let dir = 0;
        if (pickCardsString[0] === '-') {
            dir = 1;
            pickCardsString = pickCardsString.substring(1);
        }
        pickCardsString = pickCardsString.trim();
        const cardIds = pickCardsString.toLowerCase().split(/[ ,;]/).filter(Boolean);
        for (let i = cardIds.length-1; i >= 0; i--) {
            const cardId = cardIds[i];
            const card = this.getById(cardId);
            if (!card) continue;
            if (dir == 0) this.cards.push(card);
            else this.cards.unshift(card);
        }
        this.updateDeckDisplay();
    }

    shuffle() {
        for (let i = this.cards.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]];
        }
        this.updateDeckDisplay();
    }


    updateDeckDisplay() {
        for (let elem of  this.element.children) {
            if (!elem.classList.contains('card')) continue;
            this.element.removeChild(elem);
        }
        for (let card of this.cards) {
            this.element.appendChild(card.element);
        }
        for (let i = 0; i < this.cards.length; i++) {
            this.cards[i].element.style.top = i * this.dy + 'px';
            this.cards[i].element.style.left = i * this.dx + 'px';
        }
    }

    pop() {
        if (this.cards.length === 0) {
            return null;
        }
        const topCard = this.cards.pop();
        this.element.removeChild(topCard.element);
        topCard.deck = null;
        return topCard;
    }

    getById(cardId) {
        const index = this.cards.findIndex(c => c.id === cardId);
        if (index < 0) return null;
        const card = this.cards[index];
        this.cards.splice(index, 1);
        return card;
    }

    setCenterText(text) {
        if (!text) return;
        let textElement = this.textElement
        if (!textElement) {
            textElement = document.createElement('div');
            textElement.className = 'center-text';
            this.element.appendChild(textElement);
            textElement.style.position = 'absolute';
            textElement.style.top = '50%';
            textElement.style.left = '50%';
            textElement.style.transform = 'translate(-50%, -50%)';
            textElement.style.pointerEvents = 'none'; // Ensure it doesn't interfere with card interactions
            textElement.style.color = 'black';
            this.textElement = textElement;
        }
        textElement.textContent = text;
    }

    setTextSize(size) {
        if (!this.textElement) return;
        this.textElement.style.fontSize = size;
    }

    setTextTop(size) {
        if (!this.textElement) return;
        this.textElement.style.top = size;
    }

    setTextLeft(size) {
        if (!this.textElement) return;
        this.textElement.style.left = size;
    }

    setTextColor(color) {
        if (!this.textElement) return;
        this.textElement.style.color = color;
    }

    setVisible(visible) {
        this.visible = visible;
        for (let card of this.cards) card.setVisible(visible);
    }

    peek() {
        if (this.cards.length === 0) return null;
        return this.cards[this.cards.length - 1];
    }

    peekValue() {
        const card = this.peek();
        if (!card) return -100000;
        return card.value;
    }

    sendCards(targetDeck) {
        while (this.cards.length > 0) {
            targetDeck.animateAddCard(this.pop());
        }
    }

    moveCards(targetDeck) {
        while (this.cards.length > 0) {
            targetDeck.addCard(this.pop());
        }
    }
    sortDeck(sortFirst = 0, useSuit = false) {
        if (!sortFirst) return;
        let n = sortFirst;
        let dir = -1;
        if (n < 0) {
            n = -n;
            dir = 1;
        }
        if (n <= 1) return;
        if (n > 52) n = 52;
        // sort only this.settings.sortFirst first cards
        const topCards = this.cards.slice(-n);
        topCards.sort((a, b) => { return dir*Card.compareCards(a, b, useSuit);});
        this.cards = this.cards.slice(0, -n).concat(topCards);
        this.updateDeckDisplay();
    }

    doSwaps(swapsString) {
        if (!swapsString) return;
        let dir = 0;
        if (swapsString[0] === '-')  {
            dir = this.cards.length - 1;
            swapsString = swapsString.substring(1);
        }
        swapsString = swapsString.trim();
        const swaps = swapsString.toLowerCase().split(/[ ,;]/).filter(Boolean);
        for (let swap of swaps) {
            const [s1, s2] = swap.split('-');
            let i1 = safeParseInt(s1);
            let i2 = safeParseInt(s2);
            if (i1 < 0 || this.cards.length <= i1) continue;
            if (i2 < 0 || this.cards.length <= i2) continue;
            if (dir) {
                i1 = dir - i1;
                i2 = dir - i2;
            }
            [this.cards[i1], this.cards[i2]] = [this.cards[i2], this.cards[i1]];
        }
        this.updateDeckDisplay();
    }

    getCardsArray() {
        return this.cards.map(c => c? c.id: "");
    }

    createNewCard(cardId) {
        if (cardId == null || cardId.length == 0) return null;
        return new Card(cardId, cardId, cardId, cardId, false);
    }

    addCardsFromArray(cardsArray, dealDeck, emptyFirst = true, createNew = false, forceNew = false) {
        if (emptyFirst) this.removeAllCards();
        for (let cardId of cardsArray) {
            if (!cardId) continue;
            let card = null;
            if (!forceNew) card = dealDeck.getById(cardId);
            if (!card) {
                if (createNew || forceNew)
                    card = dealDeck.createNewCard(cardId);
            }
            if (card) this.addCard(card);
        }
    }
}


class PlayingCardDeck extends Deck {
    constructor(elementId, text) {
        super(elementId, text);
    }

    isAllowedToDrop(card) {
        if (!(card instanceof PlayingCard)) return false;
        return super.isAllowedToDrop(card);
    }
}


class DealDeck extends PlayingCardDeck {
    constructor(elementId) {
        super(elementId);
        this.element.classList.add('dealdeck');
        this.maxCards = 52;
        this.createDeck();
    }

    createDeck() {
        const suits = ['c', 'd', 's', 'h'];
        this.maxCards = 0;
        for (let suit of suits) {
            for (let value = 1; value <= 13; value++) {
                const card = new PlayingCard(suit, value, false);
                this.addCard(card);
                this.maxCards++;
            }
        }
    }

    createNewCard(cardId) {
        if (cardId == null || cardId.length < 2) return null;
        if (cardId === "rnd") {
            cardId = cardSettings.suits[Math.floor(Math.random() * 4)] + String(Math.floor(Math.random() * 13) + 1).padStart(2, '0');
        }
        const suite = cardId[0].toLowerCase();
        if (!(cardSettings.suits.includes(suite))) return null;
        const value = parseInt(cardId.substring(1));
        if (isNaN(value) || value < 1 || value > 13) return null;
        return new PlayingCard(suite, value, false);
    }
}


class RuleDeck extends PlayingCardDeck {
    constructor(elementId, text, rules) {
        super(elementId, text);
        this.element.classList.add('ruledeck');
        this.rules = rules;
    }

    isAllowedToDrop(card) {
        if (!super.isAllowedToDrop(card)) return false;
        const topCard = this.peek();
        if (!topCard) { // emtpy deck
            if (this.rules.first === ruleAny) return true;
            return card.value === this.rules.first;
        }
        if (this.rules.diff === ruleAny) return true;
        if (topCard.value === this.rules.end)   // new round?
            return card.value === this.rules.nextFirst;
        return card.value === topCard.value + this.rules.diff;
    }
}


class Table {
    constructor(elementId, visible) {
        this.element = this.createTableElement(elementId);
        this.element.classList.add('table');
        this.element.poytaInstance = this;
        this.decks = [];
        this.dx = 0;
        this.dy = 0;
        this.mx = 0; // margin X for first deck
        this.my = 0; // margin Y
        this.visible = visible;
    }

    createTableElement(elementId) {
        const tableElement = document.createElement('div');
        tableElement.id = elementId;
        tableElement.style.color = "#009F00";
        return tableElement;
    }

    addDeck(deck) {
        deck.setVisible(this.visible);
        this.decks.push(deck);
        this.element.appendChild(deck.element);
        deck.element.style.position = "absolute";
        const n = this.decks.length - 1;
        deck.element.style.top = this.mx + n * this.dy + 'px';
        deck.element.style.left = this.my + n * this.dx + 'px';
    }

    removeDeck(deck) {
        const index = this.decks.indexOf(deck);
        if (index > -1) {
            this.decks.splice(index, 1);
            this.element.removeChild(deck.element);
        }
    }

    removeAllDecks() {
        while (this.decks.length > 0) {
            this.removeDeck(this.decks[this.decks.length - 1]);
        }
    }

    removeAllCards() {
        for (let deck of this.decks)
            deck.removeAllCards();
    }


    setVisible(visible) {
        this.visible = visible;
        for (let deck of this.decks) deck.setVisible(visible);
    }

    sendCards(targetDeck) {
        for (let deck of this.decks) {
            deck.sendCards(targetDeck);
        }
    }

    moveCards(targetDeck) {
        for (let deck of this.decks) {
            deck.moveCards(targetDeck);
        }
    }
}


class Counter {
    constructor(id) {
        this.value = 0;
        this.id = id;
        this.element = this.createCounterElement(id);
        this.element.classList.add('counter');
        this.setValue(0);
    }

    createCounterElement(id) {
        const element = document.createElement('div');
        element.id = id;
        element.style.position = "absolute";
        element.style.color = 'black';
        element.style.backgroundColor = 'aqua';
        element.style.width = "60px";
        element.style.height = "30px";
        element.textContent = "0";
        element.style.textAlign = "center";
        element.style.lineHeight = "30px";
        element.style.fontSize = "25px";
        element.style.fontWeight = "bold";
        return element;
    }

    setValue(value) {
        this.value = value;
        this.element.textContent = value;
    }

    inc() {
        this.setValue(this.value + 1);
    }
}


function areObjectsEqual(obj1, obj2) {
    const sortedObj1 = sortObjectProperties(obj1);
    const sortedObj2 = sortObjectProperties(obj2);
    return JSON.stringify(sortedObj1) === JSON.stringify(sortedObj2);
}


function sortObjectProperties(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (Array.isArray(obj)) {
        return obj.map(sortObjectProperties);
    }
    const sortedKeys = Object.keys(obj).sort();
    const result = {};
    for (const key of sortedKeys) {
        result[key] = sortObjectProperties(obj[key]);
    }
    return result;
}

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.