javascript - 纸牌游戏手牌评估的组合学,带通配符和重复

标签 javascript algorithm combinatorics

正在开发一款具有多种曲折的拉米风格游戏:

使用两副 5 组套牌而不是一组 4 组套牌(总共 116 张牌)。
套房从 3 到 King,每副牌有 3 张王牌(所以没有 2 也没有 A)。
11轮,第一轮每人3张牌,最后一轮每人13张牌。
除了 clown 是百搭外,每张牌的值(value)都会轮到百搭,这对应于您手中的牌数。
所以第一轮 3 是狂野的,第二轮 4 是狂野的……第 11 轮国王是狂野的(国王的数值为 13)。

目标是放下所有牌。一旦有人“出去”(放下所有牌),剩下的玩家就有一个回合放下所有牌或尽可能多的有效套牌/运行。无论您手中还剩下什么牌,您都会获得积分。

玩家只能在至少有 3 张牌的组或回合中放下牌,即 set: {3:c, 3:d, 3:h}, 运行:{3:c, 4:c, 5:c}。还有一些回合,您必须使用 3 张以上的牌来获得 set/run,因为手牌的数量不能被 3 整除。

为了开始手牌评估,我将卡片分成以下结构:

var suites = {
    'clubs' : [],
    'diamonds' : [],
    'hearts' : [],
    'spades' : [],
    'stars' : []
};
var wilds = [];
var buckets = {
    3 : [],
    4 : [],
    5 : [],
    6 : [],
    7 : [],
    8 : [],
    9 : [],
    10 : [],
    11 : [],
    12 : [],
    13 : []
};
//can have more then one run/set so these are used to hold valid runs/sets
var runs = [];
var sets = [];
var r_num = -1; //starts at -1 so ++ will give 0 index
var s_num = -1;

然后删除所有没有任何卡片的套房/桶。
集合评估非常简单,但运行评估......不是那么简单。

我想出了这个基于范围的运行评估

    for (var s in suites){
        if(suites.hasOwnProperty(s)){
            var suite_length = suites[s].length;
            if(suite_length >= 2){ //only evaluate suits with at least 2 cards in them
                var range = suites[s][suite_length - 1].value - suites[s][0].value;
                r_num++;
                runs[r_num] = [];

                if( (range - 1) >= wilds.length && (range - 1) == suite_length){
                    suites[s].forEach(function(card){ //large range needs several wilds
                        runs[r_num].push(card);
                    });
                    while(runs[r_num].length <= (range + 1) && wilds.length != 0){
                        runs[r_num].push(wilds.pop());
                    }
               }else if(range == 1 && wilds.length >= 1){ //needs one wild
                    suites[s].forEach(function(card){
                        runs[r_num].push(card);
                    });
                    runs[r_num].push(wilds.pop()); 
                }else if( range == suite_length && wilds.length >= 1){ //also needs one wild
                    suites[s].forEach(function(card){
                        runs[r_num].push(card);
                    });
                    runs[r_num].push(wilds.pop());
                }else if( (range + 1) == suite_length){ //perfect run
                    suites[s].forEach(function(card){
                        runs[r_num].push(card);
                    });
                }
            }
        }
    } 
};

