ぬるーむ

Unity初心者が誰もが知っているゲームの模倣をしています。個人的な備忘録ですが、入門書を読み終えたばかりの初心者の方は「こんなへなちょこでもいいのか!」「俺の方がうまく作れる」と作成意欲がわいたりするかもしれません。

Unityによる大富豪の作り方 4 ~役、手札の評価~


スポンサードリンク

評価値の計算

場にどのカードを出すか決定する為に必要な3つの評価値 - 役評価値、手札評価値、優先評価値 - の計算について説明します。
主に以下のサイトを参考にしました。

コンピュータ大貧民におけるヒューリスティック戦略の実装と効果

役評価値

役自体の強さを表します。役が単体かグループか階段なのかで計算方法が異なります。

単体

対象のカードよりランクが高く、相手が保持している可能性のあるカードのランクの総数をnとしたき役評価値は 100 - n × 30となります。


対象が[ ♠️Q ]ですでに♠️K ♠️1 ♦️1 ♣️1 ♠️2 ♦️2 ♣️2 ❤️2が使われている場合、役評価値は100 - 2 × 30 = 40 となります。

グループ
  • そのグループと同じ枚数で、対象よりランクが高く、相手が保持している可能性のあるカード(H)を抽出します。
  • Hに含まれる各ランク(Ri)ごとに以下の計算をします。
    • Si = Riの総数 - 計算対象の枚数 - プレイ中の人数 + プレイヤー数
    • Siの値によってNiの値を決定します。
      • Si <= 0 なら Ni = 4
      • Si = 1 なら Ni =9
      • Si = 2 なら Ni =15
      • Si >= 3 なら Ni =24
  • 役評価値は 100 - Niの総和 となります。

対象が[ ♠️J ♦️J ♣️J ]ですでに♠️K ♠️1 ♦️1 ♣️1 ♠️2 ♦️2 ♣️2 ❤️2が使われておりプレイ人数4人で誰も上がっていない場合
Jより高く、相手が保持している可能性のあるカード(H)はQ、K。
Q、KそれぞれのNiの値Nq、Nkは

Sq = 4 - 3 - 4 + 4 = 1 より Nq = 9

Sk = 3 - 3 - 4 + 4 = 0 より Nk = 4

これより、役評価値は 100 - 9 + 4 = 87 となります。
階段

対象のカードよりランクが高く、相手が保持している可能性のある階段の総数をnとしたき、役評価値は 100 - n × (プレイヤー数 + 1 - プレイ中の人数) となります。


対象が[ ♠️9 ♠️10 ♠️J ]ですでに♦️Q ♣️Q ♣️K ❤️Q ❤️K ❤️1が使われておりプレイ人数4人で誰も上がっていない場合
相手が保持している可能性のある対象より高ランクの階段は[ ♠️Q ♠️K ♠️1 ] [♠️K ♠️1 ♠️2 ] [ ♦️K ♦️1 ♦️2 ]の3種類なので
役評価値は 100 - 3 × (4 + 1 - 4) = 97 となります。

手札評価値

手札にどれだけ弱いカードが残っているかを表します。弱い役が多いほど値が大きくなります。役評価値より各役に点数(S)を付けます。

  • 役評価値が 1以上30以下の場合 S = 2
  • 役評価値が 31以上60以下の場合 S = 1
  • 役評価値が 61以上90以下の場足 S = 0
  • 役評価値が 91以上かつ対象の役よりランクが高いカードを相手が保持している可能性がある場合 S = -1
  • 役評価値が 91以上かつ対象の役よりランクが高いカードを相手が保持している可能性がない場合 S = 対象の役の枚数 × -1

手札評価値は、Sの総和となります。


手札が♠️3 ♦️9 ♦️10 ♦️J ♦️2 ♣️2でどのカードも使われていなく、プレイ人数4人で誰も上がっていない場合
[ ♠️3 ]の役評価値は1なので S = 2
[ ♦️9 ♦️10 ♦️J ]の役評価値は94で、相手が高ランクの役を保持している可能性があるので S = -1
[ ♦️2 ♣️2 ]の役評価値は100で、相手が高ランクの役を保持している可能性がないので S = -2
手札評価値は 2 - 1 - 2 = -1 となります。

優先評価値

親のときに場に出す優先順位を表します。革命できる役を持っているかどうかで計算方法が異なります。

革命できない場合

まず、各役が「場を流せそうな役」なのか、手札が「あがれそうな手札」なのかを計算します。

  • 場を流せそうな役
    • 役評価値が86以上の役。
  • あがれそうな手札
    • 手札内の役の数 - 場を流せそうな役の数 が1以下の手札。

あがれそうな手札の場合、場を流せない役の優先評価値を低くし、場を流せる役を優先的に場に出し続け上がりきるようにします(ずっと俺のターン!)。
あがれそうにない手札の場合、役ランク(役を構成するカード内で最も高いランク)が低く、手札内に同一枚数のものが多いほど高くします。

  • 場を流せそうな役の優先評価値は 110 - 役評価値 となります。
  • 場を流せなそうな役の優先評価値は
    • 上がれそうな手札の場合は 2 となります。
    • そうでない場合は 40 + (13 - 役ランク) × 2 + 手札内の同じ枚数の役の数 × 4 となります。