这适用于很多情况,但很难解决套件中的重复问题。 我在玩游戏测试时遇到的最棘手的情况之一如下:
{3:梅花,百搭,3:黑桃,4:梅花,5:梅花,6:梅花,6:梅花 7:梅花,8:梅花,10:黑桃,10:红心,百搭

因此可以形成 12 张手牌和有效的下注:看起来像这样:
集合:[{3:c, 3:s, wild},{10:s, 10:h, wild}]
运行:[{4:c, 5:c, 6:c}, {6:c, 7:c, 8:c}]

不幸的是,我最终得到了这样的结果:
集合:[{6:c, 6:c, wild}, {10:s, 10:h, wild}]
运行:[{3:c,4:c,5:c}]
(取决于我是先评估组还是先跑)
集合:[{10:s, 10:h, wild, wild}]
运行:[{3:c, 4:c, 5:c, 6:c, 7:c, 8:c}]
剩下的其他卡片。

当我有这样的牌时,我也会在前几轮遇到问题:
{6:h, 7:h, 7:h, 8:h}
以上是尝试放下部分手时的问题。
{6:h, 7:h, 8:h} 部分手牌情况下的有效运行,玩家将只剩下 7 分。但是因为我没有去除重复项,所以它不会将此评估为可能的运行,玩家将获得所有积分 (28)。

我的问题是: 是否有更好的方法/算法来完成整个过程?

我目前解决这个问题的想法有点令人沮丧,因为它们涉及大量针对特定手牌长度/游戏情况/套件长度的 case/if 语句。有时我删除重复项,有时我不删除,有时我不删除套件,有时我不删除……很多令人头疼的问题,基本上我不确定我是否涵盖了所有可能的手牌/部分放下的情况。

最佳答案

这是一个功能齐全的牌手评估器(尽管有一部分仍需要优化以提高效率)。它使用递归来检查所有可能的组合,并返回具有最低剩余值的组合。

卡片存储为 5 x 11 矩阵,值 0、1 或 2 代表该类型卡片的数量,如下所示:

    3 4 5 6 w 8 9 T J Q K
CLB 1,0,0,0,2,0,1,0,0,0,0 
DMD 0,0,1,0,1,0,1,0,0,0,0 
HRT 0,0,0,0,0,0,1,1,1,0,0 
SPD 0,1,0,0,1,0,0,0,0,0,0 
STR 0,0,0,0,0,0,0,0,0,0,0 
    jokers: 1   value: 93  

在此表示中,游程是非零的水平序列,集合是加起来等于或大于 3 的垂直线。

该函数通过搜索组合、创建手的副本、从副本中删除组合,然后在副本上调用自身来递归工作。

(实际上,递归是从矩阵的开头开始运行的;使用更好的算法来排列长组合的较短部分,递归可以从当前点开始运行,从而避免检查组合两次。)

该示例生成并评估一手随机的 13 张 K 牌。 通过取消注释底部的代码,您可以输入特定的手牌进行检查。 (数组克隆函数之后的所有代码只是运行示例并输出到控制台)。

// OBJECT HAND CONSTRUCTOR

function Hand(cards, jokers, round, wilds) {
    this.cards = clone(cards);
    this.jokers = jokers;
    this.wilds = wilds || 0;
    this.round = round;
    this.melds = [];
    this.value = this.leftoverValue();
}

// FIND MELDS AFTER CURRENT POINT

Hand.prototype.findMelds = function(suit, number, multi) {

    if (suit == undefined || number == undefined || multi == undefined) {
        // NOT A RECURSION
        suit = number = multi = 0;
        this.convertWilds();
    }

    // START WITH ONLY JOKERS AS OPTIMAL COMBINATION
    if (this.jokers > 2) {
        for (i = 0; i < this.jokers; i++) {
            this.melds.push({s:-1, n:-1, m:-1});
        }
    this.value -= 25 * this.jokers - (22 - round) * (this.jokers < this.wilds ? this.jokers : this.wilds);
    }

    // SEARCH UNTIL END OR FULL LAY-DOWN
    while (this.value > 0) {

        // FIND NEXT CARD IN MATRIX
        while (this.cards[suit][number] <= multi) {
            multi = 0;
            if (++number > 10) {
                number = 0;
                if (++suit > 4) return;
            }
        }

        for (var type = 0; type < 2; type++) {
            // FIND MELD AT CURRENT POINT
            var meld = type ? this.findSet(suit, number, multi) : this.findRun(suit, number, multi);

            // TRY DIFFERENT LENGTHS OF LONG MELD
            // (to be improved: try permutations with jokers of each length)
            for (var len = 3; len <= meld.length; len++) {

                // CREATE COPY OF HAND AND REMOVE CURRENT MELD
                var test = new Hand(this.cards, this.jokers, this.round, this.wilds);
                test.removeCards(meld.slice(0, len));

                // RECURSION ON THE COPY
                // test.findMelds(suit, number, multi); // USE ONLY WITH MELD PERMUTATION ALGORITHM
                test.findMelds(0,0,0);                 // RESULT OK, BUT MAY CHECK THINGS MULTIPLE TIMES

                // BETTER COMBINATION FOUND
                if (test.value < this.value) {
                    this.value = test.value;
                    while (this.melds.length) this.melds.pop();
                    for (var i = 0; i < len; i++) {
                        // ADD CURRENT MELD (DON'T DESTROY YET)
                        this.melds.push(meld[i]);
                    }
                    // ADD MELDS FOUND THROUGH RECURSION
                    while (test.melds.length) this.melds.push(test.melds.shift());
                }
            }
        }
        multi++;
    }
}

// CHECK IF CARD IS START OF SET

Hand.prototype.findSet = function(s, n, m) {
    var set = [];
    while (s < 5) {
        while (m < this.cards[s][n]) {
            set.push({s:s, n:n, m:m++});
        }
        m = 0; s++;
    }

    // ADD JOKERS
    for (i = 0; i < this.jokers; i++) {
        set.push({s:-1, n:-1, m:-1});
    }
    return set;
}

// CHECK IF CARD IS START OF RUN

Hand.prototype.findRun = function(s, n, m) {
    var run = [], jokers = this.jokers;
    while (n < 11) {
        if (this.cards[s][n] > m) {
            run.push({s:s, n:n, m:m});
        } else if (jokers > 0) {
            run.push({s:-1, n:-1, m:-1});
            jokers--;
        }
        else break;
        m = 0; n++;
    }

    // ADD JOKERS (TRAILING OR LEADING)
    while (jokers-- > 0 && run.length < 11) {
        if (n++ < 11) run.push({s:-1, n:-1, m:-1});
        else run.unshift({s:-1, n:-1, m:-1});
    }
    return run;
}

// REMOVE ARRAY OF CARDS FROM HAND: [{s:x, n:y} ... ]

Hand.prototype.removeCards = function(cards) {
    for (var i = 0; i < cards.length; i++) {
        if (cards[i].s >= 0) {
            this.cards[cards[i].s][cards[i].n]--;
        } else this.jokers--;
    }
    this.value = this.leftoverValue();
}

// GET VALUE OF LEFTOVER CARDS

Hand.prototype.leftoverValue = function() {
    var leftover = 0;
    for (var i = 0; i < 5; i++) {
        for (var j = 0; j < 11; j++) {
            leftover += this.cards[i][j] * (j + 3) // cards count from 0 vs 3
        }
    }
    return leftover + 25 * this.jokers - (22 - round) * (this.jokers < this.wilds ? this.jokers : this.wilds);
}

// CONVERT WILDCARDS TO JOKERS

Hand.prototype.convertWilds = function() {
    for (var i = 0; i < 5; i++) {
        while (this.cards[i][this.round] > 0) {
            this.cards[i][this.round]--;
            this.jokers++; this.wilds++;
        }
    }
    this.value = this.leftoverValue();
}

// UTILS: 2D ARRAY DEEP-COPIER

function clone(a) {
    var b = [];
    for (var i = 0; i < a.length; i++) {
        b[i] = a[i].slice();
    }
    return b;
}

/* ******************************************************************************** */

// UTILS: SHOW HAND IN CONSOLE

function showHand(c, j, r, v) {
    var num = "    3 4 5 6 7 8 9 T J Q K";
    console.log(num.slice(0, 2*r+4) + "w" + num.slice(2*r+5));
    for (var i = 0; i < 5; i++) {
        console.log(["CLB ","DMD ","HRT ","SPD ","STR "][i] + c[i]);
    }
    console.log("    jokers: " + j + "  value: " + v);
}

// UTILS: SHOW RESULT IN CONSOLE

function showResult(m, v) {
    if (m.length == 0) console.log("no melds found");
    while (m.length) {
        var c = m.shift();
        if (c.s == -1) console.log("joker *");
        else console.log(["clubs","dmnds","heart","spade","stars"][c.s] + " " + "3456789TJQK".charAt(c.n));
    }
    console.log("leftover value: " + v);
}

// TEST DATA: CREATE ARRAY OF ALL CARDS TO DRAW FROM

var pack = [{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1}];
for (var i = 0; i < 5 ; i++) {
    for (var j = 0; j < 11; j++) {
        pack.push({s:i, n:j}, {s:i, n:j});
    }
}

// TEST DATA: CREATE 2D ARRAY TO STORE CARDS

var matrix = [];
var jokers = 0;
var round = 10; // count from zero

for (var i = 0; i < 5; i++) {
    matrix[i] = [];
    for (var j = 0; j < 11; j++) {
        matrix[i][j] = 0;
    }
}

// TEST DATA: DRAW CARDS AND FILL 2D ARRAY

for (i = 0; i < round + 3; i++)
{
    var j = Math.floor(Math.random() * pack.length);
    var pick = pack[j];
    pack[j] = pack.pop();
    if (pick.s > -1) matrix[pick.s][pick.n]++;
    else jokers++;
}

// USE THIS TO TEST SPECIFIC HANDS

// round = 10; // count from zero
// matrix = [[0,0,0,0,0,1,0,0,0,0,0],
//           [0,0,0,0,0,1,0,0,0,0,0],
//           [0,0,0,0,0,1,1,1,0,0,0],
//           [0,0,0,0,0,1,0,0,0,0,0],
//           [0,0,0,0,0,1,0,0,0,0,0]];
// jokers = 1;

// CREATE INSTANCE OF HAND OBJECT, EVALUATE AND SHOW RESULT

var x = new Hand(matrix, jokers, round); // no wilds parameter: automatic conversion
showHand(x.cards, x.jokers, x.round, x.value);
x.findMelds();
showResult(x.melds, x.value);

这是一个优化版本。集合的递归现在从当前点运行,运行的递归仍然从 0,0 运行。
将 findSets 函数优化到可以从当前点运行所有递归的点会增加复杂性,我认为这会取消任何可能的速度增益。
findRuns 和 findSets 函数现在返回运行和集合的变体数组;这也解决了运行中领先的 clown 的问题。
“multi”变量也已被删除,因为它只在一种特定情况下需要,这也可以通过对从 0,0 开始的集合运行递归来解决。

// CODE FOR SECOND VERSION REMOVED BECAUSE OF ANSWER LENGTH CONSTRAINTS

实际上,理论上“优化”的版本被证明只对多张双牌的手牌更快,而对于简单的手牌要慢得多,所以我决定制作一个混合版本。对于任何复杂的手牌,这比之前的两种算法都快 10%。
需要注意的是,为了简化代码,它在运行末尾添加了领先的 jokers,因此 *QK 运行将显示为 QK*

// OBJECT HAND CONSTRUCTOR

function Hand(cards, jokers, round, wilds) {
    this.cards = clone(cards);
    this.jokers = jokers;
    this.wilds = wilds || 0;
    this.round = round;
    this.melds = [];
    this.value = this.leftoverValue();
}

// FIND MELDS AFTER CURRENT POINT

Hand.prototype.findMelds = function(suit, number) {

    if (suit == undefined || number == undefined) {
        // NOT A RECURSION: CONVERT WILDS TO JOKERS
        suit = number = 0;
        this.convertWilds();
    }

    // START WITH ONLY JOKERS AS OPTIMAL COMBINATION
    if (this.jokers > 2) {
        for (var i = 0; i < this.jokers; i++) {
            this.melds.push({s:-1, n:-1});
        }
        this.value -= 25 * this.jokers - (22 - round) * (this.jokers < this.wilds ? this.jokers : this.wilds);
    }

    // SEARCH UNTIL END OR FULL LAY-DOWN
    while (this.value > 0) {

        // FIND NEXT CARD IN MATRIX
        while (number > 10 || this.cards[suit][number] == 0) {
            if (++number > 10) {
                number = 0;
                if (++suit > 4) return;
            }
        }

        // FIND RUNS OR SETS STARTING AT CURRENT POINT
        for (var meldType = 0; meldType < 2; meldType++) {

            var meld = meldType ? this.findSet(suit, number) : this.findRun(suit, number);

            // TRY DIFFERENT LENGTHS OF LONG MELD
            for (var len = 3; len <= meld.length; len++) {

                // CREATE COPY OF HAND AND REMOVE CURRENT MELD
                var test = new Hand(this.cards, this.jokers, this.round, this.wilds);
                test.removeCards(meld.slice(0, len));

                // RECURSION ON THE COPY
                meldType ? test.findMelds(suit, number) : test.findMelds(0, 0);

                // BETTER COMBINATION FOUND
                if (test.value < this.value) {
                    this.value = test.value;
                    // REPLACE BEST COMBINATION BY CURRENT MELD + MELDS FOUND THROUGH RECURSION
                    this.melds.length = 0;
                    this.melds = [].concat(meld.slice(0, len), test.melds);
                }
            }
        }
        number++;
    }
}

// FIND RUN STARTING WITH CURRENT CARD

Hand.prototype.findRun = function(s, n) {
    var run = [], jokers = this.jokers;
    while (n < 11) {
        if (this.cards[s][n] > 0) {
            run.push({s:s, n:n});
        } else if (jokers > 0) {
            run.push({s:-1, n:-1});
            jokers--;
        }
        else break;
        n++;
    }

    // ADD LEADING JOKERS (ADDED TO END FOR CODE SIMPLICITY)
    while (jokers-- > 0) {
        run.push({s:-1, n:-1});
    }
    return run;
}

// FIND SET STARTING WITH CURRENT CARD

Hand.prototype.findSet = function(s, n) {
    var set = [];
    while (s < 5) {
        for (var i = 0; i < this.cards[s][n]; i++) {
            set.push({s:s, n:n});
        }
        s++;
    }

    // ADD JOKERS
    for (var i = 0; i < this.jokers; i++) {
        set.push({s:-1, n:-1});
    }
    return set;
}

// REMOVE ARRAY OF CARDS FROM HAND

Hand.prototype.removeCards = function(cards) {
    for (var i = 0; i < cards.length; i++) {
        if (cards[i].s >= 0) {
            this.cards[cards[i].s][cards[i].n]--;
        } else this.jokers--;
    }
    this.value = this.leftoverValue();
}

// GET VALUE OF LEFTOVER CARDS

Hand.prototype.leftoverValue = function() {
    var leftover = 0;
    for (var i = 0; i < 5; i++) {
        for (var j = 0; j < 11; j++) {
            leftover += this.cards[i][j] * (j + 3) // cards count from 0 vs 3
        }
    }
    return leftover + 25 * this.jokers - (22 - round) * (this.jokers < this.wilds ? this.jokers : this.wilds);
}

// CONVERT WILDCARDS TO JOKERS

Hand.prototype.convertWilds = function() {
    for (var i = 0; i < 5; i++) {
        while (this.cards[i][this.round] > 0) {
            this.cards[i][this.round]--;
            this.jokers++; this.wilds++;
        }
    }
    this.value = this.leftoverValue();
}

// UTILS: 2D ARRAY DEEP-COPIER

function clone(a) {
    var b = [];
    for (var i = 0; i < a.length; i++) {
        b[i] = a[i].slice();
    }
    return b;
}

// UTILS: SHOW HAND IN CONSOLE

function showHand(c, j, r, v) {
    var num = "    3 4 5 6 7 8 9 T J Q K";
    console.log(num.slice(0, 2*r+4) + "w" + num.slice(2*r+5));
    for (var i = 0; i < 5; i++) {
        console.log(["CLB ","DMD ","HRT ","SPD ","STR "][i] + c[i]);
    }
    console.log("    jokers: " + j + "  value: " + v);
}

// UTILS: SHOW RESULT IN CONSOLE

function showResult(m, v) {
    if (m.length == 0) console.log("no melds found");
    while (m.length) {
        var c = m.shift();
        if (c.s == -1) console.log("joker *");
        else console.log(["clubs","dmnds","heart","spade","stars"][c.s] + " " + "3456789TJQK".charAt(c.n));
    }
    console.log("leftover value: " + v);
}

// TEST DATA: CREATE ARRAY OF ALL CARDS TO DRAW FROM

var pack = [{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1}];
for (var i = 0; i < 5 ; i++) {
    for (var j = 0; j < 11; j++) {
        pack.push({s:i, n:j}, {s:i, n:j});
    }
}

// TEST DATA: CREATE 2D ARRAY TO STORE CARDS

var matrix = [];
var jokers = 0;
var round = 10; // count from zero

for (var i = 0; i < 5; i++) {
    matrix[i] = [];
    for (var j = 0; j < 11; j++) {
        matrix[i][j] = 0;
    }
}

// TEST DATA: DRAW CARDS AND FILL 2D ARRAY

for (i = 0; i < round + 3; i++)
{
    var j = Math.floor(Math.random() * pack.length);
    var pick = pack[j];
    pack[j] = pack.pop();
    if (pick.s > -1) matrix[pick.s][pick.n]++;
    else jokers++;
}

// USE THIS TO TEST SPECIFIC HANDS

// round = 10; // count from zero
// matrix = [[0,0,0,0,0,1,0,0,0,0,0],
//           [0,0,0,0,0,2,1,1,0,0,0],
//           [0,0,0,1,1,2,0,0,0,0,0],
//           [0,0,0,0,0,1,0,0,0,0,0],
//           [0,0,0,0,0,0,0,0,0,0,0]];
// jokers = 1;

// CREATE INSTANCE OF HAND OBJECT, EVALUATE AND SHOW RESULT


var x = new Hand(matrix, jokers, round); // no wilds parameter: automatic conversion
showHand(x.cards, x.jokers, x.round, x.value);
x.findMelds();
showResult(x.melds, x.value);

之前的版本并不是最优的,因为它们会多次检查某些组合。在尝试找到确保每个选项只检查一次的最佳方法时,我意识到运行和集合的不同性质需要两种不同的方法,并且查找集合不需要递归。最后,这是最优策略:

  • 首先找到运行;仅在必要时使用 clown 。
  • 找到运行后,递归手牌的副本,然后继续寻找运行。
  • 当找不到更多运行时,切换到查找集。
  • 使用所有可用的没有百搭的完整套装。
  • 在有王牌可用的情况下,完成最有值(value)的一张或两张牌组。
  • 添加剩余的 clown 。

令我惊讶的是,尽管集合的代码有点繁琐,但对于复杂的手牌,该算法的速度提高了 10 倍以上。

// OBJECT HAND CONSTRUCTOR

function Hand(cards, jokers, round, wilds) {
    this.cards = clone(cards);
    this.jokers = jokers;
    this.wilds = wilds || 0;
    this.round = round;
    this.melds = [];
    this.value = this.leftoverValue();
}

// FIND MELDS FROM CURRENT POINT

Hand.prototype.findMelds = function(suit, number) {

    // IF NOT A RECURSION: CONVERT WILDCARDS TO JOKERS AND START FROM 0,0
    if (suit == undefined || number == undefined) {
        suit = number = 0;
        var noRecursion = true;
        this.convertWilds();
    }

    // FIND CARDS FROM CURRENT POINT UNTIL END OR FULL LAY-DOWN
    while (suit < 5 && this.value > 0) {
        if (this.cards[suit][number] > 0) {

            // FIND RUNS STARTING WITH CURRENT CARD
            var run = this.findRun(suit, number);

            // TRY DIFFERENT LENGTHS OF LONG RUN
            for (var len = 3; len <= run.length; len++) {

                // SKIP LONG RUNS ENDING WITH A JOKER
                if (len > 3 && run[len - 1].s == -1) continue;

                // CREATE COPY OF HAND, REMOVE RUN AND START RECURSION
                var test = new Hand(this.cards, this.jokers, this.round, this.wilds);
                test.removeCards(run.slice(0, len));
                test.findMelds(suit, number);

                // SAVE CURRENT RUN AND MELDS FOUND THROUGH RECURSION IF BETTER VALUE IS FOUND
                if (test.value < this.value) {
                    this.value = test.value;
                    this.melds.length = 0;
                    this.melds = [].concat(run.slice(0, len), test.melds);
                }
            }
        }

        // MOVE THROUGH CARD MATRIX
        if (++number > 10) {
            number = 0;
            ++suit;
        }
    }

    // AFTER ALL CARDS HAVE BEEN CHECKED FOR RUNS, CREATE COPY OF HAND AND FIND SETS
    if (this.value > 0) {
        var test = new Hand(this.cards, this.jokers, this.round, this.wilds);
        test.findSets();

        // SAVE SETS IF BETTER VALUE IS FOUND
        if (test.value < this.value) {
            this.value = test.value;
            this.melds.length = 0;
            while (test.melds.length) this.melds.push(test.melds.shift());
        }
    }

    // FIX NO MELDS AND ONE JOKER EXCEPTION
    if (noRecursion && this.melds.length < 3) {
        this.melds.length = 0;
        this.value = this.leftoverValue();
    }
}

// FIND RUN STARTING WITH CURRENT CARD

Hand.prototype.findRun = function(s, n) {
    var run = [], jokers = this.jokers;
    while (n < 11) {
        if (this.cards[s][n] > 0) {
            run.push({s:s, n:n});
        } else if (jokers > 0) {
            run.push({s:-1, n:-1});
            jokers--;
        } else break;
        n++;
    }

    // ADD LEADING JOKERS (AT END FOR CODE SIMPLICITY)
    while (run.length < 3 && jokers--) {
        run.push({s:-1, n:-1});
    }

    // REMOVE UNNECESSARY TRAILING JOKERS
    while (run.length > 3 && run[run.length - 1].s == -1) {
        run.pop();
    }
    return run;
}

// FIND SETS

Hand.prototype.findSets = function() {
    var sets = [[], []], values = [[], []];
    for (var n = 0; n < 11; n++) {
        var set = [], value = 0;
        for (var s = 0; s < 5; s++) {
            for (var i = 0; i < this.cards[s][n]; i++) {
                set.push({s:s, n:n});
                value += n + 3;
            }
        }

        // ADD COMPLETE SET TO MELDS, OR INCOMPLETE SET TO CANDIDATES TO RECEIVE JOKERS
        if (set.length >= 3) {
            this.addToMelds(set);
        }
        else if (set.length > 0) {
            // STORE ONE-CARD SETS IN sets[0] AND TWO-CARD SETS IN sets[1]
            sets[set.length - 1].push(set);
            values[set.length - 1].push(value);
        }
    }

    // IF TWO JOKERS ARE AVAILABLE: COMPLETE MOST VALUABLE TWO-CARD SET OR ONE-CARD SET(S)
    while (this.jokers > 1 && sets[0].length > 0) {
        var select = values[0][sets[0].length - 1];
        for (var i = sets[1].length - 1; i >= 0 && i > sets[1].length - 3; i--) {
            select -= values[1][i];
        }
        if (select > 0) {
            set = sets[0].pop(); values[0].pop();
            set.push({s:-1, n:-1}, {s:-1, n:-1});
            this.addToMelds(set);
        } else {
            for (var i = 0; i < 2 && sets[1].length > 0; i++) {
                set = sets[1].pop(); values[1].pop();
                set.push({s:-1, n:-1});
                this.addToMelds(set);
            }
        }
    }

    // IF JOKER IS AVAILABLE: COMPLETE MOST VALUABLE TWO-CARD SET
    while (this.jokers > 0 && sets[1].length > 0) {
        set = sets[1].pop();
        set.push({s:-1, n:-1});
        this.addToMelds(set);
    }

    // ADD LEFT-OVER JOKERS
    while (this.jokers > 0) {
        this.addToMelds([{s:-1, n:-1}]);
    }
}

// ADD SET TO MELDS

Hand.prototype.addToMelds = function(set) {
    this.removeCards(set);
while (set.length) this.melds.push(set.shift());
}

// REMOVE ARRAY OF CARDS FROM HAND

Hand.prototype.removeCards = function(cards) {
    for (var i = 0; i < cards.length; i++) {
        if (cards[i].s >= 0) {
            this.cards[cards[i].s][cards[i].n]--;
        } else this.jokers--;
    }
    this.value = this.leftoverValue();
}

// GET VALUE OF LEFTOVER CARDS

Hand.prototype.leftoverValue = function() {
    var leftover = 0;
    for (var i = 0; i < 5; i++) {
        for (var j = 0; j < 11; j++) {
            leftover += this.cards[i][j] * (j + 3) // cards count from 0 vs 3
        }
    }
    return leftover + 25 * this.jokers - (22 - round) * (this.jokers < this.wilds ? this.jokers : this.wilds);
}

// CONVERT WILDCARDS TO JOKERS

Hand.prototype.convertWilds = function() {
    for (var i = 0; i < 5; i++) {
        while (this.cards[i][this.round] > 0) {
	this.cards[i][this.round]--;
            this.jokers++; this.wilds++;
        }
    }
    this.value = this.leftoverValue();
}

// UTILS: 2D ARRAY DEEP-COPIER

function clone(a) {
    var b = [];
    for (var i = 0; i < a.length; i++) {
        b[i] = a[i].slice();
    }
    return b;
}

// UTILS: SHOW HAND IN CONSOLE

function showHand(c, j, r, v) {
    var num = "    3 4 5 6 7 8 9 T J Q K";
    console.log(num.slice(0, 2*r+4) + "w" + num.slice(2*r+5));
    for (var i = 0; i < 5; i++) {
        console.log(["CLB ","DMD ","HRT ","SPD ","STR "][i] + c[i]);
    }
    console.log("    jokers: " + j + "  value: " + v);
}

// UTILS: SHOW RESULT IN CONSOLE

function showResult(m, v) {
    if (m.length == 0) console.log("no melds found");
    while (m.length) {
        var c = m.shift();
        if (c.s == -1) console.log("joker *");
        else console.log(["clubs","dmnds","heart","spade","stars"][c.s] + " " + "3456789TJQK".charAt(c.n));
    }
    console.log("leftover value: " + v);
}

// TEST DATA: CREATE ARRAY OF ALL CARDS TO DRAW FROM

var pack = [{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1},{s:-1,n:-1}];
for (var i = 0; i < 5 ; i++) {
    for (var j = 0; j < 11; j++) {
        pack.push({s:i, n:j}, {s:i, n:j});
    }
}

// TEST DATA: CREATE 2D ARRAY TO STORE CARDS

var matrix = [];
for (var i = 0; i < 5; i++) {
    matrix[i] = [];
    for (var j = 0; j < 11; j++) {
        matrix[i][j] = 0;
    }
}

// TEST DATA: DRAW CARDS AND FILL 2D ARRAY

var round = 10; // count from zero
var jokers = 0;
for (i = 0; i < round + 3; i++)
{
    var j = Math.floor(Math.random() * pack.length);
    var pick = pack[j];
    pack[j] = pack.pop();
    if (pick.s > -1) matrix[pick.s][pick.n]++;
    else jokers++;
}

// USE THIS TO TEST SPECIFIC HANDS

// round = 10; // count from zero
// matrix = [[0,0,0,0,0,1,0,0,0,0,1],
//           [0,0,0,0,0,1,0,0,0,0,0],
//           [0,1,0,0,1,2,0,0,0,0,0],
//           [0,0,0,0,0,2,1,0,0,1,1],
//           [0,0,0,0,0,0,0,0,0,0,0]];
// jokers = 1;

// CREATE INSTANCE OF HAND OBJECT, EVALUATE AND SHOW RESULT


var x = new Hand(matrix, jokers, round); // CALL WITHOUT FOURTH (WILDS) PARAMETER
showHand(x.cards, x.jokers, x.round, x.value);
x.findMelds();                           // CALL WITHOUT PARAMETERS TO INITIATE EVALUATION
showResult(x.melds, x.value);

关于javascript - 纸牌游戏手牌评估的组合学,带通配符和重复,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31615597/

相关文章:

javascript - 如何在 CKEDITOR 中以编程方式执行撤消和重做,并重置它们的堆栈?

algorithm - 寻找最优解

python - 确定两个数组是否是彼此的旋转版本

java - 扫描大量文件几十个字

python - 第 k 个排列的第 i 个元素

javascript - 当您只能检查元素是否可见时,为什么要使用 matchMedia?

javascript - 将 SVG 字符串插入 Dom?

javascript - 通过 React setState 使用子键的计算值

algorithm - 在长度为 N 的路径上采取 k 步的方法数

haskell - Haskell 中数字列表的成对距离