例1
手札が❤️3 ❤️4 ❤️5 ♦️6 ♣️7 ♠️7 ♠️8 ♦️8 ♣️8 ♦️J ♣️J ❤️2でどのカードも使われていなく、プレイ人数4人で誰も上がっていない場合
[ ❤️3 ❤️4 ❤️5 ]の役評価値は86
[ ♦️6 ]の役評価値は1
[ ♣️7 ♠️7 ]の役評価値は12
[ ♠️8 ♦️8 ♣️8 ]の役評価値は51
[ ♦️J ♣️J ]の役評価値は46
[ ❤️2 ]の役評価値は100
手札内の役が6つ、場を流せそうな役が2つなのであがれそうな手札ではありません。よって、
[ ❤️3 ❤️4 ❤️5 ]の優先評価値は 110 - 86 = 24
[ ♦️6 ]の優先評価値は 40 + (13 - 3) × 2 + 2 × 4 = 68
[ ♣️7 ♠️7 ]の優先評価値は 40 + (13 - 4) × 2 + 2 × 4 = 66
[ ♠️8 ♦️8 ♣️8 ]の優先評価値は 40 + (13 - 5) × 2 + 1 × 4 = 60
[ ♦️J ♣️J ]の優先評価値は 40 + (13 - 8) × 2 + 2 × 4 = 58
[ ❤️2 ]の優先評価値は 110 - 100 = 10
となります。

例2
手札が❤️3 ❤️4 ❤️5 ♠️6 ♠️7 ♠️8 ♣️9でどのカードも使われていなく、プレイ人数4人で誰も上がっていない場合
[ ❤️3 ❤️4 ❤️5 ]の役評価値は74
[ ♠️6 ♠️7 ♠️8 ]の役評価値は81
[ ♣️9 ]の役評価値は1
場を流せる役はないので上がれそうな手札ではありません。よって、
[ ❤️3 ❤️4 ❤️5 ]の優先評価値は 40 + (13 - 2) × 2 + 2 × 4 = 68
[ ♠️6 ♠️7 ♠️8 ]の優先評価値は 40 + (13 - 5) × 2 + 2 × 4 = 64
[ ♣️9 ]の優先評価値は 40 + (13 - 6) × 2 + 1 × 4 = 58
となります。

例3
手札が♠️3 ♠️4 ♠️5 ♦️8 ♦️9 ♦️10 ♣️2でどのカードも使われていなく、プレイ人数4人で誰も上がっていない場合
[ ♠️3 ♠️4 ♠️5 ]の役評価値は74
[ ♦️8 ♦️9 ♦️10 ]の役評価値は89
[ ♣️2 ]の役評価値は100
手札内の役が3つ、場を流せそうな役が2つなのであがれそうな手札です。よって、
[ ♠️3 ♠️4 ♠️5 ]の優先評価値は2
[ ♦️8 ♦️9 ♦️10 ]の優先評価値は110 - 89 = 21
[ ♣️2 ]の優先評価値は110 - 100 = 10

革命できる場合
  • 2枚以下のカードで構成される役の役評価値が90以下 かつ
  • 出した後の手札評価値 < 出す前の手札評価値

の場合

  • 革命の役の優先評価値を90
  • 階段以外で場を流せそうな役の優先評価値を91
  • 残りは革命できない場合と同様

とします。上記条件を満たさない場合は革命役の優先評価値を1とし革命しないようにします。

実装

public class Hand
{
    private static readonly ulong MAll = 0x000fffffffffffff;
    private static BitUtility bit;
    private readonly ulong bitHand;
    private readonly int sameQuantity;
    private readonly int rank;

    //場を流せる役かどうか
    public bool CanEndTurn { get => Score >= 86; }

    public int Rank { get => rank; }

    //役の構成枚数
    public int Quantity { get => bit.CountBit(bitHand); }
    
    //役評価値
    public float Score { get; private set; }

    //優先評価値
    public float Priority { get; set; }

    public int EnemyHigherRankQuantity { get; private set; }

    public Hand(ulong bitHand, ulong playerCard, GameData gameData, int same = 0)
    {
        this.bitHand = bitHand;
        bit = BitUtility.Instance;

        //役のランクを求める。革命時は逆になる
        rank = gameData.IsRevolutionalizing ?
            12 - (int)(bit.BitScanForward(bitHand) / 4) :
            (int)(bit.BitScanReverse(bitHand) / 4);

        //場に出ていないカードを取得
        var enemyCard = ~(playerCard | gameData.BitUsedCard) & MAll;
        //場に出ていないカードの中で、役より強いカードを取得
        var enemyHigherCard = bit.GetHigherBitCard(enemyCard, bitHand,
            gameData.IsRevolutionalizing);
        EnemyHigherRankQuantity = CountRank(enemyHigherCard);
        
        //手札内にある同じ枚数の役の数
        sameQuantity = same;
        
        Score = EvaluateHand(enemyHigherCard, gameData);
    }

    //役評価値を計算
    public float EvaluateHand(ulong higher, GameData gameData)
    {
        float score = 0f;

        if (Quantity == 1)
        {
            score = EvaluateSingleHand();
        }
        else if (bit.IsSequence(bitHand, Quantity))
        {
            score = EvaluateSequenceHand(higher, Quantity, gameData);
        }
        else if (bit.IsGroup(bitHand, Quantity))
        {
            score = EvaluateGroupHand(higher, Quantity, gameData);
        }

        return Mathf.Max(score, 1);
    }

    //優先評価値を求める
    public void SetPriority(bool canEndGame)
    {
        if (canEndGame)
        {
            if (!CanEndTurn)
            {
                Priority = 2;
                return;
            }
        }

        if (CanEndTurn)
        {
            Priority = 110 - Score;
        }
        else
        {
            Priority = 40 + (13 - Rank) * 2
                + sameQuantity * 4;
        }
    }

    //単体のカードの強さ評価値を計算
    private float EvaluateSingleHand()
    {
        return 100 - EnemyHigherRankQuantity * 30;
    }

    //階段の強さ評価値を計算
    private float EvaluateSequenceHand(ulong higher, int cnt, GameData gameData)
    {
        var seqs = bit.GetSequenceList(higher, cnt);

        return 100 - seqs.Count * (gameData.PlayerNumber + 1 - gameData.PlayingNumber);
    }

    //グループの強さの評価値を計算
    private float EvaluateGroupHand(ulong higher, int cnt, GameData gameData)
    {
        int score = 0;
        var cnts = CountOverQuantityEveryRank(higher, cnt);
        
        for (int i = 0; i < cnts.Length; i++)
        {
            if (cnts[i] == 0) continue;

            var s = cnts[i] - cnt - gameData.PlayingNumber + gameData.PlayerNumber;
            int n;
            
            if (s <= 0) n = 4;
            else if (s == 1) n = 9;
            else if (s == 2) n = 15;
            else n = 24;
            
            score += n;
        }
        
        return 100 - score;
    }

    private int CountRank(ulong bitCard)
    {
        var qpCard = bit.ToQPCard(bitCard);

        return bit.CountBit(qpCard);
    }

    //指定の枚数以上あるランク内で、何枚カードがあるか数える
    private int[] CountOverQuantityEveryRank(ulong higher, int cnt)
    {
        int n = cnt - 1;
        var noless = 16 - (1ul << n);
        var cntMask = BitUtility.M0001 * noless;
        var qpOver = bit.ToQPCard(higher) & cntMask;

        var cnts = new int[13];

        for (int i = 0; i < 13; i++)
        {
            var c = (qpOver >> i * 4) & 15ul;
            
            if (c == 0) cnts[i] = 0;
            else cnts[i] = (int)Math.Log((int)c, 2) + 1;
        }

        return cnts;
    }
}
public class PlayerCard
{
    //手札内のすべての役
    private List<Hand> hands;

    //あがれる手札かどうか
    public bool CanEndGame
    {
        get
        {
            int cnt = hands.Count;
            int end = hands.Count(h => h.CanEndTurn);

            return cnt == 0 || cnt - end <= 1;
        }
    }

    //手札評価値を取得
    public float Score
    {
        get
        {
            float score = 0f;

            hands.ForEach(h =>
            {
                if (h.Score >= 1 && h.Score <= 30)
                {
                    score += 2;
                }
                else if (h.Score >= 31 && h.Score <= 60)
                {
                    score += 1;
                }
                else if (h.Score >= 91 && h.EnemyHigherRankQuantity > 0)
                {
                    score += -1;
                }
                else if (h.Score >= 91 && h.EnemyHigherRankQuantity == 0)
                {
                    score += -h.Quantity;
                }
            });

            return score;
        }
    }
}

//優先評価値を計算
private void SetPriority(GameData gameData)
{
    var canEnd = CanEndGame;
    //革命役を取得
    var rev = hands.Find(h => h.CanRevolutionalize);

    if (rev != null)
    {
        SetPriorityOnRevolution(rev, canEnd, gameData);
    }
    else
    {
        hands.ForEach(h => h.SetPriority(canEnd));
    }
}

//革命できる場合の優先評価値を計算
private void SetPriorityOnRevolution(Hand rev, bool canEnd, GameData gameData)
{
    var next = CreateNextPlayerCard(rev.BitHand, gameData);

    if (hands.Exists(h => h.Quantity <= 2 && h.Score > 90) || Score < next.Score)
    {
        rev.Priority = 1;

        foreach (var h in hands.Where(h => !h.CanRevolutionalize))
        {
            h.SetPriority(canEnd);
        }
    }
    else
    {
        rev.Priority = 90;

        foreach (var h in hands.Where(h => !h.CanRevolutionalize))
        {
            if (!bit.IsSequence(h.BitHand) && h.CanEndTurn)
            {
                h.Priority = 91;
            }
            else
            {
                h.SetPriority(canEnd);
            }
        }
    }
}